diff --git a/.betterer.results b/.betterer.results index fbe46404e01..3a494c83187 100644 --- a/.betterer.results +++ b/.betterer.results @@ -86,8 +86,8 @@ exports[`no enzyme tests`] = { "public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:227258837": [ - [0, 19, 13, "RegExp match", "2409514259"] + "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:4057721851": [ + [1, 19, 13, "RegExp match", "2409514259"] ], "public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [ [0, 26, 13, "RegExp match", "2409514259"] @@ -4771,9 +4771,6 @@ exports[`better eslint`] = { "public/app/features/datasources/DataSourceDashboards.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/datasources/DataSourcesListPage.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/datasources/__mocks__/dataSourcesMocks.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -5720,11 +5717,9 @@ exports[`better eslint`] = { "public/app/features/scenes/components/SceneFlexLayout.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/scenes/core/SceneComponentEditWrapper.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + "public/app/features/scenes/core/SceneComponentWrapper.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/scenes/core/SceneObjectBase.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6050,9 +6045,6 @@ exports[`better eslint`] = { "public/app/features/transformers/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/users/UsersActionBar.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/users/__mocks__/userMocks.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -6655,8 +6647,12 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], + "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -6664,7 +6660,8 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -7564,10 +7561,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"] + [0, 0, 0, "Unexpected any. Specify a different type.", "16"] ], "public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -9650,11 +9644,10 @@ exports[`better eslint`] = { "public/app/plugins/panel/histogram/Histogram.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], "public/app/plugins/panel/icon/IconPanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -10354,9 +10347,6 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/QueryField/QueryField.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], - "packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx:5381": [ - [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] - ], "packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], diff --git a/.drone.star b/.drone.star index 0829f79f299..b9d6ab67c91 100644 --- a/.drone.star +++ b/.drone.star @@ -7,7 +7,7 @@ load('scripts/drone/pipelines/pr.star', 'pr_pipelines') load('scripts/drone/pipelines/main.star', 'main_pipelines') load('scripts/drone/pipelines/docs.star', 'docs_pipelines') -load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline') +load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline', 'artifacts_page_pipeline') load('scripts/drone/version.star', 'version_branch_pipelines') load('scripts/drone/pipelines/cron.star', 'cronjobs') load('scripts/drone/vault.star', 'secrets') @@ -17,5 +17,5 @@ def main(ctx): return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \ publish_image_pipelines('public') + publish_image_pipelines('security') + \ publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \ - publish_npm_pipelines('public') + publish_packages_pipeline() + \ + publish_npm_pipelines('public') + publish_packages_pipeline() + artifacts_page_pipeline() + \ version_branch_pipelines() + cronjobs(edition=edition) + secrets() diff --git a/.drone.yml b/.drone.yml index 9dac0630448..d83de1d157f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,49 @@ --- depends_on: [] kind: pipeline +name: pr-verify-drone +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15 + name: identify-runner +- commands: + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: + - ./bin/grabpl verify-drone + depends_on: + - grabpl + image: byrnedo/alpine-curl:0.1.8 + name: lint-drone +trigger: + event: + - pull_request + paths: + exclude: + - docs/** + - '*.md' + include: + - scripts/drone/** + - .drone.yml + - .drone.star +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +depends_on: [] +kind: pipeline name: pr-test-frontend node: type: no-parallel @@ -15,7 +58,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -91,7 +134,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -116,12 +159,6 @@ steps: - verify-gen-cue image: grafana/build-container:1.5.7 name: wire-install -- commands: - - ./bin/grabpl verify-drone - depends_on: - - grabpl - image: byrnedo/alpine-curl:0.1.8 - name: lint-drone - commands: - |- echo -e "unknwon @@ -197,7 +234,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -243,7 +280,6 @@ steps: from_secret: drone_token - commands: - ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} - --variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64 depends_on: - gen-version - wire-install @@ -455,7 +491,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -542,7 +578,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -632,7 +668,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -721,7 +757,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -789,7 +825,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -884,7 +920,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -979,7 +1015,7 @@ steps: image: grafana/build-container:1.5.7 name: build-frontend-packages - commands: - - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin + - ./bin/grabpl build-plugins --jobs 8 --edition oss depends_on: - gen-version - yarn-install @@ -1230,7 +1266,7 @@ steps: repo: - grafana/grafana - commands: - - ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads + - ./bin/grabpl upload-packages --edition oss depends_on: - end-to-end-tests-dashboards-suite - end-to-end-tests-panels-suite @@ -1247,7 +1283,7 @@ steps: repo: - grafana/grafana - commands: - - ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition oss depends_on: - grafana-server environment: @@ -1308,7 +1344,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1398,7 +1434,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -1486,10 +1522,16 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl +- commands: + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + depends_on: + - grabpl + image: grafana/build-container:1.5.7 + name: gen-version - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 @@ -1498,7 +1540,7 @@ steps: - ./bin/grabpl store-packages --edition oss --gcp-key /tmp/gcpkey.json --build-id ${DRONE_BUILD_NUMBER} depends_on: - - grabpl + - gen-version environment: GCP_KEY: from_secret: gcp_key @@ -1571,7 +1613,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1628,7 +1670,7 @@ steps: image: grafana/build-container:1.5.7 name: build-frontend-packages - commands: - - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin + - ./bin/grabpl build-plugins --jobs 8 --edition oss depends_on: - gen-version - yarn-install @@ -1778,8 +1820,7 @@ steps: image: grafana/build-container:1.5.7 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition oss --src-bucket "$${PRERELEASE_BUCKET}" --src-dir - artifacts/static-assets + - ./bin/grabpl upload-cdn --edition oss depends_on: - grafana-server environment: @@ -1790,7 +1831,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: upload-cdn-assets - commands: - - ./bin/grabpl upload-packages --edition oss --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads + - ./bin/grabpl upload-packages --edition oss depends_on: - end-to-end-tests-dashboards-suite - end-to-end-tests-panels-suite @@ -1869,7 +1910,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2004,7 +2045,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2098,7 +2139,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -2157,7 +2198,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2244,7 +2285,7 @@ steps: image: grafana/build-container:1.5.7 name: build-frontend-packages - commands: - - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin + - ./bin/grabpl build-plugins --jobs 8 --edition enterprise depends_on: - gen-version - yarn-install @@ -2393,8 +2434,7 @@ steps: - success - failure - commands: - - ./bin/grabpl upload-cdn --edition enterprise --src-bucket "$${PRERELEASE_BUCKET}" - --src-dir artifacts/static-assets + - ./bin/grabpl upload-cdn --edition enterprise depends_on: - package environment: @@ -2405,7 +2445,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: upload-cdn-assets - commands: - - ./bin/grabpl upload-packages --edition enterprise --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads + - ./bin/grabpl upload-packages --edition enterprise depends_on: - package environment: @@ -2446,8 +2486,7 @@ steps: image: grafana/build-container:1.5.7 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "$${PRERELEASE_BUCKET}" - --src-dir artifacts/static-assets + - ./bin/grabpl upload-cdn --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -2458,7 +2497,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: upload-cdn-assets-enterprise2 - commands: - - ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads-enterprise2 + - ./bin/grabpl upload-packages --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -2505,7 +2544,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2688,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.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2830,7 +2869,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2905,7 +2944,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2983,7 +3022,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3044,7 +3083,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3123,7 +3162,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3185,7 +3224,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3223,7 +3262,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3261,7 +3300,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3317,15 +3356,21 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl +- commands: + - ./bin/grabpl gen-version ${DRONE_TAG} + depends_on: + - grabpl + image: grafana/build-container:1.5.7 + name: gen-version - commands: - ./bin/grabpl store-packages --edition oss --packages-bucket grafana-downloads --gcp-key /tmp/gcpkey.json ${DRONE_TAG} depends_on: - - grabpl + - gen-version environment: GCP_KEY: from_secret: gcp_key @@ -3365,15 +3410,21 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl +- commands: + - ./bin/grabpl gen-version ${DRONE_TAG} + depends_on: + - grabpl + image: grafana/build-container:1.5.7 + name: gen-version - commands: - ./bin/grabpl store-packages --edition enterprise --packages-bucket grafana-downloads --gcp-key /tmp/gcpkey.json ${DRONE_TAG} depends_on: - - grabpl + - gen-version environment: GCP_KEY: from_secret: gcp_key @@ -3400,6 +3451,38 @@ volumes: --- depends_on: [] kind: pipeline +name: publish-artifacts-page +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: + - ./bin/grabpl artifacts-page + depends_on: + - grabpl + image: grafana/build-container:1.5.7 + name: artifacts-page +trigger: + event: + - promote + target: security +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +depends_on: [] +kind: pipeline name: release-branch-oss-build-e2e-publish node: type: no-parallel @@ -3414,7 +3497,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3471,7 +3554,7 @@ steps: image: grafana/build-container:1.5.7 name: build-frontend-packages - commands: - - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin + - ./bin/grabpl build-plugins --jobs 8 --edition oss depends_on: - gen-version - yarn-install @@ -3621,7 +3704,7 @@ steps: image: grafana/build-container:1.5.7 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition oss depends_on: - grafana-server environment: @@ -3635,7 +3718,7 @@ steps: repo: - grafana/grafana - commands: - - ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads + - ./bin/grabpl upload-packages --edition oss depends_on: - end-to-end-tests-dashboards-suite - end-to-end-tests-panels-suite @@ -3682,7 +3765,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3811,7 +3894,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3899,7 +3982,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -3947,7 +4030,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4032,7 +4115,7 @@ steps: image: grafana/build-container:1.5.7 name: build-frontend-packages - commands: - - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin + - ./bin/grabpl build-plugins --jobs 8 --edition enterprise depends_on: - gen-version - yarn-install @@ -4193,7 +4276,7 @@ steps: image: grafana/build-container:1.5.7 name: build-storybook - commands: - - ./bin/grabpl upload-cdn --edition enterprise --src-bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition enterprise depends_on: - package environment: @@ -4207,7 +4290,7 @@ steps: repo: - grafana/grafana - commands: - - ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads + - ./bin/grabpl upload-packages --edition enterprise depends_on: - package environment: @@ -4241,7 +4324,7 @@ steps: image: grafana/build-container:1.5.7 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "grafana-static-assets" + - ./bin/grabpl upload-cdn --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -4252,7 +4335,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: upload-cdn-assets-enterprise2 - commands: - - ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket grafana-downloads-enterprise2 + - ./bin/grabpl upload-packages --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -4293,7 +4376,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4467,7 +4550,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4600,7 +4683,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4797,6 +4880,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 12fbd61337ed1ae006e8c0ff71d1290e27b5910211369cdd47e1d604afd9befa +hmac: 55383abbbc205824d35aa689a0e00f374e74520d77dc387e354b063e4ade0869 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad9fff7905b..83359aea04a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -144,6 +144,7 @@ lerna.json @grafana/frontend-ops .eslintrc @grafana/frontend-ops .pa11yci.conf.js @grafana/user-essentials .pa11yci-pr.conf.js @grafana/user-essentials +.betterer.results @joshhunt # @grafana/ui component documentation *.mdx @marcusolsson @jessover9000 @grafana/plugins-platform-frontend diff --git a/.github/commands.json b/.github/commands.json index abf9b8f0745..5543c5fb2f3 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -79,7 +79,7 @@ "name":"datasource/Azure", "action":"addToProject", "addToProject":{ - "url":"https://github.com/orgs/grafana/projects/97" + "url":"https://github.com/orgs/grafana/projects/190" } }, { @@ -103,7 +103,7 @@ "name":"datasource/GoogleCloudMonitoring", "action":"addToProject", "addToProject":{ - "url":"https://github.com/orgs/grafana/projects/97" + "url":"https://github.com/orgs/grafana/projects/190" } }, { diff --git a/conf/defaults.ini b/conf/defaults.ini index 131db397561..9aa4a63459f 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -576,7 +576,7 @@ tls_client_cert = tls_client_key = tls_client_ca = use_pkce = false -auth_style = +auth_style = #################################### Basic Auth ########################## [auth.basic] @@ -597,6 +597,7 @@ enable_login_token = false #################################### Auth JWT ########################## [auth.jwt] enabled = false +enable_login_token = false header_name = email_claim = username_claim = @@ -762,7 +763,7 @@ instrumentations_console_enabled = false instrumentations_webvitals_enabled = false # Api Key, only applies to Grafana Javascript Agent provider -api_key = +api_key = #################################### Usage Quotas ######################## [quota] diff --git a/conf/ldap.toml b/conf/ldap.toml index 4d890b180fc..49b95536b36 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -25,6 +25,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org" # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = 'grafana' +# Timeout in seconds (applies to each host specified in the 'host' entry (space separated)) +timeout = 10 + # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" search_filter = "(cn=%s)" diff --git a/devenv/docker/blocks/jwt_proxy/cloak.sql b/devenv/docker/blocks/jwt_proxy/cloak.sql new file mode 100644 index 00000000000..ac0cfde1d44 --- /dev/null +++ b/devenv/docker/blocks/jwt_proxy/cloak.sql @@ -0,0 +1,5486 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 12.2 (Debian 12.2-2.pgdg100+1) +-- Dumped by pg_dump version 12.2 (Debian 12.2-2.pgdg100+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: admin_event_entity; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.admin_event_entity ( + id character varying(36) NOT NULL, + admin_event_time bigint, + realm_id character varying(255), + operation_type character varying(255), + auth_realm_id character varying(255), + auth_client_id character varying(255), + auth_user_id character varying(255), + ip_address character varying(255), + resource_path character varying(2550), + representation text, + error character varying(255), + resource_type character varying(64) +); + + +ALTER TABLE public.admin_event_entity OWNER TO keycloak; + +-- +-- Name: associated_policy; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.associated_policy ( + policy_id character varying(36) NOT NULL, + associated_policy_id character varying(36) NOT NULL +); + + +ALTER TABLE public.associated_policy OWNER TO keycloak; + +-- +-- Name: authentication_execution; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.authentication_execution ( + id character varying(36) NOT NULL, + alias character varying(255), + authenticator character varying(36), + realm_id character varying(36), + flow_id character varying(36), + requirement integer, + priority integer, + authenticator_flow boolean DEFAULT false NOT NULL, + auth_flow_id character varying(36), + auth_config character varying(36) +); + + +ALTER TABLE public.authentication_execution OWNER TO keycloak; + +-- +-- Name: authentication_flow; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.authentication_flow ( + id character varying(36) NOT NULL, + alias character varying(255), + description character varying(255), + realm_id character varying(36), + provider_id character varying(36) DEFAULT 'basic-flow'::character varying NOT NULL, + top_level boolean DEFAULT false NOT NULL, + built_in boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.authentication_flow OWNER TO keycloak; + +-- +-- Name: authenticator_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.authenticator_config ( + id character varying(36) NOT NULL, + alias character varying(255), + realm_id character varying(36) +); + + +ALTER TABLE public.authenticator_config OWNER TO keycloak; + +-- +-- Name: authenticator_config_entry; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.authenticator_config_entry ( + authenticator_id character varying(36) NOT NULL, + value text, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.authenticator_config_entry OWNER TO keycloak; + +-- +-- Name: broker_link; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.broker_link ( + identity_provider character varying(255) NOT NULL, + storage_provider_id character varying(255), + realm_id character varying(36) NOT NULL, + broker_user_id character varying(255), + broker_username character varying(255), + token text, + user_id character varying(255) NOT NULL +); + + +ALTER TABLE public.broker_link OWNER TO keycloak; + +-- +-- Name: client; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client ( + id character varying(36) NOT NULL, + enabled boolean DEFAULT false NOT NULL, + full_scope_allowed boolean DEFAULT false NOT NULL, + client_id character varying(255), + not_before integer, + public_client boolean DEFAULT false NOT NULL, + secret character varying(255), + base_url character varying(255), + bearer_only boolean DEFAULT false NOT NULL, + management_url character varying(255), + surrogate_auth_required boolean DEFAULT false NOT NULL, + realm_id character varying(36), + protocol character varying(255), + node_rereg_timeout integer DEFAULT 0, + frontchannel_logout boolean DEFAULT false NOT NULL, + consent_required boolean DEFAULT false NOT NULL, + name character varying(255), + service_accounts_enabled boolean DEFAULT false NOT NULL, + client_authenticator_type character varying(255), + root_url character varying(255), + description character varying(255), + registration_token character varying(255), + standard_flow_enabled boolean DEFAULT true NOT NULL, + implicit_flow_enabled boolean DEFAULT false NOT NULL, + direct_access_grants_enabled boolean DEFAULT false NOT NULL, + always_display_in_console boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.client OWNER TO keycloak; + +-- +-- Name: client_attributes; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_attributes ( + client_id character varying(36) NOT NULL, + value character varying(4000), + name character varying(255) NOT NULL +); + + +ALTER TABLE public.client_attributes OWNER TO keycloak; + +-- +-- Name: client_auth_flow_bindings; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_auth_flow_bindings ( + client_id character varying(36) NOT NULL, + flow_id character varying(36), + binding_name character varying(255) NOT NULL +); + + +ALTER TABLE public.client_auth_flow_bindings OWNER TO keycloak; + +-- +-- Name: client_default_roles; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_default_roles ( + client_id character varying(36) NOT NULL, + role_id character varying(36) NOT NULL +); + + +ALTER TABLE public.client_default_roles OWNER TO keycloak; + +-- +-- Name: client_initial_access; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_initial_access ( + id character varying(36) NOT NULL, + realm_id character varying(36) NOT NULL, + "timestamp" integer, + expiration integer, + count integer, + remaining_count integer +); + + +ALTER TABLE public.client_initial_access OWNER TO keycloak; + +-- +-- Name: client_node_registrations; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_node_registrations ( + client_id character varying(36) NOT NULL, + value integer, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.client_node_registrations OWNER TO keycloak; + +-- +-- Name: client_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_scope ( + id character varying(36) NOT NULL, + name character varying(255), + realm_id character varying(36), + description character varying(255), + protocol character varying(255) +); + + +ALTER TABLE public.client_scope OWNER TO keycloak; + +-- +-- Name: client_scope_attributes; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_scope_attributes ( + scope_id character varying(36) NOT NULL, + value character varying(2048), + name character varying(255) NOT NULL +); + + +ALTER TABLE public.client_scope_attributes OWNER TO keycloak; + +-- +-- Name: client_scope_client; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_scope_client ( + client_id character varying(36) NOT NULL, + scope_id character varying(36) NOT NULL, + default_scope boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.client_scope_client OWNER TO keycloak; + +-- +-- Name: client_scope_role_mapping; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_scope_role_mapping ( + scope_id character varying(36) NOT NULL, + role_id character varying(36) NOT NULL +); + + +ALTER TABLE public.client_scope_role_mapping OWNER TO keycloak; + +-- +-- Name: client_session; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_session ( + id character varying(36) NOT NULL, + client_id character varying(36), + redirect_uri character varying(255), + state character varying(255), + "timestamp" integer, + session_id character varying(36), + auth_method character varying(255), + realm_id character varying(255), + auth_user_id character varying(36), + current_action character varying(36) +); + + +ALTER TABLE public.client_session OWNER TO keycloak; + +-- +-- Name: client_session_auth_status; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_session_auth_status ( + authenticator character varying(36) NOT NULL, + status integer, + client_session character varying(36) NOT NULL +); + + +ALTER TABLE public.client_session_auth_status OWNER TO keycloak; + +-- +-- Name: client_session_note; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_session_note ( + name character varying(255) NOT NULL, + value character varying(255), + client_session character varying(36) NOT NULL +); + + +ALTER TABLE public.client_session_note OWNER TO keycloak; + +-- +-- Name: client_session_prot_mapper; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_session_prot_mapper ( + protocol_mapper_id character varying(36) NOT NULL, + client_session character varying(36) NOT NULL +); + + +ALTER TABLE public.client_session_prot_mapper OWNER TO keycloak; + +-- +-- Name: client_session_role; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_session_role ( + role_id character varying(255) NOT NULL, + client_session character varying(36) NOT NULL +); + + +ALTER TABLE public.client_session_role OWNER TO keycloak; + +-- +-- Name: client_user_session_note; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.client_user_session_note ( + name character varying(255) NOT NULL, + value character varying(2048), + client_session character varying(36) NOT NULL +); + + +ALTER TABLE public.client_user_session_note OWNER TO keycloak; + +-- +-- Name: component; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.component ( + id character varying(36) NOT NULL, + name character varying(255), + parent_id character varying(36), + provider_id character varying(36), + provider_type character varying(255), + realm_id character varying(36), + sub_type character varying(255) +); + + +ALTER TABLE public.component OWNER TO keycloak; + +-- +-- Name: component_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.component_config ( + id character varying(36) NOT NULL, + component_id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + value character varying(4000) +); + + +ALTER TABLE public.component_config OWNER TO keycloak; + +-- +-- Name: composite_role; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.composite_role ( + composite character varying(36) NOT NULL, + child_role character varying(36) NOT NULL +); + + +ALTER TABLE public.composite_role OWNER TO keycloak; + +-- +-- Name: credential; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.credential ( + id character varying(36) NOT NULL, + salt bytea, + type character varying(255), + user_id character varying(36), + created_date bigint, + user_label character varying(255), + secret_data text, + credential_data text, + priority integer +); + + +ALTER TABLE public.credential OWNER TO keycloak; + +-- +-- Name: databasechangelog; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.databasechangelog ( + id character varying(255) NOT NULL, + author character varying(255) NOT NULL, + filename character varying(255) NOT NULL, + dateexecuted timestamp without time zone NOT NULL, + orderexecuted integer NOT NULL, + exectype character varying(10) NOT NULL, + md5sum character varying(35), + description character varying(255), + comments character varying(255), + tag character varying(255), + liquibase character varying(20), + contexts character varying(255), + labels character varying(255), + deployment_id character varying(10) +); + + +ALTER TABLE public.databasechangelog OWNER TO keycloak; + +-- +-- Name: databasechangeloglock; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.databasechangeloglock ( + id integer NOT NULL, + locked boolean NOT NULL, + lockgranted timestamp without time zone, + lockedby character varying(255) +); + + +ALTER TABLE public.databasechangeloglock OWNER TO keycloak; + +-- +-- Name: default_client_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.default_client_scope ( + realm_id character varying(36) NOT NULL, + scope_id character varying(36) NOT NULL, + default_scope boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.default_client_scope OWNER TO keycloak; + +-- +-- Name: event_entity; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.event_entity ( + id character varying(36) NOT NULL, + client_id character varying(255), + details_json character varying(2550), + error character varying(255), + ip_address character varying(255), + realm_id character varying(255), + session_id character varying(255), + event_time bigint, + type character varying(255), + user_id character varying(255) +); + + +ALTER TABLE public.event_entity OWNER TO keycloak; + +-- +-- Name: fed_user_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_attribute ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36), + value character varying(2024) +); + + +ALTER TABLE public.fed_user_attribute OWNER TO keycloak; + +-- +-- Name: fed_user_consent; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_consent ( + id character varying(36) NOT NULL, + client_id character varying(255), + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36), + created_date bigint, + last_updated_date bigint, + client_storage_provider character varying(36), + external_client_id character varying(255) +); + + +ALTER TABLE public.fed_user_consent OWNER TO keycloak; + +-- +-- Name: fed_user_consent_cl_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_consent_cl_scope ( + user_consent_id character varying(36) NOT NULL, + scope_id character varying(36) NOT NULL +); + + +ALTER TABLE public.fed_user_consent_cl_scope OWNER TO keycloak; + +-- +-- Name: fed_user_credential; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_credential ( + id character varying(36) NOT NULL, + salt bytea, + type character varying(255), + created_date bigint, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36), + user_label character varying(255), + secret_data text, + credential_data text, + priority integer +); + + +ALTER TABLE public.fed_user_credential OWNER TO keycloak; + +-- +-- Name: fed_user_group_membership; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_group_membership ( + group_id character varying(36) NOT NULL, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36) +); + + +ALTER TABLE public.fed_user_group_membership OWNER TO keycloak; + +-- +-- Name: fed_user_required_action; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_required_action ( + required_action character varying(255) DEFAULT ' '::character varying NOT NULL, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36) +); + + +ALTER TABLE public.fed_user_required_action OWNER TO keycloak; + +-- +-- Name: fed_user_role_mapping; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.fed_user_role_mapping ( + role_id character varying(36) NOT NULL, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + storage_provider_id character varying(36) +); + + +ALTER TABLE public.fed_user_role_mapping OWNER TO keycloak; + +-- +-- Name: federated_identity; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.federated_identity ( + identity_provider character varying(255) NOT NULL, + realm_id character varying(36), + federated_user_id character varying(255), + federated_username character varying(255), + token text, + user_id character varying(36) NOT NULL +); + + +ALTER TABLE public.federated_identity OWNER TO keycloak; + +-- +-- Name: federated_user; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.federated_user ( + id character varying(255) NOT NULL, + storage_provider_id character varying(255), + realm_id character varying(36) NOT NULL +); + + +ALTER TABLE public.federated_user OWNER TO keycloak; + +-- +-- Name: group_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.group_attribute ( + id character varying(36) DEFAULT 'sybase-needs-something-here'::character varying NOT NULL, + name character varying(255) NOT NULL, + value character varying(255), + group_id character varying(36) NOT NULL +); + + +ALTER TABLE public.group_attribute OWNER TO keycloak; + +-- +-- Name: group_role_mapping; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.group_role_mapping ( + role_id character varying(36) NOT NULL, + group_id character varying(36) NOT NULL +); + + +ALTER TABLE public.group_role_mapping OWNER TO keycloak; + +-- +-- Name: identity_provider; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.identity_provider ( + internal_id character varying(36) NOT NULL, + enabled boolean DEFAULT false NOT NULL, + provider_alias character varying(255), + provider_id character varying(255), + store_token boolean DEFAULT false NOT NULL, + authenticate_by_default boolean DEFAULT false NOT NULL, + realm_id character varying(36), + add_token_role boolean DEFAULT true NOT NULL, + trust_email boolean DEFAULT false NOT NULL, + first_broker_login_flow_id character varying(36), + post_broker_login_flow_id character varying(36), + provider_display_name character varying(255), + link_only boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.identity_provider OWNER TO keycloak; + +-- +-- Name: identity_provider_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.identity_provider_config ( + identity_provider_id character varying(36) NOT NULL, + value text, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.identity_provider_config OWNER TO keycloak; + +-- +-- Name: identity_provider_mapper; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.identity_provider_mapper ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + idp_alias character varying(255) NOT NULL, + idp_mapper_name character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL +); + + +ALTER TABLE public.identity_provider_mapper OWNER TO keycloak; + +-- +-- Name: idp_mapper_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.idp_mapper_config ( + idp_mapper_id character varying(36) NOT NULL, + value text, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.idp_mapper_config OWNER TO keycloak; + +-- +-- Name: keycloak_group; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.keycloak_group ( + id character varying(36) NOT NULL, + name character varying(255), + parent_group character varying(36) NOT NULL, + realm_id character varying(36) +); + + +ALTER TABLE public.keycloak_group OWNER TO keycloak; + +-- +-- Name: keycloak_role; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.keycloak_role ( + id character varying(36) NOT NULL, + client_realm_constraint character varying(255), + client_role boolean DEFAULT false NOT NULL, + description character varying(255), + name character varying(255), + realm_id character varying(255), + client character varying(36), + realm character varying(36) +); + + +ALTER TABLE public.keycloak_role OWNER TO keycloak; + +-- +-- Name: migration_model; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.migration_model ( + id character varying(36) NOT NULL, + version character varying(36), + update_time bigint DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.migration_model OWNER TO keycloak; + +-- +-- Name: offline_client_session; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.offline_client_session ( + user_session_id character varying(36) NOT NULL, + client_id character varying(255) NOT NULL, + offline_flag character varying(4) NOT NULL, + "timestamp" integer, + data text, + client_storage_provider character varying(36) DEFAULT 'local'::character varying NOT NULL, + external_client_id character varying(255) DEFAULT 'local'::character varying NOT NULL +); + + +ALTER TABLE public.offline_client_session OWNER TO keycloak; + +-- +-- Name: offline_user_session; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.offline_user_session ( + user_session_id character varying(36) NOT NULL, + user_id character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL, + created_on integer NOT NULL, + offline_flag character varying(4) NOT NULL, + data text, + last_session_refresh integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.offline_user_session OWNER TO keycloak; + +-- +-- Name: policy_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.policy_config ( + policy_id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + value text +); + + +ALTER TABLE public.policy_config OWNER TO keycloak; + +-- +-- Name: protocol_mapper; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.protocol_mapper ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + protocol character varying(255) NOT NULL, + protocol_mapper_name character varying(255) NOT NULL, + client_id character varying(36), + client_scope_id character varying(36) +); + + +ALTER TABLE public.protocol_mapper OWNER TO keycloak; + +-- +-- Name: protocol_mapper_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.protocol_mapper_config ( + protocol_mapper_id character varying(36) NOT NULL, + value text, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.protocol_mapper_config OWNER TO keycloak; + +-- +-- Name: realm; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm ( + id character varying(36) NOT NULL, + access_code_lifespan integer, + user_action_lifespan integer, + access_token_lifespan integer, + account_theme character varying(255), + admin_theme character varying(255), + email_theme character varying(255), + enabled boolean DEFAULT false NOT NULL, + events_enabled boolean DEFAULT false NOT NULL, + events_expiration bigint, + login_theme character varying(255), + name character varying(255), + not_before integer, + password_policy character varying(2550), + registration_allowed boolean DEFAULT false NOT NULL, + remember_me boolean DEFAULT false NOT NULL, + reset_password_allowed boolean DEFAULT false NOT NULL, + social boolean DEFAULT false NOT NULL, + ssl_required character varying(255), + sso_idle_timeout integer, + sso_max_lifespan integer, + update_profile_on_soc_login boolean DEFAULT false NOT NULL, + verify_email boolean DEFAULT false NOT NULL, + master_admin_client character varying(36), + login_lifespan integer, + internationalization_enabled boolean DEFAULT false NOT NULL, + default_locale character varying(255), + reg_email_as_username boolean DEFAULT false NOT NULL, + admin_events_enabled boolean DEFAULT false NOT NULL, + admin_events_details_enabled boolean DEFAULT false NOT NULL, + edit_username_allowed boolean DEFAULT false NOT NULL, + otp_policy_counter integer DEFAULT 0, + otp_policy_window integer DEFAULT 1, + otp_policy_period integer DEFAULT 30, + otp_policy_digits integer DEFAULT 6, + otp_policy_alg character varying(36) DEFAULT 'HmacSHA1'::character varying, + otp_policy_type character varying(36) DEFAULT 'totp'::character varying, + browser_flow character varying(36), + registration_flow character varying(36), + direct_grant_flow character varying(36), + reset_credentials_flow character varying(36), + client_auth_flow character varying(36), + offline_session_idle_timeout integer DEFAULT 0, + revoke_refresh_token boolean DEFAULT false NOT NULL, + access_token_life_implicit integer DEFAULT 0, + login_with_email_allowed boolean DEFAULT true NOT NULL, + duplicate_emails_allowed boolean DEFAULT false NOT NULL, + docker_auth_flow character varying(36), + refresh_token_max_reuse integer DEFAULT 0, + allow_user_managed_access boolean DEFAULT false NOT NULL, + sso_max_lifespan_remember_me integer DEFAULT 0 NOT NULL, + sso_idle_timeout_remember_me integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.realm OWNER TO keycloak; + +-- +-- Name: realm_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_attribute ( + name character varying(255) NOT NULL, + value character varying(255), + realm_id character varying(36) NOT NULL +); + + +ALTER TABLE public.realm_attribute OWNER TO keycloak; + +-- +-- Name: realm_default_groups; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_default_groups ( + realm_id character varying(36) NOT NULL, + group_id character varying(36) NOT NULL +); + + +ALTER TABLE public.realm_default_groups OWNER TO keycloak; + +-- +-- Name: realm_default_roles; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_default_roles ( + realm_id character varying(36) NOT NULL, + role_id character varying(36) NOT NULL +); + + +ALTER TABLE public.realm_default_roles OWNER TO keycloak; + +-- +-- Name: realm_enabled_event_types; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_enabled_event_types ( + realm_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.realm_enabled_event_types OWNER TO keycloak; + +-- +-- Name: realm_events_listeners; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_events_listeners ( + realm_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.realm_events_listeners OWNER TO keycloak; + +-- +-- Name: realm_localizations; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_localizations ( + realm_id character varying(255) NOT NULL, + locale character varying(255) NOT NULL, + texts text NOT NULL +); + + +ALTER TABLE public.realm_localizations OWNER TO keycloak; + +-- +-- Name: realm_required_credential; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_required_credential ( + type character varying(255) NOT NULL, + form_label character varying(255), + input boolean DEFAULT false NOT NULL, + secret boolean DEFAULT false NOT NULL, + realm_id character varying(36) NOT NULL +); + + +ALTER TABLE public.realm_required_credential OWNER TO keycloak; + +-- +-- Name: realm_smtp_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_smtp_config ( + realm_id character varying(36) NOT NULL, + value character varying(255), + name character varying(255) NOT NULL +); + + +ALTER TABLE public.realm_smtp_config OWNER TO keycloak; + +-- +-- Name: realm_supported_locales; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.realm_supported_locales ( + realm_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.realm_supported_locales OWNER TO keycloak; + +-- +-- Name: redirect_uris; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.redirect_uris ( + client_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.redirect_uris OWNER TO keycloak; + +-- +-- Name: required_action_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.required_action_config ( + required_action_id character varying(36) NOT NULL, + value text, + name character varying(255) NOT NULL +); + + +ALTER TABLE public.required_action_config OWNER TO keycloak; + +-- +-- Name: required_action_provider; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.required_action_provider ( + id character varying(36) NOT NULL, + alias character varying(255), + name character varying(255), + realm_id character varying(36), + enabled boolean DEFAULT false NOT NULL, + default_action boolean DEFAULT false NOT NULL, + provider_id character varying(255), + priority integer +); + + +ALTER TABLE public.required_action_provider OWNER TO keycloak; + +-- +-- Name: resource_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_attribute ( + id character varying(36) DEFAULT 'sybase-needs-something-here'::character varying NOT NULL, + name character varying(255) NOT NULL, + value character varying(255), + resource_id character varying(36) NOT NULL +); + + +ALTER TABLE public.resource_attribute OWNER TO keycloak; + +-- +-- Name: resource_policy; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_policy ( + resource_id character varying(36) NOT NULL, + policy_id character varying(36) NOT NULL +); + + +ALTER TABLE public.resource_policy OWNER TO keycloak; + +-- +-- Name: resource_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_scope ( + resource_id character varying(36) NOT NULL, + scope_id character varying(36) NOT NULL +); + + +ALTER TABLE public.resource_scope OWNER TO keycloak; + +-- +-- Name: resource_server; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_server ( + id character varying(36) NOT NULL, + allow_rs_remote_mgmt boolean DEFAULT false NOT NULL, + policy_enforce_mode character varying(15) NOT NULL, + decision_strategy smallint DEFAULT 1 NOT NULL +); + + +ALTER TABLE public.resource_server OWNER TO keycloak; + +-- +-- Name: resource_server_perm_ticket; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_server_perm_ticket ( + id character varying(36) NOT NULL, + owner character varying(255) NOT NULL, + requester character varying(255) NOT NULL, + created_timestamp bigint NOT NULL, + granted_timestamp bigint, + resource_id character varying(36) NOT NULL, + scope_id character varying(36), + resource_server_id character varying(36) NOT NULL, + policy_id character varying(36) +); + + +ALTER TABLE public.resource_server_perm_ticket OWNER TO keycloak; + +-- +-- Name: resource_server_policy; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_server_policy ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + description character varying(255), + type character varying(255) NOT NULL, + decision_strategy character varying(20), + logic character varying(20), + resource_server_id character varying(36) NOT NULL, + owner character varying(255) +); + + +ALTER TABLE public.resource_server_policy OWNER TO keycloak; + +-- +-- Name: resource_server_resource; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_server_resource ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + type character varying(255), + icon_uri character varying(255), + owner character varying(255) NOT NULL, + resource_server_id character varying(36) NOT NULL, + owner_managed_access boolean DEFAULT false NOT NULL, + display_name character varying(255) +); + + +ALTER TABLE public.resource_server_resource OWNER TO keycloak; + +-- +-- Name: resource_server_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_server_scope ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + icon_uri character varying(255), + resource_server_id character varying(36) NOT NULL, + display_name character varying(255) +); + + +ALTER TABLE public.resource_server_scope OWNER TO keycloak; + +-- +-- Name: resource_uris; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.resource_uris ( + resource_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.resource_uris OWNER TO keycloak; + +-- +-- Name: role_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.role_attribute ( + id character varying(36) NOT NULL, + role_id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + value character varying(255) +); + + +ALTER TABLE public.role_attribute OWNER TO keycloak; + +-- +-- Name: scope_mapping; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.scope_mapping ( + client_id character varying(36) NOT NULL, + role_id character varying(36) NOT NULL +); + + +ALTER TABLE public.scope_mapping OWNER TO keycloak; + +-- +-- Name: scope_policy; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.scope_policy ( + scope_id character varying(36) NOT NULL, + policy_id character varying(36) NOT NULL +); + + +ALTER TABLE public.scope_policy OWNER TO keycloak; + +-- +-- Name: user_attribute; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_attribute ( + name character varying(255) NOT NULL, + value character varying(255), + user_id character varying(36) NOT NULL, + id character varying(36) DEFAULT 'sybase-needs-something-here'::character varying NOT NULL +); + + +ALTER TABLE public.user_attribute OWNER TO keycloak; + +-- +-- Name: user_consent; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_consent ( + id character varying(36) NOT NULL, + client_id character varying(255), + user_id character varying(36) NOT NULL, + created_date bigint, + last_updated_date bigint, + client_storage_provider character varying(36), + external_client_id character varying(255) +); + + +ALTER TABLE public.user_consent OWNER TO keycloak; + +-- +-- Name: user_consent_client_scope; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_consent_client_scope ( + user_consent_id character varying(36) NOT NULL, + scope_id character varying(36) NOT NULL +); + + +ALTER TABLE public.user_consent_client_scope OWNER TO keycloak; + +-- +-- Name: user_entity; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_entity ( + id character varying(36) NOT NULL, + email character varying(255), + email_constraint character varying(255), + email_verified boolean DEFAULT false NOT NULL, + enabled boolean DEFAULT false NOT NULL, + federation_link character varying(255), + first_name character varying(255), + last_name character varying(255), + realm_id character varying(255), + username character varying(255), + created_timestamp bigint, + service_account_client_link character varying(255), + not_before integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.user_entity OWNER TO keycloak; + +-- +-- Name: user_federation_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_federation_config ( + user_federation_provider_id character varying(36) NOT NULL, + value character varying(255), + name character varying(255) NOT NULL +); + + +ALTER TABLE public.user_federation_config OWNER TO keycloak; + +-- +-- Name: user_federation_mapper; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_federation_mapper ( + id character varying(36) NOT NULL, + name character varying(255) NOT NULL, + federation_provider_id character varying(36) NOT NULL, + federation_mapper_type character varying(255) NOT NULL, + realm_id character varying(36) NOT NULL +); + + +ALTER TABLE public.user_federation_mapper OWNER TO keycloak; + +-- +-- Name: user_federation_mapper_config; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_federation_mapper_config ( + user_federation_mapper_id character varying(36) NOT NULL, + value character varying(255), + name character varying(255) NOT NULL +); + + +ALTER TABLE public.user_federation_mapper_config OWNER TO keycloak; + +-- +-- Name: user_federation_provider; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_federation_provider ( + id character varying(36) NOT NULL, + changed_sync_period integer, + display_name character varying(255), + full_sync_period integer, + last_sync integer, + priority integer, + provider_name character varying(255), + realm_id character varying(36) +); + + +ALTER TABLE public.user_federation_provider OWNER TO keycloak; + +-- +-- Name: user_group_membership; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_group_membership ( + group_id character varying(36) NOT NULL, + user_id character varying(36) NOT NULL +); + + +ALTER TABLE public.user_group_membership OWNER TO keycloak; + +-- +-- Name: user_required_action; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_required_action ( + user_id character varying(36) NOT NULL, + required_action character varying(255) DEFAULT ' '::character varying NOT NULL +); + + +ALTER TABLE public.user_required_action OWNER TO keycloak; + +-- +-- Name: user_role_mapping; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_role_mapping ( + role_id character varying(255) NOT NULL, + user_id character varying(36) NOT NULL +); + + +ALTER TABLE public.user_role_mapping OWNER TO keycloak; + +-- +-- Name: user_session; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_session ( + id character varying(36) NOT NULL, + auth_method character varying(255), + ip_address character varying(255), + last_session_refresh integer, + login_username character varying(255), + realm_id character varying(255), + remember_me boolean DEFAULT false NOT NULL, + started integer, + user_id character varying(255), + user_session_state integer, + broker_session_id character varying(255), + broker_user_id character varying(255) +); + + +ALTER TABLE public.user_session OWNER TO keycloak; + +-- +-- Name: user_session_note; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.user_session_note ( + user_session character varying(36) NOT NULL, + name character varying(255) NOT NULL, + value character varying(2048) +); + + +ALTER TABLE public.user_session_note OWNER TO keycloak; + +-- +-- Name: username_login_failure; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.username_login_failure ( + realm_id character varying(36) NOT NULL, + username character varying(255) NOT NULL, + failed_login_not_before integer, + last_failure bigint, + last_ip_failure character varying(255), + num_failures integer +); + + +ALTER TABLE public.username_login_failure OWNER TO keycloak; + +-- +-- Name: web_origins; Type: TABLE; Schema: public; Owner: keycloak +-- + +CREATE TABLE public.web_origins ( + client_id character varying(36) NOT NULL, + value character varying(255) NOT NULL +); + + +ALTER TABLE public.web_origins OWNER TO keycloak; + +-- +-- Data for Name: admin_event_entity; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.admin_event_entity (id, admin_event_time, realm_id, operation_type, auth_realm_id, auth_client_id, auth_user_id, ip_address, resource_path, representation, error, resource_type) FROM stdin; +\. + + +-- +-- Data for Name: associated_policy; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.associated_policy (policy_id, associated_policy_id) FROM stdin; +\. + + +-- +-- Data for Name: authentication_execution; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.authentication_execution (id, alias, authenticator, realm_id, flow_id, requirement, priority, authenticator_flow, auth_flow_id, auth_config) FROM stdin; +a3eef0c8-a14f-4d33-b4ee-d9eba1e14350 \N auth-cookie master ef998ef5-ca12-45db-a252-2e71b1419039 2 10 f \N \N +c4489997-ee7b-4649-845e-70b79d3cd49f \N auth-spnego master ef998ef5-ca12-45db-a252-2e71b1419039 3 20 f \N \N +6ae8f57d-d882-4e46-ad47-1e634302f979 \N identity-provider-redirector master ef998ef5-ca12-45db-a252-2e71b1419039 2 25 f \N \N +8478e3a6-1659-47a9-b7eb-503148adec2d \N \N master ef998ef5-ca12-45db-a252-2e71b1419039 2 30 t 4e407b0a-c011-4aef-bcf5-e8c5e649493e \N +da80fc4b-ebb0-4ef8-8594-8ba7a03506b9 \N auth-username-password-form master 4e407b0a-c011-4aef-bcf5-e8c5e649493e 0 10 f \N \N +c54d81b7-e944-4b9d-9657-b01ee5bff6a4 \N \N master 4e407b0a-c011-4aef-bcf5-e8c5e649493e 1 20 t 8561a6a9-da18-4977-a92d-2c85763d042a \N +e57dc48f-3217-4401-a8dc-4d386396525a \N conditional-user-configured master 8561a6a9-da18-4977-a92d-2c85763d042a 0 10 f \N \N +69a60f10-cc10-4604-890a-59fe4eb255b7 \N auth-otp-form master 8561a6a9-da18-4977-a92d-2c85763d042a 0 20 f \N \N +96dbd0ee-fcc4-4e27-85ac-a89bee432892 \N direct-grant-validate-username master 5f6f801e-0588-4a6e-860a-35483f5c1ec7 0 10 f \N \N +bfbcc1e9-f129-4336-a0cd-b6960a811bd9 \N direct-grant-validate-password master 5f6f801e-0588-4a6e-860a-35483f5c1ec7 0 20 f \N \N +079621e7-6daf-4df0-b3d3-97a3b53cdec1 \N \N master 5f6f801e-0588-4a6e-860a-35483f5c1ec7 1 30 t 99865746-4232-46f0-84b5-20952fe9eb51 \N +511afd6e-e447-4877-8fe9-c54c938e70a6 \N conditional-user-configured master 99865746-4232-46f0-84b5-20952fe9eb51 0 10 f \N \N +5a2470c4-3136-4cf0-8383-e74a413ccd48 \N direct-grant-validate-otp master 99865746-4232-46f0-84b5-20952fe9eb51 0 20 f \N \N +47c96943-ad68-4d93-afff-ff54fc86eb0b \N registration-page-form master 1695e7d2-ad80-4502-8479-8121a6e2a2f0 0 10 t 8fb96669-d28d-4173-a8f4-dc24d41c7d27 \N +a6678624-1bd4-4793-bfac-68551cf0ac7c \N registration-user-creation master 8fb96669-d28d-4173-a8f4-dc24d41c7d27 0 20 f \N \N +5b45827d-5dfd-4152-a99e-373cb975ef87 \N registration-profile-action master 8fb96669-d28d-4173-a8f4-dc24d41c7d27 0 40 f \N \N +5a2fb70d-63ae-4604-b37c-ae043d6a900d \N registration-password-action master 8fb96669-d28d-4173-a8f4-dc24d41c7d27 0 50 f \N \N +0b06a30e-daa7-498a-9fdb-899abbf36450 \N registration-recaptcha-action master 8fb96669-d28d-4173-a8f4-dc24d41c7d27 3 60 f \N \N +b6ab0b5d-8184-4609-bb81-da8413dfb858 \N reset-credentials-choose-user master 954b046d-2b24-405e-84ee-c44ffe603df2 0 10 f \N \N +300fb529-aee1-416d-803c-ca24e01af5a0 \N reset-credential-email master 954b046d-2b24-405e-84ee-c44ffe603df2 0 20 f \N \N +de127e3d-11fa-4ddb-bd33-86ed8006be63 \N reset-password master 954b046d-2b24-405e-84ee-c44ffe603df2 0 30 f \N \N +9d819c31-4238-4b6f-9318-95e1826d4a4c \N \N master 954b046d-2b24-405e-84ee-c44ffe603df2 1 40 t b379b44c-beef-4065-882c-d04cf6d4ffc8 \N +65875994-0342-452e-b7ad-547a9092e302 \N conditional-user-configured master b379b44c-beef-4065-882c-d04cf6d4ffc8 0 10 f \N \N +0f202e0b-da55-43a5-95a6-a38cfeb97529 \N reset-otp master b379b44c-beef-4065-882c-d04cf6d4ffc8 0 20 f \N \N +988c39a0-4c44-4090-bdfd-4b5a8060e822 \N client-secret master 023dc515-c259-42bb-88a8-2e8d84abca92 2 10 f \N \N +cd4e6875-71cf-436c-bf3f-5f5ac4402627 \N client-jwt master 023dc515-c259-42bb-88a8-2e8d84abca92 2 20 f \N \N +fac88bab-1ea5-4617-bbda-d187cba68a45 \N client-secret-jwt master 023dc515-c259-42bb-88a8-2e8d84abca92 2 30 f \N \N +d19571b1-5eeb-44d4-8866-273da4b34850 \N client-x509 master 023dc515-c259-42bb-88a8-2e8d84abca92 2 40 f \N \N +4198a01d-d3cd-49b2-8e8a-f506f2c46fc1 \N idp-review-profile master 242efff2-c3b7-42c0-a48a-77bb1b54502a 0 10 f \N 8ab33625-af83-4fcd-aa77-6bd365100d7b +d9b78c97-27b0-4eef-8d54-6143ca48cffd \N \N master 242efff2-c3b7-42c0-a48a-77bb1b54502a 0 20 t d46ab605-5f1e-4649-88bf-6c2dc79d636d \N +2a3faef7-dc89-4cf2-b299-d754a15af259 \N idp-create-user-if-unique master d46ab605-5f1e-4649-88bf-6c2dc79d636d 2 10 f \N c7d1ba52-6053-4219-8118-a64cebfab1e1 +12c7ff48-2eec-4091-920f-6a1ad3d2d3ad \N \N master d46ab605-5f1e-4649-88bf-6c2dc79d636d 2 20 t c39b0bc2-aba3-414d-ad3e-b648708e24d1 \N +015a4d49-de8a-4cb0-b5c5-1868c30085d3 \N idp-confirm-link master c39b0bc2-aba3-414d-ad3e-b648708e24d1 0 10 f \N \N +0bfffcda-f282-4370-b55f-1b44519be4da \N \N master c39b0bc2-aba3-414d-ad3e-b648708e24d1 0 20 t a7ca6b5a-fa8a-4f4a-bafa-ae178db785a3 \N +8e0d10d1-47ff-4998-a26b-882ed2b71ab4 \N idp-email-verification master a7ca6b5a-fa8a-4f4a-bafa-ae178db785a3 2 10 f \N \N +6a396600-1b18-472c-b755-ad36857abf68 \N \N master a7ca6b5a-fa8a-4f4a-bafa-ae178db785a3 2 20 t ca3a3600-552c-4849-9a9d-826c8aa3e646 \N +d600bb67-e258-44be-8f69-f1bae9c35a0f \N idp-username-password-form master ca3a3600-552c-4849-9a9d-826c8aa3e646 0 10 f \N \N +638e46e8-cf88-4dfa-911d-5659854dd390 \N \N master ca3a3600-552c-4849-9a9d-826c8aa3e646 1 20 t a7d23655-efbb-4950-8ab6-50dbc85681a0 \N +61fc6720-91ff-4ba3-880b-9d0a22deb7dc \N conditional-user-configured master a7d23655-efbb-4950-8ab6-50dbc85681a0 0 10 f \N \N +90cc39a9-cddb-49bd-b9f5-d64d03341333 \N auth-otp-form master a7d23655-efbb-4950-8ab6-50dbc85681a0 0 20 f \N \N +b0634301-594e-42db-9736-6c90ebbeb8b2 \N http-basic-authenticator master 57c56583-d91c-4399-bd15-05a1a17d48c1 0 10 f \N \N +34fa4d44-716b-4b2a-b98e-aa9748154292 \N docker-http-basic-authenticator master 032b05cf-0007-44da-a370-b42039f6b762 0 10 f \N \N +4838277a-46ea-4d95-bd86-d8dc6fdce352 \N no-cookie-redirect master 1c7af06b-3085-46c3-849c-34c67f581b9e 0 10 f \N \N +59a349ee-20ce-42d8-b20b-8f902c09742d \N \N master 1c7af06b-3085-46c3-849c-34c67f581b9e 0 20 t 85c00992-77dd-4262-8744-a9dd8521e98e \N +d9b5fa46-6595-4406-9841-2c0720dbf644 \N basic-auth master 85c00992-77dd-4262-8744-a9dd8521e98e 0 10 f \N \N +3a4ee6f1-1528-47c7-aeda-f317248b3b93 \N basic-auth-otp master 85c00992-77dd-4262-8744-a9dd8521e98e 3 20 f \N \N +014847fc-06df-4ddf-a8f2-deeb0f1eb59a \N auth-spnego master 85c00992-77dd-4262-8744-a9dd8521e98e 3 30 f \N \N +b46bc4f6-2fe5-44d5-b47f-36880742cf50 \N auth-cookie grafana a38aeb47-f27e-4e68-82ff-7cc7371a47a7 2 10 f \N \N +6cec48cc-066a-4e3e-8158-85351bfa4c27 \N auth-spnego grafana a38aeb47-f27e-4e68-82ff-7cc7371a47a7 3 20 f \N \N +63c55c5a-ad11-4f83-9d6e-d8ca2efcaf66 \N identity-provider-redirector grafana a38aeb47-f27e-4e68-82ff-7cc7371a47a7 2 25 f \N \N +9a986c59-e016-45e2-8eb6-77ccdd0fd0f5 \N \N grafana a38aeb47-f27e-4e68-82ff-7cc7371a47a7 2 30 t c53e357f-e276-43aa-b36c-46366a7ffd35 \N +85672b45-ebc9-40e8-a579-fbf5c4e2de9f \N auth-username-password-form grafana c53e357f-e276-43aa-b36c-46366a7ffd35 0 10 f \N \N +09025e52-b379-4457-8ab4-74a2426a7139 \N \N grafana c53e357f-e276-43aa-b36c-46366a7ffd35 1 20 t cf4831e9-3e1d-452e-984e-e6d4d9eeafb5 \N +64d5c6d6-1dde-4c42-b502-1abdf939e55b \N conditional-user-configured grafana cf4831e9-3e1d-452e-984e-e6d4d9eeafb5 0 10 f \N \N +4b782423-ec3d-4e88-8fcf-fa12b4a34fc3 \N auth-otp-form grafana cf4831e9-3e1d-452e-984e-e6d4d9eeafb5 0 20 f \N \N +07052e96-64b2-41b5-95fc-e3ac6abcc577 \N direct-grant-validate-username grafana b478ecfb-db7e-4797-a245-8fc3b4dec884 0 10 f \N \N +10c22bdd-d243-44be-810f-d2fedbb973e1 \N direct-grant-validate-password grafana b478ecfb-db7e-4797-a245-8fc3b4dec884 0 20 f \N \N +6a6273e9-146c-4b4e-b7ce-42ed72cbc03f \N conditional-user-configured grafana b3491338-0630-4232-97e7-a518c254b248 0 10 f \N \N +d87abeef-9f1d-46f5-9f36-acd7eaf21a72 \N direct-grant-validate-otp grafana b3491338-0630-4232-97e7-a518c254b248 0 20 f \N \N +4f204bab-0311-44b4-80b6-37d23fd0fd5a \N registration-page-form grafana 9d02badd-cb1c-4655-bf5e-f888861433ff 0 10 t c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 \N +2d4ee446-623c-42a0-8d4a-9f6c4f7f28ec \N registration-user-creation grafana c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 0 20 f \N \N +d806effc-dd17-4468-9a98-4e1c2f9e799d \N registration-profile-action grafana c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 0 40 f \N \N +306fa749-c191-43c6-bf04-0eb6d3d02732 \N registration-password-action grafana c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 0 50 f \N \N +7de9bbee-eb3d-4f3e-a134-e7e8d4a6df25 \N registration-recaptcha-action grafana c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 3 60 f \N \N +8a31d18e-1622-4eac-8eff-9434fa9cade3 \N reset-credentials-choose-user grafana 3085fb68-fc1f-4e1c-a8be-33fb45194b04 0 10 f \N \N +6006359c-b678-4526-90de-3dcfb3200868 \N reset-credential-email grafana 3085fb68-fc1f-4e1c-a8be-33fb45194b04 0 20 f \N \N +e74add33-2692-4c20-8605-cc98c1901b98 \N reset-password grafana 3085fb68-fc1f-4e1c-a8be-33fb45194b04 0 30 f \N \N +52942b96-3bf2-47e7-9863-a917f6df716c \N \N grafana 3085fb68-fc1f-4e1c-a8be-33fb45194b04 1 40 t 079166ff-6d61-4bb2-a26d-374b8558f628 \N +bc646947-4121-4d38-96b1-a8ed4b534cc7 \N conditional-user-configured grafana 079166ff-6d61-4bb2-a26d-374b8558f628 0 10 f \N \N +efd9bb77-2d97-4ec0-9653-cd8fef30b307 \N reset-otp grafana 079166ff-6d61-4bb2-a26d-374b8558f628 0 20 f \N \N +0bdc0916-5d84-4ac8-8cc9-0235cfb18262 \N client-secret grafana cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2 10 f \N \N +919f02c4-745b-43f6-a50f-5bdc9792f017 \N client-jwt grafana cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2 20 f \N \N +84e716ef-c5b6-4c8e-b4ca-acaa35b6e2a0 \N client-secret-jwt grafana cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2 30 f \N \N +1da09fa3-1b81-4e97-bbd5-e5b5baccb73b \N client-x509 grafana cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2 40 f \N \N +f707b2f6-05b1-4cc8-8eaf-ed7006975583 \N idp-review-profile grafana 0af1201c-a206-4393-9528-cb6083b9caa0 0 10 f \N e159a12b-cd0b-4241-a41c-e13f84e92052 +9762f956-a74f-40c7-b0a3-52a699892652 \N \N grafana 0af1201c-a206-4393-9528-cb6083b9caa0 0 20 t df86516c-dcb1-41a8-877e-eb8805bcac8c \N +186e5f38-1a9f-4fa2-bc61-749118e4f76b \N idp-create-user-if-unique grafana df86516c-dcb1-41a8-877e-eb8805bcac8c 2 10 f \N f159d9c3-3ea7-460d-a719-b9c88bcbf650 +1b08ecd3-fc9f-4f17-bfa1-5bf0cc482d47 \N \N grafana df86516c-dcb1-41a8-877e-eb8805bcac8c 2 20 t 9947a1b3-c26c-423f-b380-deadb5dce1ad \N +3fa35527-e92c-4159-a80d-98412291f023 \N idp-confirm-link grafana 9947a1b3-c26c-423f-b380-deadb5dce1ad 0 10 f \N \N +ce94d52f-caf6-4388-a9e0-ac00870a8c6b \N \N grafana 9947a1b3-c26c-423f-b380-deadb5dce1ad 0 20 t 3c15ac69-f452-49ab-94d6-92e6bf809ebc \N +4a901e36-7af7-406a-a274-ac39a7bc5c8f \N idp-email-verification grafana 3c15ac69-f452-49ab-94d6-92e6bf809ebc 2 10 f \N \N +5ff95eb9-2f72-42ea-92da-7bd11a7bc4f8 \N \N grafana 3c15ac69-f452-49ab-94d6-92e6bf809ebc 2 20 t 1075a862-7836-4d28-a191-8be19a6574cf \N +72dbe3c2-0648-493f-a1e4-4b8fd6fc73ea \N idp-username-password-form grafana 1075a862-7836-4d28-a191-8be19a6574cf 0 10 f \N \N +e42d20ac-5167-4048-8658-3ebe3e7b9a70 \N \N grafana 1075a862-7836-4d28-a191-8be19a6574cf 1 20 t 21fbd70a-286f-431a-abc4-fbf6590fcdc3 \N +b8ce6905-73eb-493b-9ce1-408ec55e3c46 \N conditional-user-configured grafana 21fbd70a-286f-431a-abc4-fbf6590fcdc3 0 10 f \N \N +54d7692d-c0e3-40ef-9ef9-d9e8227d618d \N auth-otp-form grafana 21fbd70a-286f-431a-abc4-fbf6590fcdc3 0 20 f \N \N +3722f24d-6ffb-4b20-a481-1fd8a17afdf6 \N http-basic-authenticator grafana ba53abf5-9a64-4371-810b-67378eb3d781 0 10 f \N \N +a700b05f-a61d-4eeb-ad75-1a3df05ed429 \N docker-http-basic-authenticator grafana 95e02703-f5bc-4e04-8bef-f6adc2d8173f 0 10 f \N \N +035c4f94-03a6-4101-a729-f3c01ee4c490 \N no-cookie-redirect grafana f397495e-d073-4ef1-babf-569a338db596 0 10 f \N \N +29f310db-b302-44b2-9182-4b91648cbabf \N \N grafana f397495e-d073-4ef1-babf-569a338db596 0 20 t 56c40f89-4d69-46fd-bb18-d6c01808d2af \N +4e7d257c-e013-4597-a44d-b186a85606af \N basic-auth grafana 56c40f89-4d69-46fd-bb18-d6c01808d2af 0 10 f \N \N +2ba05817-a59f-4e72-a565-f3b4591390dc \N basic-auth-otp grafana 56c40f89-4d69-46fd-bb18-d6c01808d2af 3 20 f \N \N +5db9c781-6718-4674-a833-9a4ac3e8212e \N auth-spnego grafana 56c40f89-4d69-46fd-bb18-d6c01808d2af 3 30 f \N \N +5f032dbb-bd37-425b-af1e-ba555c7a8245 \N \N grafana b478ecfb-db7e-4797-a245-8fc3b4dec884 1 30 t b3491338-0630-4232-97e7-a518c254b248 \N +\. + + +-- +-- Data for Name: authentication_flow; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.authentication_flow (id, alias, description, realm_id, provider_id, top_level, built_in) FROM stdin; +ef998ef5-ca12-45db-a252-2e71b1419039 browser browser based authentication master basic-flow t t +4e407b0a-c011-4aef-bcf5-e8c5e649493e forms Username, password, otp and other auth forms. master basic-flow f t +8561a6a9-da18-4977-a92d-2c85763d042a Browser - Conditional OTP Flow to determine if the OTP is required for the authentication master basic-flow f t +5f6f801e-0588-4a6e-860a-35483f5c1ec7 direct grant OpenID Connect Resource Owner Grant master basic-flow t t +99865746-4232-46f0-84b5-20952fe9eb51 Direct Grant - Conditional OTP Flow to determine if the OTP is required for the authentication master basic-flow f t +1695e7d2-ad80-4502-8479-8121a6e2a2f0 registration registration flow master basic-flow t t +8fb96669-d28d-4173-a8f4-dc24d41c7d27 registration form registration form master form-flow f t +954b046d-2b24-405e-84ee-c44ffe603df2 reset credentials Reset credentials for a user if they forgot their password or something master basic-flow t t +b379b44c-beef-4065-882c-d04cf6d4ffc8 Reset - Conditional OTP Flow to determine if the OTP should be reset or not. Set to REQUIRED to force. master basic-flow f t +023dc515-c259-42bb-88a8-2e8d84abca92 clients Base authentication for clients master client-flow t t +242efff2-c3b7-42c0-a48a-77bb1b54502a first broker login Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account master basic-flow t t +d46ab605-5f1e-4649-88bf-6c2dc79d636d User creation or linking Flow for the existing/non-existing user alternatives master basic-flow f t +c39b0bc2-aba3-414d-ad3e-b648708e24d1 Handle Existing Account Handle what to do if there is existing account with same email/username like authenticated identity provider master basic-flow f t +a7ca6b5a-fa8a-4f4a-bafa-ae178db785a3 Account verification options Method with which to verity the existing account master basic-flow f t +ca3a3600-552c-4849-9a9d-826c8aa3e646 Verify Existing Account by Re-authentication Reauthentication of existing account master basic-flow f t +a7d23655-efbb-4950-8ab6-50dbc85681a0 First broker login - Conditional OTP Flow to determine if the OTP is required for the authentication master basic-flow f t +57c56583-d91c-4399-bd15-05a1a17d48c1 saml ecp SAML ECP Profile Authentication Flow master basic-flow t t +032b05cf-0007-44da-a370-b42039f6b762 docker auth Used by Docker clients to authenticate against the IDP master basic-flow t t +1c7af06b-3085-46c3-849c-34c67f581b9e http challenge An authentication flow based on challenge-response HTTP Authentication Schemes master basic-flow t t +85c00992-77dd-4262-8744-a9dd8521e98e Authentication Options Authentication options. master basic-flow f t +a38aeb47-f27e-4e68-82ff-7cc7371a47a7 browser browser based authentication grafana basic-flow t t +c53e357f-e276-43aa-b36c-46366a7ffd35 forms Username, password, otp and other auth forms. grafana basic-flow f t +cf4831e9-3e1d-452e-984e-e6d4d9eeafb5 Browser - Conditional OTP Flow to determine if the OTP is required for the authentication grafana basic-flow f t +b478ecfb-db7e-4797-a245-8fc3b4dec884 direct grant OpenID Connect Resource Owner Grant grafana basic-flow t t +b3491338-0630-4232-97e7-a518c254b248 Direct Grant - Conditional OTP Flow to determine if the OTP is required for the authentication grafana basic-flow f t +9d02badd-cb1c-4655-bf5e-f888861433ff registration registration flow grafana basic-flow t t +c3ed2ad1-cfb4-49fa-8c75-cf5047527c68 registration form registration form grafana form-flow f t +3085fb68-fc1f-4e1c-a8be-33fb45194b04 reset credentials Reset credentials for a user if they forgot their password or something grafana basic-flow t t +079166ff-6d61-4bb2-a26d-374b8558f628 Reset - Conditional OTP Flow to determine if the OTP should be reset or not. Set to REQUIRED to force. grafana basic-flow f t +cbb4b3ca-ced6-4046-8b59-f1c3959c7948 clients Base authentication for clients grafana client-flow t t +0af1201c-a206-4393-9528-cb6083b9caa0 first broker login Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account grafana basic-flow t t +df86516c-dcb1-41a8-877e-eb8805bcac8c User creation or linking Flow for the existing/non-existing user alternatives grafana basic-flow f t +9947a1b3-c26c-423f-b380-deadb5dce1ad Handle Existing Account Handle what to do if there is existing account with same email/username like authenticated identity provider grafana basic-flow f t +3c15ac69-f452-49ab-94d6-92e6bf809ebc Account verification options Method with which to verity the existing account grafana basic-flow f t +1075a862-7836-4d28-a191-8be19a6574cf Verify Existing Account by Re-authentication Reauthentication of existing account grafana basic-flow f t +21fbd70a-286f-431a-abc4-fbf6590fcdc3 First broker login - Conditional OTP Flow to determine if the OTP is required for the authentication grafana basic-flow f t +ba53abf5-9a64-4371-810b-67378eb3d781 saml ecp SAML ECP Profile Authentication Flow grafana basic-flow t t +95e02703-f5bc-4e04-8bef-f6adc2d8173f docker auth Used by Docker clients to authenticate against the IDP grafana basic-flow t t +f397495e-d073-4ef1-babf-569a338db596 http challenge An authentication flow based on challenge-response HTTP Authentication Schemes grafana basic-flow t t +56c40f89-4d69-46fd-bb18-d6c01808d2af Authentication Options Authentication options. grafana basic-flow f t +\. + + +-- +-- Data for Name: authenticator_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.authenticator_config (id, alias, realm_id) FROM stdin; +8ab33625-af83-4fcd-aa77-6bd365100d7b review profile config master +c7d1ba52-6053-4219-8118-a64cebfab1e1 create unique user config master +e159a12b-cd0b-4241-a41c-e13f84e92052 review profile config grafana +f159d9c3-3ea7-460d-a719-b9c88bcbf650 create unique user config grafana +\. + + +-- +-- Data for Name: authenticator_config_entry; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.authenticator_config_entry (authenticator_id, value, name) FROM stdin; +8ab33625-af83-4fcd-aa77-6bd365100d7b missing update.profile.on.first.login +c7d1ba52-6053-4219-8118-a64cebfab1e1 false require.password.update.after.registration +e159a12b-cd0b-4241-a41c-e13f84e92052 missing update.profile.on.first.login +f159d9c3-3ea7-460d-a719-b9c88bcbf650 false require.password.update.after.registration +\. + + +-- +-- Data for Name: broker_link; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.broker_link (identity_provider, storage_provider_id, realm_id, broker_user_id, broker_username, token, user_id) FROM stdin; +\. + + +-- +-- Data for Name: client; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client (id, enabled, full_scope_allowed, client_id, not_before, public_client, secret, base_url, bearer_only, management_url, surrogate_auth_required, realm_id, protocol, node_rereg_timeout, frontchannel_logout, consent_required, name, service_accounts_enabled, client_authenticator_type, root_url, description, registration_token, standard_flow_enabled, implicit_flow_enabled, direct_access_grants_enabled, always_display_in_console) FROM stdin; +3cd285ea-0f6e-43b6-ab5c-d021c33a551b t t master-realm 0 f e223073e-1025-4f3a-90d3-e79e3e4e8ffe \N t \N f master \N 0 f f master Realm f client-secret \N \N \N t f f f +eed689c6-49da-4d91-98eb-cd495bcc07a3 t f account 0 f edbe696c-b249-49c5-af33-b7e36f28a259 /realms/master/account/ f \N f master openid-connect 0 f f ${client_account} f client-secret ${authBaseUrl} \N \N t f f f +11c67f5b-dde7-4680-b05b-c9c59d78bda4 t f account-console 0 t 3c802dbd-ab38-4f29-a7cd-799000d7fa6b /realms/master/account/ f \N f master openid-connect 0 f f ${client_account-console} f client-secret ${authBaseUrl} \N \N t f f f +1e30397c-eac2-41fb-87bc-d90484992e65 t f broker 0 f 44f53260-bed3-434f-b44f-bc4a8a546243 \N f \N f master openid-connect 0 f f ${client_broker} f client-secret \N \N \N t f f f +2f521d09-7304-4b5e-a94b-7cc7300b8b50 t f security-admin-console 0 t 0abe5b86-38bd-458c-aee5-c88495207eef /admin/master/console/ f \N f master openid-connect 0 f f ${client_security-admin-console} f client-secret ${authAdminUrl} \N \N t f f f +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 t f admin-cli 0 t 1cf461d4-8b50-45d9-b69a-7703c4d99f54 \N f \N f master openid-connect 0 f f ${client_admin-cli} f client-secret \N \N \N f f t f +ef7f6eac-9fff-44aa-a86c-5125d52acc82 t t grafana-realm 0 f 969c7bb6-18d9-47d9-bd3a-b4440be4afe6 \N t \N f master \N 0 f f grafana Realm f client-secret \N \N \N t f f f +a8698f4f-5fa1-4baa-be05-87d03052af49 t f realm-management 0 f a313dae0-428d-4b35-b5cd-724201173481 \N t \N f grafana openid-connect 0 f f ${client_realm-management} f client-secret \N \N \N t f f f +a5a8fed6-0bca-4646-9946-2fe84175353b t f account 0 f d0b8b6b6-2a02-412c-84d1-716418c4f591 /realms/grafana/account/ f \N f grafana openid-connect 0 f f ${client_account} f client-secret ${authBaseUrl} \N \N t f f f +230081b5-9161-45c3-9e08-9eda5412f7f7 t f account-console 0 t 5cf0655c-c137-438c-9c3c-bea9541f41f1 /realms/grafana/account/ f \N f grafana openid-connect 0 f f ${client_account-console} f client-secret ${authBaseUrl} \N \N t f f f +77ff47f8-f578-477d-8c06-e70a846332f5 t f broker 0 f 589951e9-e77f-4d1d-90cd-796848190eff \N f \N f grafana openid-connect 0 f f ${client_broker} f client-secret \N \N \N t f f f +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 t f security-admin-console 0 t 27d2217e-9934-4971-93b8-77969e47ecf7 /admin/grafana/console/ f \N f grafana openid-connect 0 f f ${client_security-admin-console} f client-secret ${authAdminUrl} \N \N t f f f +6bd2d943-9800-4839-9ddc-03c04930cd9f t f admin-cli 0 t da0811c3-5031-4f35-9dc5-441050461a37 \N f \N f grafana openid-connect 0 f f ${client_admin-cli} f client-secret \N \N \N f f t f +09b79548-8426-4c0e-8e0b-7488467532c7 t t grafana-oauth 0 f d17b9ea9-bcb1-43d2-b132-d339e55872a8 http://127.0.0.1:8087 f http://127.0.0.1:8087 f grafana openid-connect -1 f f \N f client-secret http://127.0.0.1:8087 \N \N t f t f +\. + + +-- +-- Data for Name: client_attributes; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_attributes (client_id, value, name) FROM stdin; +11c67f5b-dde7-4680-b05b-c9c59d78bda4 S256 pkce.code.challenge.method +2f521d09-7304-4b5e-a94b-7cc7300b8b50 S256 pkce.code.challenge.method +230081b5-9161-45c3-9e08-9eda5412f7f7 S256 pkce.code.challenge.method +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 S256 pkce.code.challenge.method +09b79548-8426-4c0e-8e0b-7488467532c7 true backchannel.logout.session.required +09b79548-8426-4c0e-8e0b-7488467532c7 false backchannel.logout.revoke.offline.tokens +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.server.signature +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.server.signature.keyinfo.ext +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.assertion.signature +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.client.signature +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.encrypt +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.authnstatement +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.onetimeuse.condition +09b79548-8426-4c0e-8e0b-7488467532c7 false saml_force_name_id_format +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.multivalued.roles +09b79548-8426-4c0e-8e0b-7488467532c7 false saml.force.post.binding +09b79548-8426-4c0e-8e0b-7488467532c7 false exclude.session.state.from.auth.response +09b79548-8426-4c0e-8e0b-7488467532c7 false tls.client.certificate.bound.access.tokens +09b79548-8426-4c0e-8e0b-7488467532c7 false client_credentials.use_refresh_token +09b79548-8426-4c0e-8e0b-7488467532c7 false display.on.consent.screen +09b79548-8426-4c0e-8e0b-7488467532c7 backchannel.logout.url +\. + + +-- +-- Data for Name: client_auth_flow_bindings; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_auth_flow_bindings (client_id, flow_id, binding_name) FROM stdin; +\. + + +-- +-- Data for Name: client_default_roles; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_default_roles (client_id, role_id) FROM stdin; +eed689c6-49da-4d91-98eb-cd495bcc07a3 86a4b6a9-93db-4177-a72f-95fd937a2c8d +eed689c6-49da-4d91-98eb-cd495bcc07a3 619ba870-921e-4f28-b26c-89b11f39dddf +a5a8fed6-0bca-4646-9946-2fe84175353b f1311ecb-6a6a-49d6-bb16-5132daf93a64 +a5a8fed6-0bca-4646-9946-2fe84175353b 18a7066b-fe71-410e-9581-69f78347ec29 +\. + + +-- +-- Data for Name: client_initial_access; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_initial_access (id, realm_id, "timestamp", expiration, count, remaining_count) FROM stdin; +\. + + +-- +-- Data for Name: client_node_registrations; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_node_registrations (client_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: client_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_scope (id, name, realm_id, description, protocol) FROM stdin; +0cc71c8c-fb37-41f2-b4d8-13210d3cf8be offline_access master OpenID Connect built-in scope: offline_access openid-connect +47f35d4b-35c7-4c6d-8bae-eff0a5046861 role_list master SAML role list saml +66deef47-2158-4d5b-a75f-0bf42f642e7b profile master OpenID Connect built-in scope: profile openid-connect +94ef659c-4c4a-4a33-98e8-bfcf443e9268 email master OpenID Connect built-in scope: email openid-connect +96a960d2-c203-4ef0-a53c-c3edd01f2305 address master OpenID Connect built-in scope: address openid-connect +3f705379-3361-486d-b75a-f7b4e4be492c phone master OpenID Connect built-in scope: phone openid-connect +b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 roles master OpenID Connect scope for add user roles to the access token openid-connect +619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 web-origins master OpenID Connect scope for add allowed web origins to the access token openid-connect +42bfb506-bf0d-424e-8649-53a9a93d252d microprofile-jwt master Microprofile - JWT built-in scope openid-connect +0e98d5f9-d3f7-4b1d-9791-d442524fc2ab offline_access grafana OpenID Connect built-in scope: offline_access openid-connect +a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 role_list grafana SAML role list saml +74daf2cd-40d4-4304-87a8-92cdca808512 profile grafana OpenID Connect built-in scope: profile openid-connect +96d521d3-facc-4b5a-a8b4-a879bae6be07 email grafana OpenID Connect built-in scope: email openid-connect +a5bb3a5f-fd26-4be6-9557-26e20a03d33d address grafana OpenID Connect built-in scope: address openid-connect +d6ffe9fc-a03c-4496-85dc-dbb5e7754587 phone grafana OpenID Connect built-in scope: phone openid-connect +d6077ed7-b265-4f82-9336-24614967bd5d roles grafana OpenID Connect scope for add user roles to the access token openid-connect +699671ab-e7c1-4fcf-beb8-ea54f1471fc1 web-origins grafana OpenID Connect scope for add allowed web origins to the access token openid-connect +c61f5b19-c17e-49a1-91b8-a0296411b928 microprofile-jwt grafana Microprofile - JWT built-in scope openid-connect +f619a55a-d565-4cc0-8bf4-4dbaab5382fe username grafana openid-connect +0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 login grafana login openid-connect +d4723cd4-f717-44b7-a9b0-6c32c5ecd23f name grafana user name openid-connect +\. + + +-- +-- Data for Name: client_scope_attributes; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_scope_attributes (scope_id, value, name) FROM stdin; +0cc71c8c-fb37-41f2-b4d8-13210d3cf8be true display.on.consent.screen +0cc71c8c-fb37-41f2-b4d8-13210d3cf8be ${offlineAccessScopeConsentText} consent.screen.text +47f35d4b-35c7-4c6d-8bae-eff0a5046861 true display.on.consent.screen +47f35d4b-35c7-4c6d-8bae-eff0a5046861 ${samlRoleListScopeConsentText} consent.screen.text +66deef47-2158-4d5b-a75f-0bf42f642e7b true display.on.consent.screen +66deef47-2158-4d5b-a75f-0bf42f642e7b ${profileScopeConsentText} consent.screen.text +66deef47-2158-4d5b-a75f-0bf42f642e7b true include.in.token.scope +94ef659c-4c4a-4a33-98e8-bfcf443e9268 true display.on.consent.screen +94ef659c-4c4a-4a33-98e8-bfcf443e9268 ${emailScopeConsentText} consent.screen.text +94ef659c-4c4a-4a33-98e8-bfcf443e9268 true include.in.token.scope +96a960d2-c203-4ef0-a53c-c3edd01f2305 true display.on.consent.screen +96a960d2-c203-4ef0-a53c-c3edd01f2305 ${addressScopeConsentText} consent.screen.text +96a960d2-c203-4ef0-a53c-c3edd01f2305 true include.in.token.scope +3f705379-3361-486d-b75a-f7b4e4be492c true display.on.consent.screen +3f705379-3361-486d-b75a-f7b4e4be492c ${phoneScopeConsentText} consent.screen.text +3f705379-3361-486d-b75a-f7b4e4be492c true include.in.token.scope +b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 true display.on.consent.screen +b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 ${rolesScopeConsentText} consent.screen.text +b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 false include.in.token.scope +619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 false display.on.consent.screen +619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 consent.screen.text +619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 false include.in.token.scope +42bfb506-bf0d-424e-8649-53a9a93d252d false display.on.consent.screen +42bfb506-bf0d-424e-8649-53a9a93d252d true include.in.token.scope +0e98d5f9-d3f7-4b1d-9791-d442524fc2ab true display.on.consent.screen +0e98d5f9-d3f7-4b1d-9791-d442524fc2ab ${offlineAccessScopeConsentText} consent.screen.text +a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 true display.on.consent.screen +a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 ${samlRoleListScopeConsentText} consent.screen.text +74daf2cd-40d4-4304-87a8-92cdca808512 true display.on.consent.screen +74daf2cd-40d4-4304-87a8-92cdca808512 ${profileScopeConsentText} consent.screen.text +74daf2cd-40d4-4304-87a8-92cdca808512 true include.in.token.scope +96d521d3-facc-4b5a-a8b4-a879bae6be07 true display.on.consent.screen +96d521d3-facc-4b5a-a8b4-a879bae6be07 ${emailScopeConsentText} consent.screen.text +96d521d3-facc-4b5a-a8b4-a879bae6be07 true include.in.token.scope +a5bb3a5f-fd26-4be6-9557-26e20a03d33d true display.on.consent.screen +a5bb3a5f-fd26-4be6-9557-26e20a03d33d ${addressScopeConsentText} consent.screen.text +a5bb3a5f-fd26-4be6-9557-26e20a03d33d true include.in.token.scope +d6ffe9fc-a03c-4496-85dc-dbb5e7754587 true display.on.consent.screen +d6ffe9fc-a03c-4496-85dc-dbb5e7754587 ${phoneScopeConsentText} consent.screen.text +d6ffe9fc-a03c-4496-85dc-dbb5e7754587 true include.in.token.scope +d6077ed7-b265-4f82-9336-24614967bd5d true display.on.consent.screen +d6077ed7-b265-4f82-9336-24614967bd5d ${rolesScopeConsentText} consent.screen.text +d6077ed7-b265-4f82-9336-24614967bd5d false include.in.token.scope +699671ab-e7c1-4fcf-beb8-ea54f1471fc1 false display.on.consent.screen +699671ab-e7c1-4fcf-beb8-ea54f1471fc1 consent.screen.text +699671ab-e7c1-4fcf-beb8-ea54f1471fc1 false include.in.token.scope +c61f5b19-c17e-49a1-91b8-a0296411b928 false display.on.consent.screen +c61f5b19-c17e-49a1-91b8-a0296411b928 true include.in.token.scope +f619a55a-d565-4cc0-8bf4-4dbaab5382fe true display.on.consent.screen +f619a55a-d565-4cc0-8bf4-4dbaab5382fe true include.in.token.scope +0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 true display.on.consent.screen +0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 true include.in.token.scope +d4723cd4-f717-44b7-a9b0-6c32c5ecd23f true display.on.consent.screen +d4723cd4-f717-44b7-a9b0-6c32c5ecd23f true include.in.token.scope +\. + + +-- +-- Data for Name: client_scope_client; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_scope_client (client_id, scope_id, default_scope) FROM stdin; +eed689c6-49da-4d91-98eb-cd495bcc07a3 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +11c67f5b-dde7-4680-b05b-c9c59d78bda4 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +1e30397c-eac2-41fb-87bc-d90484992e65 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +2f521d09-7304-4b5e-a94b-7cc7300b8b50 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +eed689c6-49da-4d91-98eb-cd495bcc07a3 66deef47-2158-4d5b-a75f-0bf42f642e7b t +eed689c6-49da-4d91-98eb-cd495bcc07a3 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +eed689c6-49da-4d91-98eb-cd495bcc07a3 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +eed689c6-49da-4d91-98eb-cd495bcc07a3 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +eed689c6-49da-4d91-98eb-cd495bcc07a3 3f705379-3361-486d-b75a-f7b4e4be492c f +eed689c6-49da-4d91-98eb-cd495bcc07a3 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +eed689c6-49da-4d91-98eb-cd495bcc07a3 42bfb506-bf0d-424e-8649-53a9a93d252d f +eed689c6-49da-4d91-98eb-cd495bcc07a3 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +11c67f5b-dde7-4680-b05b-c9c59d78bda4 66deef47-2158-4d5b-a75f-0bf42f642e7b t +11c67f5b-dde7-4680-b05b-c9c59d78bda4 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +11c67f5b-dde7-4680-b05b-c9c59d78bda4 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +11c67f5b-dde7-4680-b05b-c9c59d78bda4 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +11c67f5b-dde7-4680-b05b-c9c59d78bda4 3f705379-3361-486d-b75a-f7b4e4be492c f +11c67f5b-dde7-4680-b05b-c9c59d78bda4 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +11c67f5b-dde7-4680-b05b-c9c59d78bda4 42bfb506-bf0d-424e-8649-53a9a93d252d f +11c67f5b-dde7-4680-b05b-c9c59d78bda4 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 66deef47-2158-4d5b-a75f-0bf42f642e7b t +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 3f705379-3361-486d-b75a-f7b4e4be492c f +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 42bfb506-bf0d-424e-8649-53a9a93d252d f +63d16a7e-aa65-486e-a0e1-81f928d3e3b8 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +1e30397c-eac2-41fb-87bc-d90484992e65 66deef47-2158-4d5b-a75f-0bf42f642e7b t +1e30397c-eac2-41fb-87bc-d90484992e65 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +1e30397c-eac2-41fb-87bc-d90484992e65 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +1e30397c-eac2-41fb-87bc-d90484992e65 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +1e30397c-eac2-41fb-87bc-d90484992e65 3f705379-3361-486d-b75a-f7b4e4be492c f +1e30397c-eac2-41fb-87bc-d90484992e65 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +1e30397c-eac2-41fb-87bc-d90484992e65 42bfb506-bf0d-424e-8649-53a9a93d252d f +1e30397c-eac2-41fb-87bc-d90484992e65 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 66deef47-2158-4d5b-a75f-0bf42f642e7b t +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +3cd285ea-0f6e-43b6-ab5c-d021c33a551b b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 3f705379-3361-486d-b75a-f7b4e4be492c f +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 42bfb506-bf0d-424e-8649-53a9a93d252d f +3cd285ea-0f6e-43b6-ab5c-d021c33a551b 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +2f521d09-7304-4b5e-a94b-7cc7300b8b50 66deef47-2158-4d5b-a75f-0bf42f642e7b t +2f521d09-7304-4b5e-a94b-7cc7300b8b50 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +2f521d09-7304-4b5e-a94b-7cc7300b8b50 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +2f521d09-7304-4b5e-a94b-7cc7300b8b50 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +2f521d09-7304-4b5e-a94b-7cc7300b8b50 3f705379-3361-486d-b75a-f7b4e4be492c f +2f521d09-7304-4b5e-a94b-7cc7300b8b50 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +2f521d09-7304-4b5e-a94b-7cc7300b8b50 42bfb506-bf0d-424e-8649-53a9a93d252d f +2f521d09-7304-4b5e-a94b-7cc7300b8b50 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +ef7f6eac-9fff-44aa-a86c-5125d52acc82 47f35d4b-35c7-4c6d-8bae-eff0a5046861 t +ef7f6eac-9fff-44aa-a86c-5125d52acc82 66deef47-2158-4d5b-a75f-0bf42f642e7b t +ef7f6eac-9fff-44aa-a86c-5125d52acc82 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +ef7f6eac-9fff-44aa-a86c-5125d52acc82 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +ef7f6eac-9fff-44aa-a86c-5125d52acc82 b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +ef7f6eac-9fff-44aa-a86c-5125d52acc82 3f705379-3361-486d-b75a-f7b4e4be492c f +ef7f6eac-9fff-44aa-a86c-5125d52acc82 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +ef7f6eac-9fff-44aa-a86c-5125d52acc82 42bfb506-bf0d-424e-8649-53a9a93d252d f +ef7f6eac-9fff-44aa-a86c-5125d52acc82 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +a5a8fed6-0bca-4646-9946-2fe84175353b a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +230081b5-9161-45c3-9e08-9eda5412f7f7 a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +6bd2d943-9800-4839-9ddc-03c04930cd9f a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +77ff47f8-f578-477d-8c06-e70a846332f5 a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +a8698f4f-5fa1-4baa-be05-87d03052af49 a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +a5a8fed6-0bca-4646-9946-2fe84175353b d6077ed7-b265-4f82-9336-24614967bd5d t +a5a8fed6-0bca-4646-9946-2fe84175353b 74daf2cd-40d4-4304-87a8-92cdca808512 t +a5a8fed6-0bca-4646-9946-2fe84175353b 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +a5a8fed6-0bca-4646-9946-2fe84175353b 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +a5a8fed6-0bca-4646-9946-2fe84175353b 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +a5a8fed6-0bca-4646-9946-2fe84175353b a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +a5a8fed6-0bca-4646-9946-2fe84175353b d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +a5a8fed6-0bca-4646-9946-2fe84175353b c61f5b19-c17e-49a1-91b8-a0296411b928 f +230081b5-9161-45c3-9e08-9eda5412f7f7 d6077ed7-b265-4f82-9336-24614967bd5d t +230081b5-9161-45c3-9e08-9eda5412f7f7 74daf2cd-40d4-4304-87a8-92cdca808512 t +230081b5-9161-45c3-9e08-9eda5412f7f7 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +230081b5-9161-45c3-9e08-9eda5412f7f7 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +230081b5-9161-45c3-9e08-9eda5412f7f7 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +230081b5-9161-45c3-9e08-9eda5412f7f7 a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +230081b5-9161-45c3-9e08-9eda5412f7f7 d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +230081b5-9161-45c3-9e08-9eda5412f7f7 c61f5b19-c17e-49a1-91b8-a0296411b928 f +6bd2d943-9800-4839-9ddc-03c04930cd9f d6077ed7-b265-4f82-9336-24614967bd5d t +6bd2d943-9800-4839-9ddc-03c04930cd9f 74daf2cd-40d4-4304-87a8-92cdca808512 t +6bd2d943-9800-4839-9ddc-03c04930cd9f 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +6bd2d943-9800-4839-9ddc-03c04930cd9f 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +6bd2d943-9800-4839-9ddc-03c04930cd9f 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +6bd2d943-9800-4839-9ddc-03c04930cd9f a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +6bd2d943-9800-4839-9ddc-03c04930cd9f d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +6bd2d943-9800-4839-9ddc-03c04930cd9f c61f5b19-c17e-49a1-91b8-a0296411b928 f +77ff47f8-f578-477d-8c06-e70a846332f5 d6077ed7-b265-4f82-9336-24614967bd5d t +77ff47f8-f578-477d-8c06-e70a846332f5 74daf2cd-40d4-4304-87a8-92cdca808512 t +77ff47f8-f578-477d-8c06-e70a846332f5 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +77ff47f8-f578-477d-8c06-e70a846332f5 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +77ff47f8-f578-477d-8c06-e70a846332f5 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +77ff47f8-f578-477d-8c06-e70a846332f5 a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +77ff47f8-f578-477d-8c06-e70a846332f5 d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +77ff47f8-f578-477d-8c06-e70a846332f5 c61f5b19-c17e-49a1-91b8-a0296411b928 f +a8698f4f-5fa1-4baa-be05-87d03052af49 d6077ed7-b265-4f82-9336-24614967bd5d t +a8698f4f-5fa1-4baa-be05-87d03052af49 74daf2cd-40d4-4304-87a8-92cdca808512 t +a8698f4f-5fa1-4baa-be05-87d03052af49 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +a8698f4f-5fa1-4baa-be05-87d03052af49 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +a8698f4f-5fa1-4baa-be05-87d03052af49 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +a8698f4f-5fa1-4baa-be05-87d03052af49 a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +a8698f4f-5fa1-4baa-be05-87d03052af49 d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +a8698f4f-5fa1-4baa-be05-87d03052af49 c61f5b19-c17e-49a1-91b8-a0296411b928 f +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 d6077ed7-b265-4f82-9336-24614967bd5d t +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 74daf2cd-40d4-4304-87a8-92cdca808512 t +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 c61f5b19-c17e-49a1-91b8-a0296411b928 f +09b79548-8426-4c0e-8e0b-7488467532c7 a1d5ab0b-6c06-4dc5-bdca-3fefe915f4f3 t +09b79548-8426-4c0e-8e0b-7488467532c7 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +09b79548-8426-4c0e-8e0b-7488467532c7 d6077ed7-b265-4f82-9336-24614967bd5d t +09b79548-8426-4c0e-8e0b-7488467532c7 d4723cd4-f717-44b7-a9b0-6c32c5ecd23f t +09b79548-8426-4c0e-8e0b-7488467532c7 0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 t +09b79548-8426-4c0e-8e0b-7488467532c7 74daf2cd-40d4-4304-87a8-92cdca808512 t +\. + + +-- +-- Data for Name: client_scope_role_mapping; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_scope_role_mapping (scope_id, role_id) FROM stdin; +0cc71c8c-fb37-41f2-b4d8-13210d3cf8be 16d5987b-dcbb-4650-8f52-3469f3974846 +0e98d5f9-d3f7-4b1d-9791-d442524fc2ab c49bddc6-ec92-4caa-bc04-57ba80a92eb9 +\. + + +-- +-- Data for Name: client_session; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_session (id, client_id, redirect_uri, state, "timestamp", session_id, auth_method, realm_id, auth_user_id, current_action) FROM stdin; +\. + + +-- +-- Data for Name: client_session_auth_status; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_session_auth_status (authenticator, status, client_session) FROM stdin; +\. + + +-- +-- Data for Name: client_session_note; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_session_note (name, value, client_session) FROM stdin; +\. + + +-- +-- Data for Name: client_session_prot_mapper; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_session_prot_mapper (protocol_mapper_id, client_session) FROM stdin; +\. + + +-- +-- Data for Name: client_session_role; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_session_role (role_id, client_session) FROM stdin; +\. + + +-- +-- Data for Name: client_user_session_note; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.client_user_session_note (name, value, client_session) FROM stdin; +\. + + +-- +-- Data for Name: component; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.component (id, name, parent_id, provider_id, provider_type, realm_id, sub_type) FROM stdin; +bf743b0a-d8f9-4635-bcbe-e1c8b92075e2 Trusted Hosts master trusted-hosts org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +d6e91e34-9d10-46e6-a343-c767cd9817ab Consent Required master consent-required org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +b914dfd7-6556-40b2-8055-bf0a131d9b6a Full Scope Disabled master scope org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +2ab822d8-3278-42f3-a27a-e9d7104ce361 Max Clients Limit master max-clients org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +f95566ed-b955-4668-88fc-e7413fd98615 Allowed Protocol Mapper Types master allowed-protocol-mappers org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +588cf9d4-1fb1-43d5-b454-9f9239d1dda7 Allowed Client Scopes master allowed-client-templates org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master anonymous +28d2466c-5af6-4786-a8a2-c25d6cb4833f Allowed Protocol Mapper Types master allowed-protocol-mappers org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master authenticated +1dc5700c-668d-4988-8920-f1b21f22aaa2 Allowed Client Scopes master allowed-client-templates org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy master authenticated +ec24e563-c82e-4a0f-89db-6c2b75e5383a fallback-HS256 master hmac-generated org.keycloak.keys.KeyProvider master \N +281b3291-097c-42af-8d52-46606d3b669f fallback-RS256 master rsa-generated org.keycloak.keys.KeyProvider master \N +80af2f23-4a51-498a-a011-732cf9cfa8f8 rsa-generated grafana rsa-generated org.keycloak.keys.KeyProvider grafana \N +a5b75d44-0bf1-400e-9e87-4293efeb3051 hmac-generated grafana hmac-generated org.keycloak.keys.KeyProvider grafana \N +9877acf2-e1cc-4038-a3c2-75db29b432e0 aes-generated grafana aes-generated org.keycloak.keys.KeyProvider grafana \N +5a1232c0-4243-454f-a26e-5d771efd3585 Trusted Hosts grafana trusted-hosts org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +5382514f-29bc-4cfd-b1b9-e1cf85dc1ed3 Consent Required grafana consent-required org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +1e55b1d2-5402-4b33-8c4a-59d0d5ddba32 Full Scope Disabled grafana scope org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +3021f045-3220-4e56-872c-d3491f6601f6 Max Clients Limit grafana max-clients org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +cb7cf482-8ac6-4999-ab67-1d48fef549f5 Allowed Protocol Mapper Types grafana allowed-protocol-mappers org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +7488860e-5bba-4f89-bde2-c63b0290cc0f Allowed Client Scopes grafana allowed-client-templates org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana anonymous +261e38de-3e1f-40a3-9200-f5aac1975701 Allowed Protocol Mapper Types grafana allowed-protocol-mappers org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana authenticated +4223e0de-8a82-464f-b466-048d3682d8df Allowed Client Scopes grafana allowed-client-templates org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy grafana authenticated +\. + + +-- +-- Data for Name: component_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.component_config (id, component_id, name, value) FROM stdin; +9a09b41f-3340-49d7-b65c-fb02300ac5a0 1dc5700c-668d-4988-8920-f1b21f22aaa2 allow-default-scopes true +81cf1edb-60cb-4336-8e61-3676e82a8496 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types saml-role-list-mapper +ebd9fd67-3468-4640-a130-dadf8bd0d3dd f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types oidc-address-mapper +5a2075e1-ff50-4221-ba0f-13c221b745f1 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types oidc-sha256-pairwise-sub-mapper +d47557df-07ff-4cae-bedd-b584c0697852 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types saml-user-attribute-mapper +91a7d433-6520-4710-a95a-b1d6ed1932f7 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types oidc-usermodel-attribute-mapper +35c3fff3-1779-4ea1-b8c2-b93b746ad4df f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types oidc-usermodel-property-mapper +64312a90-809f-455e-b89e-52b9f0a34229 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types oidc-full-name-mapper +5f0a8c49-4279-41bc-9b1f-cdf3acf14bc9 f95566ed-b955-4668-88fc-e7413fd98615 allowed-protocol-mapper-types saml-user-property-mapper +2ae4acc4-c6d6-4d2f-92c6-3a222a7d078a 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types oidc-sha256-pairwise-sub-mapper +880a35e4-65a1-4697-836d-fbc46641d676 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types oidc-full-name-mapper +b087e631-754f-4c03-8cfc-354c7e7456fe 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types oidc-usermodel-property-mapper +079e63d4-0862-4e63-a62f-1a168cbbc25c 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types saml-user-property-mapper +da61fbc2-7533-4bc1-b0c4-357db9f108e4 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types oidc-address-mapper +37a4be58-26b0-4d4a-9c41-a89b27fc25f6 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types saml-user-attribute-mapper +0eb7fcb8-1afb-4d73-bbf7-9827e7669990 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types oidc-usermodel-attribute-mapper +1e0e9459-5116-46b7-a247-0212c2e8d719 28d2466c-5af6-4786-a8a2-c25d6cb4833f allowed-protocol-mapper-types saml-role-list-mapper +6bd5567c-e438-4884-a7ad-4f0450f2c75b 2ab822d8-3278-42f3-a27a-e9d7104ce361 max-clients 200 +13ec6e6a-b4a9-4ca6-bee5-fe6d7df07bd4 588cf9d4-1fb1-43d5-b454-9f9239d1dda7 allow-default-scopes true +9e4f8b41-d2e6-4b67-9784-f319b804b66b bf743b0a-d8f9-4635-bcbe-e1c8b92075e2 host-sending-registration-request-must-match true +4ca659f4-e9cf-45ff-a891-42b973fee220 bf743b0a-d8f9-4635-bcbe-e1c8b92075e2 client-uris-must-match true +69a74c94-26ec-40d1-8859-c3077a84c374 ec24e563-c82e-4a0f-89db-6c2b75e5383a secret R0msuv6OjhKyzluLnbkkgkM1s0Mi5aK0Ck-3o-kbGMwsE2TPdzsoH-9Z_P2OEmJ6dqppkp9H8eZE9pdC8uDJHA +19b2af74-0667-4ade-993e-d734eb465a16 ec24e563-c82e-4a0f-89db-6c2b75e5383a algorithm HS256 +b2a0d967-1111-4c79-9273-e05450fe26c5 ec24e563-c82e-4a0f-89db-6c2b75e5383a priority -100 +63fba7e3-ad5f-475b-a367-8c0af5302b0b ec24e563-c82e-4a0f-89db-6c2b75e5383a kid e19299b5-a2bc-45d4-bd71-2beeaab7dfbf +c2e6f8d9-fa94-4cda-8cab-558695b50ae4 281b3291-097c-42af-8d52-46606d3b669f privateKey MIIEowIBAAKCAQEAjuRpHyJ/1ki93NW9sMrppl7MC4DfzODtvirvslRfYvhVMm0rMWPw2q1bqrrzQMR+VdKQbp+M7jt9PYryNrzsGy+iiD7mi+KTrLnmQ+c+r867flwbivBB0LR63LidcpOExVUuREwilOcywfFXeFiBQ9Tbxcwztu6J+b9kjlSYNSg+JCzn2Wn2kMQXSTd2ddxBHs8bmxyibg18UMovJLt42E8/C8+3iP7++5gSy9FeVCjf1eIh7EF3LwPNJxeyJhd3fNfmB8YnWGoz2Jw1RZE7Hm/BW5uSsM7rQpkvEACHbsMCSlzxvJjzSGHYG+PksMgfiDLiezxBQhgD+5Z5DhImywIDAQABAoIBAFAAKaq40gHS8Bm3wWA9+tqesHawTJyUQgb6WwDopA7xIiH9ZPVeEvcbn/rSeGaGnITIQvzsbybiP5g5NqrW0wnVfZXyQXmH/U3zNqxFx57+i5KPVwxOv0puAWuaIOyJEwi4TBMI3UOovY4/5M0IIDct8W2oijudCbq+ITpeumjnrJxwV0aSlD6hYKiL4OWtcFJiAu+ZeSOlQOTY2jielAvlnOmFygx2KWE7I/JUhu2w2hO2Q1aQHPgbbPR8VxRZLID411VztuW3T4w9ur057YRIqB16ZwI6knMETGxHc1rYo6mMh+t45rWHzJ+hKJC/NKQ+IRI1U4YfUXDkdAg2b+ECgYEAyHdHS9p9lcQQFqkZVL2FLeEAbhkosAZjIe7gblj2Kt57QKJ6NJangV61hqHAr93uU3DJu5bertsCZjqN7SHelusyUXEBc4SrFhftjZNFsnRB/yj3jU3ag11Ro40cBW7fosUgrieUI3CZEB0SPdXoc7LgwheEViKu9zyPl+PcyD8CgYEAtnoZBJMaEl6mjkXjLhKM5U5eT50pDPct9p/pl5fwpBxPkJJXBYMuybjff1D+VlAboMOImLOijjjgS7bcr/DvnOCIhqy9FMT+57KxAaa1ZcrQ4MUj3Tj4Vg0kDMbu5GMZ40lmOjR6tTrIy0riOk64rJAPztcLoV2gFl98Dwp83nUCgYA6UpGYnQGqn/c6UIpBID5uAac5YPJ4e/M9fR0onZNJF59uR5ccU7R6LA7OE6NWx0++UPMwM42n+6nwChseoZr794OVNDaC4FdSPzXq2a0OZUqKLOYQ41SuoWjOF5DOd9pypb2DTZqI0QqHKJ4VBXXyq1k+vs7OrJqQ7bqtKyshywKBgQCw/1PfBRTH9rlVzWJkISg7kD2YudfEtMoHq+s32PBZLwDaOahhN3Kdxk47v4NEk6WI1cFcZPnrPC4MIw6DNpAlOgITp+AsEj0y3zgkYuEXIJhlPbPg9E6loU9zeU7lh17oAR1AngDcY227CyLO7ebhs0cyGZM1bYxHx0ydhk3CtQKBgHIT08PBgZYhHPJZ09H76gKTKcoL2XS4uEJb6/xjp4ACa1wlFhWZmUOq/DhWeAx313Mf5sRAGQGieiaPamBwt9cOai6V+Agk1YMS97Fg4eV/aYThor8qi1O3dEGSFw4GaGGwvL7Trvcpogk2N6+Pm2LJ5aPy048FPnR+YZ9/yIzr +c35d99d1-ca8d-4f82-bbb8-0a7b663e9d09 281b3291-097c-42af-8d52-46606d3b669f algorithm RS256 +3f4c9dd6-37cc-444d-b9d0-caba781e20cd 281b3291-097c-42af-8d52-46606d3b669f priority -100 +4bc9ffd3-9f15-442f-bdbf-aa5a73adfa65 281b3291-097c-42af-8d52-46606d3b669f certificate MIICmzCCAYMCBgF+u1WYqDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIwMjAyMTY0NTU2WhcNMzIwMjAyMTY0NzM2WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCO5GkfIn/WSL3c1b2wyummXswLgN/M4O2+Ku+yVF9i+FUybSsxY/DarVuquvNAxH5V0pBun4zuO309ivI2vOwbL6KIPuaL4pOsueZD5z6vzrt+XBuK8EHQtHrcuJ1yk4TFVS5ETCKU5zLB8Vd4WIFD1NvFzDO27on5v2SOVJg1KD4kLOfZafaQxBdJN3Z13EEezxubHKJuDXxQyi8ku3jYTz8Lz7eI/v77mBLL0V5UKN/V4iHsQXcvA80nF7ImF3d81+YHxidYajPYnDVFkTseb8Fbm5KwzutCmS8QAIduwwJKXPG8mPNIYdgb4+SwyB+IMuJ7PEFCGAP7lnkOEibLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIXG0C7YyWpCrpIUfq3dKFSISjUJ3yfJsm7UX43YeFhSk0XKaeNaP0IfDa1zu4x2BbmsKZn8QI6DHENRPxcVFutkjuVbdLUWz8JxnpKzvKHiJX+CnL1PZ1RKM6coQi/hX1uI3dNM/kQk/FzkhspD2l3Q/7F2ix5sFk6G0UrlvJ7NgR2hy4DujhvsO+tF5hq2ZvLn4WCbiMgLPzDIcvDSm1ytWPSpSKGJs+zANotZnVRTkYF5bXKQH8EEdbgcU9sIHy/UJ08bSQ/9H/j2X8W+62eOchQjzNK5DslwoN2jakK0JJeNvRfIM8ETucLC4e+jCAB2QaeKnjpX7CrA8hiN8AM= +a3fdfa84-e986-426d-ba8e-49f2bdb91cd8 a5b75d44-0bf1-400e-9e87-4293efeb3051 priority 100 +0240dece-6d4d-41e0-a64f-5e863a44354a a5b75d44-0bf1-400e-9e87-4293efeb3051 algorithm HS256 +93341052-b55f-43fd-8f15-72b5812024d3 a5b75d44-0bf1-400e-9e87-4293efeb3051 secret YrYYWSiul7DxTUeKd5ZeFlyrDLvJj0aOY5UMijdFx6qaDkwjMSl74kMAso4cID-qX582X_n-_vcbWFkwpdJDyA +ea21db3f-c474-445c-926d-e7d161e5721f a5b75d44-0bf1-400e-9e87-4293efeb3051 kid bb678665-e694-435a-b5bb-8c11e4727c1c +bb8c28ca-bb74-4a07-82c3-8293354517be 9877acf2-e1cc-4038-a3c2-75db29b432e0 secret mjCx83NwCZkLHZ5sRvZ7lw +08300dda-1f12-45b7-98b5-23805cb1ed84 9877acf2-e1cc-4038-a3c2-75db29b432e0 priority 100 +64ea89c8-a2ce-4d2d-896d-aa49e7ca9fcc 9877acf2-e1cc-4038-a3c2-75db29b432e0 kid 7d80efc5-222b-4b6d-9b99-c3b516a59733 +bec1483f-75e7-46e8-916c-102db4cbefb5 80af2f23-4a51-498a-a011-732cf9cfa8f8 certificate MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg== +48e8b904-1393-43a4-aaa1-30e2d9634b36 80af2f23-4a51-498a-a011-732cf9cfa8f8 privateKey MIIEowIBAAKCAQEAqDmQHfTcOQOzmNJbVvtvuS8p/EgmiscP7vA/PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5/x8aAIl/+TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5/CeCCoKwKSmheD3u1xTJ8+VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP/MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K/vDJBM/3C933vMY8mqSQXbI3G54x+0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv/W/fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQIDAQABAoIBAF3kEt3FZoyj1j97WOOJXmf7PPHDy081n1z61jEl9FjBFqse2gbPiBmfkU3JsVMbB70WYN1D/KOIX3EdZBELKbhQoMgJ826SSPi4vJ+jWYHVRTLB+h+B70E3X6mvXa+O6uB1rIgTNl2Gxp/rTtM/scLwAiZXR/n2hzGgNr9b1gT7D7kyCUIKDiJsQed2pA6ZbSDNTQQDE2qeN/Rr/+VV4XRuIIdBXn+Brap8ihjx4Gnn/SDBKM3MZoacwgS+9CZNhLjs8Ou4xD+KyitbbGGY4ZK/ZW2eSAH4ra8vVGTDK6bJrMTAE/73A23Evp/sKtRkFqcNumJ3rCykcAJorDUK48ECgYEA0TpkTwK6f2a72ncmw7Xzy4zdu+FdkI9PzWmS4IrbYOYRwsRMQLowsUPIdtU0EMrs7PRl1dSQa3/kAnw5J8wogkqVf4dJ3/lb/N5qtsjHP87N35QMXnMdE/BC6o7t5e+iRoHMloAuSBonGC9uBn1UeHWrNeLCB8+upJoSEBKbEQ0CgYEAzdSoKL4CJo31gVEqoJFPzPemsYGmV6Zl1Vj8mqMbQd8wcJoSJWbNV/3sfpIqdvOJOXQrzm+LYIbhhUnS3ZlGBmFsqUtC7dw3AqLg53msCyJLxmCIib0m/ONCY9qczV4OkUM6HVAdgzmMqqhk5ZB70IdUyStn2meqXKXLGFlLJKkCgYEAuizHTTcUVIFJ7x/PMp8ZjKqQM7pZ02Rykkm7FGr6wsJ2U2TwpTgIU/QI0RTt+3NWV5MxepBm4gEvFrcK9MrJ0QYk+RGdPttYay5Ors8B3Vlb//Jw/ypXWYKVSLpeHhiZwTuGnPT6OdZrqy2pLcUgAQBTlONt3B2FPZqLMBoeOZECgYBuCwm0bpF7x13AO4LMwaOmc6jdMfGa3s2G2MKEcjt6ZjbhnJ2i/Wk/Z/RuXvrxCZcN7nwVLDGZ88LSnftsmiuD8cZEZIZt4NRQRoBzgOtoMHfOoYGeElCr11yBQjme2nBzXTvOvCxrIfOAsfLvgOWRQSklPF2TuOSuD72bUPIJsQKBgBbeb8rIXKRRFC3A0IkKm8C51gG8mMgPdxZMKKuVhpb6D8B5aPh+WW44yNOpn7LInYI4jzf3Kv+kcj0PNH3nFaplnyCGFfmUIg27SAsRcrJcTlXtIKooZw6oPU+dQfAo11suArVAVD6OQc/7z99VoIpLN0cUHfS/g0H1dx/r/32o +258b8704-0587-4208-996f-96627992e370 80af2f23-4a51-498a-a011-732cf9cfa8f8 priority 100 +12cd94c5-bd7e-4a5c-95e2-256b0bcf14bd 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types oidc-address-mapper +f33b34f1-3793-4786-a281-b286fce52f45 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types saml-role-list-mapper +d056c1de-9aa1-46a0-a644-fafb33088967 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types saml-user-property-mapper +de895b7b-16e0-4f8e-95c4-055b4fd70c91 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types saml-user-attribute-mapper +aec970eb-6722-4ed6-b8d3-4bbbfb2e3324 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types oidc-usermodel-attribute-mapper +43020e0a-dac7-4255-9e30-b838279905d4 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types oidc-usermodel-property-mapper +b8ec71f0-39a3-476d-baae-ee3632cadd2a 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types oidc-full-name-mapper +7bbfd5ae-854e-42b6-ac26-017656bafa61 261e38de-3e1f-40a3-9200-f5aac1975701 allowed-protocol-mapper-types oidc-sha256-pairwise-sub-mapper +07031bc1-b7b6-44b1-bdf7-8b9f5a31db40 3021f045-3220-4e56-872c-d3491f6601f6 max-clients 200 +c9bba7d6-e8f7-46bd-9c58-15cf58860eae cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types saml-role-list-mapper +5c047fce-366d-4c39-8846-14a975a4dc07 cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types saml-user-attribute-mapper +8961856f-6ae2-4e98-b372-0e9c18ee8e17 cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types oidc-usermodel-attribute-mapper +79445172-82cc-46fd-97ef-059b9f75ea39 cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types oidc-full-name-mapper +db4114c8-392d-427a-9e23-c430401cd93f cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types oidc-sha256-pairwise-sub-mapper +b52190d4-ee85-492b-9589-dbfee4afa60d cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types oidc-address-mapper +a0173a1b-dd1a-450c-8a6e-f2e1d7a5d3d8 cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types saml-user-property-mapper +0e4535b0-2979-40f7-ad71-b57275cd0fdc cb7cf482-8ac6-4999-ab67-1d48fef549f5 allowed-protocol-mapper-types oidc-usermodel-property-mapper +aa3d34ee-58f9-4017-83ff-69f252d2b54b 4223e0de-8a82-464f-b466-048d3682d8df allow-default-scopes true +e77f7d11-31d8-468e-8232-cd5045556d23 5a1232c0-4243-454f-a26e-5d771efd3585 host-sending-registration-request-must-match true +230d9d99-1b5c-49c5-853a-75967443a767 5a1232c0-4243-454f-a26e-5d771efd3585 client-uris-must-match true +53a1ee77-7350-40cb-be63-6ac417f14e6f 7488860e-5bba-4f89-bde2-c63b0290cc0f allow-default-scopes true +\. + + +-- +-- Data for Name: composite_role; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.composite_role (composite, child_role) FROM stdin; +4a3204aa-320e-4584-b8ee-ea2989b3f330 847ebc80-6849-4c47-9f9e-5bba0c0d754d +4a3204aa-320e-4584-b8ee-ea2989b3f330 103dc6a6-5e7a-4c27-b4f0-9dbb1fdcf214 +4a3204aa-320e-4584-b8ee-ea2989b3f330 13c94e3b-b22f-4503-bc56-75e1bd2a927f +4a3204aa-320e-4584-b8ee-ea2989b3f330 4364a376-4ed0-4051-aeab-609f62420e5d +4a3204aa-320e-4584-b8ee-ea2989b3f330 f12af4b7-8828-47a5-abbc-dbb09b9d409e +4a3204aa-320e-4584-b8ee-ea2989b3f330 2606a5b9-699b-488a-a819-d6f368e66697 +4a3204aa-320e-4584-b8ee-ea2989b3f330 2cf34980-2606-4faf-bc40-b9a47c69ef1c +4a3204aa-320e-4584-b8ee-ea2989b3f330 13e61c6b-aff6-4ef8-ab56-ad4abefcb101 +4a3204aa-320e-4584-b8ee-ea2989b3f330 632bad74-a33f-4fd5-9393-ec0a07898b1a +4a3204aa-320e-4584-b8ee-ea2989b3f330 4607a008-f45c-45f5-b506-6de020b7e366 +4a3204aa-320e-4584-b8ee-ea2989b3f330 edd471cc-81d5-43e4-bb43-41fe88ff537d +4a3204aa-320e-4584-b8ee-ea2989b3f330 4c2b4e2a-e792-4ffd-969d-e33ecdf7158f +4a3204aa-320e-4584-b8ee-ea2989b3f330 38282bc7-ea21-46db-a36e-ca621d3275b4 +4a3204aa-320e-4584-b8ee-ea2989b3f330 12111f4a-16ee-4ee7-8576-7956b9440dc5 +4a3204aa-320e-4584-b8ee-ea2989b3f330 f417ae21-5fb4-40fb-bda8-54c61ce7461d +4a3204aa-320e-4584-b8ee-ea2989b3f330 7adeaf33-05d3-4a81-a7bf-f99c721b5d9c +4a3204aa-320e-4584-b8ee-ea2989b3f330 60870d03-d96a-4371-bdad-e3fac925a8df +4a3204aa-320e-4584-b8ee-ea2989b3f330 94363dbd-a6b8-4678-8231-50208c32c22c +f12af4b7-8828-47a5-abbc-dbb09b9d409e 7adeaf33-05d3-4a81-a7bf-f99c721b5d9c +4364a376-4ed0-4051-aeab-609f62420e5d f417ae21-5fb4-40fb-bda8-54c61ce7461d +4364a376-4ed0-4051-aeab-609f62420e5d 94363dbd-a6b8-4678-8231-50208c32c22c +619ba870-921e-4f28-b26c-89b11f39dddf a42d235d-2864-4a99-9592-211d89d0407d +828c3ba8-a13d-49f5-8975-8eb00afbf7de a1a08dbc-4553-4be7-85f5-88c417bdcd45 +4a3204aa-320e-4584-b8ee-ea2989b3f330 b44e0fe0-0fb7-4e12-a6f0-b352431a0f57 +4a3204aa-320e-4584-b8ee-ea2989b3f330 95dfed9c-47fe-489b-aa28-52f0d7aa7c49 +4a3204aa-320e-4584-b8ee-ea2989b3f330 07e1586d-a943-46d9-9c3d-1f3544c8c27f +4a3204aa-320e-4584-b8ee-ea2989b3f330 293d0c06-6dce-4303-9cd3-dfdd6d1275b8 +4a3204aa-320e-4584-b8ee-ea2989b3f330 cfdeeb7b-c70e-496b-9605-70377168a6cb +4a3204aa-320e-4584-b8ee-ea2989b3f330 74252705-a339-4513-97ca-d5617977d5ff +4a3204aa-320e-4584-b8ee-ea2989b3f330 77c3f67e-21d7-4c18-9971-4baf4c20eeaa +4a3204aa-320e-4584-b8ee-ea2989b3f330 5de01bf1-bfac-4ea2-8fb1-ed95594fe1da +4a3204aa-320e-4584-b8ee-ea2989b3f330 a72adc0b-5220-48e4-a66a-9e15dca5f574 +4a3204aa-320e-4584-b8ee-ea2989b3f330 f29b8efa-3c08-410a-a5c0-15b52253d2e2 +4a3204aa-320e-4584-b8ee-ea2989b3f330 dd3ecc72-aaee-43d5-8f7e-f6dcdfb5a608 +4a3204aa-320e-4584-b8ee-ea2989b3f330 9d5a8bab-e112-4e1c-8196-604f3d0143ea +4a3204aa-320e-4584-b8ee-ea2989b3f330 ffff4251-e0a4-4f9c-8bf6-5461b2f52766 +4a3204aa-320e-4584-b8ee-ea2989b3f330 5fafdde9-71f7-4f67-9c1d-f3f4bc7f5128 +4a3204aa-320e-4584-b8ee-ea2989b3f330 6cfc2ac6-bdd7-4b90-ac16-27a75f2eb00a +4a3204aa-320e-4584-b8ee-ea2989b3f330 c3ded8eb-c970-4e43-bea9-5e07795d20ef +4a3204aa-320e-4584-b8ee-ea2989b3f330 811c2a39-6614-46fb-acf5-889d52248171 +4a3204aa-320e-4584-b8ee-ea2989b3f330 2a90f228-2ca4-413f-bc4b-7939af8abcbf +cfdeeb7b-c70e-496b-9605-70377168a6cb c3ded8eb-c970-4e43-bea9-5e07795d20ef +293d0c06-6dce-4303-9cd3-dfdd6d1275b8 2a90f228-2ca4-413f-bc4b-7939af8abcbf +293d0c06-6dce-4303-9cd3-dfdd6d1275b8 6cfc2ac6-bdd7-4b90-ac16-27a75f2eb00a +85afffb5-2069-4873-b6c8-08159c1e4bdd d0e4028d-a604-427a-9262-a1a9513dafc8 +85afffb5-2069-4873-b6c8-08159c1e4bdd 2b8b60c5-d388-4925-b735-858df38dae6e +85afffb5-2069-4873-b6c8-08159c1e4bdd e9c997c8-ad6b-4a99-81e1-c248e94fbeac +85afffb5-2069-4873-b6c8-08159c1e4bdd 8c4449b9-5add-40ba-a19f-cf5d80425e68 +85afffb5-2069-4873-b6c8-08159c1e4bdd a5f31b90-986b-46d5-a385-a639b4e19e37 +85afffb5-2069-4873-b6c8-08159c1e4bdd 99bd546f-a5ed-47f8-862c-9a5e8345bf3b +85afffb5-2069-4873-b6c8-08159c1e4bdd 9096d8df-9d5b-4fb5-b93e-49acc6df0be5 +85afffb5-2069-4873-b6c8-08159c1e4bdd 03230264-ed7a-46b2-939d-53ebe9a59812 +85afffb5-2069-4873-b6c8-08159c1e4bdd 2240d1de-5ac4-44ac-91be-cee70e1dd22b +85afffb5-2069-4873-b6c8-08159c1e4bdd 6d2fd708-445b-44a8-b950-f1350a15dd14 +85afffb5-2069-4873-b6c8-08159c1e4bdd 82266aa3-67ea-485a-a078-4671eb141853 +85afffb5-2069-4873-b6c8-08159c1e4bdd d6dad388-8c69-4bba-940e-371afc98042e +85afffb5-2069-4873-b6c8-08159c1e4bdd 5d7868e1-0c4a-46cc-8bac-bd19c0ea1bde +85afffb5-2069-4873-b6c8-08159c1e4bdd 85e6229e-e246-4e9a-8b39-7bae49754f7d +85afffb5-2069-4873-b6c8-08159c1e4bdd bc618c28-98d1-477d-b4fc-c5ec7cd7f271 +85afffb5-2069-4873-b6c8-08159c1e4bdd 5059b239-0dce-4bb2-9c55-a6afc8dcbe3b +85afffb5-2069-4873-b6c8-08159c1e4bdd ac28461f-3416-4af4-be65-abc739dbeee5 +8c4449b9-5add-40ba-a19f-cf5d80425e68 bc618c28-98d1-477d-b4fc-c5ec7cd7f271 +e9c997c8-ad6b-4a99-81e1-c248e94fbeac ac28461f-3416-4af4-be65-abc739dbeee5 +e9c997c8-ad6b-4a99-81e1-c248e94fbeac 85e6229e-e246-4e9a-8b39-7bae49754f7d +18a7066b-fe71-410e-9581-69f78347ec29 68fdbd76-8688-47a6-b68d-3298a5401f05 +c7e799a5-1250-4bc8-b7c6-ffdc58361477 daaedcc6-e7a6-488e-921e-7022aa808da7 +4a3204aa-320e-4584-b8ee-ea2989b3f330 b8a4faaf-86d9-43eb-bb18-0eaa654b35a7 +85afffb5-2069-4873-b6c8-08159c1e4bdd 5e2301d7-2a9e-4f2d-a940-9bd442b15d8c +\. + + +-- +-- Data for Name: credential; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.credential (id, salt, type, user_id, created_date, user_label, secret_data, credential_data, priority) FROM stdin; +d4b2c483-1dd3-47f6-86bf-42548009918d \N password 74e29604-ff35-42bb-a26d-4d0b81ef0917 1643820449817 \N {"value":"Hou7HlbGvohOx6II0VSCP4BIGI4Cyzy+BcXbPUQe/kaMQzNU77kH2pOKZ236UPfkiCyOLe7A3oS0afExA+ymAQ==","salt":"urXvCw0KdWf9s74km4G+lA==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 +cb2bd4ed-94b8-4259-bcaa-9250c3fb28d3 \N password 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d 1657026827644 \N {"value":"q3Z59Nh/5bdezDEpCwEbMPu8d+VgJ5WetafXkR8l0FlsTTkSDQgW+j6GaM3seJR93p3/jCxyfsvZl062d1pq7w==","salt":"ohuHnjLnwF9dBZ38DRJJWg==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 +b58e1964-6466-40b2-879c-982b724d7f9c \N password 88692d07-bb9a-46cf-844c-7ff5c529cd04 1657026904515 \N {"value":"+/0zWjiJyE3+dCOEf0SO6G3n1/LsFAVoDAZREKTfN4vQ5xJH8srJoCjxcgb+bI1crMr8gknDlFyGRy7CpYn2VQ==","salt":"v/2okNt3wGOZz+x4DjOCDQ==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 +3ff7dd8f-a299-4b51-bf5d-99665ccfd313 \N password 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e 1657026943075 \N {"value":"nMYodMJMiq/J8g9vRPktGc7WSWnOKr6leMDZX4p9K9KgAUYeXFDSu+d29PWWn0rFn93dL0PNdIdHWNQhfkIDMg==","salt":"rmi9WLHgarmIXGukecSIig==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 +\. + + +-- +-- Data for Name: databasechangelog; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.databasechangelog (id, author, filename, dateexecuted, orderexecuted, exectype, md5sum, description, comments, tag, liquibase, contexts, labels, deployment_id) FROM stdin; +1.0.0.Final-KEYCLOAK-5461 sthorger@redhat.com META-INF/jpa-changelog-1.0.0.Final.xml 2022-02-02 16:47:26.017844 1 EXECUTED 7:4e70412f24a3f382c82183742ec79317 createTable tableName=APPLICATION_DEFAULT_ROLES; createTable tableName=CLIENT; createTable tableName=CLIENT_SESSION; createTable tableName=CLIENT_SESSION_ROLE; createTable tableName=COMPOSITE_ROLE; createTable tableName=CREDENTIAL; createTable tab... \N 3.5.4 \N \N 3820445829 +1.0.0.Final-KEYCLOAK-5461 sthorger@redhat.com META-INF/db2-jpa-changelog-1.0.0.Final.xml 2022-02-02 16:47:26.03122 2 MARK_RAN 7:cb16724583e9675711801c6875114f28 createTable tableName=APPLICATION_DEFAULT_ROLES; createTable tableName=CLIENT; createTable tableName=CLIENT_SESSION; createTable tableName=CLIENT_SESSION_ROLE; createTable tableName=COMPOSITE_ROLE; createTable tableName=CREDENTIAL; createTable tab... \N 3.5.4 \N \N 3820445829 +1.1.0.Beta1 sthorger@redhat.com META-INF/jpa-changelog-1.1.0.Beta1.xml 2022-02-02 16:47:26.06085 3 EXECUTED 7:0310eb8ba07cec616460794d42ade0fa delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION; createTable tableName=CLIENT_ATTRIBUTES; createTable tableName=CLIENT_SESSION_NOTE; createTable tableName=APP_NODE_REGISTRATIONS; addColumn table... \N 3.5.4 \N \N 3820445829 +1.1.0.Final sthorger@redhat.com META-INF/jpa-changelog-1.1.0.Final.xml 2022-02-02 16:47:26.065284 4 EXECUTED 7:5d25857e708c3233ef4439df1f93f012 renameColumn newColumnName=EVENT_TIME, oldColumnName=TIME, tableName=EVENT_ENTITY \N 3.5.4 \N \N 3820445829 +1.2.0.Beta1 psilva@redhat.com META-INF/jpa-changelog-1.2.0.Beta1.xml 2022-02-02 16:47:26.130908 5 EXECUTED 7:c7a54a1041d58eb3817a4a883b4d4e84 delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION; createTable tableName=PROTOCOL_MAPPER; createTable tableName=PROTOCOL_MAPPER_CONFIG; createTable tableName=... \N 3.5.4 \N \N 3820445829 +1.2.0.Beta1 psilva@redhat.com META-INF/db2-jpa-changelog-1.2.0.Beta1.xml 2022-02-02 16:47:26.133863 6 MARK_RAN 7:2e01012df20974c1c2a605ef8afe25b7 delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION; createTable tableName=PROTOCOL_MAPPER; createTable tableName=PROTOCOL_MAPPER_CONFIG; createTable tableName=... \N 3.5.4 \N \N 3820445829 +1.2.0.RC1 bburke@redhat.com META-INF/jpa-changelog-1.2.0.CR1.xml 2022-02-02 16:47:26.183318 7 EXECUTED 7:0f08df48468428e0f30ee59a8ec01a41 delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete tableName=USER_SESSION; createTable tableName=MIGRATION_MODEL; createTable tableName=IDENTITY_P... \N 3.5.4 \N \N 3820445829 +1.2.0.RC1 bburke@redhat.com META-INF/db2-jpa-changelog-1.2.0.CR1.xml 2022-02-02 16:47:26.186858 8 MARK_RAN 7:a77ea2ad226b345e7d689d366f185c8c delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete tableName=USER_SESSION; createTable tableName=MIGRATION_MODEL; createTable tableName=IDENTITY_P... \N 3.5.4 \N \N 3820445829 +1.2.0.Final keycloak META-INF/jpa-changelog-1.2.0.Final.xml 2022-02-02 16:47:26.19172 9 EXECUTED 7:a3377a2059aefbf3b90ebb4c4cc8e2ab update tableName=CLIENT; update tableName=CLIENT; update tableName=CLIENT \N 3.5.4 \N \N 3820445829 +1.3.0 bburke@redhat.com META-INF/jpa-changelog-1.3.0.xml 2022-02-02 16:47:26.242162 10 EXECUTED 7:04c1dbedc2aa3e9756d1a1668e003451 delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_PROT_MAPPER; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete tableName=USER_SESSION; createTable tableName=ADMI... \N 3.5.4 \N \N 3820445829 +1.4.0 bburke@redhat.com META-INF/jpa-changelog-1.4.0.xml 2022-02-02 16:47:26.275929 11 EXECUTED 7:36ef39ed560ad07062d956db861042ba delete tableName=CLIENT_SESSION_AUTH_STATUS; delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_PROT_MAPPER; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete table... \N 3.5.4 \N \N 3820445829 +1.4.0 bburke@redhat.com META-INF/db2-jpa-changelog-1.4.0.xml 2022-02-02 16:47:26.278548 12 MARK_RAN 7:d909180b2530479a716d3f9c9eaea3d7 delete tableName=CLIENT_SESSION_AUTH_STATUS; delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_PROT_MAPPER; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete table... \N 3.5.4 \N \N 3820445829 +1.5.0 bburke@redhat.com META-INF/jpa-changelog-1.5.0.xml 2022-02-02 16:47:26.287616 13 EXECUTED 7:cf12b04b79bea5152f165eb41f3955f6 delete tableName=CLIENT_SESSION_AUTH_STATUS; delete tableName=CLIENT_SESSION_ROLE; delete tableName=CLIENT_SESSION_PROT_MAPPER; delete tableName=CLIENT_SESSION_NOTE; delete tableName=CLIENT_SESSION; delete tableName=USER_SESSION_NOTE; delete table... \N 3.5.4 \N \N 3820445829 +1.6.1_from15 mposolda@redhat.com META-INF/jpa-changelog-1.6.1.xml 2022-02-02 16:47:26.299798 14 EXECUTED 7:7e32c8f05c755e8675764e7d5f514509 addColumn tableName=REALM; addColumn tableName=KEYCLOAK_ROLE; addColumn tableName=CLIENT; createTable tableName=OFFLINE_USER_SESSION; createTable tableName=OFFLINE_CLIENT_SESSION; addPrimaryKey constraintName=CONSTRAINT_OFFL_US_SES_PK2, tableName=... \N 3.5.4 \N \N 3820445829 +1.6.1_from16-pre mposolda@redhat.com META-INF/jpa-changelog-1.6.1.xml 2022-02-02 16:47:26.302088 15 MARK_RAN 7:980ba23cc0ec39cab731ce903dd01291 delete tableName=OFFLINE_CLIENT_SESSION; delete tableName=OFFLINE_USER_SESSION \N 3.5.4 \N \N 3820445829 +1.6.1_from16 mposolda@redhat.com META-INF/jpa-changelog-1.6.1.xml 2022-02-02 16:47:26.303889 16 MARK_RAN 7:2fa220758991285312eb84f3b4ff5336 dropPrimaryKey constraintName=CONSTRAINT_OFFLINE_US_SES_PK, tableName=OFFLINE_USER_SESSION; dropPrimaryKey constraintName=CONSTRAINT_OFFLINE_CL_SES_PK, tableName=OFFLINE_CLIENT_SESSION; addColumn tableName=OFFLINE_USER_SESSION; update tableName=OF... \N 3.5.4 \N \N 3820445829 +1.6.1 mposolda@redhat.com META-INF/jpa-changelog-1.6.1.xml 2022-02-02 16:47:26.306641 17 EXECUTED 7:d41d8cd98f00b204e9800998ecf8427e empty \N 3.5.4 \N \N 3820445829 +1.7.0 bburke@redhat.com META-INF/jpa-changelog-1.7.0.xml 2022-02-02 16:47:26.338791 18 EXECUTED 7:91ace540896df890cc00a0490ee52bbc createTable tableName=KEYCLOAK_GROUP; createTable tableName=GROUP_ROLE_MAPPING; createTable tableName=GROUP_ATTRIBUTE; createTable tableName=USER_GROUP_MEMBERSHIP; createTable tableName=REALM_DEFAULT_GROUPS; addColumn tableName=IDENTITY_PROVIDER; ... \N 3.5.4 \N \N 3820445829 +1.8.0 mposolda@redhat.com META-INF/jpa-changelog-1.8.0.xml 2022-02-02 16:47:26.381463 19 EXECUTED 7:c31d1646dfa2618a9335c00e07f89f24 addColumn tableName=IDENTITY_PROVIDER; createTable tableName=CLIENT_TEMPLATE; createTable tableName=CLIENT_TEMPLATE_ATTRIBUTES; createTable tableName=TEMPLATE_SCOPE_MAPPING; dropNotNullConstraint columnName=CLIENT_ID, tableName=PROTOCOL_MAPPER; ad... \N 3.5.4 \N \N 3820445829 +1.8.0-2 keycloak META-INF/jpa-changelog-1.8.0.xml 2022-02-02 16:47:26.390165 20 EXECUTED 7:df8bc21027a4f7cbbb01f6344e89ce07 dropDefaultValue columnName=ALGORITHM, tableName=CREDENTIAL; update tableName=CREDENTIAL \N 3.5.4 \N \N 3820445829 +authz-3.4.0.CR1-resource-server-pk-change-part1 glavoie@gmail.com META-INF/jpa-changelog-authz-3.4.0.CR1.xml 2022-02-02 16:47:26.679075 45 EXECUTED 7:6a48ce645a3525488a90fbf76adf3bb3 addColumn tableName=RESOURCE_SERVER_POLICY; addColumn tableName=RESOURCE_SERVER_RESOURCE; addColumn tableName=RESOURCE_SERVER_SCOPE \N 3.5.4 \N \N 3820445829 +1.8.0 mposolda@redhat.com META-INF/db2-jpa-changelog-1.8.0.xml 2022-02-02 16:47:26.392862 21 MARK_RAN 7:f987971fe6b37d963bc95fee2b27f8df addColumn tableName=IDENTITY_PROVIDER; createTable tableName=CLIENT_TEMPLATE; createTable tableName=CLIENT_TEMPLATE_ATTRIBUTES; createTable tableName=TEMPLATE_SCOPE_MAPPING; dropNotNullConstraint columnName=CLIENT_ID, tableName=PROTOCOL_MAPPER; ad... \N 3.5.4 \N \N 3820445829 +1.8.0-2 keycloak META-INF/db2-jpa-changelog-1.8.0.xml 2022-02-02 16:47:26.395652 22 MARK_RAN 7:df8bc21027a4f7cbbb01f6344e89ce07 dropDefaultValue columnName=ALGORITHM, tableName=CREDENTIAL; update tableName=CREDENTIAL \N 3.5.4 \N \N 3820445829 +1.9.0 mposolda@redhat.com META-INF/jpa-changelog-1.9.0.xml 2022-02-02 16:47:26.40969 23 EXECUTED 7:ed2dc7f799d19ac452cbcda56c929e47 update tableName=REALM; update tableName=REALM; update tableName=REALM; update tableName=REALM; update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=REALM; update tableName=REALM; customChange; dr... \N 3.5.4 \N \N 3820445829 +1.9.1 keycloak META-INF/jpa-changelog-1.9.1.xml 2022-02-02 16:47:26.414344 24 EXECUTED 7:80b5db88a5dda36ece5f235be8757615 modifyDataType columnName=PRIVATE_KEY, tableName=REALM; modifyDataType columnName=PUBLIC_KEY, tableName=REALM; modifyDataType columnName=CERTIFICATE, tableName=REALM \N 3.5.4 \N \N 3820445829 +1.9.1 keycloak META-INF/db2-jpa-changelog-1.9.1.xml 2022-02-02 16:47:26.416193 25 MARK_RAN 7:1437310ed1305a9b93f8848f301726ce modifyDataType columnName=PRIVATE_KEY, tableName=REALM; modifyDataType columnName=CERTIFICATE, tableName=REALM \N 3.5.4 \N \N 3820445829 +1.9.2 keycloak META-INF/jpa-changelog-1.9.2.xml 2022-02-02 16:47:26.437367 26 EXECUTED 7:b82ffb34850fa0836be16deefc6a87c4 createIndex indexName=IDX_USER_EMAIL, tableName=USER_ENTITY; createIndex indexName=IDX_USER_ROLE_MAPPING, tableName=USER_ROLE_MAPPING; createIndex indexName=IDX_USER_GROUP_MAPPING, tableName=USER_GROUP_MEMBERSHIP; createIndex indexName=IDX_USER_CO... \N 3.5.4 \N \N 3820445829 +authz-2.0.0 psilva@redhat.com META-INF/jpa-changelog-authz-2.0.0.xml 2022-02-02 16:47:26.481647 27 EXECUTED 7:9cc98082921330d8d9266decdd4bd658 createTable tableName=RESOURCE_SERVER; addPrimaryKey constraintName=CONSTRAINT_FARS, tableName=RESOURCE_SERVER; addUniqueConstraint constraintName=UK_AU8TT6T700S9V50BU18WS5HA6, tableName=RESOURCE_SERVER; createTable tableName=RESOURCE_SERVER_RESOU... \N 3.5.4 \N \N 3820445829 +authz-2.5.1 psilva@redhat.com META-INF/jpa-changelog-authz-2.5.1.xml 2022-02-02 16:47:26.484459 28 EXECUTED 7:03d64aeed9cb52b969bd30a7ac0db57e update tableName=RESOURCE_SERVER_POLICY \N 3.5.4 \N \N 3820445829 +2.1.0-KEYCLOAK-5461 bburke@redhat.com META-INF/jpa-changelog-2.1.0.xml 2022-02-02 16:47:26.523006 29 EXECUTED 7:f1f9fd8710399d725b780f463c6b21cd createTable tableName=BROKER_LINK; createTable tableName=FED_USER_ATTRIBUTE; createTable tableName=FED_USER_CONSENT; createTable tableName=FED_USER_CONSENT_ROLE; createTable tableName=FED_USER_CONSENT_PROT_MAPPER; createTable tableName=FED_USER_CR... \N 3.5.4 \N \N 3820445829 +2.2.0 bburke@redhat.com META-INF/jpa-changelog-2.2.0.xml 2022-02-02 16:47:26.532066 30 EXECUTED 7:53188c3eb1107546e6f765835705b6c1 addColumn tableName=ADMIN_EVENT_ENTITY; createTable tableName=CREDENTIAL_ATTRIBUTE; createTable tableName=FED_CREDENTIAL_ATTRIBUTE; modifyDataType columnName=VALUE, tableName=CREDENTIAL; addForeignKeyConstraint baseTableName=FED_CREDENTIAL_ATTRIBU... \N 3.5.4 \N \N 3820445829 +2.3.0 bburke@redhat.com META-INF/jpa-changelog-2.3.0.xml 2022-02-02 16:47:26.541837 31 EXECUTED 7:d6e6f3bc57a0c5586737d1351725d4d4 createTable tableName=FEDERATED_USER; addPrimaryKey constraintName=CONSTR_FEDERATED_USER, tableName=FEDERATED_USER; dropDefaultValue columnName=TOTP, tableName=USER_ENTITY; dropColumn columnName=TOTP, tableName=USER_ENTITY; addColumn tableName=IDE... \N 3.5.4 \N \N 3820445829 +2.4.0 bburke@redhat.com META-INF/jpa-changelog-2.4.0.xml 2022-02-02 16:47:26.545809 32 EXECUTED 7:454d604fbd755d9df3fd9c6329043aa5 customChange \N 3.5.4 \N \N 3820445829 +2.5.0 bburke@redhat.com META-INF/jpa-changelog-2.5.0.xml 2022-02-02 16:47:26.549823 33 EXECUTED 7:57e98a3077e29caf562f7dbf80c72600 customChange; modifyDataType columnName=USER_ID, tableName=OFFLINE_USER_SESSION \N 3.5.4 \N \N 3820445829 +2.5.0-unicode-oracle hmlnarik@redhat.com META-INF/jpa-changelog-2.5.0.xml 2022-02-02 16:47:26.55176 34 MARK_RAN 7:e4c7e8f2256210aee71ddc42f538b57a modifyDataType columnName=DESCRIPTION, tableName=AUTHENTICATION_FLOW; modifyDataType columnName=DESCRIPTION, tableName=CLIENT_TEMPLATE; modifyDataType columnName=DESCRIPTION, tableName=RESOURCE_SERVER_POLICY; modifyDataType columnName=DESCRIPTION,... \N 3.5.4 \N \N 3820445829 +2.5.0-unicode-other-dbs hmlnarik@redhat.com META-INF/jpa-changelog-2.5.0.xml 2022-02-02 16:47:26.567305 35 EXECUTED 7:09a43c97e49bc626460480aa1379b522 modifyDataType columnName=DESCRIPTION, tableName=AUTHENTICATION_FLOW; modifyDataType columnName=DESCRIPTION, tableName=CLIENT_TEMPLATE; modifyDataType columnName=DESCRIPTION, tableName=RESOURCE_SERVER_POLICY; modifyDataType columnName=DESCRIPTION,... \N 3.5.4 \N \N 3820445829 +2.5.0-duplicate-email-support slawomir@dabek.name META-INF/jpa-changelog-2.5.0.xml 2022-02-02 16:47:26.570727 36 EXECUTED 7:26bfc7c74fefa9126f2ce702fb775553 addColumn tableName=REALM \N 3.5.4 \N \N 3820445829 +2.5.0-unique-group-names hmlnarik@redhat.com META-INF/jpa-changelog-2.5.0.xml 2022-02-02 16:47:26.578396 37 EXECUTED 7:a161e2ae671a9020fff61e996a207377 addUniqueConstraint constraintName=SIBLING_NAMES, tableName=KEYCLOAK_GROUP \N 3.5.4 \N \N 3820445829 +2.5.1 bburke@redhat.com META-INF/jpa-changelog-2.5.1.xml 2022-02-02 16:47:26.581391 38 EXECUTED 7:37fc1781855ac5388c494f1442b3f717 addColumn tableName=FED_USER_CONSENT \N 3.5.4 \N \N 3820445829 +3.0.0 bburke@redhat.com META-INF/jpa-changelog-3.0.0.xml 2022-02-02 16:47:26.584204 39 EXECUTED 7:13a27db0dae6049541136adad7261d27 addColumn tableName=IDENTITY_PROVIDER \N 3.5.4 \N \N 3820445829 +3.2.0-fix keycloak META-INF/jpa-changelog-3.2.0.xml 2022-02-02 16:47:26.585877 40 MARK_RAN 7:550300617e3b59e8af3a6294df8248a3 addNotNullConstraint columnName=REALM_ID, tableName=CLIENT_INITIAL_ACCESS \N 3.5.4 \N \N 3820445829 +3.2.0-fix-with-keycloak-5416 keycloak META-INF/jpa-changelog-3.2.0.xml 2022-02-02 16:47:26.587657 41 MARK_RAN 7:e3a9482b8931481dc2772a5c07c44f17 dropIndex indexName=IDX_CLIENT_INIT_ACC_REALM, tableName=CLIENT_INITIAL_ACCESS; addNotNullConstraint columnName=REALM_ID, tableName=CLIENT_INITIAL_ACCESS; createIndex indexName=IDX_CLIENT_INIT_ACC_REALM, tableName=CLIENT_INITIAL_ACCESS \N 3.5.4 \N \N 3820445829 +3.2.0-fix-offline-sessions hmlnarik META-INF/jpa-changelog-3.2.0.xml 2022-02-02 16:47:26.591561 42 EXECUTED 7:72b07d85a2677cb257edb02b408f332d customChange \N 3.5.4 \N \N 3820445829 +3.2.0-fixed keycloak META-INF/jpa-changelog-3.2.0.xml 2022-02-02 16:47:26.669981 43 EXECUTED 7:a72a7858967bd414835d19e04d880312 addColumn tableName=REALM; dropPrimaryKey constraintName=CONSTRAINT_OFFL_CL_SES_PK2, tableName=OFFLINE_CLIENT_SESSION; dropColumn columnName=CLIENT_SESSION_ID, tableName=OFFLINE_CLIENT_SESSION; addPrimaryKey constraintName=CONSTRAINT_OFFL_CL_SES_P... \N 3.5.4 \N \N 3820445829 +3.3.0 keycloak META-INF/jpa-changelog-3.3.0.xml 2022-02-02 16:47:26.673701 44 EXECUTED 7:94edff7cf9ce179e7e85f0cd78a3cf2c addColumn tableName=USER_ENTITY \N 3.5.4 \N \N 3820445829 +authz-3.4.0.CR1-resource-server-pk-change-part2-KEYCLOAK-6095 hmlnarik@redhat.com META-INF/jpa-changelog-authz-3.4.0.CR1.xml 2022-02-02 16:47:26.681987 46 EXECUTED 7:e64b5dcea7db06077c6e57d3b9e5ca14 customChange \N 3.5.4 \N \N 3820445829 +authz-3.4.0.CR1-resource-server-pk-change-part3-fixed glavoie@gmail.com META-INF/jpa-changelog-authz-3.4.0.CR1.xml 2022-02-02 16:47:26.683661 47 MARK_RAN 7:fd8cf02498f8b1e72496a20afc75178c dropIndex indexName=IDX_RES_SERV_POL_RES_SERV, tableName=RESOURCE_SERVER_POLICY; dropIndex indexName=IDX_RES_SRV_RES_RES_SRV, tableName=RESOURCE_SERVER_RESOURCE; dropIndex indexName=IDX_RES_SRV_SCOPE_RES_SRV, tableName=RESOURCE_SERVER_SCOPE \N 3.5.4 \N \N 3820445829 +authz-3.4.0.CR1-resource-server-pk-change-part3-fixed-nodropindex glavoie@gmail.com META-INF/jpa-changelog-authz-3.4.0.CR1.xml 2022-02-02 16:47:26.702743 48 EXECUTED 7:542794f25aa2b1fbabb7e577d6646319 addNotNullConstraint columnName=RESOURCE_SERVER_CLIENT_ID, tableName=RESOURCE_SERVER_POLICY; addNotNullConstraint columnName=RESOURCE_SERVER_CLIENT_ID, tableName=RESOURCE_SERVER_RESOURCE; addNotNullConstraint columnName=RESOURCE_SERVER_CLIENT_ID, ... \N 3.5.4 \N \N 3820445829 +authn-3.4.0.CR1-refresh-token-max-reuse glavoie@gmail.com META-INF/jpa-changelog-authz-3.4.0.CR1.xml 2022-02-02 16:47:26.706593 49 EXECUTED 7:edad604c882df12f74941dac3cc6d650 addColumn tableName=REALM \N 3.5.4 \N \N 3820445829 +3.4.0 keycloak META-INF/jpa-changelog-3.4.0.xml 2022-02-02 16:47:26.734467 50 EXECUTED 7:0f88b78b7b46480eb92690cbf5e44900 addPrimaryKey constraintName=CONSTRAINT_REALM_DEFAULT_ROLES, tableName=REALM_DEFAULT_ROLES; addPrimaryKey constraintName=CONSTRAINT_COMPOSITE_ROLE, tableName=COMPOSITE_ROLE; addPrimaryKey constraintName=CONSTR_REALM_DEFAULT_GROUPS, tableName=REALM... \N 3.5.4 \N \N 3820445829 +3.4.0-KEYCLOAK-5230 hmlnarik@redhat.com META-INF/jpa-changelog-3.4.0.xml 2022-02-02 16:47:26.78037 51 EXECUTED 7:d560e43982611d936457c327f872dd59 createIndex indexName=IDX_FU_ATTRIBUTE, tableName=FED_USER_ATTRIBUTE; createIndex indexName=IDX_FU_CONSENT, tableName=FED_USER_CONSENT; createIndex indexName=IDX_FU_CONSENT_RU, tableName=FED_USER_CONSENT; createIndex indexName=IDX_FU_CREDENTIAL, t... \N 3.5.4 \N \N 3820445829 +3.4.1 psilva@redhat.com META-INF/jpa-changelog-3.4.1.xml 2022-02-02 16:47:26.783989 52 EXECUTED 7:c155566c42b4d14ef07059ec3b3bbd8e modifyDataType columnName=VALUE, tableName=CLIENT_ATTRIBUTES \N 3.5.4 \N \N 3820445829 +3.4.2 keycloak META-INF/jpa-changelog-3.4.2.xml 2022-02-02 16:47:26.786619 53 EXECUTED 7:b40376581f12d70f3c89ba8ddf5b7dea update tableName=REALM \N 3.5.4 \N \N 3820445829 +3.4.2-KEYCLOAK-5172 mkanis@redhat.com META-INF/jpa-changelog-3.4.2.xml 2022-02-02 16:47:26.788788 54 EXECUTED 7:a1132cc395f7b95b3646146c2e38f168 update tableName=CLIENT \N 3.5.4 \N \N 3820445829 +4.0.0-KEYCLOAK-6335 bburke@redhat.com META-INF/jpa-changelog-4.0.0.xml 2022-02-02 16:47:26.794881 55 EXECUTED 7:d8dc5d89c789105cfa7ca0e82cba60af createTable tableName=CLIENT_AUTH_FLOW_BINDINGS; addPrimaryKey constraintName=C_CLI_FLOW_BIND, tableName=CLIENT_AUTH_FLOW_BINDINGS \N 3.5.4 \N \N 3820445829 +4.0.0-CLEANUP-UNUSED-TABLE bburke@redhat.com META-INF/jpa-changelog-4.0.0.xml 2022-02-02 16:47:26.799493 56 EXECUTED 7:7822e0165097182e8f653c35517656a3 dropTable tableName=CLIENT_IDENTITY_PROV_MAPPING \N 3.5.4 \N \N 3820445829 +4.0.0-KEYCLOAK-6228 bburke@redhat.com META-INF/jpa-changelog-4.0.0.xml 2022-02-02 16:47:26.810686 57 EXECUTED 7:c6538c29b9c9a08f9e9ea2de5c2b6375 dropUniqueConstraint constraintName=UK_JKUWUVD56ONTGSUHOGM8UEWRT, tableName=USER_CONSENT; dropNotNullConstraint columnName=CLIENT_ID, tableName=USER_CONSENT; addColumn tableName=USER_CONSENT; addUniqueConstraint constraintName=UK_JKUWUVD56ONTGSUHO... \N 3.5.4 \N \N 3820445829 +4.0.0-KEYCLOAK-5579-fixed mposolda@redhat.com META-INF/jpa-changelog-4.0.0.xml 2022-02-02 16:47:26.861332 58 EXECUTED 7:6d4893e36de22369cf73bcb051ded875 dropForeignKeyConstraint baseTableName=CLIENT_TEMPLATE_ATTRIBUTES, constraintName=FK_CL_TEMPL_ATTR_TEMPL; renameTable newTableName=CLIENT_SCOPE_ATTRIBUTES, oldTableName=CLIENT_TEMPLATE_ATTRIBUTES; renameColumn newColumnName=SCOPE_ID, oldColumnName... \N 3.5.4 \N \N 3820445829 +authz-4.0.0.CR1 psilva@redhat.com META-INF/jpa-changelog-authz-4.0.0.CR1.xml 2022-02-02 16:47:26.877018 59 EXECUTED 7:57960fc0b0f0dd0563ea6f8b2e4a1707 createTable tableName=RESOURCE_SERVER_PERM_TICKET; addPrimaryKey constraintName=CONSTRAINT_FAPMT, tableName=RESOURCE_SERVER_PERM_TICKET; addForeignKeyConstraint baseTableName=RESOURCE_SERVER_PERM_TICKET, constraintName=FK_FRSRHO213XCX4WNKOG82SSPMT... \N 3.5.4 \N \N 3820445829 +authz-4.0.0.Beta3 psilva@redhat.com META-INF/jpa-changelog-authz-4.0.0.Beta3.xml 2022-02-02 16:47:26.881203 60 EXECUTED 7:2b4b8bff39944c7097977cc18dbceb3b addColumn tableName=RESOURCE_SERVER_POLICY; addColumn tableName=RESOURCE_SERVER_PERM_TICKET; addForeignKeyConstraint baseTableName=RESOURCE_SERVER_PERM_TICKET, constraintName=FK_FRSRPO2128CX4WNKOG82SSRFY, referencedTableName=RESOURCE_SERVER_POLICY \N 3.5.4 \N \N 3820445829 +authz-4.2.0.Final mhajas@redhat.com META-INF/jpa-changelog-authz-4.2.0.Final.xml 2022-02-02 16:47:26.886177 61 EXECUTED 7:2aa42a964c59cd5b8ca9822340ba33a8 createTable tableName=RESOURCE_URIS; addForeignKeyConstraint baseTableName=RESOURCE_URIS, constraintName=FK_RESOURCE_SERVER_URIS, referencedTableName=RESOURCE_SERVER_RESOURCE; customChange; dropColumn columnName=URI, tableName=RESOURCE_SERVER_RESO... \N 3.5.4 \N \N 3820445829 +authz-4.2.0.Final-KEYCLOAK-9944 hmlnarik@redhat.com META-INF/jpa-changelog-authz-4.2.0.Final.xml 2022-02-02 16:47:26.890482 62 EXECUTED 7:9ac9e58545479929ba23f4a3087a0346 addPrimaryKey constraintName=CONSTRAINT_RESOUR_URIS_PK, tableName=RESOURCE_URIS \N 3.5.4 \N \N 3820445829 +4.2.0-KEYCLOAK-6313 wadahiro@gmail.com META-INF/jpa-changelog-4.2.0.xml 2022-02-02 16:47:26.893518 63 EXECUTED 7:14d407c35bc4fe1976867756bcea0c36 addColumn tableName=REQUIRED_ACTION_PROVIDER \N 3.5.4 \N \N 3820445829 +4.3.0-KEYCLOAK-7984 wadahiro@gmail.com META-INF/jpa-changelog-4.3.0.xml 2022-02-02 16:47:26.895621 64 EXECUTED 7:241a8030c748c8548e346adee548fa93 update tableName=REQUIRED_ACTION_PROVIDER \N 3.5.4 \N \N 3820445829 +4.6.0-KEYCLOAK-7950 psilva@redhat.com META-INF/jpa-changelog-4.6.0.xml 2022-02-02 16:47:26.89756 65 EXECUTED 7:7d3182f65a34fcc61e8d23def037dc3f update tableName=RESOURCE_SERVER_RESOURCE \N 3.5.4 \N \N 3820445829 +4.6.0-KEYCLOAK-8377 keycloak META-INF/jpa-changelog-4.6.0.xml 2022-02-02 16:47:26.908059 66 EXECUTED 7:b30039e00a0b9715d430d1b0636728fa createTable tableName=ROLE_ATTRIBUTE; addPrimaryKey constraintName=CONSTRAINT_ROLE_ATTRIBUTE_PK, tableName=ROLE_ATTRIBUTE; addForeignKeyConstraint baseTableName=ROLE_ATTRIBUTE, constraintName=FK_ROLE_ATTRIBUTE_ID, referencedTableName=KEYCLOAK_ROLE... \N 3.5.4 \N \N 3820445829 +4.6.0-KEYCLOAK-8555 gideonray@gmail.com META-INF/jpa-changelog-4.6.0.xml 2022-02-02 16:47:26.912693 67 EXECUTED 7:3797315ca61d531780f8e6f82f258159 createIndex indexName=IDX_COMPONENT_PROVIDER_TYPE, tableName=COMPONENT \N 3.5.4 \N \N 3820445829 +4.7.0-KEYCLOAK-1267 sguilhen@redhat.com META-INF/jpa-changelog-4.7.0.xml 2022-02-02 16:47:26.915771 68 EXECUTED 7:c7aa4c8d9573500c2d347c1941ff0301 addColumn tableName=REALM \N 3.5.4 \N \N 3820445829 +4.7.0-KEYCLOAK-7275 keycloak META-INF/jpa-changelog-4.7.0.xml 2022-02-02 16:47:26.924465 69 EXECUTED 7:b207faee394fc074a442ecd42185a5dd renameColumn newColumnName=CREATED_ON, oldColumnName=LAST_SESSION_REFRESH, tableName=OFFLINE_USER_SESSION; addNotNullConstraint columnName=CREATED_ON, tableName=OFFLINE_USER_SESSION; addColumn tableName=OFFLINE_USER_SESSION; customChange; createIn... \N 3.5.4 \N \N 3820445829 +4.8.0-KEYCLOAK-8835 sguilhen@redhat.com META-INF/jpa-changelog-4.8.0.xml 2022-02-02 16:47:26.928034 70 EXECUTED 7:ab9a9762faaba4ddfa35514b212c4922 addNotNullConstraint columnName=SSO_MAX_LIFESPAN_REMEMBER_ME, tableName=REALM; addNotNullConstraint columnName=SSO_IDLE_TIMEOUT_REMEMBER_ME, tableName=REALM \N 3.5.4 \N \N 3820445829 +authz-7.0.0-KEYCLOAK-10443 psilva@redhat.com META-INF/jpa-changelog-authz-7.0.0.xml 2022-02-02 16:47:26.93061 71 EXECUTED 7:b9710f74515a6ccb51b72dc0d19df8c4 addColumn tableName=RESOURCE_SERVER \N 3.5.4 \N \N 3820445829 +8.0.0-adding-credential-columns keycloak META-INF/jpa-changelog-8.0.0.xml 2022-02-02 16:47:26.933771 72 EXECUTED 7:ec9707ae4d4f0b7452fee20128083879 addColumn tableName=CREDENTIAL; addColumn tableName=FED_USER_CREDENTIAL \N 3.5.4 \N \N 3820445829 +8.0.0-updating-credential-data-not-oracle keycloak META-INF/jpa-changelog-8.0.0.xml 2022-02-02 16:47:26.937673 73 EXECUTED 7:03b3f4b264c3c68ba082250a80b74216 update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=FED_USER_CREDENTIAL; update tableName=FED_USER_CREDENTIAL; update tableName=FED_USER_CREDENTIAL \N 3.5.4 \N \N 3820445829 +8.0.0-updating-credential-data-oracle keycloak META-INF/jpa-changelog-8.0.0.xml 2022-02-02 16:47:26.939218 74 MARK_RAN 7:64c5728f5ca1f5aa4392217701c4fe23 update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=CREDENTIAL; update tableName=FED_USER_CREDENTIAL; update tableName=FED_USER_CREDENTIAL; update tableName=FED_USER_CREDENTIAL \N 3.5.4 \N \N 3820445829 +8.0.0-credential-cleanup-fixed keycloak META-INF/jpa-changelog-8.0.0.xml 2022-02-02 16:47:26.945819 75 EXECUTED 7:b48da8c11a3d83ddd6b7d0c8c2219345 dropDefaultValue columnName=COUNTER, tableName=CREDENTIAL; dropDefaultValue columnName=DIGITS, tableName=CREDENTIAL; dropDefaultValue columnName=PERIOD, tableName=CREDENTIAL; dropDefaultValue columnName=ALGORITHM, tableName=CREDENTIAL; dropColumn ... \N 3.5.4 \N \N 3820445829 +8.0.0-resource-tag-support keycloak META-INF/jpa-changelog-8.0.0.xml 2022-02-02 16:47:26.950255 76 EXECUTED 7:a73379915c23bfad3e8f5c6d5c0aa4bd addColumn tableName=MIGRATION_MODEL; createIndex indexName=IDX_UPDATE_TIME, tableName=MIGRATION_MODEL \N 3.5.4 \N \N 3820445829 +9.0.0-always-display-client keycloak META-INF/jpa-changelog-9.0.0.xml 2022-02-02 16:47:26.955505 77 EXECUTED 7:39e0073779aba192646291aa2332493d addColumn tableName=CLIENT \N 3.5.4 \N \N 3820445829 +9.0.0-drop-constraints-for-column-increase keycloak META-INF/jpa-changelog-9.0.0.xml 2022-02-02 16:47:26.957216 78 MARK_RAN 7:81f87368f00450799b4bf42ea0b3ec34 dropUniqueConstraint constraintName=UK_FRSR6T700S9V50BU18WS5PMT, tableName=RESOURCE_SERVER_PERM_TICKET; dropUniqueConstraint constraintName=UK_FRSR6T700S9V50BU18WS5HA6, tableName=RESOURCE_SERVER_RESOURCE; dropPrimaryKey constraintName=CONSTRAINT_O... \N 3.5.4 \N \N 3820445829 +9.0.0-increase-column-size-federated-fk keycloak META-INF/jpa-changelog-9.0.0.xml 2022-02-02 16:47:26.966746 79 EXECUTED 7:20b37422abb9fb6571c618148f013a15 modifyDataType columnName=CLIENT_ID, tableName=FED_USER_CONSENT; modifyDataType columnName=CLIENT_REALM_CONSTRAINT, tableName=KEYCLOAK_ROLE; modifyDataType columnName=OWNER, tableName=RESOURCE_SERVER_POLICY; modifyDataType columnName=CLIENT_ID, ta... \N 3.5.4 \N \N 3820445829 +9.0.0-recreate-constraints-after-column-increase keycloak META-INF/jpa-changelog-9.0.0.xml 2022-02-02 16:47:26.969643 80 MARK_RAN 7:1970bb6cfb5ee800736b95ad3fb3c78a addNotNullConstraint columnName=CLIENT_ID, tableName=OFFLINE_CLIENT_SESSION; addNotNullConstraint columnName=OWNER, tableName=RESOURCE_SERVER_PERM_TICKET; addNotNullConstraint columnName=REQUESTER, tableName=RESOURCE_SERVER_PERM_TICKET; addNotNull... \N 3.5.4 \N \N 3820445829 +9.0.1-add-index-to-client.client_id keycloak META-INF/jpa-changelog-9.0.1.xml 2022-02-02 16:47:26.975764 81 EXECUTED 7:45d9b25fc3b455d522d8dcc10a0f4c80 createIndex indexName=IDX_CLIENT_ID, tableName=CLIENT \N 3.5.4 \N \N 3820445829 +9.0.1-KEYCLOAK-12579-drop-constraints keycloak META-INF/jpa-changelog-9.0.1.xml 2022-02-02 16:47:26.977227 82 MARK_RAN 7:890ae73712bc187a66c2813a724d037f dropUniqueConstraint constraintName=SIBLING_NAMES, tableName=KEYCLOAK_GROUP \N 3.5.4 \N \N 3820445829 +9.0.1-KEYCLOAK-12579-add-not-null-constraint keycloak META-INF/jpa-changelog-9.0.1.xml 2022-02-02 16:47:26.980058 83 EXECUTED 7:0a211980d27fafe3ff50d19a3a29b538 addNotNullConstraint columnName=PARENT_GROUP, tableName=KEYCLOAK_GROUP \N 3.5.4 \N \N 3820445829 +9.0.1-KEYCLOAK-12579-recreate-constraints keycloak META-INF/jpa-changelog-9.0.1.xml 2022-02-02 16:47:26.981645 84 MARK_RAN 7:a161e2ae671a9020fff61e996a207377 addUniqueConstraint constraintName=SIBLING_NAMES, tableName=KEYCLOAK_GROUP \N 3.5.4 \N \N 3820445829 +9.0.1-add-index-to-events keycloak META-INF/jpa-changelog-9.0.1.xml 2022-02-02 16:47:26.985465 85 EXECUTED 7:01c49302201bdf815b0a18d1f98a55dc createIndex indexName=IDX_EVENT_TIME, tableName=EVENT_ENTITY \N 3.5.4 \N \N 3820445829 +map-remove-ri keycloak META-INF/jpa-changelog-11.0.0.xml 2022-02-02 16:47:26.98869 86 EXECUTED 7:3dace6b144c11f53f1ad2c0361279b86 dropForeignKeyConstraint baseTableName=REALM, constraintName=FK_TRAF444KK6QRKMS7N56AIWQ5Y; dropForeignKeyConstraint baseTableName=KEYCLOAK_ROLE, constraintName=FK_KJHO5LE2C0RAL09FL8CM9WFW9 \N 3.5.4 \N \N 3820445829 +map-remove-ri keycloak META-INF/jpa-changelog-12.0.0.xml 2022-02-02 16:47:26.992854 87 EXECUTED 7:578d0b92077eaf2ab95ad0ec087aa903 dropForeignKeyConstraint baseTableName=REALM_DEFAULT_GROUPS, constraintName=FK_DEF_GROUPS_GROUP; dropForeignKeyConstraint baseTableName=REALM_DEFAULT_ROLES, constraintName=FK_H4WPD7W4HSOOLNI3H0SW7BTJE; dropForeignKeyConstraint baseTableName=CLIENT... \N 3.5.4 \N \N 3820445829 +12.1.0-add-realm-localization-table keycloak META-INF/jpa-changelog-12.0.0.xml 2022-02-02 16:47:26.999694 88 EXECUTED 7:c95abe90d962c57a09ecaee57972835d createTable tableName=REALM_LOCALIZATIONS; addPrimaryKey tableName=REALM_LOCALIZATIONS \N 3.5.4 \N \N 3820445829 +\. + + +-- +-- Data for Name: databasechangeloglock; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.databasechangeloglock (id, locked, lockgranted, lockedby) FROM stdin; +1 f \N \N +1000 f \N \N +1001 f \N \N +\. + + +-- +-- Data for Name: default_client_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.default_client_scope (realm_id, scope_id, default_scope) FROM stdin; +master 0cc71c8c-fb37-41f2-b4d8-13210d3cf8be f +master 66deef47-2158-4d5b-a75f-0bf42f642e7b t +master 94ef659c-4c4a-4a33-98e8-bfcf443e9268 t +master 96a960d2-c203-4ef0-a53c-c3edd01f2305 f +master 3f705379-3361-486d-b75a-f7b4e4be492c f +master b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 t +master 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 t +master 42bfb506-bf0d-424e-8649-53a9a93d252d f +grafana 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f +grafana 74daf2cd-40d4-4304-87a8-92cdca808512 t +grafana 96d521d3-facc-4b5a-a8b4-a879bae6be07 t +grafana a5bb3a5f-fd26-4be6-9557-26e20a03d33d f +grafana d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f +grafana d6077ed7-b265-4f82-9336-24614967bd5d t +grafana 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t +grafana c61f5b19-c17e-49a1-91b8-a0296411b928 f +\. + + +-- +-- Data for Name: event_entity; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.event_entity (id, client_id, details_json, error, ip_address, realm_id, session_id, event_time, type, user_id) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_attribute (id, name, user_id, realm_id, storage_provider_id, value) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_consent; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_consent (id, client_id, user_id, realm_id, storage_provider_id, created_date, last_updated_date, client_storage_provider, external_client_id) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_consent_cl_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_consent_cl_scope (user_consent_id, scope_id) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_credential; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_credential (id, salt, type, created_date, user_id, realm_id, storage_provider_id, user_label, secret_data, credential_data, priority) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_group_membership; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_group_membership (group_id, user_id, realm_id, storage_provider_id) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_required_action; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_required_action (required_action, user_id, realm_id, storage_provider_id) FROM stdin; +\. + + +-- +-- Data for Name: fed_user_role_mapping; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.fed_user_role_mapping (role_id, user_id, realm_id, storage_provider_id) FROM stdin; +\. + + +-- +-- Data for Name: federated_identity; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.federated_identity (identity_provider, realm_id, federated_user_id, federated_username, token, user_id) FROM stdin; +\. + + +-- +-- Data for Name: federated_user; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.federated_user (id, storage_provider_id, realm_id) FROM stdin; +\. + + +-- +-- Data for Name: group_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.group_attribute (id, name, value, group_id) FROM stdin; +\. + + +-- +-- Data for Name: group_role_mapping; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.group_role_mapping (role_id, group_id) FROM stdin; +\. + + +-- +-- Data for Name: identity_provider; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.identity_provider (internal_id, enabled, provider_alias, provider_id, store_token, authenticate_by_default, realm_id, add_token_role, trust_email, first_broker_login_flow_id, post_broker_login_flow_id, provider_display_name, link_only) FROM stdin; +\. + + +-- +-- Data for Name: identity_provider_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.identity_provider_config (identity_provider_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: identity_provider_mapper; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.identity_provider_mapper (id, name, idp_alias, idp_mapper_name, realm_id) FROM stdin; +\. + + +-- +-- Data for Name: idp_mapper_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.idp_mapper_config (idp_mapper_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: keycloak_group; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.keycloak_group (id, name, parent_group, realm_id) FROM stdin; +\. + + +-- +-- Data for Name: keycloak_role; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.keycloak_role (id, client_realm_constraint, client_role, description, name, realm_id, client, realm) FROM stdin; +4a3204aa-320e-4584-b8ee-ea2989b3f330 master f ${role_admin} admin master \N master +847ebc80-6849-4c47-9f9e-5bba0c0d754d master f ${role_create-realm} create-realm master \N master +103dc6a6-5e7a-4c27-b4f0-9dbb1fdcf214 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_create-client} create-client master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +13c94e3b-b22f-4503-bc56-75e1bd2a927f 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-realm} view-realm master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +4364a376-4ed0-4051-aeab-609f62420e5d 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-users} view-users master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +f12af4b7-8828-47a5-abbc-dbb09b9d409e 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-clients} view-clients master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +2606a5b9-699b-488a-a819-d6f368e66697 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-events} view-events master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +2cf34980-2606-4faf-bc40-b9a47c69ef1c 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-identity-providers} view-identity-providers master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +13e61c6b-aff6-4ef8-ab56-ad4abefcb101 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_view-authorization} view-authorization master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +632bad74-a33f-4fd5-9393-ec0a07898b1a 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-realm} manage-realm master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +4607a008-f45c-45f5-b506-6de020b7e366 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-users} manage-users master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +edd471cc-81d5-43e4-bb43-41fe88ff537d 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-clients} manage-clients master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +4c2b4e2a-e792-4ffd-969d-e33ecdf7158f 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-events} manage-events master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +38282bc7-ea21-46db-a36e-ca621d3275b4 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-identity-providers} manage-identity-providers master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +12111f4a-16ee-4ee7-8576-7956b9440dc5 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_manage-authorization} manage-authorization master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +f417ae21-5fb4-40fb-bda8-54c61ce7461d 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_query-users} query-users master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +7adeaf33-05d3-4a81-a7bf-f99c721b5d9c 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_query-clients} query-clients master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +60870d03-d96a-4371-bdad-e3fac925a8df 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_query-realms} query-realms master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +94363dbd-a6b8-4678-8231-50208c32c22c 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_query-groups} query-groups master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +86a4b6a9-93db-4177-a72f-95fd937a2c8d eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_view-profile} view-profile master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +619ba870-921e-4f28-b26c-89b11f39dddf eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_manage-account} manage-account master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +a42d235d-2864-4a99-9592-211d89d0407d eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_manage-account-links} manage-account-links master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +44798fae-3813-41e6-9352-6fb2d28a15a6 eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_view-applications} view-applications master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +a1a08dbc-4553-4be7-85f5-88c417bdcd45 eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_view-consent} view-consent master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +828c3ba8-a13d-49f5-8975-8eb00afbf7de eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_manage-consent} manage-consent master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +f537ddeb-0973-445c-8f32-3beed99461ba eed689c6-49da-4d91-98eb-cd495bcc07a3 t ${role_delete-account} delete-account master eed689c6-49da-4d91-98eb-cd495bcc07a3 \N +102d3759-c50d-4325-932d-c7a02fc17cb8 1e30397c-eac2-41fb-87bc-d90484992e65 t ${role_read-token} read-token master 1e30397c-eac2-41fb-87bc-d90484992e65 \N +b44e0fe0-0fb7-4e12-a6f0-b352431a0f57 3cd285ea-0f6e-43b6-ab5c-d021c33a551b t ${role_impersonation} impersonation master 3cd285ea-0f6e-43b6-ab5c-d021c33a551b \N +16d5987b-dcbb-4650-8f52-3469f3974846 master f ${role_offline-access} offline_access master \N master +c014bfd1-a210-4e7a-8a26-35d1f5e8f1ed master f ${role_uma_authorization} uma_authorization master \N master +95dfed9c-47fe-489b-aa28-52f0d7aa7c49 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_create-client} create-client master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +07e1586d-a943-46d9-9c3d-1f3544c8c27f ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-realm} view-realm master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +293d0c06-6dce-4303-9cd3-dfdd6d1275b8 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-users} view-users master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +cfdeeb7b-c70e-496b-9605-70377168a6cb ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-clients} view-clients master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +74252705-a339-4513-97ca-d5617977d5ff ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-events} view-events master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +77c3f67e-21d7-4c18-9971-4baf4c20eeaa ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-identity-providers} view-identity-providers master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +5de01bf1-bfac-4ea2-8fb1-ed95594fe1da ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_view-authorization} view-authorization master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +a72adc0b-5220-48e4-a66a-9e15dca5f574 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-realm} manage-realm master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +f29b8efa-3c08-410a-a5c0-15b52253d2e2 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-users} manage-users master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +dd3ecc72-aaee-43d5-8f7e-f6dcdfb5a608 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-clients} manage-clients master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +9d5a8bab-e112-4e1c-8196-604f3d0143ea ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-events} manage-events master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +ffff4251-e0a4-4f9c-8bf6-5461b2f52766 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-identity-providers} manage-identity-providers master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +5fafdde9-71f7-4f67-9c1d-f3f4bc7f5128 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_manage-authorization} manage-authorization master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +6cfc2ac6-bdd7-4b90-ac16-27a75f2eb00a ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_query-users} query-users master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +c3ded8eb-c970-4e43-bea9-5e07795d20ef ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_query-clients} query-clients master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +811c2a39-6614-46fb-acf5-889d52248171 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_query-realms} query-realms master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +2a90f228-2ca4-413f-bc4b-7939af8abcbf ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_query-groups} query-groups master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +85afffb5-2069-4873-b6c8-08159c1e4bdd a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_realm-admin} realm-admin grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +d0e4028d-a604-427a-9262-a1a9513dafc8 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_create-client} create-client grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +2b8b60c5-d388-4925-b735-858df38dae6e a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-realm} view-realm grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +e9c997c8-ad6b-4a99-81e1-c248e94fbeac a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-users} view-users grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +8c4449b9-5add-40ba-a19f-cf5d80425e68 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-clients} view-clients grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +a5f31b90-986b-46d5-a385-a639b4e19e37 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-events} view-events grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +99bd546f-a5ed-47f8-862c-9a5e8345bf3b a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-identity-providers} view-identity-providers grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +9096d8df-9d5b-4fb5-b93e-49acc6df0be5 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_view-authorization} view-authorization grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +03230264-ed7a-46b2-939d-53ebe9a59812 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-realm} manage-realm grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +2240d1de-5ac4-44ac-91be-cee70e1dd22b a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-users} manage-users grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +6d2fd708-445b-44a8-b950-f1350a15dd14 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-clients} manage-clients grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +82266aa3-67ea-485a-a078-4671eb141853 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-events} manage-events grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +d6dad388-8c69-4bba-940e-371afc98042e a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-identity-providers} manage-identity-providers grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +5d7868e1-0c4a-46cc-8bac-bd19c0ea1bde a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_manage-authorization} manage-authorization grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +85e6229e-e246-4e9a-8b39-7bae49754f7d a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_query-users} query-users grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +bc618c28-98d1-477d-b4fc-c5ec7cd7f271 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_query-clients} query-clients grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +5059b239-0dce-4bb2-9c55-a6afc8dcbe3b a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_query-realms} query-realms grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +ac28461f-3416-4af4-be65-abc739dbeee5 a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_query-groups} query-groups grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +f1311ecb-6a6a-49d6-bb16-5132daf93a64 a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_view-profile} view-profile grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +18a7066b-fe71-410e-9581-69f78347ec29 a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_manage-account} manage-account grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +68fdbd76-8688-47a6-b68d-3298a5401f05 a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_manage-account-links} manage-account-links grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +cb37c15a-5330-4e30-9421-e0b962a266de a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_view-applications} view-applications grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +daaedcc6-e7a6-488e-921e-7022aa808da7 a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_view-consent} view-consent grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +c7e799a5-1250-4bc8-b7c6-ffdc58361477 a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_manage-consent} manage-consent grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +744bfdff-0e88-438a-b852-282a1b2aad3e a5a8fed6-0bca-4646-9946-2fe84175353b t ${role_delete-account} delete-account grafana a5a8fed6-0bca-4646-9946-2fe84175353b \N +b8a4faaf-86d9-43eb-bb18-0eaa654b35a7 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${role_impersonation} impersonation master ef7f6eac-9fff-44aa-a86c-5125d52acc82 \N +5e2301d7-2a9e-4f2d-a940-9bd442b15d8c a8698f4f-5fa1-4baa-be05-87d03052af49 t ${role_impersonation} impersonation grafana a8698f4f-5fa1-4baa-be05-87d03052af49 \N +77ba7b40-e312-40d7-9da0-de41f0ed3b8c 77ff47f8-f578-477d-8c06-e70a846332f5 t ${role_read-token} read-token grafana 77ff47f8-f578-477d-8c06-e70a846332f5 \N +c49bddc6-ec92-4caa-bc04-57ba80a92eb9 grafana f ${role_offline-access} offline_access grafana \N grafana +0f3d47bb-002a-4cd0-a502-725f224308a7 grafana f ${role_uma_authorization} uma_authorization grafana \N grafana +60f1b1ea-9059-41ea-acef-573643b24709 grafana f Grafana Organization Administrator admin grafana \N grafana +c029a218-4519-4537-ae12-d8f3c27a0003 grafana f Grafana Server Admin serveradmin grafana \N grafana +c9a776f9-2740-435f-a725-4dbcc17a6c91 grafana f Grafana Viewer viewer grafana \N grafana +c4c74006-c346-48cf-8cf1-1617e3e1cde1 grafana f Grafana Editor editor grafana \N grafana +\. + + +-- +-- Data for Name: migration_model; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.migration_model (id, version, update_time) FROM stdin; +g5slr 12.0.1 1643820448 +\. + + +-- +-- Data for Name: offline_client_session; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.offline_client_session (user_session_id, client_id, offline_flag, "timestamp", data, client_storage_provider, external_client_id) FROM stdin; +\. + + +-- +-- Data for Name: offline_user_session; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.offline_user_session (user_session_id, user_id, realm_id, created_on, offline_flag, data, last_session_refresh) FROM stdin; +\. + + +-- +-- Data for Name: policy_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.policy_config (policy_id, name, value) FROM stdin; +\. + + +-- +-- Data for Name: protocol_mapper; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.protocol_mapper (id, name, protocol, protocol_mapper_name, client_id, client_scope_id) FROM stdin; +e4931993-ceb0-4048-8a37-ca4f438099f3 audience resolve openid-connect oidc-audience-resolve-mapper 11c67f5b-dde7-4680-b05b-c9c59d78bda4 \N +c1c53a76-92ee-42b8-8420-92a815267f71 locale openid-connect oidc-usermodel-attribute-mapper 2f521d09-7304-4b5e-a94b-7cc7300b8b50 \N +ddc7c8be-0753-417f-9d0e-ea22008f23f9 full name openid-connect oidc-full-name-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 family name openid-connect oidc-usermodel-property-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +563b93e7-e66d-454f-9f34-4538cab6f260 given name openid-connect oidc-usermodel-property-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +30fa9092-eb2a-4a55-8d73-82e6e23334ec middle name openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +6aff4774-b4cf-4775-b4e5-0b20c549d181 nickname openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +5a603582-2511-483b-8e05-be891c7642b1 username openid-connect oidc-usermodel-property-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +9e111324-2508-4a4b-841a-19883a331f66 profile openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 picture openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +412ba9b5-f535-4263-9600-b23c2f682fc9 website openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +3741c094-0c4f-42fb-a178-89ceb85adeda gender openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 birthdate openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +7f9b6774-17f5-417a-8fad-576fc862920c zoneinfo openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 locale openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +1ab8f9c8-42cc-4604-8c04-43f8243acc9b updated at openid-connect oidc-usermodel-attribute-mapper \N 66deef47-2158-4d5b-a75f-0bf42f642e7b +b47f8f1c-0242-40c3-973a-d58a25022d6e email openid-connect oidc-usermodel-property-mapper \N 94ef659c-4c4a-4a33-98e8-bfcf443e9268 +a5fcd319-279d-4995-8896-4bf810343ad2 email verified openid-connect oidc-usermodel-property-mapper \N 94ef659c-4c4a-4a33-98e8-bfcf443e9268 +4d697f62-b924-4b0c-8202-0a82ee08684c address openid-connect oidc-address-mapper \N 96a960d2-c203-4ef0-a53c-c3edd01f2305 +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 phone number openid-connect oidc-usermodel-attribute-mapper \N 3f705379-3361-486d-b75a-f7b4e4be492c +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc phone number verified openid-connect oidc-usermodel-attribute-mapper \N 3f705379-3361-486d-b75a-f7b4e4be492c +bc41b27d-2e1b-48af-8184-e88e03f950e2 realm roles openid-connect oidc-usermodel-realm-role-mapper \N b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 +967cee35-09fd-400f-a634-db3fdbab2420 client roles openid-connect oidc-usermodel-client-role-mapper \N b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 +543542f4-71b1-4fef-8832-bb14b553ad9a audience resolve openid-connect oidc-audience-resolve-mapper \N b8a9cdd1-2f30-4e23-a721-78b01cfba1d7 +bf72f65b-9d9c-4c88-9cb9-478edbb721db allowed web origins openid-connect oidc-allowed-origins-mapper \N 619cf41a-5ff8-4a04-9f1e-50717e5f7ce8 +98402b93-9012-4e47-b008-99ffaf93043e upn openid-connect oidc-usermodel-property-mapper \N 42bfb506-bf0d-424e-8649-53a9a93d252d +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 groups openid-connect oidc-usermodel-realm-role-mapper \N 42bfb506-bf0d-424e-8649-53a9a93d252d +7be7c4e2-7281-4226-acec-77f77b3072dc audience resolve openid-connect oidc-audience-resolve-mapper 230081b5-9161-45c3-9e08-9eda5412f7f7 \N +c5adae03-51f5-4acb-baeb-c0241a16757e full name openid-connect oidc-full-name-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +6d019964-a5e5-4737-a8bf-90c34ce33c0f family name openid-connect oidc-usermodel-property-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 given name openid-connect oidc-usermodel-property-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +4cec49ad-50de-4fed-bf61-3928d88b9cfc middle name openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +21dd6189-62cb-4039-9590-9096ff6d14b2 nickname openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c username openid-connect oidc-usermodel-property-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +c21b39cc-c761-4cf4-a4a4-6de3ff05476d profile openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +aeec7bd1-953e-4ba0-b146-c87f1e20f73f picture openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +02f83a6b-7a50-4541-9b12-968a23e2cf78 website openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea gender openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 birthdate openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +49703eaa-a556-431d-b828-c64d8c791d00 zoneinfo openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +2b9ace9b-a654-4178-bb28-c8062569453c locale openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +60babdab-a8a4-41a4-98b0-08bd40182cdf updated at openid-connect oidc-usermodel-attribute-mapper \N 74daf2cd-40d4-4304-87a8-92cdca808512 +75ae2f8d-a382-47e7-978a-f51bf12b80ae email openid-connect oidc-usermodel-property-mapper \N 96d521d3-facc-4b5a-a8b4-a879bae6be07 +b75ba788-217a-47ad-bc81-2e8f4dcce913 email verified openid-connect oidc-usermodel-property-mapper \N 96d521d3-facc-4b5a-a8b4-a879bae6be07 +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 address openid-connect oidc-address-mapper \N a5bb3a5f-fd26-4be6-9557-26e20a03d33d +13c34a80-7711-4a0d-97b0-b29a501294fa phone number openid-connect oidc-usermodel-attribute-mapper \N d6ffe9fc-a03c-4496-85dc-dbb5e7754587 +b4854867-3bfb-409b-92a8-6ec37db17f99 phone number verified openid-connect oidc-usermodel-attribute-mapper \N d6ffe9fc-a03c-4496-85dc-dbb5e7754587 +1fc8999a-04d9-421b-8557-e417a3750358 realm roles openid-connect oidc-usermodel-realm-role-mapper \N d6077ed7-b265-4f82-9336-24614967bd5d +384e97dd-36ad-4b0e-af63-d0cb3a2153d4 allowed web origins openid-connect oidc-allowed-origins-mapper \N 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 +f03cac68-3f0e-4068-9adf-ee64567689a7 upn openid-connect oidc-usermodel-property-mapper \N c61f5b19-c17e-49a1-91b8-a0296411b928 +04183ee1-b558-4f63-839f-922d30b34a9e groups openid-connect oidc-usermodel-realm-role-mapper \N c61f5b19-c17e-49a1-91b8-a0296411b928 +df78645e-c32b-4160-b79f-42e622d71982 locale openid-connect oidc-usermodel-attribute-mapper 805aebc8-9d01-42b6-bcce-6ce48ca63ef0 \N +0108b99f-2f31-4e73-9597-cb29e0e8c486 username openid-connect oidc-usermodel-property-mapper \N f619a55a-d565-4cc0-8bf4-4dbaab5382fe +70b0a264-a7c3-43ff-b24f-14ca4f5f118e login openid-connect oidc-usermodel-property-mapper \N 0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 +2f8ee9af-b6dd-4790-9e7b-cce83a603566 name openid-connect oidc-full-name-mapper \N d4723cd4-f717-44b7-a9b0-6c32c5ecd23f +\. + + +-- +-- Data for Name: protocol_mapper_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.protocol_mapper_config (protocol_mapper_id, value, name) FROM stdin; +c1c53a76-92ee-42b8-8420-92a815267f71 true userinfo.token.claim +c1c53a76-92ee-42b8-8420-92a815267f71 locale user.attribute +c1c53a76-92ee-42b8-8420-92a815267f71 true id.token.claim +c1c53a76-92ee-42b8-8420-92a815267f71 true access.token.claim +c1c53a76-92ee-42b8-8420-92a815267f71 locale claim.name +c1c53a76-92ee-42b8-8420-92a815267f71 String jsonType.label +ddc7c8be-0753-417f-9d0e-ea22008f23f9 true userinfo.token.claim +ddc7c8be-0753-417f-9d0e-ea22008f23f9 true id.token.claim +ddc7c8be-0753-417f-9d0e-ea22008f23f9 true access.token.claim +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 true userinfo.token.claim +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 lastName user.attribute +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 true id.token.claim +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 true access.token.claim +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 family_name claim.name +fc77e0b8-b586-40dc-bc3d-7aa04a9c0f19 String jsonType.label +563b93e7-e66d-454f-9f34-4538cab6f260 true userinfo.token.claim +563b93e7-e66d-454f-9f34-4538cab6f260 firstName user.attribute +563b93e7-e66d-454f-9f34-4538cab6f260 true id.token.claim +563b93e7-e66d-454f-9f34-4538cab6f260 true access.token.claim +563b93e7-e66d-454f-9f34-4538cab6f260 given_name claim.name +563b93e7-e66d-454f-9f34-4538cab6f260 String jsonType.label +30fa9092-eb2a-4a55-8d73-82e6e23334ec true userinfo.token.claim +30fa9092-eb2a-4a55-8d73-82e6e23334ec middleName user.attribute +30fa9092-eb2a-4a55-8d73-82e6e23334ec true id.token.claim +30fa9092-eb2a-4a55-8d73-82e6e23334ec true access.token.claim +30fa9092-eb2a-4a55-8d73-82e6e23334ec middle_name claim.name +30fa9092-eb2a-4a55-8d73-82e6e23334ec String jsonType.label +6aff4774-b4cf-4775-b4e5-0b20c549d181 true userinfo.token.claim +6aff4774-b4cf-4775-b4e5-0b20c549d181 nickname user.attribute +6aff4774-b4cf-4775-b4e5-0b20c549d181 true id.token.claim +6aff4774-b4cf-4775-b4e5-0b20c549d181 true access.token.claim +6aff4774-b4cf-4775-b4e5-0b20c549d181 nickname claim.name +6aff4774-b4cf-4775-b4e5-0b20c549d181 String jsonType.label +5a603582-2511-483b-8e05-be891c7642b1 true userinfo.token.claim +5a603582-2511-483b-8e05-be891c7642b1 username user.attribute +5a603582-2511-483b-8e05-be891c7642b1 true id.token.claim +5a603582-2511-483b-8e05-be891c7642b1 true access.token.claim +5a603582-2511-483b-8e05-be891c7642b1 preferred_username claim.name +5a603582-2511-483b-8e05-be891c7642b1 String jsonType.label +9e111324-2508-4a4b-841a-19883a331f66 true userinfo.token.claim +9e111324-2508-4a4b-841a-19883a331f66 profile user.attribute +9e111324-2508-4a4b-841a-19883a331f66 true id.token.claim +9e111324-2508-4a4b-841a-19883a331f66 true access.token.claim +9e111324-2508-4a4b-841a-19883a331f66 profile claim.name +9e111324-2508-4a4b-841a-19883a331f66 String jsonType.label +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 true userinfo.token.claim +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 picture user.attribute +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 true id.token.claim +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 true access.token.claim +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 picture claim.name +5906ef3c-7b55-4b10-9ba1-0f3a25f3b005 String jsonType.label +412ba9b5-f535-4263-9600-b23c2f682fc9 true userinfo.token.claim +412ba9b5-f535-4263-9600-b23c2f682fc9 website user.attribute +412ba9b5-f535-4263-9600-b23c2f682fc9 true id.token.claim +412ba9b5-f535-4263-9600-b23c2f682fc9 true access.token.claim +412ba9b5-f535-4263-9600-b23c2f682fc9 website claim.name +412ba9b5-f535-4263-9600-b23c2f682fc9 String jsonType.label +3741c094-0c4f-42fb-a178-89ceb85adeda true userinfo.token.claim +3741c094-0c4f-42fb-a178-89ceb85adeda gender user.attribute +3741c094-0c4f-42fb-a178-89ceb85adeda true id.token.claim +3741c094-0c4f-42fb-a178-89ceb85adeda true access.token.claim +3741c094-0c4f-42fb-a178-89ceb85adeda gender claim.name +3741c094-0c4f-42fb-a178-89ceb85adeda String jsonType.label +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 true userinfo.token.claim +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 birthdate user.attribute +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 true id.token.claim +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 true access.token.claim +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 birthdate claim.name +ae6e2dbc-b310-4443-acd2-894d4e9dcb79 String jsonType.label +7f9b6774-17f5-417a-8fad-576fc862920c true userinfo.token.claim +7f9b6774-17f5-417a-8fad-576fc862920c zoneinfo user.attribute +7f9b6774-17f5-417a-8fad-576fc862920c true id.token.claim +7f9b6774-17f5-417a-8fad-576fc862920c true access.token.claim +7f9b6774-17f5-417a-8fad-576fc862920c zoneinfo claim.name +7f9b6774-17f5-417a-8fad-576fc862920c String jsonType.label +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 true userinfo.token.claim +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 locale user.attribute +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 true id.token.claim +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 true access.token.claim +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 locale claim.name +7257c710-d01b-4c50-bb4f-060cfc8fe4b3 String jsonType.label +1ab8f9c8-42cc-4604-8c04-43f8243acc9b true userinfo.token.claim +1ab8f9c8-42cc-4604-8c04-43f8243acc9b updatedAt user.attribute +1ab8f9c8-42cc-4604-8c04-43f8243acc9b true id.token.claim +1ab8f9c8-42cc-4604-8c04-43f8243acc9b true access.token.claim +1ab8f9c8-42cc-4604-8c04-43f8243acc9b updated_at claim.name +1ab8f9c8-42cc-4604-8c04-43f8243acc9b String jsonType.label +b47f8f1c-0242-40c3-973a-d58a25022d6e true userinfo.token.claim +b47f8f1c-0242-40c3-973a-d58a25022d6e email user.attribute +b47f8f1c-0242-40c3-973a-d58a25022d6e true id.token.claim +b47f8f1c-0242-40c3-973a-d58a25022d6e true access.token.claim +b47f8f1c-0242-40c3-973a-d58a25022d6e email claim.name +b47f8f1c-0242-40c3-973a-d58a25022d6e String jsonType.label +a5fcd319-279d-4995-8896-4bf810343ad2 true userinfo.token.claim +a5fcd319-279d-4995-8896-4bf810343ad2 emailVerified user.attribute +a5fcd319-279d-4995-8896-4bf810343ad2 true id.token.claim +a5fcd319-279d-4995-8896-4bf810343ad2 true access.token.claim +a5fcd319-279d-4995-8896-4bf810343ad2 email_verified claim.name +a5fcd319-279d-4995-8896-4bf810343ad2 boolean jsonType.label +4d697f62-b924-4b0c-8202-0a82ee08684c formatted user.attribute.formatted +4d697f62-b924-4b0c-8202-0a82ee08684c country user.attribute.country +4d697f62-b924-4b0c-8202-0a82ee08684c postal_code user.attribute.postal_code +4d697f62-b924-4b0c-8202-0a82ee08684c true userinfo.token.claim +4d697f62-b924-4b0c-8202-0a82ee08684c street user.attribute.street +4d697f62-b924-4b0c-8202-0a82ee08684c true id.token.claim +4d697f62-b924-4b0c-8202-0a82ee08684c region user.attribute.region +4d697f62-b924-4b0c-8202-0a82ee08684c true access.token.claim +4d697f62-b924-4b0c-8202-0a82ee08684c locality user.attribute.locality +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 true userinfo.token.claim +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 phoneNumber user.attribute +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 true id.token.claim +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 true access.token.claim +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 phone_number claim.name +d1eaf34e-6818-419c-b3c1-8f1b3627ca17 String jsonType.label +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc true userinfo.token.claim +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc phoneNumberVerified user.attribute +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc true id.token.claim +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc true access.token.claim +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc phone_number_verified claim.name +ee0ec8fa-c020-4cb9-991e-30180fe0c5dc boolean jsonType.label +bc41b27d-2e1b-48af-8184-e88e03f950e2 true multivalued +bc41b27d-2e1b-48af-8184-e88e03f950e2 foo user.attribute +bc41b27d-2e1b-48af-8184-e88e03f950e2 true access.token.claim +bc41b27d-2e1b-48af-8184-e88e03f950e2 realm_access.roles claim.name +bc41b27d-2e1b-48af-8184-e88e03f950e2 String jsonType.label +967cee35-09fd-400f-a634-db3fdbab2420 true multivalued +967cee35-09fd-400f-a634-db3fdbab2420 foo user.attribute +967cee35-09fd-400f-a634-db3fdbab2420 true access.token.claim +967cee35-09fd-400f-a634-db3fdbab2420 resource_access.${client_id}.roles claim.name +967cee35-09fd-400f-a634-db3fdbab2420 String jsonType.label +98402b93-9012-4e47-b008-99ffaf93043e true userinfo.token.claim +98402b93-9012-4e47-b008-99ffaf93043e username user.attribute +98402b93-9012-4e47-b008-99ffaf93043e true id.token.claim +98402b93-9012-4e47-b008-99ffaf93043e true access.token.claim +98402b93-9012-4e47-b008-99ffaf93043e upn claim.name +98402b93-9012-4e47-b008-99ffaf93043e String jsonType.label +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 true multivalued +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 foo user.attribute +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 true id.token.claim +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 true access.token.claim +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 groups claim.name +39d571e6-0b8b-4b6d-aa2d-9cff126decd0 String jsonType.label +c5adae03-51f5-4acb-baeb-c0241a16757e true userinfo.token.claim +c5adae03-51f5-4acb-baeb-c0241a16757e true id.token.claim +c5adae03-51f5-4acb-baeb-c0241a16757e true access.token.claim +6d019964-a5e5-4737-a8bf-90c34ce33c0f true userinfo.token.claim +6d019964-a5e5-4737-a8bf-90c34ce33c0f lastName user.attribute +6d019964-a5e5-4737-a8bf-90c34ce33c0f true id.token.claim +6d019964-a5e5-4737-a8bf-90c34ce33c0f true access.token.claim +6d019964-a5e5-4737-a8bf-90c34ce33c0f family_name claim.name +6d019964-a5e5-4737-a8bf-90c34ce33c0f String jsonType.label +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 true userinfo.token.claim +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 firstName user.attribute +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 true id.token.claim +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 true access.token.claim +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 given_name claim.name +e9cb431c-e1f1-4ce9-941e-a8a88bfce413 String jsonType.label +4cec49ad-50de-4fed-bf61-3928d88b9cfc true userinfo.token.claim +4cec49ad-50de-4fed-bf61-3928d88b9cfc middleName user.attribute +4cec49ad-50de-4fed-bf61-3928d88b9cfc true id.token.claim +4cec49ad-50de-4fed-bf61-3928d88b9cfc true access.token.claim +4cec49ad-50de-4fed-bf61-3928d88b9cfc middle_name claim.name +4cec49ad-50de-4fed-bf61-3928d88b9cfc String jsonType.label +21dd6189-62cb-4039-9590-9096ff6d14b2 true userinfo.token.claim +21dd6189-62cb-4039-9590-9096ff6d14b2 nickname user.attribute +21dd6189-62cb-4039-9590-9096ff6d14b2 true id.token.claim +21dd6189-62cb-4039-9590-9096ff6d14b2 true access.token.claim +21dd6189-62cb-4039-9590-9096ff6d14b2 nickname claim.name +21dd6189-62cb-4039-9590-9096ff6d14b2 String jsonType.label +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c true userinfo.token.claim +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c username user.attribute +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c true id.token.claim +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c true access.token.claim +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c preferred_username claim.name +bcb6bed8-ebfc-450b-b4a6-17f5bdfaa37c String jsonType.label +c21b39cc-c761-4cf4-a4a4-6de3ff05476d true userinfo.token.claim +c21b39cc-c761-4cf4-a4a4-6de3ff05476d profile user.attribute +c21b39cc-c761-4cf4-a4a4-6de3ff05476d true id.token.claim +c21b39cc-c761-4cf4-a4a4-6de3ff05476d true access.token.claim +c21b39cc-c761-4cf4-a4a4-6de3ff05476d profile claim.name +c21b39cc-c761-4cf4-a4a4-6de3ff05476d String jsonType.label +aeec7bd1-953e-4ba0-b146-c87f1e20f73f true userinfo.token.claim +aeec7bd1-953e-4ba0-b146-c87f1e20f73f picture user.attribute +aeec7bd1-953e-4ba0-b146-c87f1e20f73f true id.token.claim +aeec7bd1-953e-4ba0-b146-c87f1e20f73f true access.token.claim +aeec7bd1-953e-4ba0-b146-c87f1e20f73f picture claim.name +aeec7bd1-953e-4ba0-b146-c87f1e20f73f String jsonType.label +02f83a6b-7a50-4541-9b12-968a23e2cf78 true userinfo.token.claim +02f83a6b-7a50-4541-9b12-968a23e2cf78 website user.attribute +02f83a6b-7a50-4541-9b12-968a23e2cf78 true id.token.claim +02f83a6b-7a50-4541-9b12-968a23e2cf78 true access.token.claim +02f83a6b-7a50-4541-9b12-968a23e2cf78 website claim.name +02f83a6b-7a50-4541-9b12-968a23e2cf78 String jsonType.label +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea true userinfo.token.claim +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea gender user.attribute +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea true id.token.claim +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea true access.token.claim +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea gender claim.name +013a3f59-6a7f-42e4-9fce-4fc420a1b3ea String jsonType.label +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 true userinfo.token.claim +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 birthdate user.attribute +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 true id.token.claim +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 true access.token.claim +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 birthdate claim.name +04b7ca11-80bd-44a1-87c3-835e7fb9e9f5 String jsonType.label +49703eaa-a556-431d-b828-c64d8c791d00 true userinfo.token.claim +49703eaa-a556-431d-b828-c64d8c791d00 zoneinfo user.attribute +49703eaa-a556-431d-b828-c64d8c791d00 true id.token.claim +49703eaa-a556-431d-b828-c64d8c791d00 true access.token.claim +49703eaa-a556-431d-b828-c64d8c791d00 zoneinfo claim.name +49703eaa-a556-431d-b828-c64d8c791d00 String jsonType.label +2b9ace9b-a654-4178-bb28-c8062569453c true userinfo.token.claim +2b9ace9b-a654-4178-bb28-c8062569453c locale user.attribute +2b9ace9b-a654-4178-bb28-c8062569453c true id.token.claim +2b9ace9b-a654-4178-bb28-c8062569453c true access.token.claim +2b9ace9b-a654-4178-bb28-c8062569453c locale claim.name +2b9ace9b-a654-4178-bb28-c8062569453c String jsonType.label +60babdab-a8a4-41a4-98b0-08bd40182cdf true userinfo.token.claim +60babdab-a8a4-41a4-98b0-08bd40182cdf updatedAt user.attribute +60babdab-a8a4-41a4-98b0-08bd40182cdf true id.token.claim +60babdab-a8a4-41a4-98b0-08bd40182cdf true access.token.claim +60babdab-a8a4-41a4-98b0-08bd40182cdf updated_at claim.name +60babdab-a8a4-41a4-98b0-08bd40182cdf String jsonType.label +75ae2f8d-a382-47e7-978a-f51bf12b80ae true userinfo.token.claim +75ae2f8d-a382-47e7-978a-f51bf12b80ae email user.attribute +75ae2f8d-a382-47e7-978a-f51bf12b80ae true id.token.claim +75ae2f8d-a382-47e7-978a-f51bf12b80ae true access.token.claim +75ae2f8d-a382-47e7-978a-f51bf12b80ae email claim.name +75ae2f8d-a382-47e7-978a-f51bf12b80ae String jsonType.label +b75ba788-217a-47ad-bc81-2e8f4dcce913 true userinfo.token.claim +b75ba788-217a-47ad-bc81-2e8f4dcce913 emailVerified user.attribute +b75ba788-217a-47ad-bc81-2e8f4dcce913 true id.token.claim +b75ba788-217a-47ad-bc81-2e8f4dcce913 true access.token.claim +b75ba788-217a-47ad-bc81-2e8f4dcce913 email_verified claim.name +b75ba788-217a-47ad-bc81-2e8f4dcce913 boolean jsonType.label +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 formatted user.attribute.formatted +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 country user.attribute.country +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 postal_code user.attribute.postal_code +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 true userinfo.token.claim +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 street user.attribute.street +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 true id.token.claim +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 region user.attribute.region +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 true access.token.claim +c83418a1-6b68-4fd7-8b97-d22f0e2e0ad0 locality user.attribute.locality +13c34a80-7711-4a0d-97b0-b29a501294fa true userinfo.token.claim +13c34a80-7711-4a0d-97b0-b29a501294fa phoneNumber user.attribute +13c34a80-7711-4a0d-97b0-b29a501294fa true id.token.claim +13c34a80-7711-4a0d-97b0-b29a501294fa true access.token.claim +13c34a80-7711-4a0d-97b0-b29a501294fa phone_number claim.name +13c34a80-7711-4a0d-97b0-b29a501294fa String jsonType.label +b4854867-3bfb-409b-92a8-6ec37db17f99 true userinfo.token.claim +b4854867-3bfb-409b-92a8-6ec37db17f99 phoneNumberVerified user.attribute +b4854867-3bfb-409b-92a8-6ec37db17f99 true id.token.claim +b4854867-3bfb-409b-92a8-6ec37db17f99 true access.token.claim +b4854867-3bfb-409b-92a8-6ec37db17f99 phone_number_verified claim.name +b4854867-3bfb-409b-92a8-6ec37db17f99 boolean jsonType.label +1fc8999a-04d9-421b-8557-e417a3750358 true multivalued +1fc8999a-04d9-421b-8557-e417a3750358 foo user.attribute +1fc8999a-04d9-421b-8557-e417a3750358 true access.token.claim +1fc8999a-04d9-421b-8557-e417a3750358 String jsonType.label +f03cac68-3f0e-4068-9adf-ee64567689a7 true userinfo.token.claim +f03cac68-3f0e-4068-9adf-ee64567689a7 username user.attribute +f03cac68-3f0e-4068-9adf-ee64567689a7 true id.token.claim +f03cac68-3f0e-4068-9adf-ee64567689a7 true access.token.claim +f03cac68-3f0e-4068-9adf-ee64567689a7 upn claim.name +f03cac68-3f0e-4068-9adf-ee64567689a7 String jsonType.label +04183ee1-b558-4f63-839f-922d30b34a9e true multivalued +04183ee1-b558-4f63-839f-922d30b34a9e foo user.attribute +04183ee1-b558-4f63-839f-922d30b34a9e true id.token.claim +04183ee1-b558-4f63-839f-922d30b34a9e true access.token.claim +04183ee1-b558-4f63-839f-922d30b34a9e groups claim.name +04183ee1-b558-4f63-839f-922d30b34a9e String jsonType.label +df78645e-c32b-4160-b79f-42e622d71982 true userinfo.token.claim +df78645e-c32b-4160-b79f-42e622d71982 locale user.attribute +df78645e-c32b-4160-b79f-42e622d71982 true id.token.claim +df78645e-c32b-4160-b79f-42e622d71982 true access.token.claim +df78645e-c32b-4160-b79f-42e622d71982 locale claim.name +df78645e-c32b-4160-b79f-42e622d71982 String jsonType.label +0108b99f-2f31-4e73-9597-cb29e0e8c486 true userinfo.token.claim +0108b99f-2f31-4e73-9597-cb29e0e8c486 username user.attribute +0108b99f-2f31-4e73-9597-cb29e0e8c486 true id.token.claim +0108b99f-2f31-4e73-9597-cb29e0e8c486 true access.token.claim +0108b99f-2f31-4e73-9597-cb29e0e8c486 preferred_username claim.name +0108b99f-2f31-4e73-9597-cb29e0e8c486 String jsonType.label +1fc8999a-04d9-421b-8557-e417a3750358 true userinfo.token.claim +1fc8999a-04d9-421b-8557-e417a3750358 roles claim.name +70b0a264-a7c3-43ff-b24f-14ca4f5f118e true userinfo.token.claim +70b0a264-a7c3-43ff-b24f-14ca4f5f118e username user.attribute +70b0a264-a7c3-43ff-b24f-14ca4f5f118e true id.token.claim +70b0a264-a7c3-43ff-b24f-14ca4f5f118e true access.token.claim +70b0a264-a7c3-43ff-b24f-14ca4f5f118e login claim.name +70b0a264-a7c3-43ff-b24f-14ca4f5f118e String jsonType.label +2f8ee9af-b6dd-4790-9e7b-cce83a603566 true id.token.claim +2f8ee9af-b6dd-4790-9e7b-cce83a603566 true access.token.claim +2f8ee9af-b6dd-4790-9e7b-cce83a603566 true userinfo.token.claim +1fc8999a-04d9-421b-8557-e417a3750358 true id.token.claim +\. + + +-- +-- Data for Name: realm; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm (id, access_code_lifespan, user_action_lifespan, access_token_lifespan, account_theme, admin_theme, email_theme, enabled, events_enabled, events_expiration, login_theme, name, not_before, password_policy, registration_allowed, remember_me, reset_password_allowed, social, ssl_required, sso_idle_timeout, sso_max_lifespan, update_profile_on_soc_login, verify_email, master_admin_client, login_lifespan, internationalization_enabled, default_locale, reg_email_as_username, admin_events_enabled, admin_events_details_enabled, edit_username_allowed, otp_policy_counter, otp_policy_window, otp_policy_period, otp_policy_digits, otp_policy_alg, otp_policy_type, browser_flow, registration_flow, direct_grant_flow, reset_credentials_flow, client_auth_flow, offline_session_idle_timeout, revoke_refresh_token, access_token_life_implicit, login_with_email_allowed, duplicate_emails_allowed, docker_auth_flow, refresh_token_max_reuse, allow_user_managed_access, sso_max_lifespan_remember_me, sso_idle_timeout_remember_me) FROM stdin; +master 60 300 60 \N \N \N t f 0 \N master 1643820855 \N f f f f EXTERNAL 1800 36000 f f 3cd285ea-0f6e-43b6-ab5c-d021c33a551b 1800 f \N f f f f 0 1 30 6 HmacSHA1 totp ef998ef5-ca12-45db-a252-2e71b1419039 1695e7d2-ad80-4502-8479-8121a6e2a2f0 5f6f801e-0588-4a6e-860a-35483f5c1ec7 954b046d-2b24-405e-84ee-c44ffe603df2 023dc515-c259-42bb-88a8-2e8d84abca92 2592000 f 900 t f 032b05cf-0007-44da-a370-b42039f6b762 0 f 0 0 +grafana 60 300 300 \N \N \N t f 0 \N grafana 1643820879 \N f f f f EXTERNAL 1800 36000 f f ef7f6eac-9fff-44aa-a86c-5125d52acc82 1800 f \N f f f f 0 1 30 6 HmacSHA1 totp a38aeb47-f27e-4e68-82ff-7cc7371a47a7 9d02badd-cb1c-4655-bf5e-f888861433ff b478ecfb-db7e-4797-a245-8fc3b4dec884 3085fb68-fc1f-4e1c-a8be-33fb45194b04 cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2592000 f 900 t f 95e02703-f5bc-4e04-8bef-f6adc2d8173f 0 f 0 0 +\. + + +-- +-- Data for Name: realm_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_attribute (name, value, realm_id) FROM stdin; +_browser_header.contentSecurityPolicyReportOnly master +_browser_header.xContentTypeOptions nosniff master +_browser_header.xRobotsTag none master +_browser_header.xFrameOptions SAMEORIGIN master +_browser_header.contentSecurityPolicy frame-src 'self'; frame-ancestors 'self'; object-src 'none'; master +_browser_header.xXSSProtection 1; mode=block master +_browser_header.strictTransportSecurity max-age=31536000; includeSubDomains master +bruteForceProtected false master +permanentLockout false master +maxFailureWaitSeconds 900 master +minimumQuickLoginWaitSeconds 60 master +waitIncrementSeconds 60 master +quickLoginCheckMilliSeconds 1000 master +maxDeltaTimeSeconds 43200 master +failureFactor 30 master +displayName Keycloak master +displayNameHtml
Keycloak
master +offlineSessionMaxLifespanEnabled false master +offlineSessionMaxLifespan 5184000 master +_browser_header.contentSecurityPolicyReportOnly grafana +_browser_header.xContentTypeOptions nosniff grafana +_browser_header.xRobotsTag none grafana +_browser_header.xFrameOptions SAMEORIGIN grafana +_browser_header.contentSecurityPolicy frame-src 'self'; frame-ancestors 'self'; object-src 'none'; grafana +_browser_header.xXSSProtection 1; mode=block grafana +_browser_header.strictTransportSecurity max-age=31536000; includeSubDomains grafana +bruteForceProtected false grafana +permanentLockout false grafana +maxFailureWaitSeconds 900 grafana +minimumQuickLoginWaitSeconds 60 grafana +waitIncrementSeconds 60 grafana +quickLoginCheckMilliSeconds 1000 grafana +maxDeltaTimeSeconds 43200 grafana +failureFactor 30 grafana +offlineSessionMaxLifespanEnabled false grafana +offlineSessionMaxLifespan 5184000 grafana +actionTokenGeneratedByAdminLifespan 43200 grafana +actionTokenGeneratedByUserLifespan 300 grafana +webAuthnPolicyRpEntityName keycloak grafana +webAuthnPolicySignatureAlgorithms ES256 grafana +webAuthnPolicyRpId grafana +webAuthnPolicyAttestationConveyancePreference not specified grafana +webAuthnPolicyAuthenticatorAttachment not specified grafana +webAuthnPolicyRequireResidentKey not specified grafana +webAuthnPolicyUserVerificationRequirement not specified grafana +webAuthnPolicyCreateTimeout 0 grafana +webAuthnPolicyAvoidSameAuthenticatorRegister false grafana +webAuthnPolicyRpEntityNamePasswordless keycloak grafana +webAuthnPolicySignatureAlgorithmsPasswordless ES256 grafana +webAuthnPolicyRpIdPasswordless grafana +webAuthnPolicyAttestationConveyancePreferencePasswordless not specified grafana +webAuthnPolicyAuthenticatorAttachmentPasswordless not specified grafana +webAuthnPolicyRequireResidentKeyPasswordless not specified grafana +webAuthnPolicyUserVerificationRequirementPasswordless not specified grafana +webAuthnPolicyCreateTimeoutPasswordless 0 grafana +webAuthnPolicyAvoidSameAuthenticatorRegisterPasswordless false grafana +\. + + +-- +-- Data for Name: realm_default_groups; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_default_groups (realm_id, group_id) FROM stdin; +\. + + +-- +-- Data for Name: realm_default_roles; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_default_roles (realm_id, role_id) FROM stdin; +master 16d5987b-dcbb-4650-8f52-3469f3974846 +master c014bfd1-a210-4e7a-8a26-35d1f5e8f1ed +grafana c49bddc6-ec92-4caa-bc04-57ba80a92eb9 +grafana 0f3d47bb-002a-4cd0-a502-725f224308a7 +\. + + +-- +-- Data for Name: realm_enabled_event_types; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_enabled_event_types (realm_id, value) FROM stdin; +\. + + +-- +-- Data for Name: realm_events_listeners; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_events_listeners (realm_id, value) FROM stdin; +master jboss-logging +grafana jboss-logging +\. + + +-- +-- Data for Name: realm_localizations; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_localizations (realm_id, locale, texts) FROM stdin; +\. + + +-- +-- Data for Name: realm_required_credential; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_required_credential (type, form_label, input, secret, realm_id) FROM stdin; +password password t t master +password password t t grafana +\. + + +-- +-- Data for Name: realm_smtp_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_smtp_config (realm_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: realm_supported_locales; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.realm_supported_locales (realm_id, value) FROM stdin; +\. + + +-- +-- Data for Name: redirect_uris; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.redirect_uris (client_id, value) FROM stdin; +eed689c6-49da-4d91-98eb-cd495bcc07a3 /realms/master/account/* +11c67f5b-dde7-4680-b05b-c9c59d78bda4 /realms/master/account/* +2f521d09-7304-4b5e-a94b-7cc7300b8b50 /admin/master/console/* +a5a8fed6-0bca-4646-9946-2fe84175353b /realms/grafana/account/* +230081b5-9161-45c3-9e08-9eda5412f7f7 /realms/grafana/account/* +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 /admin/grafana/console/* +09b79548-8426-4c0e-8e0b-7488467532c7 http://127.0.0.1:8088/oauth2/callback +\. + + +-- +-- Data for Name: required_action_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.required_action_config (required_action_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: required_action_provider; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.required_action_provider (id, alias, name, realm_id, enabled, default_action, provider_id, priority) FROM stdin; +ad4dfd2c-307a-4563-b93a-0bb726b4ccaa VERIFY_EMAIL Verify Email master t f VERIFY_EMAIL 50 +2c7fffa4-ff20-4015-9a97-cc6a19e698ba UPDATE_PROFILE Update Profile master t f UPDATE_PROFILE 40 +c76d17f4-eacf-497a-ab5a-f78936bbc50e CONFIGURE_TOTP Configure OTP master t f CONFIGURE_TOTP 10 +83de9f97-43df-4265-982c-5414a2b19985 UPDATE_PASSWORD Update Password master t f UPDATE_PASSWORD 30 +9f538737-770e-4731-abd9-e98172a85d2f terms_and_conditions Terms and Conditions master f f terms_and_conditions 20 +306fc47e-d8ae-4bb1-b2bc-53608a44536c update_user_locale Update User Locale master t f update_user_locale 1000 +f158f7d8-8b7f-414c-b1bd-0dde83c91133 delete_account Delete Account master f f delete_account 60 +969a57d1-c906-4f49-87d6-3cbba2f3898a VERIFY_EMAIL Verify Email grafana t f VERIFY_EMAIL 50 +233d5b8e-6f36-450f-bffd-43b82e27295c UPDATE_PROFILE Update Profile grafana t f UPDATE_PROFILE 40 +ab3a9aa7-3d1b-4fb1-93ad-9412142deed3 CONFIGURE_TOTP Configure OTP grafana t f CONFIGURE_TOTP 10 +988d8e0d-35ef-4e6a-8b48-821cca56acf2 UPDATE_PASSWORD Update Password grafana t f UPDATE_PASSWORD 30 +0e2b6144-5c2c-4dcb-92d8-00529b19a7a5 terms_and_conditions Terms and Conditions grafana f f terms_and_conditions 20 +94993a02-f883-4f8a-a549-d48f95aabed2 update_user_locale Update User Locale grafana t f update_user_locale 1000 +72d09b7f-acde-4b90-af9a-ea3c642a2f6d delete_account Delete Account grafana f f delete_account 60 +\. + + +-- +-- Data for Name: resource_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_attribute (id, name, value, resource_id) FROM stdin; +\. + + +-- +-- Data for Name: resource_policy; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_policy (resource_id, policy_id) FROM stdin; +\. + + +-- +-- Data for Name: resource_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_scope (resource_id, scope_id) FROM stdin; +\. + + +-- +-- Data for Name: resource_server; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_server (id, allow_rs_remote_mgmt, policy_enforce_mode, decision_strategy) FROM stdin; +\. + + +-- +-- Data for Name: resource_server_perm_ticket; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_server_perm_ticket (id, owner, requester, created_timestamp, granted_timestamp, resource_id, scope_id, resource_server_id, policy_id) FROM stdin; +\. + + +-- +-- Data for Name: resource_server_policy; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_server_policy (id, name, description, type, decision_strategy, logic, resource_server_id, owner) FROM stdin; +\. + + +-- +-- Data for Name: resource_server_resource; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_server_resource (id, name, type, icon_uri, owner, resource_server_id, owner_managed_access, display_name) FROM stdin; +\. + + +-- +-- Data for Name: resource_server_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_server_scope (id, name, icon_uri, resource_server_id, display_name) FROM stdin; +\. + + +-- +-- Data for Name: resource_uris; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.resource_uris (resource_id, value) FROM stdin; +\. + + +-- +-- Data for Name: role_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.role_attribute (id, role_id, name, value) FROM stdin; +\. + + +-- +-- Data for Name: scope_mapping; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.scope_mapping (client_id, role_id) FROM stdin; +11c67f5b-dde7-4680-b05b-c9c59d78bda4 619ba870-921e-4f28-b26c-89b11f39dddf +230081b5-9161-45c3-9e08-9eda5412f7f7 18a7066b-fe71-410e-9581-69f78347ec29 +\. + + +-- +-- Data for Name: scope_policy; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.scope_policy (scope_id, policy_id) FROM stdin; +\. + + +-- +-- Data for Name: user_attribute; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_attribute (name, value, user_id, id) FROM stdin; +\. + + +-- +-- Data for Name: user_consent; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_consent (id, client_id, user_id, created_date, last_updated_date, client_storage_provider, external_client_id) FROM stdin; +\. + + +-- +-- Data for Name: user_consent_client_scope; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_consent_client_scope (user_consent_id, scope_id) FROM stdin; +\. + + +-- +-- Data for Name: user_entity; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_entity (id, email, email_constraint, email_verified, enabled, federation_link, first_name, last_name, realm_id, username, created_timestamp, service_account_client_link, not_before) FROM stdin; +74e29604-ff35-42bb-a26d-4d0b81ef0917 \N c8a5d425-4bad-4b76-8828-0e39bae03b67 f t \N \N \N master admin 1643820449683 \N 0 +6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d jwt-admin@example.org jwt-admin@example.org f t \N Admin JWT grafana jwt-admin 1657026796311 \N 0 +88692d07-bb9a-46cf-844c-7ff5c529cd04 jwt-editor@example.com jwt-editor@example.com f t \N Editor JWT grafana jwt-editor 1657026894275 \N 0 +8f58cbec-6e40-4bab-bff0-1c5ff899fe2e jwt-viewer@example.com jwt-viewer@example.com f t \N Viewer JWT grafana jwt-viewer 1657026933578 \N 0 +\. + + +-- +-- Data for Name: user_federation_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_federation_config (user_federation_provider_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: user_federation_mapper; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_federation_mapper (id, name, federation_provider_id, federation_mapper_type, realm_id) FROM stdin; +\. + + +-- +-- Data for Name: user_federation_mapper_config; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_federation_mapper_config (user_federation_mapper_id, value, name) FROM stdin; +\. + + +-- +-- Data for Name: user_federation_provider; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_federation_provider (id, changed_sync_period, display_name, full_sync_period, last_sync, priority, provider_name, realm_id) FROM stdin; +\. + + +-- +-- Data for Name: user_group_membership; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_group_membership (group_id, user_id) FROM stdin; +\. + + +-- +-- Data for Name: user_required_action; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_required_action (user_id, required_action) FROM stdin; +\. + + +-- +-- Data for Name: user_role_mapping; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_role_mapping (role_id, user_id) FROM stdin; +16d5987b-dcbb-4650-8f52-3469f3974846 74e29604-ff35-42bb-a26d-4d0b81ef0917 +c014bfd1-a210-4e7a-8a26-35d1f5e8f1ed 74e29604-ff35-42bb-a26d-4d0b81ef0917 +86a4b6a9-93db-4177-a72f-95fd937a2c8d 74e29604-ff35-42bb-a26d-4d0b81ef0917 +619ba870-921e-4f28-b26c-89b11f39dddf 74e29604-ff35-42bb-a26d-4d0b81ef0917 +4a3204aa-320e-4584-b8ee-ea2989b3f330 74e29604-ff35-42bb-a26d-4d0b81ef0917 +c49bddc6-ec92-4caa-bc04-57ba80a92eb9 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d +0f3d47bb-002a-4cd0-a502-725f224308a7 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d +f1311ecb-6a6a-49d6-bb16-5132daf93a64 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d +18a7066b-fe71-410e-9581-69f78347ec29 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d +60f1b1ea-9059-41ea-acef-573643b24709 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d +c49bddc6-ec92-4caa-bc04-57ba80a92eb9 88692d07-bb9a-46cf-844c-7ff5c529cd04 +0f3d47bb-002a-4cd0-a502-725f224308a7 88692d07-bb9a-46cf-844c-7ff5c529cd04 +f1311ecb-6a6a-49d6-bb16-5132daf93a64 88692d07-bb9a-46cf-844c-7ff5c529cd04 +18a7066b-fe71-410e-9581-69f78347ec29 88692d07-bb9a-46cf-844c-7ff5c529cd04 +c49bddc6-ec92-4caa-bc04-57ba80a92eb9 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e +0f3d47bb-002a-4cd0-a502-725f224308a7 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e +f1311ecb-6a6a-49d6-bb16-5132daf93a64 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e +18a7066b-fe71-410e-9581-69f78347ec29 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e +c9a776f9-2740-435f-a725-4dbcc17a6c91 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e +c4c74006-c346-48cf-8cf1-1617e3e1cde1 88692d07-bb9a-46cf-844c-7ff5c529cd04 +\. + + +-- +-- Data for Name: user_session; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_session (id, auth_method, ip_address, last_session_refresh, login_username, realm_id, remember_me, started, user_id, user_session_state, broker_session_id, broker_user_id) FROM stdin; +\. + + +-- +-- Data for Name: user_session_note; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.user_session_note (user_session, name, value) FROM stdin; +\. + + +-- +-- Data for Name: username_login_failure; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.username_login_failure (realm_id, username, failed_login_not_before, last_failure, last_ip_failure, num_failures) FROM stdin; +\. + + +-- +-- Data for Name: web_origins; Type: TABLE DATA; Schema: public; Owner: keycloak +-- + +COPY public.web_origins (client_id, value) FROM stdin; +2f521d09-7304-4b5e-a94b-7cc7300b8b50 + +805aebc8-9d01-42b6-bcce-6ce48ca63ef0 + +09b79548-8426-4c0e-8e0b-7488467532c7 http://127.0.0.1:8087 +\. + + +-- +-- Name: username_login_failure CONSTRAINT_17-2; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.username_login_failure + ADD CONSTRAINT "CONSTRAINT_17-2" PRIMARY KEY (realm_id, username); + + +-- +-- Name: keycloak_role UK_J3RWUVD56ONTGSUHOGM184WW2-2; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_role + ADD CONSTRAINT "UK_J3RWUVD56ONTGSUHOGM184WW2-2" UNIQUE (name, client_realm_constraint); + + +-- +-- Name: client_auth_flow_bindings c_cli_flow_bind; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_auth_flow_bindings + ADD CONSTRAINT c_cli_flow_bind PRIMARY KEY (client_id, binding_name); + + +-- +-- Name: client_scope_client c_cli_scope_bind; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_client + ADD CONSTRAINT c_cli_scope_bind PRIMARY KEY (client_id, scope_id); + + +-- +-- Name: client_initial_access cnstr_client_init_acc_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_initial_access + ADD CONSTRAINT cnstr_client_init_acc_pk PRIMARY KEY (id); + + +-- +-- Name: realm_default_groups con_group_id_def_groups; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_groups + ADD CONSTRAINT con_group_id_def_groups UNIQUE (group_id); + + +-- +-- Name: broker_link constr_broker_link_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.broker_link + ADD CONSTRAINT constr_broker_link_pk PRIMARY KEY (identity_provider, user_id); + + +-- +-- Name: client_user_session_note constr_cl_usr_ses_note; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_user_session_note + ADD CONSTRAINT constr_cl_usr_ses_note PRIMARY KEY (client_session, name); + + +-- +-- Name: client_default_roles constr_client_default_roles; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_default_roles + ADD CONSTRAINT constr_client_default_roles PRIMARY KEY (client_id, role_id); + + +-- +-- Name: component_config constr_component_config_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.component_config + ADD CONSTRAINT constr_component_config_pk PRIMARY KEY (id); + + +-- +-- Name: component constr_component_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.component + ADD CONSTRAINT constr_component_pk PRIMARY KEY (id); + + +-- +-- Name: fed_user_required_action constr_fed_required_action; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_required_action + ADD CONSTRAINT constr_fed_required_action PRIMARY KEY (required_action, user_id); + + +-- +-- Name: fed_user_attribute constr_fed_user_attr_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_attribute + ADD CONSTRAINT constr_fed_user_attr_pk PRIMARY KEY (id); + + +-- +-- Name: fed_user_consent constr_fed_user_consent_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_consent + ADD CONSTRAINT constr_fed_user_consent_pk PRIMARY KEY (id); + + +-- +-- Name: fed_user_credential constr_fed_user_cred_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_credential + ADD CONSTRAINT constr_fed_user_cred_pk PRIMARY KEY (id); + + +-- +-- Name: fed_user_group_membership constr_fed_user_group; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_group_membership + ADD CONSTRAINT constr_fed_user_group PRIMARY KEY (group_id, user_id); + + +-- +-- Name: fed_user_role_mapping constr_fed_user_role; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_role_mapping + ADD CONSTRAINT constr_fed_user_role PRIMARY KEY (role_id, user_id); + + +-- +-- Name: federated_user constr_federated_user; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.federated_user + ADD CONSTRAINT constr_federated_user PRIMARY KEY (id); + + +-- +-- Name: realm_default_groups constr_realm_default_groups; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_groups + ADD CONSTRAINT constr_realm_default_groups PRIMARY KEY (realm_id, group_id); + + +-- +-- Name: realm_enabled_event_types constr_realm_enabl_event_types; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_enabled_event_types + ADD CONSTRAINT constr_realm_enabl_event_types PRIMARY KEY (realm_id, value); + + +-- +-- Name: realm_events_listeners constr_realm_events_listeners; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_events_listeners + ADD CONSTRAINT constr_realm_events_listeners PRIMARY KEY (realm_id, value); + + +-- +-- Name: realm_supported_locales constr_realm_supported_locales; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_supported_locales + ADD CONSTRAINT constr_realm_supported_locales PRIMARY KEY (realm_id, value); + + +-- +-- Name: identity_provider constraint_2b; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider + ADD CONSTRAINT constraint_2b PRIMARY KEY (internal_id); + + +-- +-- Name: client_attributes constraint_3c; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_attributes + ADD CONSTRAINT constraint_3c PRIMARY KEY (client_id, name); + + +-- +-- Name: event_entity constraint_4; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.event_entity + ADD CONSTRAINT constraint_4 PRIMARY KEY (id); + + +-- +-- Name: federated_identity constraint_40; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.federated_identity + ADD CONSTRAINT constraint_40 PRIMARY KEY (identity_provider, user_id); + + +-- +-- Name: realm constraint_4a; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm + ADD CONSTRAINT constraint_4a PRIMARY KEY (id); + + +-- +-- Name: client_session_role constraint_5; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_role + ADD CONSTRAINT constraint_5 PRIMARY KEY (client_session, role_id); + + +-- +-- Name: user_session constraint_57; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_session + ADD CONSTRAINT constraint_57 PRIMARY KEY (id); + + +-- +-- Name: user_federation_provider constraint_5c; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_provider + ADD CONSTRAINT constraint_5c PRIMARY KEY (id); + + +-- +-- Name: client_session_note constraint_5e; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_note + ADD CONSTRAINT constraint_5e PRIMARY KEY (client_session, name); + + +-- +-- Name: client constraint_7; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client + ADD CONSTRAINT constraint_7 PRIMARY KEY (id); + + +-- +-- Name: client_session constraint_8; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session + ADD CONSTRAINT constraint_8 PRIMARY KEY (id); + + +-- +-- Name: scope_mapping constraint_81; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.scope_mapping + ADD CONSTRAINT constraint_81 PRIMARY KEY (client_id, role_id); + + +-- +-- Name: client_node_registrations constraint_84; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_node_registrations + ADD CONSTRAINT constraint_84 PRIMARY KEY (client_id, name); + + +-- +-- Name: realm_attribute constraint_9; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_attribute + ADD CONSTRAINT constraint_9 PRIMARY KEY (name, realm_id); + + +-- +-- Name: realm_required_credential constraint_92; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_required_credential + ADD CONSTRAINT constraint_92 PRIMARY KEY (realm_id, type); + + +-- +-- Name: keycloak_role constraint_a; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_role + ADD CONSTRAINT constraint_a PRIMARY KEY (id); + + +-- +-- Name: admin_event_entity constraint_admin_event_entity; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.admin_event_entity + ADD CONSTRAINT constraint_admin_event_entity PRIMARY KEY (id); + + +-- +-- Name: authenticator_config_entry constraint_auth_cfg_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authenticator_config_entry + ADD CONSTRAINT constraint_auth_cfg_pk PRIMARY KEY (authenticator_id, name); + + +-- +-- Name: authentication_execution constraint_auth_exec_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authentication_execution + ADD CONSTRAINT constraint_auth_exec_pk PRIMARY KEY (id); + + +-- +-- Name: authentication_flow constraint_auth_flow_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authentication_flow + ADD CONSTRAINT constraint_auth_flow_pk PRIMARY KEY (id); + + +-- +-- Name: authenticator_config constraint_auth_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authenticator_config + ADD CONSTRAINT constraint_auth_pk PRIMARY KEY (id); + + +-- +-- Name: client_session_auth_status constraint_auth_status_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_auth_status + ADD CONSTRAINT constraint_auth_status_pk PRIMARY KEY (client_session, authenticator); + + +-- +-- Name: user_role_mapping constraint_c; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_role_mapping + ADD CONSTRAINT constraint_c PRIMARY KEY (role_id, user_id); + + +-- +-- Name: composite_role constraint_composite_role; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.composite_role + ADD CONSTRAINT constraint_composite_role PRIMARY KEY (composite, child_role); + + +-- +-- Name: client_session_prot_mapper constraint_cs_pmp_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_prot_mapper + ADD CONSTRAINT constraint_cs_pmp_pk PRIMARY KEY (client_session, protocol_mapper_id); + + +-- +-- Name: identity_provider_config constraint_d; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider_config + ADD CONSTRAINT constraint_d PRIMARY KEY (identity_provider_id, name); + + +-- +-- Name: policy_config constraint_dpc; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.policy_config + ADD CONSTRAINT constraint_dpc PRIMARY KEY (policy_id, name); + + +-- +-- Name: realm_smtp_config constraint_e; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_smtp_config + ADD CONSTRAINT constraint_e PRIMARY KEY (realm_id, name); + + +-- +-- Name: credential constraint_f; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.credential + ADD CONSTRAINT constraint_f PRIMARY KEY (id); + + +-- +-- Name: user_federation_config constraint_f9; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_config + ADD CONSTRAINT constraint_f9 PRIMARY KEY (user_federation_provider_id, name); + + +-- +-- Name: resource_server_perm_ticket constraint_fapmt; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT constraint_fapmt PRIMARY KEY (id); + + +-- +-- Name: resource_server_resource constraint_farsr; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_resource + ADD CONSTRAINT constraint_farsr PRIMARY KEY (id); + + +-- +-- Name: resource_server_policy constraint_farsrp; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_policy + ADD CONSTRAINT constraint_farsrp PRIMARY KEY (id); + + +-- +-- Name: associated_policy constraint_farsrpap; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.associated_policy + ADD CONSTRAINT constraint_farsrpap PRIMARY KEY (policy_id, associated_policy_id); + + +-- +-- Name: resource_policy constraint_farsrpp; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_policy + ADD CONSTRAINT constraint_farsrpp PRIMARY KEY (resource_id, policy_id); + + +-- +-- Name: resource_server_scope constraint_farsrs; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_scope + ADD CONSTRAINT constraint_farsrs PRIMARY KEY (id); + + +-- +-- Name: resource_scope constraint_farsrsp; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_scope + ADD CONSTRAINT constraint_farsrsp PRIMARY KEY (resource_id, scope_id); + + +-- +-- Name: scope_policy constraint_farsrsps; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.scope_policy + ADD CONSTRAINT constraint_farsrsps PRIMARY KEY (scope_id, policy_id); + + +-- +-- Name: user_entity constraint_fb; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_entity + ADD CONSTRAINT constraint_fb PRIMARY KEY (id); + + +-- +-- Name: user_federation_mapper_config constraint_fedmapper_cfg_pm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_mapper_config + ADD CONSTRAINT constraint_fedmapper_cfg_pm PRIMARY KEY (user_federation_mapper_id, name); + + +-- +-- Name: user_federation_mapper constraint_fedmapperpm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_mapper + ADD CONSTRAINT constraint_fedmapperpm PRIMARY KEY (id); + + +-- +-- Name: fed_user_consent_cl_scope constraint_fgrntcsnt_clsc_pm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.fed_user_consent_cl_scope + ADD CONSTRAINT constraint_fgrntcsnt_clsc_pm PRIMARY KEY (user_consent_id, scope_id); + + +-- +-- Name: user_consent_client_scope constraint_grntcsnt_clsc_pm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_consent_client_scope + ADD CONSTRAINT constraint_grntcsnt_clsc_pm PRIMARY KEY (user_consent_id, scope_id); + + +-- +-- Name: user_consent constraint_grntcsnt_pm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_consent + ADD CONSTRAINT constraint_grntcsnt_pm PRIMARY KEY (id); + + +-- +-- Name: keycloak_group constraint_group; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_group + ADD CONSTRAINT constraint_group PRIMARY KEY (id); + + +-- +-- Name: group_attribute constraint_group_attribute_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.group_attribute + ADD CONSTRAINT constraint_group_attribute_pk PRIMARY KEY (id); + + +-- +-- Name: group_role_mapping constraint_group_role; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.group_role_mapping + ADD CONSTRAINT constraint_group_role PRIMARY KEY (role_id, group_id); + + +-- +-- Name: identity_provider_mapper constraint_idpm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider_mapper + ADD CONSTRAINT constraint_idpm PRIMARY KEY (id); + + +-- +-- Name: idp_mapper_config constraint_idpmconfig; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.idp_mapper_config + ADD CONSTRAINT constraint_idpmconfig PRIMARY KEY (idp_mapper_id, name); + + +-- +-- Name: migration_model constraint_migmod; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.migration_model + ADD CONSTRAINT constraint_migmod PRIMARY KEY (id); + + +-- +-- Name: offline_client_session constraint_offl_cl_ses_pk3; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.offline_client_session + ADD CONSTRAINT constraint_offl_cl_ses_pk3 PRIMARY KEY (user_session_id, client_id, client_storage_provider, external_client_id, offline_flag); + + +-- +-- Name: offline_user_session constraint_offl_us_ses_pk2; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.offline_user_session + ADD CONSTRAINT constraint_offl_us_ses_pk2 PRIMARY KEY (user_session_id, offline_flag); + + +-- +-- Name: protocol_mapper constraint_pcm; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.protocol_mapper + ADD CONSTRAINT constraint_pcm PRIMARY KEY (id); + + +-- +-- Name: protocol_mapper_config constraint_pmconfig; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.protocol_mapper_config + ADD CONSTRAINT constraint_pmconfig PRIMARY KEY (protocol_mapper_id, name); + + +-- +-- Name: realm_default_roles constraint_realm_default_roles; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_roles + ADD CONSTRAINT constraint_realm_default_roles PRIMARY KEY (realm_id, role_id); + + +-- +-- Name: redirect_uris constraint_redirect_uris; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.redirect_uris + ADD CONSTRAINT constraint_redirect_uris PRIMARY KEY (client_id, value); + + +-- +-- Name: required_action_config constraint_req_act_cfg_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.required_action_config + ADD CONSTRAINT constraint_req_act_cfg_pk PRIMARY KEY (required_action_id, name); + + +-- +-- Name: required_action_provider constraint_req_act_prv_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.required_action_provider + ADD CONSTRAINT constraint_req_act_prv_pk PRIMARY KEY (id); + + +-- +-- Name: user_required_action constraint_required_action; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_required_action + ADD CONSTRAINT constraint_required_action PRIMARY KEY (required_action, user_id); + + +-- +-- Name: resource_uris constraint_resour_uris_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_uris + ADD CONSTRAINT constraint_resour_uris_pk PRIMARY KEY (resource_id, value); + + +-- +-- Name: role_attribute constraint_role_attribute_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.role_attribute + ADD CONSTRAINT constraint_role_attribute_pk PRIMARY KEY (id); + + +-- +-- Name: user_attribute constraint_user_attribute_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_attribute + ADD CONSTRAINT constraint_user_attribute_pk PRIMARY KEY (id); + + +-- +-- Name: user_group_membership constraint_user_group; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_group_membership + ADD CONSTRAINT constraint_user_group PRIMARY KEY (group_id, user_id); + + +-- +-- Name: user_session_note constraint_usn_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_session_note + ADD CONSTRAINT constraint_usn_pk PRIMARY KEY (user_session, name); + + +-- +-- Name: web_origins constraint_web_origins; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.web_origins + ADD CONSTRAINT constraint_web_origins PRIMARY KEY (client_id, value); + + +-- +-- Name: client_scope_attributes pk_cl_tmpl_attr; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_attributes + ADD CONSTRAINT pk_cl_tmpl_attr PRIMARY KEY (scope_id, name); + + +-- +-- Name: client_scope pk_cli_template; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope + ADD CONSTRAINT pk_cli_template PRIMARY KEY (id); + + +-- +-- Name: databasechangeloglock pk_databasechangeloglock; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.databasechangeloglock + ADD CONSTRAINT pk_databasechangeloglock PRIMARY KEY (id); + + +-- +-- Name: resource_server pk_resource_server; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server + ADD CONSTRAINT pk_resource_server PRIMARY KEY (id); + + +-- +-- Name: client_scope_role_mapping pk_template_scope; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_role_mapping + ADD CONSTRAINT pk_template_scope PRIMARY KEY (scope_id, role_id); + + +-- +-- Name: default_client_scope r_def_cli_scope_bind; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.default_client_scope + ADD CONSTRAINT r_def_cli_scope_bind PRIMARY KEY (realm_id, scope_id); + + +-- +-- Name: realm_localizations realm_localizations_pkey; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_localizations + ADD CONSTRAINT realm_localizations_pkey PRIMARY KEY (realm_id, locale); + + +-- +-- Name: resource_attribute res_attr_pk; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_attribute + ADD CONSTRAINT res_attr_pk PRIMARY KEY (id); + + +-- +-- Name: keycloak_group sibling_names; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_group + ADD CONSTRAINT sibling_names UNIQUE (realm_id, parent_group, name); + + +-- +-- Name: identity_provider uk_2daelwnibji49avxsrtuf6xj33; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider + ADD CONSTRAINT uk_2daelwnibji49avxsrtuf6xj33 UNIQUE (provider_alias, realm_id); + + +-- +-- Name: client_default_roles uk_8aelwnibji49avxsrtuf6xjow; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_default_roles + ADD CONSTRAINT uk_8aelwnibji49avxsrtuf6xjow UNIQUE (role_id); + + +-- +-- Name: client uk_b71cjlbenv945rb6gcon438at; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client + ADD CONSTRAINT uk_b71cjlbenv945rb6gcon438at UNIQUE (realm_id, client_id); + + +-- +-- Name: client_scope uk_cli_scope; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope + ADD CONSTRAINT uk_cli_scope UNIQUE (realm_id, name); + + +-- +-- Name: user_entity uk_dykn684sl8up1crfei6eckhd7; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_entity + ADD CONSTRAINT uk_dykn684sl8up1crfei6eckhd7 UNIQUE (realm_id, email_constraint); + + +-- +-- Name: resource_server_resource uk_frsr6t700s9v50bu18ws5ha6; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_resource + ADD CONSTRAINT uk_frsr6t700s9v50bu18ws5ha6 UNIQUE (name, owner, resource_server_id); + + +-- +-- Name: resource_server_perm_ticket uk_frsr6t700s9v50bu18ws5pmt; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT uk_frsr6t700s9v50bu18ws5pmt UNIQUE (owner, requester, resource_server_id, resource_id, scope_id); + + +-- +-- Name: resource_server_policy uk_frsrpt700s9v50bu18ws5ha6; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_policy + ADD CONSTRAINT uk_frsrpt700s9v50bu18ws5ha6 UNIQUE (name, resource_server_id); + + +-- +-- Name: resource_server_scope uk_frsrst700s9v50bu18ws5ha6; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_scope + ADD CONSTRAINT uk_frsrst700s9v50bu18ws5ha6 UNIQUE (name, resource_server_id); + + +-- +-- Name: realm_default_roles uk_h4wpd7w4hsoolni3h0sw7btje; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_roles + ADD CONSTRAINT uk_h4wpd7w4hsoolni3h0sw7btje UNIQUE (role_id); + + +-- +-- Name: user_consent uk_jkuwuvd56ontgsuhogm8uewrt; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_consent + ADD CONSTRAINT uk_jkuwuvd56ontgsuhogm8uewrt UNIQUE (client_id, client_storage_provider, external_client_id, user_id); + + +-- +-- Name: realm uk_orvsdmla56612eaefiq6wl5oi; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm + ADD CONSTRAINT uk_orvsdmla56612eaefiq6wl5oi UNIQUE (name); + + +-- +-- Name: user_entity uk_ru8tt6t700s9v50bu18ws5ha6; Type: CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_entity + ADD CONSTRAINT uk_ru8tt6t700s9v50bu18ws5ha6 UNIQUE (realm_id, username); + + +-- +-- Name: idx_assoc_pol_assoc_pol_id; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_assoc_pol_assoc_pol_id ON public.associated_policy USING btree (associated_policy_id); + + +-- +-- Name: idx_auth_config_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_auth_config_realm ON public.authenticator_config USING btree (realm_id); + + +-- +-- Name: idx_auth_exec_flow; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_auth_exec_flow ON public.authentication_execution USING btree (flow_id); + + +-- +-- Name: idx_auth_exec_realm_flow; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_auth_exec_realm_flow ON public.authentication_execution USING btree (realm_id, flow_id); + + +-- +-- Name: idx_auth_flow_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_auth_flow_realm ON public.authentication_flow USING btree (realm_id); + + +-- +-- Name: idx_cl_clscope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_cl_clscope ON public.client_scope_client USING btree (scope_id); + + +-- +-- Name: idx_client_def_roles_client; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_client_def_roles_client ON public.client_default_roles USING btree (client_id); + + +-- +-- Name: idx_client_id; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_client_id ON public.client USING btree (client_id); + + +-- +-- Name: idx_client_init_acc_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_client_init_acc_realm ON public.client_initial_access USING btree (realm_id); + + +-- +-- Name: idx_client_session_session; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_client_session_session ON public.client_session USING btree (session_id); + + +-- +-- Name: idx_clscope_attrs; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_clscope_attrs ON public.client_scope_attributes USING btree (scope_id); + + +-- +-- Name: idx_clscope_cl; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_clscope_cl ON public.client_scope_client USING btree (client_id); + + +-- +-- Name: idx_clscope_protmap; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_clscope_protmap ON public.protocol_mapper USING btree (client_scope_id); + + +-- +-- Name: idx_clscope_role; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_clscope_role ON public.client_scope_role_mapping USING btree (scope_id); + + +-- +-- Name: idx_compo_config_compo; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_compo_config_compo ON public.component_config USING btree (component_id); + + +-- +-- Name: idx_component_provider_type; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_component_provider_type ON public.component USING btree (provider_type); + + +-- +-- Name: idx_component_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_component_realm ON public.component USING btree (realm_id); + + +-- +-- Name: idx_composite; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_composite ON public.composite_role USING btree (composite); + + +-- +-- Name: idx_composite_child; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_composite_child ON public.composite_role USING btree (child_role); + + +-- +-- Name: idx_defcls_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_defcls_realm ON public.default_client_scope USING btree (realm_id); + + +-- +-- Name: idx_defcls_scope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_defcls_scope ON public.default_client_scope USING btree (scope_id); + + +-- +-- Name: idx_event_time; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_event_time ON public.event_entity USING btree (realm_id, event_time); + + +-- +-- Name: idx_fedidentity_feduser; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fedidentity_feduser ON public.federated_identity USING btree (federated_user_id); + + +-- +-- Name: idx_fedidentity_user; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fedidentity_user ON public.federated_identity USING btree (user_id); + + +-- +-- Name: idx_fu_attribute; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_attribute ON public.fed_user_attribute USING btree (user_id, realm_id, name); + + +-- +-- Name: idx_fu_cnsnt_ext; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_cnsnt_ext ON public.fed_user_consent USING btree (user_id, client_storage_provider, external_client_id); + + +-- +-- Name: idx_fu_consent; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_consent ON public.fed_user_consent USING btree (user_id, client_id); + + +-- +-- Name: idx_fu_consent_ru; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_consent_ru ON public.fed_user_consent USING btree (realm_id, user_id); + + +-- +-- Name: idx_fu_credential; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_credential ON public.fed_user_credential USING btree (user_id, type); + + +-- +-- Name: idx_fu_credential_ru; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_credential_ru ON public.fed_user_credential USING btree (realm_id, user_id); + + +-- +-- Name: idx_fu_group_membership; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_group_membership ON public.fed_user_group_membership USING btree (user_id, group_id); + + +-- +-- Name: idx_fu_group_membership_ru; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_group_membership_ru ON public.fed_user_group_membership USING btree (realm_id, user_id); + + +-- +-- Name: idx_fu_required_action; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_required_action ON public.fed_user_required_action USING btree (user_id, required_action); + + +-- +-- Name: idx_fu_required_action_ru; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_required_action_ru ON public.fed_user_required_action USING btree (realm_id, user_id); + + +-- +-- Name: idx_fu_role_mapping; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_role_mapping ON public.fed_user_role_mapping USING btree (user_id, role_id); + + +-- +-- Name: idx_fu_role_mapping_ru; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_fu_role_mapping_ru ON public.fed_user_role_mapping USING btree (realm_id, user_id); + + +-- +-- Name: idx_group_attr_group; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_group_attr_group ON public.group_attribute USING btree (group_id); + + +-- +-- Name: idx_group_role_mapp_group; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_group_role_mapp_group ON public.group_role_mapping USING btree (group_id); + + +-- +-- Name: idx_id_prov_mapp_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_id_prov_mapp_realm ON public.identity_provider_mapper USING btree (realm_id); + + +-- +-- Name: idx_ident_prov_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_ident_prov_realm ON public.identity_provider USING btree (realm_id); + + +-- +-- Name: idx_keycloak_role_client; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_keycloak_role_client ON public.keycloak_role USING btree (client); + + +-- +-- Name: idx_keycloak_role_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_keycloak_role_realm ON public.keycloak_role USING btree (realm); + + +-- +-- Name: idx_offline_uss_createdon; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_offline_uss_createdon ON public.offline_user_session USING btree (created_on); + + +-- +-- Name: idx_protocol_mapper_client; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_protocol_mapper_client ON public.protocol_mapper USING btree (client_id); + + +-- +-- Name: idx_realm_attr_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_attr_realm ON public.realm_attribute USING btree (realm_id); + + +-- +-- Name: idx_realm_clscope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_clscope ON public.client_scope USING btree (realm_id); + + +-- +-- Name: idx_realm_def_grp_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_def_grp_realm ON public.realm_default_groups USING btree (realm_id); + + +-- +-- Name: idx_realm_def_roles_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_def_roles_realm ON public.realm_default_roles USING btree (realm_id); + + +-- +-- Name: idx_realm_evt_list_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_evt_list_realm ON public.realm_events_listeners USING btree (realm_id); + + +-- +-- Name: idx_realm_evt_types_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_evt_types_realm ON public.realm_enabled_event_types USING btree (realm_id); + + +-- +-- Name: idx_realm_master_adm_cli; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_master_adm_cli ON public.realm USING btree (master_admin_client); + + +-- +-- Name: idx_realm_supp_local_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_realm_supp_local_realm ON public.realm_supported_locales USING btree (realm_id); + + +-- +-- Name: idx_redir_uri_client; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_redir_uri_client ON public.redirect_uris USING btree (client_id); + + +-- +-- Name: idx_req_act_prov_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_req_act_prov_realm ON public.required_action_provider USING btree (realm_id); + + +-- +-- Name: idx_res_policy_policy; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_res_policy_policy ON public.resource_policy USING btree (policy_id); + + +-- +-- Name: idx_res_scope_scope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_res_scope_scope ON public.resource_scope USING btree (scope_id); + + +-- +-- Name: idx_res_serv_pol_res_serv; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_res_serv_pol_res_serv ON public.resource_server_policy USING btree (resource_server_id); + + +-- +-- Name: idx_res_srv_res_res_srv; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_res_srv_res_res_srv ON public.resource_server_resource USING btree (resource_server_id); + + +-- +-- Name: idx_res_srv_scope_res_srv; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_res_srv_scope_res_srv ON public.resource_server_scope USING btree (resource_server_id); + + +-- +-- Name: idx_role_attribute; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_role_attribute ON public.role_attribute USING btree (role_id); + + +-- +-- Name: idx_role_clscope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_role_clscope ON public.client_scope_role_mapping USING btree (role_id); + + +-- +-- Name: idx_scope_mapping_role; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_scope_mapping_role ON public.scope_mapping USING btree (role_id); + + +-- +-- Name: idx_scope_policy_policy; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_scope_policy_policy ON public.scope_policy USING btree (policy_id); + + +-- +-- Name: idx_update_time; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_update_time ON public.migration_model USING btree (update_time); + + +-- +-- Name: idx_us_sess_id_on_cl_sess; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_us_sess_id_on_cl_sess ON public.offline_client_session USING btree (user_session_id); + + +-- +-- Name: idx_usconsent_clscope; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_usconsent_clscope ON public.user_consent_client_scope USING btree (user_consent_id); + + +-- +-- Name: idx_user_attribute; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_attribute ON public.user_attribute USING btree (user_id); + + +-- +-- Name: idx_user_consent; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_consent ON public.user_consent USING btree (user_id); + + +-- +-- Name: idx_user_credential; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_credential ON public.credential USING btree (user_id); + + +-- +-- Name: idx_user_email; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_email ON public.user_entity USING btree (email); + + +-- +-- Name: idx_user_group_mapping; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_group_mapping ON public.user_group_membership USING btree (user_id); + + +-- +-- Name: idx_user_reqactions; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_reqactions ON public.user_required_action USING btree (user_id); + + +-- +-- Name: idx_user_role_mapping; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_user_role_mapping ON public.user_role_mapping USING btree (user_id); + + +-- +-- Name: idx_usr_fed_map_fed_prv; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_usr_fed_map_fed_prv ON public.user_federation_mapper USING btree (federation_provider_id); + + +-- +-- Name: idx_usr_fed_map_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_usr_fed_map_realm ON public.user_federation_mapper USING btree (realm_id); + + +-- +-- Name: idx_usr_fed_prv_realm; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_usr_fed_prv_realm ON public.user_federation_provider USING btree (realm_id); + + +-- +-- Name: idx_web_orig_client; Type: INDEX; Schema: public; Owner: keycloak +-- + +CREATE INDEX idx_web_orig_client ON public.web_origins USING btree (client_id); + + +-- +-- Name: client_session_auth_status auth_status_constraint; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_auth_status + ADD CONSTRAINT auth_status_constraint FOREIGN KEY (client_session) REFERENCES public.client_session(id); + + +-- +-- Name: identity_provider fk2b4ebc52ae5c3b34; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider + ADD CONSTRAINT fk2b4ebc52ae5c3b34 FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: client_attributes fk3c47c64beacca966; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_attributes + ADD CONSTRAINT fk3c47c64beacca966 FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: federated_identity fk404288b92ef007a6; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.federated_identity + ADD CONSTRAINT fk404288b92ef007a6 FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: client_node_registrations fk4129723ba992f594; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_node_registrations + ADD CONSTRAINT fk4129723ba992f594 FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: client_session_note fk5edfb00ff51c2736; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_note + ADD CONSTRAINT fk5edfb00ff51c2736 FOREIGN KEY (client_session) REFERENCES public.client_session(id); + + +-- +-- Name: user_session_note fk5edfb00ff51d3472; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_session_note + ADD CONSTRAINT fk5edfb00ff51d3472 FOREIGN KEY (user_session) REFERENCES public.user_session(id); + + +-- +-- Name: client_session_role fk_11b7sgqw18i532811v7o2dv76; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_role + ADD CONSTRAINT fk_11b7sgqw18i532811v7o2dv76 FOREIGN KEY (client_session) REFERENCES public.client_session(id); + + +-- +-- Name: redirect_uris fk_1burs8pb4ouj97h5wuppahv9f; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.redirect_uris + ADD CONSTRAINT fk_1burs8pb4ouj97h5wuppahv9f FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: user_federation_provider fk_1fj32f6ptolw2qy60cd8n01e8; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_provider + ADD CONSTRAINT fk_1fj32f6ptolw2qy60cd8n01e8 FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: client_session_prot_mapper fk_33a8sgqw18i532811v7o2dk89; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session_prot_mapper + ADD CONSTRAINT fk_33a8sgqw18i532811v7o2dk89 FOREIGN KEY (client_session) REFERENCES public.client_session(id); + + +-- +-- Name: realm_required_credential fk_5hg65lybevavkqfki3kponh9v; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_required_credential + ADD CONSTRAINT fk_5hg65lybevavkqfki3kponh9v FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: resource_attribute fk_5hrm2vlf9ql5fu022kqepovbr; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_attribute + ADD CONSTRAINT fk_5hrm2vlf9ql5fu022kqepovbr FOREIGN KEY (resource_id) REFERENCES public.resource_server_resource(id); + + +-- +-- Name: user_attribute fk_5hrm2vlf9ql5fu043kqepovbr; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_attribute + ADD CONSTRAINT fk_5hrm2vlf9ql5fu043kqepovbr FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: user_required_action fk_6qj3w1jw9cvafhe19bwsiuvmd; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_required_action + ADD CONSTRAINT fk_6qj3w1jw9cvafhe19bwsiuvmd FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: keycloak_role fk_6vyqfe4cn4wlq8r6kt5vdsj5c; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_role + ADD CONSTRAINT fk_6vyqfe4cn4wlq8r6kt5vdsj5c FOREIGN KEY (realm) REFERENCES public.realm(id); + + +-- +-- Name: realm_smtp_config fk_70ej8xdxgxd0b9hh6180irr0o; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_smtp_config + ADD CONSTRAINT fk_70ej8xdxgxd0b9hh6180irr0o FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: realm_attribute fk_8shxd6l3e9atqukacxgpffptw; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_attribute + ADD CONSTRAINT fk_8shxd6l3e9atqukacxgpffptw FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: composite_role fk_a63wvekftu8jo1pnj81e7mce2; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.composite_role + ADD CONSTRAINT fk_a63wvekftu8jo1pnj81e7mce2 FOREIGN KEY (composite) REFERENCES public.keycloak_role(id); + + +-- +-- Name: authentication_execution fk_auth_exec_flow; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authentication_execution + ADD CONSTRAINT fk_auth_exec_flow FOREIGN KEY (flow_id) REFERENCES public.authentication_flow(id); + + +-- +-- Name: authentication_execution fk_auth_exec_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authentication_execution + ADD CONSTRAINT fk_auth_exec_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: authentication_flow fk_auth_flow_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authentication_flow + ADD CONSTRAINT fk_auth_flow_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: authenticator_config fk_auth_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.authenticator_config + ADD CONSTRAINT fk_auth_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: client_session fk_b4ao2vcvat6ukau74wbwtfqo1; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_session + ADD CONSTRAINT fk_b4ao2vcvat6ukau74wbwtfqo1 FOREIGN KEY (session_id) REFERENCES public.user_session(id); + + +-- +-- Name: user_role_mapping fk_c4fqv34p1mbylloxang7b1q3l; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_role_mapping + ADD CONSTRAINT fk_c4fqv34p1mbylloxang7b1q3l FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: client_scope_client fk_c_cli_scope_client; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_client + ADD CONSTRAINT fk_c_cli_scope_client FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: client_scope_client fk_c_cli_scope_scope; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_client + ADD CONSTRAINT fk_c_cli_scope_scope FOREIGN KEY (scope_id) REFERENCES public.client_scope(id); + + +-- +-- Name: client_scope_attributes fk_cl_scope_attr_scope; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_attributes + ADD CONSTRAINT fk_cl_scope_attr_scope FOREIGN KEY (scope_id) REFERENCES public.client_scope(id); + + +-- +-- Name: client_scope_role_mapping fk_cl_scope_rm_scope; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope_role_mapping + ADD CONSTRAINT fk_cl_scope_rm_scope FOREIGN KEY (scope_id) REFERENCES public.client_scope(id); + + +-- +-- Name: client_user_session_note fk_cl_usr_ses_note; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_user_session_note + ADD CONSTRAINT fk_cl_usr_ses_note FOREIGN KEY (client_session) REFERENCES public.client_session(id); + + +-- +-- Name: protocol_mapper fk_cli_scope_mapper; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.protocol_mapper + ADD CONSTRAINT fk_cli_scope_mapper FOREIGN KEY (client_scope_id) REFERENCES public.client_scope(id); + + +-- +-- Name: client_initial_access fk_client_init_acc_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_initial_access + ADD CONSTRAINT fk_client_init_acc_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: component_config fk_component_config; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.component_config + ADD CONSTRAINT fk_component_config FOREIGN KEY (component_id) REFERENCES public.component(id); + + +-- +-- Name: component fk_component_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.component + ADD CONSTRAINT fk_component_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: realm_default_groups fk_def_groups_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_groups + ADD CONSTRAINT fk_def_groups_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: realm_default_roles fk_evudb1ppw84oxfax2drs03icc; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_default_roles + ADD CONSTRAINT fk_evudb1ppw84oxfax2drs03icc FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: user_federation_mapper_config fk_fedmapper_cfg; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_mapper_config + ADD CONSTRAINT fk_fedmapper_cfg FOREIGN KEY (user_federation_mapper_id) REFERENCES public.user_federation_mapper(id); + + +-- +-- Name: user_federation_mapper fk_fedmapperpm_fedprv; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_mapper + ADD CONSTRAINT fk_fedmapperpm_fedprv FOREIGN KEY (federation_provider_id) REFERENCES public.user_federation_provider(id); + + +-- +-- Name: user_federation_mapper fk_fedmapperpm_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_mapper + ADD CONSTRAINT fk_fedmapperpm_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: associated_policy fk_frsr5s213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.associated_policy + ADD CONSTRAINT fk_frsr5s213xcx4wnkog82ssrfy FOREIGN KEY (associated_policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: scope_policy fk_frsrasp13xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.scope_policy + ADD CONSTRAINT fk_frsrasp13xcx4wnkog82ssrfy FOREIGN KEY (policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: resource_server_perm_ticket fk_frsrho213xcx4wnkog82sspmt; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT fk_frsrho213xcx4wnkog82sspmt FOREIGN KEY (resource_server_id) REFERENCES public.resource_server(id); + + +-- +-- Name: resource_server_resource fk_frsrho213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_resource + ADD CONSTRAINT fk_frsrho213xcx4wnkog82ssrfy FOREIGN KEY (resource_server_id) REFERENCES public.resource_server(id); + + +-- +-- Name: resource_server_perm_ticket fk_frsrho213xcx4wnkog83sspmt; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT fk_frsrho213xcx4wnkog83sspmt FOREIGN KEY (resource_id) REFERENCES public.resource_server_resource(id); + + +-- +-- Name: resource_server_perm_ticket fk_frsrho213xcx4wnkog84sspmt; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT fk_frsrho213xcx4wnkog84sspmt FOREIGN KEY (scope_id) REFERENCES public.resource_server_scope(id); + + +-- +-- Name: associated_policy fk_frsrpas14xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.associated_policy + ADD CONSTRAINT fk_frsrpas14xcx4wnkog82ssrfy FOREIGN KEY (policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: scope_policy fk_frsrpass3xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.scope_policy + ADD CONSTRAINT fk_frsrpass3xcx4wnkog82ssrfy FOREIGN KEY (scope_id) REFERENCES public.resource_server_scope(id); + + +-- +-- Name: resource_server_perm_ticket fk_frsrpo2128cx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_perm_ticket + ADD CONSTRAINT fk_frsrpo2128cx4wnkog82ssrfy FOREIGN KEY (policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: resource_server_policy fk_frsrpo213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_policy + ADD CONSTRAINT fk_frsrpo213xcx4wnkog82ssrfy FOREIGN KEY (resource_server_id) REFERENCES public.resource_server(id); + + +-- +-- Name: resource_scope fk_frsrpos13xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_scope + ADD CONSTRAINT fk_frsrpos13xcx4wnkog82ssrfy FOREIGN KEY (resource_id) REFERENCES public.resource_server_resource(id); + + +-- +-- Name: resource_policy fk_frsrpos53xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_policy + ADD CONSTRAINT fk_frsrpos53xcx4wnkog82ssrfy FOREIGN KEY (resource_id) REFERENCES public.resource_server_resource(id); + + +-- +-- Name: resource_policy fk_frsrpp213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_policy + ADD CONSTRAINT fk_frsrpp213xcx4wnkog82ssrfy FOREIGN KEY (policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: resource_scope fk_frsrps213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_scope + ADD CONSTRAINT fk_frsrps213xcx4wnkog82ssrfy FOREIGN KEY (scope_id) REFERENCES public.resource_server_scope(id); + + +-- +-- Name: resource_server_scope fk_frsrso213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_server_scope + ADD CONSTRAINT fk_frsrso213xcx4wnkog82ssrfy FOREIGN KEY (resource_server_id) REFERENCES public.resource_server(id); + + +-- +-- Name: composite_role fk_gr7thllb9lu8q4vqa4524jjy8; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.composite_role + ADD CONSTRAINT fk_gr7thllb9lu8q4vqa4524jjy8 FOREIGN KEY (child_role) REFERENCES public.keycloak_role(id); + + +-- +-- Name: user_consent_client_scope fk_grntcsnt_clsc_usc; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_consent_client_scope + ADD CONSTRAINT fk_grntcsnt_clsc_usc FOREIGN KEY (user_consent_id) REFERENCES public.user_consent(id); + + +-- +-- Name: user_consent fk_grntcsnt_user; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_consent + ADD CONSTRAINT fk_grntcsnt_user FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: group_attribute fk_group_attribute_group; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.group_attribute + ADD CONSTRAINT fk_group_attribute_group FOREIGN KEY (group_id) REFERENCES public.keycloak_group(id); + + +-- +-- Name: keycloak_group fk_group_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.keycloak_group + ADD CONSTRAINT fk_group_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: group_role_mapping fk_group_role_group; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.group_role_mapping + ADD CONSTRAINT fk_group_role_group FOREIGN KEY (group_id) REFERENCES public.keycloak_group(id); + + +-- +-- Name: realm_enabled_event_types fk_h846o4h0w8epx5nwedrf5y69j; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_enabled_event_types + ADD CONSTRAINT fk_h846o4h0w8epx5nwedrf5y69j FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: realm_events_listeners fk_h846o4h0w8epx5nxev9f5y69j; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_events_listeners + ADD CONSTRAINT fk_h846o4h0w8epx5nxev9f5y69j FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: identity_provider_mapper fk_idpm_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider_mapper + ADD CONSTRAINT fk_idpm_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: idp_mapper_config fk_idpmconfig; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.idp_mapper_config + ADD CONSTRAINT fk_idpmconfig FOREIGN KEY (idp_mapper_id) REFERENCES public.identity_provider_mapper(id); + + +-- +-- Name: web_origins fk_lojpho213xcx4wnkog82ssrfy; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.web_origins + ADD CONSTRAINT fk_lojpho213xcx4wnkog82ssrfy FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: client_default_roles fk_nuilts7klwqw2h8m2b5joytky; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_default_roles + ADD CONSTRAINT fk_nuilts7klwqw2h8m2b5joytky FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: scope_mapping fk_ouse064plmlr732lxjcn1q5f1; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.scope_mapping + ADD CONSTRAINT fk_ouse064plmlr732lxjcn1q5f1 FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: client fk_p56ctinxxb9gsk57fo49f9tac; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client + ADD CONSTRAINT fk_p56ctinxxb9gsk57fo49f9tac FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: protocol_mapper fk_pcm_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.protocol_mapper + ADD CONSTRAINT fk_pcm_realm FOREIGN KEY (client_id) REFERENCES public.client(id); + + +-- +-- Name: credential fk_pfyr0glasqyl0dei3kl69r6v0; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.credential + ADD CONSTRAINT fk_pfyr0glasqyl0dei3kl69r6v0 FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: protocol_mapper_config fk_pmconfig; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.protocol_mapper_config + ADD CONSTRAINT fk_pmconfig FOREIGN KEY (protocol_mapper_id) REFERENCES public.protocol_mapper(id); + + +-- +-- Name: default_client_scope fk_r_def_cli_scope_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.default_client_scope + ADD CONSTRAINT fk_r_def_cli_scope_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: default_client_scope fk_r_def_cli_scope_scope; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.default_client_scope + ADD CONSTRAINT fk_r_def_cli_scope_scope FOREIGN KEY (scope_id) REFERENCES public.client_scope(id); + + +-- +-- Name: client_scope fk_realm_cli_scope; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.client_scope + ADD CONSTRAINT fk_realm_cli_scope FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: required_action_provider fk_req_act_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.required_action_provider + ADD CONSTRAINT fk_req_act_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: resource_uris fk_resource_server_uris; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.resource_uris + ADD CONSTRAINT fk_resource_server_uris FOREIGN KEY (resource_id) REFERENCES public.resource_server_resource(id); + + +-- +-- Name: role_attribute fk_role_attribute_id; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.role_attribute + ADD CONSTRAINT fk_role_attribute_id FOREIGN KEY (role_id) REFERENCES public.keycloak_role(id); + + +-- +-- Name: realm_supported_locales fk_supported_locales_realm; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.realm_supported_locales + ADD CONSTRAINT fk_supported_locales_realm FOREIGN KEY (realm_id) REFERENCES public.realm(id); + + +-- +-- Name: user_federation_config fk_t13hpu1j94r2ebpekr39x5eu5; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_federation_config + ADD CONSTRAINT fk_t13hpu1j94r2ebpekr39x5eu5 FOREIGN KEY (user_federation_provider_id) REFERENCES public.user_federation_provider(id); + + +-- +-- Name: user_group_membership fk_user_group_user; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.user_group_membership + ADD CONSTRAINT fk_user_group_user FOREIGN KEY (user_id) REFERENCES public.user_entity(id); + + +-- +-- Name: policy_config fkdc34197cf864c4e43; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.policy_config + ADD CONSTRAINT fkdc34197cf864c4e43 FOREIGN KEY (policy_id) REFERENCES public.resource_server_policy(id); + + +-- +-- Name: identity_provider_config fkdc4897cf864c4e43; Type: FK CONSTRAINT; Schema: public; Owner: keycloak +-- + +ALTER TABLE ONLY public.identity_provider_config + ADD CONSTRAINT fkdc4897cf864c4e43 FOREIGN KEY (identity_provider_id) REFERENCES public.identity_provider(internal_id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/devenv/docker/blocks/jwt_proxy/docker-build-keycloak-m1-image.sh b/devenv/docker/blocks/jwt_proxy/docker-build-keycloak-m1-image.sh new file mode 100644 index 00000000000..46c1ef09fa3 --- /dev/null +++ b/devenv/docker/blocks/jwt_proxy/docker-build-keycloak-m1-image.sh @@ -0,0 +1,9 @@ +#/bin/sh + +VERSION=12.0.1 # set version here + +cd /tmp +git clone git@github.com:keycloak/keycloak-containers.git +cd keycloak-containers/server +git checkout $VERSION +docker build -t "quay.io/keycloak/keycloak:${VERSION}" . diff --git a/devenv/docker/blocks/jwt_proxy/docker-compose.yaml b/devenv/docker/blocks/jwt_proxy/docker-compose.yaml new file mode 100644 index 00000000000..0bcd6c5ffe4 --- /dev/null +++ b/devenv/docker/blocks/jwt_proxy/docker-compose.yaml @@ -0,0 +1,54 @@ + oauthkeycloakdb: + image: postgres:12.2 + container_name: oauthkeycloakdb + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + volumes: + - ./docker/blocks/jwt_proxy/cloak.sql:/docker-entrypoint-initdb.d/cloak.sql + restart: unless-stopped + + oauthkeycloak: + image: quay.io/keycloak/keycloak:12.0.1 + container_name: oauthkeycloak + environment: + DB_VENDOR: POSTGRES + DB_ADDR: oauthkeycloakdb + DB_DATABASE: keycloak + DB_USER: keycloak + DB_PASSWORD: password + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + PROXY_ADDRESS_FORWARDING: "true" + ports: + - 8087:8080 + depends_on: + - oauthkeycloakdb + links: + - "oauthkeycloakdb:oauthkeycloakdb" + restart: unless-stopped + + oauthproxy: + image: docker.io/bitnami/oauth2-proxy:7.3.0 + container_name: oauthproxy + command: [ + "--cookie-secret=yI-CWT5s4sBR2Zd0DDJJlTYc0aQ3jwGH15jYA18ZAQA=", + "--upstream=http://localhost:3000", + "--provider=keycloak", + "--client-id=grafana-oauth", + "--client-secret=d17b9ea9-bcb1-43d2-b132-d339e55872a8", + "--login-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/auth", + "--redeem-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/token", + "--profile-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/userinfo", + "--validate-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/userinfo", + "--cookie-secure=false", + "--http-address=0.0.0.0:8088", + "--redirect-url=http://127.0.0.1:8088/oauth2/callback", + "--pass-access-token=true", + "--email-domain=*", + ] + network_mode: "host" + depends_on: + - oauthkeycloak + restart: unless-stopped \ No newline at end of file diff --git a/devenv/docker/blocks/jwt_proxy/jwks.json b/devenv/docker/blocks/jwt_proxy/jwks.json new file mode 100644 index 00000000000..9f79114ba44 --- /dev/null +++ b/devenv/docker/blocks/jwt_proxy/jwks.json @@ -0,0 +1 @@ +{"keys":[{"kid":"On2FQuJ8Y-909uJGWQEDkbzG-GRNmMc43HslEgVv_VQ","kty":"RSA","alg":"RS256","use":"sig","n":"qDmQHfTcOQOzmNJbVvtvuS8p_EgmiscP7vA_PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5_x8aAIl_-TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5_CeCCoKwKSmheD3u1xTJ8-VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP_MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K_vDJBM_3C933vMY8mqSQXbI3G54x-0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv_W_fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQ","e":"AQAB","x5c":["MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg=="],"x5t":"prs-h1NBqOSJMH-tQWLTqguWets","x5t#S256":"YjK3HobZW8xbNL1IPDgFhCM41UC5c0hG2cxaF6v961Q"}]} \ No newline at end of file diff --git a/devenv/docker/blocks/jwt_proxy/readme.md b/devenv/docker/blocks/jwt_proxy/readme.md new file mode 100644 index 00000000000..6aa7d713793 --- /dev/null +++ b/devenv/docker/blocks/jwt_proxy/readme.md @@ -0,0 +1,66 @@ +# OAUTH BLOCK +## Devenv setup jwt auth + +To launch the block, use the oauth source. Ex: + +```bash +make devenv sources="jwt_proxy" +``` + +Here is the conf you need to add to your configuration file (conf/custom.ini): + +```ini +[auth] +signout_redirect_url = http://127.0.0.1:8088/oauth2/sign_out + +[auth.jwt] +enabled = true +enable_login_token = true +header_name = X-Forwarded-Access-Token +username_claim = login +email_claim = email +jwk_set_file = devenv/docker/blocks/oauth/jwks.json +cache_ttl = 60m +expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"} +auto_sign_up = true +``` + +Access Grafana through: + +```sh +http://127.0.0.1:8088 +``` + +## Backing up keycloak DB + +In case you want to make changes to the devenv setup, you can dump keycloack's DB: + +```bash +cd devenv; +docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > docker/blocks/oauth/cloak.sql +``` + +## Connecting to keycloack: + +- keycloak admin: http://localhost:8087 +- keycloak admin login: admin:admin +- grafana jwt viewer login: jwt-viewer:grafana +- grafana jwt editor login: jwt-editor:grafana +- grafana jwt admin login: jwt-admin:grafana + +# Troubleshooting + +## Mac M1 Users + +The new arm64 architecture does not build for the latest docker image of keycloack. Refer to https://github.com/docker/for-mac/issues/5310 for the issue to see if it resolved. +Until then you need to build the docker image locally and then run `devenv`. + +1. Remove any lingering keycloack image +```sh +$ docker rmi $(docker images | grep 'keycloack') +``` +1. Build keycloack image locally +```sh +$ ./docker-build-keycloack-m1-image.sh +``` +1. Start from beginning of this readme diff --git a/devenv/docker/blocks/oauth/jwks.json b/devenv/docker/blocks/oauth/jwks.json new file mode 100644 index 00000000000..9f79114ba44 --- /dev/null +++ b/devenv/docker/blocks/oauth/jwks.json @@ -0,0 +1 @@ +{"keys":[{"kid":"On2FQuJ8Y-909uJGWQEDkbzG-GRNmMc43HslEgVv_VQ","kty":"RSA","alg":"RS256","use":"sig","n":"qDmQHfTcOQOzmNJbVvtvuS8p_EgmiscP7vA_PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5_x8aAIl_-TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5_CeCCoKwKSmheD3u1xTJ8-VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP_MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K_vDJBM_3C933vMY8mqSQXbI3G54x-0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv_W_fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQ","e":"AQAB","x5c":["MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg=="],"x5t":"prs-h1NBqOSJMH-tQWLTqguWets","x5t#S256":"YjK3HobZW8xbNL1IPDgFhCM41UC5c0hG2cxaF6v961Q"}]} \ No newline at end of file diff --git a/devenv/docker/blocks/oauth/readme.md b/devenv/docker/blocks/oauth/readme.md index a40f212a2a8..63c8f3ae008 100644 --- a/devenv/docker/blocks/oauth/readme.md +++ b/devenv/docker/blocks/oauth/readme.md @@ -1,6 +1,6 @@ # OAUTH BLOCK -## Devenv setup +## Devenv setup oauth To launch the block, use the oauth source. Ex: ```bash @@ -29,6 +29,76 @@ api_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/user role_attribute_path = contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' ``` +## Devenv setup jwt auth + +To launch the block, use the oauth source. Ex: + +```bash +make devenv sources="oauth" +``` + +Here is the conf you need to add to your configuration file (conf/custom.ini): + +```ini +[auth.jwt] +enabled = true +header_name = X-JWT-Assertion +username_claim = login +email_claim = email +jwk_set_file = devenv/docker/blocks/oauth/jwks.json +cache_ttl = 60m +expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"} +auto_sign_up = true +``` + +You can obtain a jwt token by using the following command for oauth-admin: + +```sh +curl --request POST \ + --url http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data client_id=grafana-oauth \ + --data grant_type=password \ + --data client_secret=d17b9ea9-bcb1-43d2-b132-d339e55872a8 \ + --data scope=openid \ + --data username=oauth-admin \ + --data password=grafana +``` + + +Grafana call example: + +```sh +curl --request GET \ + --url http://127.0.0.1:3000/api/folders \ + --header 'Accept: application/json' \ + --header 'X-JWT-Assertion: eyJ......' +``` + +### Alternative devenv setup jwk_set_url + +Run a reverse proxy pointing to the jwk_set_url (only an https-uri can be used as jwk_set_url). + +Ex (using localtunnel): + +```sh +npx localtunnel --port 8087 +``` + +And using the following conf: + +```ini +[auth.jwt] +enabled = true +header_name = X-JWT-Assertion +username_claim = login +email_claim = email +jwk_set_url = /auth/realms/grafana/protocol/openid-connect/certs +cache_ttl = 60m +expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"} +auto_sign_up = true +``` + ## Backing up keycloak DB In case you want to make changes to the devenv setup, you can dump keycloack's DB: @@ -62,4 +132,3 @@ $ docker rmi $(docker images | grep 'keycloack') $ ./docker-build-keycloack-m1-image.sh ``` 1. Start from beginning of this readme - diff --git a/docs/sources/administration/api-keys/_index.md b/docs/sources/administration/api-keys/_index.md index c7e556b5477..630058d9c85 100644 --- a/docs/sources/administration/api-keys/_index.md +++ b/docs/sources/administration/api-keys/_index.md @@ -14,24 +14,18 @@ weight: 700 # API keys -API keys can be used to interact with Grafana HTTP APIs. - -We recommend using service accounts instead of API keys if you are on Grafana 8.5+, for more information refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts/#" >}}). - -{{< section >}} - -## About API keys - An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs. -When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "create-api-key/#" >}}). +When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. + +> **Note:** If you use Grafana v8.5 or newer, use service accounts instead of API keys. For more information, refer to [Service accounts]({{< relref "../service-accounts/" >}}). + +{{< section >}} ## Create an API key Create an API key when you want to manage your computed workload with a user. -For more information about API keys, refer to [About API keys in Grafana]({{< relref "about-api-keys/" >}}). - This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}). ### Before you begin: diff --git a/docs/sources/administration/organization-preferences/_index.md b/docs/sources/administration/organization-preferences/_index.md index 9d4ff9b7380..98094f28af5 100644 --- a/docs/sources/administration/organization-preferences/_index.md +++ b/docs/sources/administration/organization-preferences/_index.md @@ -29,7 +29,7 @@ If the user is aware of the change and intended it, then that's great! But if th In Grafana, you can change your names and emails associated with groups or accounts in the Settings or Preferences. This topic provides instructions for each task. -{{< docs/shared "preferences/some-tasks-require-permissions.md" >}} +Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}). ### Change organization name @@ -39,24 +39,20 @@ Grafana server administrators and organization administrators can change organiz Follow these instructions if you are a Grafana Server Admin. -{{< docs/list >}} -{{< docs/shared "manage-users/view-server-org-list.md" >}} - +1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears. +1. Click **Orgs**. 1. In the organization list, click the name of the organization that you want to change. 1. In **Name**, enter the new organization name. 1. Click **Update**. - {{< /docs/list >}} #### Organization Admin change organization name If you are an Organization Admin, follow these steps: -{{< docs/list >}} -{{< docs/shared "preferences/org-preferences-list.md" >}} - +1. Hover your cursor over the **Configuration** (gear) icon. +1. Click **Preferences**. 1. In **Organization name**, enter the new name. 1. Click **Update organization name**. - {{< /docs/list >}} ### Change team name or email @@ -80,7 +76,7 @@ To learn how to edit your user information, refer to [Edit your profile]({{< rel In Grafana, you can modify the UI theme configured in the Settings or Preferences. Set the UI theme for the server, an organization, a team, or your personal user account using the instructions in this topic. -{{< docs/shared "preferences/some-tasks-require-permissions.md" >}} +Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}). ### Theme options @@ -112,36 +108,34 @@ To see what the current settings are, refer to [View server settings]({{< relref Organization administrators can change the UI theme for all users in an organization. -{{< docs/list >}} -{{< docs/shared "preferences/org-preferences-list.md" >}} -{{< docs/shared "preferences/select-ui-theme-list.md" >}} -{{< /docs/list >}} +1. Hover your cursor over the **Configuration** (gear) icon. +1. Click **Preferences**. +1. In the Preferences section, select the **UI theme**. +1. Click **Save**. ### Change team UI theme Organization and team administrators can change the UI theme for all users in a team. -{{< docs/list >}} -{{< docs/shared "manage-users/view-team-list.md" >}} - +1. Hover your cursor over the **Configuration** (gear) icon in the side menu. +1. Click **Teams**. Grafana displays the team list. 1. Click on the team that you want to change the UI theme for and then navigate to the **Settings** tab. - {{< docs/shared "preferences/select-ui-theme-list.md" >}} - {{< /docs/list >}} +1. In the Preferences section, select the **UI theme**. +1. Click **Save**. ### Change your personal UI theme You can change the UI theme for your user account. This setting overrides UI theme settings at higher levels. -{{< docs/list >}} -{{< docs/shared "preferences/navigate-user-preferences-list.md" >}} -{{< docs/shared "preferences/select-ui-theme-list.md" >}} -{{< /docs/list >}} +1. On the left menu, hover your cursor over your avatar and then click **Preferences**. +1. In the Preferences section, select the **UI theme**. +1. Click **Save**. ## Change the Grafana default timezone By default, Grafana uses the timezone in your web browser. However, you can override this setting at the server, organization, team, or individual user level. This topic provides instructions for each task. -{{< docs/shared "preferences/some-tasks-require-permissions.md" >}} +Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}). ### Set server timezone @@ -151,36 +145,34 @@ Grafana server administrators can choose a default timezone for all users on the Organization administrators can choose a default timezone for their organization. -{{< docs/list >}} -{{< docs/shared "preferences/org-preferences-list.md" >}} -{{< docs/shared "preferences/select-timezone-list.md" >}} -{{< /docs/list >}} +1. Hover your cursor over the **Configuration** (gear) icon. +1. Click **Preferences**. +1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings. +1. Click **Save**. ### Set team timezone Organization administrators and team administrators can choose a default timezone for all users in a team. -{{< docs/list >}} -{{< docs/shared "manage-users/view-team-list.md" >}} - +1. Hover your cursor over the **Configuration** (gear) icon in the side menu. +1. Click **Teams**. Grafana displays the team list. 1. Click on the team you that you want to change the timezone for and then navigate to the **Settings** tab. - {{< docs/shared "preferences/select-timezone-list.md" >}} - {{< /docs/list >}} +1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings. +1. Click **Save**. ### Set your personal timezone You can change the timezone for your user account. This setting overrides timezone settings at higher levels. -{{< docs/list >}} -{{< docs/shared "preferences/navigate-user-preferences-list.md" >}} -{{< docs/shared "preferences/select-timezone-list.md" >}} -{{< /docs/list >}} +1. On the left menu, hover your cursor over your avatar and then click **Preferences**. +1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings. +1. Click **Save**. ## Change the default home dashboard The home dashboard you set is the one all users will see by default when they log in. You can set the home dashboard for the server, an organization, a team, or your personal user account. This topic provides instructions for each task. -{{< docs/shared "preferences/some-tasks-require-permissions.md" >}} +Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}). ### Navigate to the home dashboard @@ -216,30 +208,31 @@ default_home_dashboard_path = data/main-dashboard.json Organization administrators can choose a home dashboard for their organization. -{{< docs/list >}} -{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}} -{{< docs/shared "preferences/org-preferences-list.md" >}} -{{< docs/shared "preferences/select-home-dashboard-list.md" >}} -{{< /docs/list >}} +1. Navigate to the dashboard you want to set as the home dashboard. +1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already. +1. Hover your cursor over the **Configuration** (gear) icon. +1. Click **Preferences**. +1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards. +1. Click **Save**. ### Set home dashboard for your team Organization administrators and Team Admins can choose a home dashboard for a team. -{{< docs/list >}} -{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}} -{{< docs/shared "manage-users/view-team-list.md" >}} - +1. Navigate to the dashboard you want to set as the home dashboard. +1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already. +1. Hover your cursor over the **Configuration** (gear) icon in the side menu. +1. Click **Teams**. Grafana displays the team list. 1. Click on the team that you want to change the home dashboard for and then navigate to the **Settings** tab. - {{< docs/shared "preferences/select-home-dashboard-list.md" >}} - {{< /docs/list >}} +1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards. +1. Click **Save**. ### Set your personal home dashboard You can choose your own personal home dashboard. This setting overrides all home dashboards set at higher levels. -{{< docs/list >}} -{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}} -{{< docs/shared "preferences/navigate-user-preferences-list.md" >}} -{{< docs/shared "preferences/select-home-dashboard-list.md" >}} -{{< /docs/list >}} +1. Navigate to the dashboard you want to set as the home dashboard. +1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already. +1. On the left menu, hover your cursor over your avatar and then click **Preferences**. +1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards. +1. Click **Save**. diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 28682a6acbb..7983ac6b0ab 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -15,7 +15,7 @@ weight: 400 # Create a Grafana managed alerting rule -Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. +Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}} diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 3284a3a5879..f229c3f7d4d 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -17,7 +17,7 @@ weight: 400 # Create a Grafana Mimir or Loki managed alerting rule -Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. +Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. ## Before you begin diff --git a/docs/sources/alerting/fundamentals/data-source-alerting.md b/docs/sources/alerting/fundamentals/data-source-alerting.md new file mode 100644 index 00000000000..08ad66165e7 --- /dev/null +++ b/docs/sources/alerting/fundamentals/data-source-alerting.md @@ -0,0 +1,39 @@ +--- +aliases: + - /docs/grafana/latest/alerting/fundamentals/data-source-alerting/ +description: Data sources in Grafana Alerting +title: Data sources +weight: 100 +--- + +# Data sources + +There are a number of data sources that are compatible with Grafana Alerting. Each data source is supported by a plugin. You can use one of the built-in data sources listed below, use [external data source plugins](https://grafana.com/grafana/plugins/?type=datasource), or create your own data source plugin. + +If you are creating your own data source plugin, make sure it is a backend plugin as Grafana Alerting requires this in order to be able to evaluate rules using the data source. Frontend data sources are not supported, because the evaluation engine runs on the backend. + +Specifying { "alerting": true, “backend”: true } in the plugin.json file indicates that the data source plugin is compatible with Grafana Alerting and includes the backend data-fetching code. For more information, refer to [Build a data source backend plugin](https://grafana.com/tutorials/build-a-data-source-backend-plugin/). + +These are the data sources that are compatible with and supported by Grafana Alerting. + +- [AWS CloudWatch]({{< relref "../../datasources/aws-cloudwatch/" >}}) +- [Azure Monitor]({{< relref "../../datasources/azuremonitor/" >}}) +- [Elasticsearch]({{< relref "../../datasources/elasticsearch/" >}}) +- [Google Cloud Monitoring]({{< relref "../../google-cloud-monitoring/" >}}) +- [Graphite]({{< relref "../../datasources/graphite/" >}}) +- [InfluxDB]({{< relref "influxdb/" >}}) +- [Loki]({{< relref "../../datasources/loki/" >}}) +- ]Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}}) +- [MySQL]({{< relref "../../datasources/mysql/" >}}) +- [Open TSDB]({{< relref "../../datasources/opentsdb/" >}}) +- [PostgreSQL]({{< relref "../../datasources/postgres/" >}}) +- [Prometheus]({{< relref "../../datasources/prometheus/" >}}) +- [Jaeger]({{< relref "../../datasources/jaeger/" >}}) +- [Zipkin]({{< relref "../../datasources/zipkin/" >}}) +- [Tempo]({{< relref "../../datasources/tempo/" >}}) +- [Testdata]({{< relref "../../datasources/testdata/" >}}) + +## Useful links + +- [Grafana data sources]({{< relref "../../data-sources/" >}}) +- [Add a data source]({{< relref "../../data-sources/add-a-data-source/" >}}) diff --git a/docs/sources/alerting/images-in-notifications.md b/docs/sources/alerting/images-in-notifications.md index 66d095dad40..b44d921d034 100644 --- a/docs/sources/alerting/images-in-notifications.md +++ b/docs/sources/alerting/images-in-notifications.md @@ -11,7 +11,7 @@ title: Images in notifications # Images in notifications -Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel for the Grafana managed alert rule. +Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel associated with the Grafana managed alert rule. > **Note**: Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager. @@ -20,6 +20,8 @@ If Grafana is set up to send images in notifications, it takes a screenshot of t 1. The alert rule transitions from pending to firing 2. The alert rule transitions from firing to OK +Grafana does not support images for alert rules that are not associated with a panel. An alert rule is associated with a panel when it has both Dashboard UID and Panel ID annotations. + Images are stored in the [data]({{< relref "../setup-grafana/configure-grafana/#paths" >}}) path and so Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. In addition to storing images on disk, Grafana can also store the image in an external image store such as Amazon S3, Azure Blob Storage, Google Cloud Storage and even Grafana where screenshots are stored in `public/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from disk but not the external image store. If Grafana is the external image store then screenshots are deleted from `data` but not from `public/img/attachments`. > **Note**: It is recommended that you use an external image store, as not all contact points support uploading images from disk. It is also possible that the image on disk is deleted before an alert notification is sent if `temp_data_lifetime` is less than the `group_wait` and `group_interval` options used in Alertmanager. diff --git a/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md index 6a94dd9a459..19df966b282 100644 --- a/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md +++ b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md @@ -1,5 +1,6 @@ --- aliases: + - /docs/grafana/latest/alerting/migrating-alerts/differences-and-limitations/ - /docs/grafana/latest/alerting/migrating-alerts/migrating-legacy-alerts/ - /docs/grafana/latest/alerting/migrating-legacy-alerts/ - /docs/grafana/latest/alerting/unified-alerting/opt-in/ @@ -10,22 +11,27 @@ weight: 106 # Differences and limitations -When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. -This topic explains how legacy dashboard alerts are migrated and some limitations of the migration. +There are some differences between Grafana Alerting and legacy dashboard alerts, and a number of features that are no +longer supported. We refer to these as [Differences]({{< relref "#differences" >}}) and [Limitations]({{< relref "#limitations" >}}). -> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Cloud stack. +## Differences -Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows: +1. When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. + This topic explains how legacy dashboard alerts are migrated and some limitations of the migration. + +2. Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows: - If alert's dashboard has permissions, it will create a folder named like `Migrated {"dashboardUid": "UID", "panelId": 1, "alertId": 1}` to match permissions of the dashboard (including the inherited permissions from the folder). - If there are no dashboard permissions and the dashboard is under a folder, then the rule is linked to this folder and inherits its permissions. - If there are no dashboard permissions and the dashboard is under the General folder, then the rule is linked to the `General Alerting` folder, and the rule inherits the default permissions. -> **Note:** Since there is no `Keep Last State` option for [`No Data`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana Alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "../silences/" >}}) for each alert rule with a duration of 1 year. +3. Since there is no `Keep Last State` option for [`No Data`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana Alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "../silences/" >}}) for each alert rule with a duration of 1 year. -Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route. +4. Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route. + +5. Unlike legacy dashboard alerts where images in notifications are enabled per contact point, images in notifications for Grafana Alerting must be enabled in the Grafana configuration, either in the configuration file or environment variables, and are enabled for either all or no contact points. Please refer to the [documentation for images in notifications]({{< relref "../images-in-notifications" >}}). ## Limitations -Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. -Silences (expiring after one year) are created for all paused dashboard alerts. +1. Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. + Silences (expiring after one year) are created for all paused dashboard alerts. diff --git a/docs/sources/datasources/_index.md b/docs/sources/datasources/_index.md index 6aa874d37a3..149a7c44078 100644 --- a/docs/sources/datasources/_index.md +++ b/docs/sources/datasources/_index.md @@ -8,7 +8,7 @@ weight: 60 # Data sources -Grafana supports many different storage backends for your time series data (data source). Refer to [Add a data source]({{< relref "../../administration/datasources/add-a-data-source/" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources. +Grafana supports many different storage backends for your time series data (data source). Refer to [Add a data source]({{< relref "../administration/data-source-management/#add-a-data-source/" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources. ## Querying @@ -18,23 +18,23 @@ Each data source has a specific Query Editor that is customized for the features The following data sources are officially supported: -- [Alertmanager]({{< relref "../../datasources/alertmanager/" >}}) -- [AWS CloudWatch]({{< relref "aws-cloudwatch/" >}}) -- [Azure Monitor]({{< relref "azuremonitor/" >}}) -- [Elasticsearch]({{< relref "../../datasources/elasticsearch/" >}}) -- [Google Cloud Monitoring]({{< relref "google-cloud-monitoring/" >}}) -- [Graphite]({{< relref "../../datasources/graphite/" >}}) -- [InfluxDB]({{< relref "influxdb/" >}}) -- [Loki]({{< relref "../../datasources/loki/" >}}) -- [Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}}) -- [MySQL]({{< relref "../../datasources/mysql/" >}}) -- [OpenTSDB]({{< relref "../../datasources/opentsdb/" >}}) -- [PostgreSQL]({{< relref "../../datasources/postgres/" >}}) -- [Prometheus]({{< relref "../../datasources/prometheus/" >}}) -- [Jaeger]({{< relref "../../datasources/jaeger/" >}}) -- [Zipkin]({{< relref "../../datasources/zipkin/" >}}) -- [Tempo]({{< relref "../../datasources/tempo/" >}}) -- [Testdata]({{< relref "../../datasources/testdata/" >}}) +- [Alertmanager]({{< relref "./alertmanager/" >}}) +- [AWS CloudWatch]({{< relref "./aws-cloudwatch/" >}}) +- [Azure Monitor]({{< relref "./azuremonitor/" >}}) +- [Elasticsearch]({{< relref "./elasticsearch/" >}}) +- [Google Cloud Monitoring]({{< relref "./google-cloud-monitoring/" >}}) +- [Graphite]({{< relref "./graphite/" >}}) +- [InfluxDB]({{< relref "./influxdb/" >}}) +- [Loki]({{< relref "./loki/" >}}) +- [Microsoft SQL Server (MSSQL)]({{< relref "./mssql/" >}}) +- [MySQL]({{< relref "./mysql/" >}}) +- [OpenTSDB]({{< relref "./opentsdb/" >}}) +- [PostgreSQL]({{< relref "./postgres/" >}}) +- [Prometheus]({{< relref "./prometheus/" >}}) +- [Jaeger]({{< relref "./jaeger/" >}}) +- [Zipkin]({{< relref "./zipkin/" >}}) +- [Tempo]({{< relref "./tempo/" >}}) +- [Testdata]({{< relref "./testdata/" >}}) In addition to the data sources that you have configured in your Grafana, there are three special data sources available: diff --git a/docs/sources/datasources/loki.md b/docs/sources/datasources/loki.md index 45bd2273c10..9b845b5a508 100644 --- a/docs/sources/datasources/loki.md +++ b/docs/sources/datasources/loki.md @@ -123,6 +123,10 @@ Operation can have additional parameters under the operation header. See the ope Some operations make sense only in specific order, if adding an operation would result in nonsensical query, operation will be added to the correct place. To order operations manually drag operation box by the operation name and drop in appropriate place. +##### Hints + +In same cases the query editor can detect which operations would be most appropriate for a selected log stream. In such cases it will show a hint next to the `+ Operations` button. Click on the hint to add the operations to your query. + #### Raw query This section is shown only if the `Raw query` switch from the query editor top toolbar is set to `on`. It shows the raw query that will be created and executed by the query editor. diff --git a/docs/sources/developers/http_api/admin.md b/docs/sources/developers/http_api/admin.md index 9780f1faaba..e83c7c941eb 100644 --- a/docs/sources/developers/http_api/admin.md +++ b/docs/sources/developers/http_api/admin.md @@ -471,6 +471,8 @@ Content-Type: application/json `POST /api/admin/pause-all-alerts` +> **Note:** This API is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only. For default alerting, use [silences]({{< relref "../../alerting/silences/" >}}) to stop alerts from being delivered. + Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. **Example Request**: diff --git a/docs/sources/developers/http_api/alerting.md b/docs/sources/developers/http_api/alerting.md index 239b12d9317..f44f44bd852 100644 --- a/docs/sources/developers/http_api/alerting.md +++ b/docs/sources/developers/http_api/alerting.md @@ -15,7 +15,7 @@ title: 'Alerting HTTP API ' # Alerting API -> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/" >}}) only. +> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only. You can find Grafana Alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana Alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts. diff --git a/docs/sources/developers/http_api/annotations.md b/docs/sources/developers/http_api/annotations.md index 251156ca36d..99c6a86195c 100644 --- a/docs/sources/developers/http_api/annotations.md +++ b/docs/sources/developers/http_api/annotations.md @@ -11,14 +11,14 @@ keywords: - annotation - annotations - comment -title: 'Annotations HTTP API ' +title: 'Annotations HTTP API' --- # Annotations API This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be organization annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Find Annotations diff --git a/docs/sources/developers/http_api/auth.md b/docs/sources/developers/http_api/auth.md index 7c21243e497..7576437375a 100644 --- a/docs/sources/developers/http_api/auth.md +++ b/docs/sources/developers/http_api/auth.md @@ -15,7 +15,7 @@ title: 'Authentication HTTP API ' # Authentication API -> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Tokens diff --git a/docs/sources/developers/http_api/dashboard.md b/docs/sources/developers/http_api/dashboard.md index 636221f0782..9378202a15d 100644 --- a/docs/sources/developers/http_api/dashboard.md +++ b/docs/sources/developers/http_api/dashboard.md @@ -9,12 +9,12 @@ keywords: - documentation - api - dashboard -title: 'Dashboard HTTP API ' +title: 'Dashboard HTTP API' --- # Dashboard API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Identifier (id) vs unique identifier (uid) diff --git a/docs/sources/developers/http_api/dashboard_permissions.md b/docs/sources/developers/http_api/dashboard_permissions.md index e111a66d29c..10ee24371fc 100644 --- a/docs/sources/developers/http_api/dashboard_permissions.md +++ b/docs/sources/developers/http_api/dashboard_permissions.md @@ -13,7 +13,7 @@ keywords: - permission - permissions - acl -title: 'Dashboard Permissions HTTP API ' +title: 'Dashboard Permissions HTTP API' --- # Dashboard Permissions API @@ -28,7 +28,7 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get permissions for a dashboard diff --git a/docs/sources/developers/http_api/data_source.md b/docs/sources/developers/http_api/data_source.md index 8f4a9da2147..0cb89c07058 100644 --- a/docs/sources/developers/http_api/data_source.md +++ b/docs/sources/developers/http_api/data_source.md @@ -10,12 +10,12 @@ keywords: - documentation - api - data source -title: 'Data source HTTP API ' +title: 'Data source HTTP API' --- # Data source API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get all data sources diff --git a/docs/sources/developers/http_api/datasource_permissions.md b/docs/sources/developers/http_api/datasource_permissions.md index e2b4128b1b2..ef2c659bc80 100644 --- a/docs/sources/developers/http_api/datasource_permissions.md +++ b/docs/sources/developers/http_api/datasource_permissions.md @@ -14,14 +14,14 @@ keywords: - permissions - acl - enterprise -title: 'Datasource Permissions HTTP API ' +title: 'Datasource Permissions HTTP API' --- # Data Source Permissions API > The Data Source Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. This API can be used to enable, disable, list, add and remove permissions for a data source. diff --git a/docs/sources/developers/http_api/external_group_sync.md b/docs/sources/developers/http_api/external_group_sync.md index 34225e0d0f2..9e62da932a4 100644 --- a/docs/sources/developers/http_api/external_group_sync.md +++ b/docs/sources/developers/http_api/external_group_sync.md @@ -13,14 +13,14 @@ keywords: - group - member - enterprise -title: 'External Group Sync HTTP API ' +title: 'External Group Sync HTTP API' --- # External Group Synchronization API > External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get External Groups diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md index bba3eb08c39..9ad4d3701a7 100644 --- a/docs/sources/developers/http_api/folder.md +++ b/docs/sources/developers/http_api/folder.md @@ -9,12 +9,12 @@ keywords: - documentation - api - folder -title: 'Folder HTTP API ' +title: 'Folder HTTP API' --- # Folder API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Identifier (id) vs unique identifier (uid) diff --git a/docs/sources/developers/http_api/folder_dashboard_search.md b/docs/sources/developers/http_api/folder_dashboard_search.md index 85ff060589f..e1c2764575c 100644 --- a/docs/sources/developers/http_api/folder_dashboard_search.md +++ b/docs/sources/developers/http_api/folder_dashboard_search.md @@ -11,7 +11,7 @@ keywords: - search - folder - dashboard -title: 'Folder/Dashboard Search HTTP API ' +title: 'Folder/Dashboard Search HTTP API' --- # Folder/Dashboard Search API @@ -20,7 +20,7 @@ title: 'Folder/Dashboard Search HTTP API ' `GET /api/search/` -> Note: When using [Role-based access control]({{< relref "../../enterprise/access-control/" >}}), search results will contain only dashboards and folders which you have access to. +> Note: When using [Role-based access control]({{< relref "../../administration/roles-and-permissions/access-control/" >}}), search results will contain only dashboards and folders which you have access to. Query parameters: diff --git a/docs/sources/developers/http_api/folder_permissions.md b/docs/sources/developers/http_api/folder_permissions.md index e5989981921..2657c27e56d 100644 --- a/docs/sources/developers/http_api/folder_permissions.md +++ b/docs/sources/developers/http_api/folder_permissions.md @@ -13,7 +13,7 @@ keywords: - permission - permissions - acl -title: 'Folder Permissions HTTP API ' +title: 'Folder Permissions HTTP API' --- # Folder Permissions API @@ -28,7 +28,7 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get permissions for a folder diff --git a/docs/sources/developers/http_api/licensing.md b/docs/sources/developers/http_api/licensing.md index 9a61e645564..d6e361307ba 100644 --- a/docs/sources/developers/http_api/licensing.md +++ b/docs/sources/developers/http_api/licensing.md @@ -10,14 +10,14 @@ keywords: - api - licensing - enterprise -title: 'Licensing HTTP API ' +title: 'Licensing HTTP API' --- # Enterprise License API Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Check license availability diff --git a/docs/sources/developers/http_api/org.md b/docs/sources/developers/http_api/org.md index 26f0fdff4d4..f0e4e98cb8d 100644 --- a/docs/sources/developers/http_api/org.md +++ b/docs/sources/developers/http_api/org.md @@ -10,7 +10,7 @@ keywords: - documentation - api - organization -title: 'Organization HTTP API ' +title: 'Organization HTTP API' --- # Organization API @@ -19,7 +19,7 @@ The Organization HTTP API is divided in two resources, `/api/org` (current organ and `/api/orgs` (admin organizations). One big difference between these are that the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Current Organization API diff --git a/docs/sources/developers/http_api/reporting.md b/docs/sources/developers/http_api/reporting.md index dffb1c4159c..6e931a50d18 100644 --- a/docs/sources/developers/http_api/reporting.md +++ b/docs/sources/developers/http_api/reporting.md @@ -17,7 +17,7 @@ This API allows you to interact programmatically with the [Reporting]({{< relref > Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Send a report diff --git a/docs/sources/developers/http_api/serviceaccount.md b/docs/sources/developers/http_api/serviceaccount.md index 0e12e8e285e..7fe15a9c298 100644 --- a/docs/sources/developers/http_api/serviceaccount.md +++ b/docs/sources/developers/http_api/serviceaccount.md @@ -9,12 +9,12 @@ keywords: - documentation - api - serviceaccount -title: 'Service account HTTP API ' +title: 'Service account HTTP API' --- # Service account API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Search service accounts with Paging @@ -105,7 +105,8 @@ Authorization: Basic YWRtaW46YWRtaW4= { "name": "grafana", - "role": "Admin", + "role": "Viewer", + "isDisabled" : false } ``` @@ -131,7 +132,7 @@ Content-Type: application/json } ``` -## Get single serviceaccount by Id +## Get a service account by ID `GET /api/serviceaccounts/:id` diff --git a/docs/sources/developers/http_api/team.md b/docs/sources/developers/http_api/team.md index 8b6aebb6bc2..e6418c42680 100644 --- a/docs/sources/developers/http_api/team.md +++ b/docs/sources/developers/http_api/team.md @@ -11,7 +11,7 @@ keywords: - team - teams - group -title: 'Team HTTP API ' +title: 'Team HTTP API' --- # Team API @@ -25,7 +25,7 @@ Access to these API endpoints is restricted as follows: - If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin. - If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Team Search With Paging diff --git a/docs/sources/developers/http_api/user.md b/docs/sources/developers/http_api/user.md index 5d8e64488ae..462ecfd3f9e 100644 --- a/docs/sources/developers/http_api/user.md +++ b/docs/sources/developers/http_api/user.md @@ -9,12 +9,12 @@ keywords: - documentation - api - user -title: 'User HTTP API ' +title: 'User HTTP API' --- # User API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. ## Search Users diff --git a/docs/sources/developers/plugins/_index.md b/docs/sources/developers/plugins/_index.md index 2027af7cd97..68f103396a5 100644 --- a/docs/sources/developers/plugins/_index.md +++ b/docs/sources/developers/plugins/_index.md @@ -3,6 +3,7 @@ aliases: - /docs/grafana/latest/developers/plugins/ - /docs/grafana/latest/plugins/developing/ title: Build a plugin +description: Resources for creating Grafana plugins weight: 200 --- diff --git a/docs/sources/developers/plugins/add-support-for-annotations.md b/docs/sources/developers/plugins/add-support-for-annotations.md index 3cabebb1081..84a5a6d083e 100644 --- a/docs/sources/developers/plugins/add-support-for-annotations.md +++ b/docs/sources/developers/plugins/add-support-for-annotations.md @@ -10,7 +10,7 @@ This guide explains how to add support for [annotations]({{< relref "../../dashb This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "/tutorials/build-a-data-source-plugin/" >}}). -> **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to the [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/). +> **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/). ## Add annotations support to your data source diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index 7a65c25bd79..46de4238d4b 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -26,7 +26,7 @@ To learn more about Grafana Enterprise, refer to [our product page](https://graf ## Enterprise features in Grafana Cloud -Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/grafana-cloud/reference/enterprise-features" >}}). +Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/docs/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/docs/grafana-cloud/reference/enterprise-features" >}}). ## Authentication diff --git a/docs/sources/introduction/_index.md b/docs/sources/introduction/_index.md index e8e942d6fc6..0faa7112174 100644 --- a/docs/sources/introduction/_index.md +++ b/docs/sources/introduction/_index.md @@ -11,19 +11,19 @@ weight: 5 [Grafana open source software](https://grafana.com/oss/) enables you to query, visualize, alert on, and explore your metrics, logs, and traces wherever they are stored. Grafana OSS provides you with tools to turn your time-series database (TSDB) data into insightful graphs and visualizations. -After you have [installed Grafana]({{< relref "../setup-grafana/installation/" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/build-first-dashboard.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/server-administration/provisioning.md" >}}) and [authentication]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}). +After you have [installed Grafana]({{< relref "../setup-grafana/installation/" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/build-first-dashboard.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/provisioning/" >}}) and [authentication]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}). The following sections provide an overview of Grafana features and links to product documentation to help you learn more. For more guidance and ideas, check out our [Grafana Community forums](https://community.grafana.com/). ## Explore metrics, logs, and traces -Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information. +Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/" >}}) for more information. ## Alerts If you're using Grafana Alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. -Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. +Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/" >}}) for your most important metrics. ## Annotations @@ -33,7 +33,7 @@ This feature, which shows up as a graph marker in Grafana, is useful for correla ## Dashboard variables -[Template variables]({{< relref "../variables/_index.md" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both. +[Template variables]({{< relref "../variables/" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both. Templating allows you to drill down into your data, say, from all data to North America data, down to Texas data, and beyond. You can also share these dashboards across teams within your organization—or if you create a great dashboard template for a popular data source, you can contribute it to the whole community to customize and use. @@ -57,11 +57,11 @@ In Grafana Enterprise, you can also map users to teams: If your company has its While it's easy to click, drag, and drop to create a single dashboard, power users in need of many dashboards will want to automate the setup with a script. You can script anything in Grafana. -For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/server-administration/provisioning.md" >}}) for more information. +For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/provisioning/" >}}) for more information. ## Permissions -When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/user-management/manage-dashboard-permissions/_index.md" >}}), and down to the [data source level]({{< relref "../administration/data-source-management/datasource-permissions.md" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}). +When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/user-management/manage-dashboard-permissions/" >}}), and down to the [data source level]({{< relref "../administration/data-source-management#data-source-permissions" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/" >}}). ## Other Grafana Labs OSS Projects diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 4ad17e02753..d5f84945499 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -49,8 +49,8 @@ With [enhanced LDAP integration]({{< relref "../setup-grafana/configure-security Grafana Enterprise adds the following features: -- [Role-based access control]({{< relref "../enterprise/access-control/" >}}) to control access with role-based permissions. -- [Data source permissions]({{< relref "../administration/data-source-management/datasource-permissions.md" >}}) to restrict query access to specific teams and users. +- [Role-based access control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) to control access with role-based permissions. +- [Data source permissions]({{< relref "../administration/data-source-management#data-source-permissions" >}}) to restrict query access to specific teams and users. - [Data source query caching]({{< relref "../enterprise/query-caching.md" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting. - [Reporting]({{< relref "../enterprise/reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose. - [Export dashboard as PDF]({{< relref "../enterprise/export-pdf.md" >}}) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md index 6a87bd93725..85fc53e6780 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md @@ -72,6 +72,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org" # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = "grafana" +# Timeout in seconds. Applies to each host specified in the 'host' entry (space separated). +timeout = 10 + # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" # Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))" search_filter = "(cn=%s)" diff --git a/docs/sources/shared/manage-users/view-server-org-list-and-edit.md b/docs/sources/shared/manage-users/view-server-org-list-and-edit.md index 8e81c1c894d..e97fb9faf35 100644 --- a/docs/sources/shared/manage-users/view-server-org-list-and-edit.md +++ b/docs/sources/shared/manage-users/view-server-org-list-and-edit.md @@ -8,4 +8,5 @@ title: View org list as server admin {{< docs/shared "manage-users/view-server-org-list.md" >}} 1. Click the name of the organization that you want to edit. - {{< /docs/list >}} + +{{< /docs/list >}} diff --git a/docs/sources/shared/manage-users/view-server-user-list-search.md b/docs/sources/shared/manage-users/view-server-user-list-search.md index aa9339bc7fb..5011a615847 100644 --- a/docs/sources/shared/manage-users/view-server-user-list-search.md +++ b/docs/sources/shared/manage-users/view-server-user-list-search.md @@ -8,4 +8,5 @@ title: View user list and search - list format {{< docs/shared "manage-users/view-server-user-list.md" >}} 1. Click the user account that you want to edit. If necessary, use the search field to find the account. - {{< /docs/list >}} + +{{< /docs/list >}} diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index f74c6104e24..435f19cf4d5 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -193,13 +193,13 @@ These features are included in the Grafana Enterprise edition. ### Licensing changes -When determining a user’s role for billing purposes, a user who has the ability to edit and save dashboards is considered an Editor. This includes any user who is an Editor or Admin at the Org level, and who has granted Admin or Edit permissions via [Dashboard permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions/#dashboard-permissions" >}}). +When determining a user’s role for billing purposes, a user who has the ability to edit and save dashboards is considered an Editor. This includes any user who is an Editor or Admin at the Org level, and who has granted Admin or Edit permissions via [Dashboard permissions]({{< relref "../administration/user-management/manage-dashboard-permissions/" >}}). After the number of Viewers or Editors has reached its license limit, only Admins will see a banner in Grafana indicating that the license limit has been reached. Previously, all users saw the banner. Grafana Enterprise license tokens update automatically on a daily basis, which means you no longer need to manually update your license, and the process for adding additional users to a license is smoother than it was before. -Refer to [Licensing restrictions]({{< relref "../enterprise/license/license-restrictions/" >}}) for more information. +Refer to [Licensing restrictions]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}) for more information. ### Export usage insights to Loki diff --git a/docs/sources/whatsnew/whats-new-in-v7-5.md b/docs/sources/whatsnew/whats-new-in-v7-5.md index 0e07db2ee76..4f79a41ae35 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-5.md +++ b/docs/sources/whatsnew/whats-new-in-v7-5.md @@ -153,7 +153,7 @@ Each Grafana Enterprise user will be limited to three concurrent user sessions. A new session is created when you sign in to Grafana from a different device or a different browser. Multiple windows and tabs in the same browser are all part of the same session, so having many Grafana tabs open will not cause any issues. -For more information on Grafana Enterprise licensing and restrictions, refer to [License restrictions]({{< relref "../enterprise/license/license-restrictions/" >}}). +For more information on Grafana Enterprise licensing and restrictions, refer to [License restrictions]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}). ## Breaking changes diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index 41cbc4d84e8..9c18e1c7d6f 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -156,7 +156,7 @@ Log navigation in Explore has been significantly improved. We added pagination t You can now use the Plugin catalog app to easily manage your plugins from within Grafana. Install, update, and uninstall plugins without requiring a server restart. -[Plugin catalog]({{< relref "../plugins/catalog/" >}}) was added as a result of this feature. +[Plugin catalog]({{< relref "../administration/plugin-management#plugin-catalog/" >}}) was added as a result of this feature. ### Performance improvements @@ -294,7 +294,7 @@ These features are included in the Grafana Enterprise edition. You can now add or remove detailed permissions from Viewer, Editor, and Admin org roles, to grant users just the right amount of access within Grafana. Available permissions include the ability to view and manage Users, Reports, and the Access Control API itself. Grafana will support more and more permissions over the coming months. -[Role-based access control docs]({{< relref "../enterprise/access-control/" >}}) were added as a result of this feature. +[Role-based access control docs]({{< relref "../administration/roles-and-permissions/access-control/" >}}) were added as a result of this feature. ### Data source query caching @@ -318,7 +318,7 @@ For more information, refer to [Reporting docs]({{< relref "../enterprise/report The Grafana Enterprise documentation has been updated to describe more specifically how licensed roles are counted, how they can be updated, and where you can see details about dashboard and folder permissions that affect users' licensed roles. -For more information, refer to [License restrictions docs]({{< relref "../enterprise/license/license-restrictions/" >}}). +For more information, refer to [License restrictions docs]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}). ## Breaking changes diff --git a/docs/sources/whatsnew/whats-new-in-v8-1.md b/docs/sources/whatsnew/whats-new-in-v8-1.md index 3c8ba92d9f7..43499869bdb 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-1.md +++ b/docs/sources/whatsnew/whats-new-in-v8-1.md @@ -146,7 +146,7 @@ These features are included in the Grafana Enterprise edition. Role-based access control remains in beta. You can now grant or revoke permissions for Viewers, Editors, or Admins to use Explore mode, configure LDAP or SAML settings, or view the admin/stats page. These new permissions enhance the existing permissions that can be customized, namely permissions to access Users, Orgs, LDAP settings, and Reports in Grafana. -Fine grained access control allows you to customize roles and permissions in Grafana beyond the built-in Viewer, Editor, and Admin roles. As of 8.1, you can modify some of the permissions for any of these built-in roles. This is helpful if you’d like users to have more or fewer access permissions than a given role allows for by default. For an overview of role-based access control and a complete list of available permissions, refer to the [Fine grained access control]({{< relref "../enterprise/access-control/" >}}) documentation. +Fine grained access control allows you to customize roles and permissions in Grafana beyond the built-in Viewer, Editor, and Admin roles. As of 8.1, you can modify some of the permissions for any of these built-in roles. This is helpful if you’d like users to have more or fewer access permissions than a given role allows for by default. For an overview of role-based access control and a complete list of available permissions, refer to the [Fine grained access control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) documentation. ### New and improved reporting scheduler diff --git a/package.json b/package.json index 17cfbda6d88..7dd8d03cd7e 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,6 @@ ], "*pkg/**/*.go": [ "gofmt -w -s" - ], - "*.star": [ - "make drone" ] }, "devDependencies": { @@ -259,7 +256,7 @@ "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "^0.0.2-canary.32", "@grafana/google-sdk": "0.0.3", - "@grafana/lezer-logql": "^0.0.12", + "@grafana/lezer-logql": "^0.0.13", "@grafana/runtime": "workspace:*", "@grafana/schema": "workspace:*", "@grafana/slate-react": "0.22.10-grafana", @@ -396,7 +393,7 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.4.0", - "uplot": "1.6.21", + "uplot": "1.6.22", "uuid": "8.3.2", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 36539778aa2..42595494334 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -38,7 +38,7 @@ "regenerator-runtime": "0.13.9", "rxjs": "7.5.5", "tslib": "2.4.0", - "uplot": "1.6.21", + "uplot": "1.6.22", "xss": "1.0.11" }, "devDependencies": { diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index 18b569e0016..63cda277a14 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -241,10 +241,9 @@ export type TypedArray = | Float32Array | Float64Array; -export type AlignedData = [ - xValues: number[] | TypedArray, - ...yValues: Array | TypedArray> -]; +export type AlignedData = + | TypedArray[] + | [xValues: number[] | TypedArray, ...yValues: Array | TypedArray>]; // nullModes const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true) diff --git a/packages/grafana-data/src/utils/url.test.ts b/packages/grafana-data/src/utils/url.test.ts index 28e70d6fb73..b486564587f 100644 --- a/packages/grafana-data/src/utils/url.test.ts +++ b/packages/grafana-data/src/utils/url.test.ts @@ -13,9 +13,6 @@ describe('toUrlParams', () => { }); expect(url).toBe('server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined='); }); -}); - -describe('toUrlParams', () => { it('should encode the same way as angularjs', () => { const url = urlUtil.toUrlParams({ server: ':@', @@ -30,6 +27,12 @@ describe('toUrlParams', () => { }); expect(url).toBe('bool1&bool2=false'); }); + it("should encode the following special characters [!'()*]", () => { + const url = urlUtil.toUrlParams({ + datasource: "testDs[!'()*]", + }); + expect(url).toBe('datasource=testDs%5B%21%27%28%29%2A%5D'); + }); }); describe('parseKeyValue', () => { diff --git a/packages/grafana-data/src/utils/url.ts b/packages/grafana-data/src/utils/url.ts index 7cf29ed8e7f..149195b4051 100644 --- a/packages/grafana-data/src/utils/url.ts +++ b/packages/grafana-data/src/utils/url.ts @@ -32,7 +32,10 @@ function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { .replace(/%24/g, '$') .replace(/%2C/gi, ',') .replace(/%3B/gi, ';') - .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); + .replace(/%20/g, pctEncodeSpaces ? '%20' : '+') + .replace(/[!'()*]/g, function (c) { + return '%' + c.charCodeAt(0).toString(16).toUpperCase(); + }); } function toUrlParams(a: any) { diff --git a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts index f72460483f8..a64884b769c 100644 --- a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts +++ b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts @@ -7,6 +7,7 @@ import { PUBLIC_DATASOURCE, PublicDashboardDataSource, } from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource'; +import { MIXED_DATASOURCE_NAME } from '../../../../public/app/plugins/datasource/mixed/MixedDataSource'; import { DataSourceWithBackend } from './DataSourceWithBackend'; @@ -75,4 +76,26 @@ describe('PublicDashboardDatasource', () => { let ds = new PublicDashboardDataSource(datasource); expect(ds.uid).toBe('abc123'); }); + + test('isMixedDatasource returns true when datasource is mixed', () => { + const datasource = new DataSourceWithBackend({ id: 1, uid: MIXED_DATASOURCE_NAME } as DataSourceInstanceSettings); + let ds = new PublicDashboardDataSource(datasource); + expect(ds.meta.mixed).toBeTruthy(); + }); + + test('isMixedDatasource returns false when datasource is not mixed', () => { + const datasource = new DataSourceWithBackend({ id: 1, uid: 'abc123' } as DataSourceInstanceSettings); + let ds = new PublicDashboardDataSource(datasource); + expect(ds.meta.mixed).toBeFalsy(); + }); + + test('isMixedDatasource returns false when datasource is a string', () => { + let ds = new PublicDashboardDataSource('abc123'); + expect(ds.meta.mixed).toBeFalsy(); + }); + + test('isMixedDatasource returns false when datasource is null', () => { + let ds = new PublicDashboardDataSource(null); + expect(ds.meta.mixed).toBeFalsy(); + }); }); diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 655102a2ea9..cf86b71249f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -91,7 +91,7 @@ "slate-plain-serializer": "0.7.10", "tinycolor2": "1.4.2", "tslib": "2.4.0", - "uplot": "1.6.21", + "uplot": "1.6.22", "uuid": "8.3.2" }, "devDependencies": { @@ -110,6 +110,7 @@ "@storybook/addons": "6.4.21", "@storybook/api": "6.4.21", "@storybook/builder-webpack5": "6.4.21", + "@storybook/client-api": "6.4.21", "@storybook/components": "6.4.21", "@storybook/core-events": "6.4.21", "@storybook/manager-webpack5": "6.4.21", diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index bf91200c338..54ac6c452f6 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -62,7 +62,7 @@ export const DataLinksContextMenu: React.FC = ({ chil onClick={linkModel.onClick} target={linkModel.target} title={linkModel.title} - style={style} + style={{ ...style, overflow: 'hidden' }} aria-label={selectors.components.DataLinksContextMenu.singleLink} > {children({})} diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 1c8f24dec6e..d52f5c1b2b6 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -1,11 +1,12 @@ import { css, cx } from '@emotion/css'; -import { uniqueId, isString } from 'lodash'; +import { isString, uniqueId } from 'lodash'; import React, { ReactNode, useCallback, useState } from 'react'; -import { DropEvent, DropzoneOptions, FileRejection, useDropzone, Accept } from 'react-dropzone'; +import { Accept, DropEvent, DropzoneOptions, FileRejection, useDropzone } from 'react-dropzone'; import { GrafanaTheme2 } from '@grafana/data'; import { useTheme2 } from '../../themes'; +import { Alert } from '../Alert/Alert'; import { Icon } from '../Icon/Icon'; import { FileListItem } from './FileListItem'; @@ -58,6 +59,7 @@ export interface DropzoneFile { export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) { const [files, setFiles] = useState([]); + const [errorMessages, setErrorMessages] = useState([]); const setFileProperty = useCallback( (customFile: DropzoneFile, action: (customFileToModify: DropzoneFile) => void) => { @@ -83,6 +85,8 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender setFiles((oldFiles) => [...oldFiles, ...customFiles]); } + setErrors(rejectedFiles); + if (options?.onDrop) { options.onDrop(acceptedFiles, rejectedFiles, event); } else { @@ -161,12 +165,40 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender return ; }); + const setErrors = (rejectedFiles: FileRejection[]) => { + let errors: string[] = []; + rejectedFiles.map((rejectedFile) => { + rejectedFile.errors.map((error) => { + errors.push(error.message); + }); + }); + + setErrorMessages(errors); + }; + + const getErrorMessages = () => { + return ( +
+ + {errorMessages.map((error) => { + return
{error}
; + })} +
+
+ ); + }; + + const clearAlert = () => { + setErrorMessages([]); + }; + return (
{children ?? }
+ {errorMessages.length > 0 && getErrorMessages()} {options?.accept && ( {getAcceptedFileTypeText(options.accept)} )} @@ -276,5 +308,8 @@ function getStyles(theme: GrafanaTheme2, isDragActive?: boolean) { small: css` color: ${theme.colors.text.secondary}; `, + errorAlert: css` + padding-top: 10px; + `, }; } diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.mdx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.mdx new file mode 100644 index 00000000000..9e7c33675e0 --- /dev/null +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.mdx @@ -0,0 +1,19 @@ +import { Meta, Preview, Props } from '@storybook/addon-docs/blocks'; +import { RefreshPicker } from './RefreshPicker'; + + + +# Refresh Picker + +This component is used on dashboards to refresh visualizations. Grafana does not do this automatically, queries run on their own schedule according to the panel settings. Grafana cancels any pending requests when a new refresh is triggered. + +**The refresh icon:** will immediately run the query and refresh the visualizations. + +**The down arrow:** will display a list of refresh intervals. If one of them is selected the dashboard will regularly refresh according to that schedule. + + + + + + + diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx index 1ec9a5bafb3..da309c0a0ef 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx @@ -1,24 +1,45 @@ import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/client-api'; +import { Story } from '@storybook/react'; import React from 'react'; import { RefreshPicker } from '@grafana/ui'; -import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; -import { StoryExample } from '../../utils/storybook/StoryExample'; -import { UseState } from '../../utils/storybook/UseState'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; -import { HorizontalGroup } from '../Layout/Layout'; + +import { Props } from './RefreshPicker'; +import mdx from './RefreshPicker.mdx'; export default { title: 'Pickers and Editors/RefreshPicker', component: RefreshPicker, decorators: [withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + controls: { + sort: 'alpha', + }, + }, + args: { + isLoading: false, + isLive: false, + width: 'auto', + text: 'Run query', + tooltip: 'My tooltip text goes here', + value: '1h', + primary: false, + noIntervalPicker: false, + intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], + }, }; -export const Examples = () => { - const intervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d']; +export const Examples: Story = (args) => { + const [, updateArgs] = useArgs(); const onIntervalChanged = (interval: string) => { action('onIntervalChanged fired')(interval); + updateArgs({ value: interval }); }; const onRefresh = () => { @@ -26,45 +47,17 @@ export const Examples = () => { }; return ( - - - {(value, updateValue) => { - return ( - - - - - - - - - - - - ); - }} - - + ); }; diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 30e72a5636d..6bf310ddefa 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -257,13 +257,14 @@ export const Table: FC = memo((props: Props) => { if (enablePagination) { const itemsRangeStart = state.pageIndex * state.pageSize + 1; let itemsRangeEnd = itemsRangeStart + state.pageSize - 1; - const isSmall = width < 500; + const isSmall = width < 550; if (itemsRangeEnd > data.length) { itemsRangeEnd = data.length; } paginationEl = (
-
+ {isSmall ? null :
} +
{ width: 100%; overflow: auto; display: flex; + flex-direction: column; `, thead: css` label: thead; @@ -182,17 +183,22 @@ export const getTableStyles = (theme: GrafanaTheme2) => { li { margin-bottom: 0; } - div:not(:only-child):first-child { - flex-grow: 0.6; - } - position: absolute; - bottom: 0; - left: 0; + `, + paginationItem: css` + flex: 20%; + `, + paginationCenterItem: css` + flex: 100%; + display: flex; + justify-content: center; `, paginationSummary: css` color: ${theme.colors.text.secondary}; font-size: ${theme.typography.bodySmall.fontSize}; - margin-left: auto; + display: flex; + justify-content: flex-end; + flex: 20%; + padding-right: ${theme.spacing(1)}; `, tableContentWrapper: (totalColumnsWidth: number) => { diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx index 6b4c3efd187..027d564b325 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -102,14 +102,6 @@ export class UPlotChart extends React.Component { this.reinitPlot(); } else if (!sameData(prevProps, this.props)) { plot?.setData(this.props.data as AlignedData); - - // this is a uPlot cache-busting hack for bar charts in case x axis labels changed - // since the x scale's "range" doesnt change, the axis size doesnt get recomputed, which is where the tick labels are regenerated & cached - // the more expensive, more proper/thorough way to do this is to force all axes to recalc: plot?.redraw(false, true); - if (plot && typeof this.props.data[0]?.[0] === 'string') { - //@ts-ignore - plot.axes[0]._values = this.props.data[0]; - } } else if (!sameTimeRange(prevProps, this.props)) { plot?.setScale('x', { min: this.props.timeRange.from.valueOf(), diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index b750ab71410..e6df844a3af 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -36,6 +37,11 @@ var ( // grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" // that HTTPServer needs func (hs *HTTPServer) declareFixedRoles() error { + // Declare plugins roles + if err := plugins.DeclareRBACRoles(hs.AccessControl); err != nil { + return err + } + provisioningWriterRole := ac.RoleRegistration{ Role: ac.RoleDTO{ Name: "fixed:provisioning:writer", @@ -423,6 +429,13 @@ var orgsCreateAccessEvaluator = ac.EvalAll( ac.EvalPermission(ActionOrgsCreate), ) +// usersInviteEvaluator is used to protect the "Configuration > Users > Invite" page access +// accessible to org admins and server admins by default +var usersInviteEvaluator = ac.EvalAny( + ac.EvalPermission(ac.ActionUsersCreate), + ac.EvalPermission(ac.ActionOrgUsersAdd), +) + // teamsAccessEvaluator is used to protect the "Configuration > Teams" page access // grants access to a user when they can either create teams or can read and update a team var teamsAccessEvaluator = ac.EvalAny( @@ -450,7 +463,10 @@ var teamsEditAccessEvaluator = ac.EvalAll( var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead) // serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access -var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead) +var serviceAccountAccessEvaluator = ac.EvalAny( + ac.EvalPermission(serviceaccounts.ActionRead), + ac.EvalPermission(serviceaccounts.ActionCreate), +) // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource diff --git a/pkg/api/api.go b/pkg/api/api.go index df4aaf68295..167a94fda63 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -59,7 +60,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index) r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) - r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) + r.Get("/org/users/invite", authorize(reqOrgAdmin, usersInviteEvaluator), hs.Index) r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) @@ -88,8 +89,10 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/plugins/:id/", reqSignedIn, hs.Index) r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index) - r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page - r.Get("/a/:id", reqSignedIn, hs.Index) + // App Root Page + appPluginIDScope := plugins.ScopeProvider.GetResourceScope(":id") + r.Get("/a/:id/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index) + r.Get("/a/:id", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index) r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) @@ -225,8 +228,10 @@ func (hs *HTTPServer) registerRoutes() { orgRoute.Get("/read/*", routing.Wrap(hs.StorageService.Read)) if hs.Features.IsEnabled(featuremgmt.FlagStorageLocalUpload) { - orgRoute.Delete("/delete/*", reqSignedIn, routing.Wrap(hs.StorageService.Delete)) - orgRoute.Post("/upload", reqSignedIn, routing.Wrap(hs.StorageService.Upload)) + orgRoute.Post("/delete/*", reqGrafanaAdmin, routing.Wrap(hs.StorageService.Delete)) + orgRoute.Post("/upload", reqGrafanaAdmin, routing.Wrap(hs.StorageService.Upload)) + orgRoute.Post("/createFolder", reqGrafanaAdmin, routing.Wrap(hs.StorageService.CreateFolder)) + orgRoute.Post("/deleteFolder", reqGrafanaAdmin, routing.Wrap(hs.StorageService.DeleteFolder)) } }) } @@ -244,7 +249,7 @@ func (hs *HTTPServer) registerRoutes() { // invites orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.GetPendingOrgInvites)) - orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), quota("user"), routing.Wrap(hs.AddOrgInvite)) + orgRoute.Post("/invites", authorize(reqOrgAdmin, usersInviteEvaluator), quota("user"), routing.Wrap(hs.AddOrgInvite)) orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.RevokeInvite)) // prefs @@ -325,12 +330,13 @@ func (hs *HTTPServer) registerRoutes() { datasourceRoute.Get("/id/:name", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionIDRead, nameScope)), routing.Wrap(hs.GetDataSourceIdByName)) }) + pluginIDScope := plugins.ScopeProvider.GetResourceScope(":pluginId") apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList)) - apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) + apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) // RBAC check performed in handler for App Plugins apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown)) apiRoute.Get("/plugins/:pluginId/health", routing.Wrap(hs.CheckHealth)) - apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource) - apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource) + apiRoute.Any("/plugins/:pluginId/resources", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource) + apiRoute.Any("/plugins/:pluginId/resources/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource) apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList)) if hs.Cfg.PluginAdminEnabled && !hs.Cfg.PluginAdminExternalManageEnabled { @@ -559,6 +565,7 @@ func (hs *HTTPServer) registerRoutes() { if hs.Features.IsEnabled(featuremgmt.FlagExport) { adminRoute.Get("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleGetStatus)) adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport)) + adminRoute.Post("/export/stop", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestStop)) } adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys)) diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index c855edde892..03d30848d72 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -42,6 +43,11 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) { ReqSignedIn: true, })) + // Preventing access to plugin routes if the user has no right to access the plugin + authorize := ac.Middleware(hs.AccessControl) + handlers = append(handlers, authorize(middleware.ReqSignedIn, + ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID)))) + if route.ReqRole != "" { if route.ReqRole == models.ROLE_ADMIN { handlers = append(handlers, middleware.RoleAuth(models.ROLE_ADMIN)) @@ -49,6 +55,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) { handlers = append(handlers, middleware.RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN)) } } + handlers = append(handlers, AppPluginRoute(route, plugin.ID, hs)) for _, method := range strings.Split(route.Method, ",") { r.Handle(strings.TrimSpace(method), url, handlers) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 05e57abbdec..2c2ae051586 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -268,24 +268,28 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res return response.Error(404, "Failed to get dashboard snapshot", nil) } - dashboardID := query.Result.Dashboard.Get("id").MustInt64() - - guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser) - canEdit, err := guardian.CanEdit() - // check for permissions only if the dahboard is found - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(500, "Error while checking permissions for snapshot", err) - } - - if !canEdit && query.Result.UserId != c.SignedInUser.UserId && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(403, "Access denied to this snapshot", nil) - } - if query.Result.External { err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl) if err != nil { return response.Error(500, "Failed to delete external dashboard", err) } + } else { + // When creating an external snapshot, its dashboard content is empty. This means that the mustInt here returns a 0, + // which before RBAC would result in a dashboard which has no ACL. A dashboard without an ACL would fallback + // to the user’s org role, which for editors and admins would essentially always be allowed here. With RBAC, + // all permissions must be explicit, so the lack of a rule for dashboard 0 means the guardian will reject. + dashboardID := query.Result.Dashboard.Get("id").MustInt64() + + guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser) + canEdit, err := guardian.CanEdit() + // check for permissions only if the dahboard is found + if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(500, "Error while checking permissions for snapshot", err) + } + + if !canEdit && query.Result.UserId != c.SignedInUser.UserId && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(403, "Access denied to this snapshot", nil) + } } cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey} diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 5c09202cb09..43251646977 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -65,11 +65,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { t.Run("When user has editor role and is not in the ACL", func(t *testing.T) { loggedInUserScenarioWithRole(t, "Should not be able to delete snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - var externalRequest *http.Request - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - externalRequest = req - }) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, ts.URL)} + hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, "")} sc.handlerFunc = hs.DeleteDashboardSnapshot dashSvc := dashboards.NewFakeDashboardService(t) @@ -79,7 +75,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() assert.Equal(t, 403, sc.resp.Code) - require.Nil(t, externalRequest) }, sqlmock) }) diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index d68af629dc1..3fcb64dce46 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -11,10 +11,10 @@ import ( "github.com/getsentry/sentry-go" "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" @@ -258,7 +258,7 @@ func TestFrontendLoggingEndpointSentry(t *testing.T) { assert.Len(t, logs, 10) assertContextContains(t, logs, "logger", "frontend") assertContextContains(t, logs, "msg", "hello world") - assertContextContains(t, logs, "lvl", level.InfoValue()) + assertContextContains(t, logs, level.Key().(string), level.InfoValue()) assertContextContains(t, logs, "logger", "frontend") assertContextContains(t, logs, "url", messageEvent.Request.URL) assertContextContains(t, logs, "user_agent", messageEvent.Request.Headers["User-Agent"]) diff --git a/pkg/api/index.go b/pkg/api/index.go index 00c375931c6..9ab27c752c5 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -76,6 +76,7 @@ func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink { } func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) { + hasAccess := ac.HasAccess(hs.AccessControl, c) enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgId) if err != nil { return nil, err @@ -87,6 +88,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) continue } + if !hasAccess(ac.ReqSignedIn, + ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) { + continue + } + appLink := &dtos.NavLink{ Text: plugin.Name, Id: "plugin-page-" + plugin.ID, diff --git a/pkg/api/login.go b/pkg/api/login.go index 54abf8ae683..1fc39b82f94 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -124,7 +124,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { user := &user.User{ID: c.SignedInUser.UserId, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login} err := hs.loginUserWithUser(user, c) if err != nil { - c.Handle(hs.Cfg, 500, "Failed to sign in user", err) + c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to sign in user", err) return } } diff --git a/pkg/api/org.go b/pkg/api/org.go index 2512938837b..d5b5c308059 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -34,10 +34,10 @@ func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response { org, err := hs.SQLStore.GetOrgByName(web.Params(c.Req)[":name"]) if err != nil { if errors.Is(err, models.ErrOrgNotFound) { - return response.Error(404, "Organization not found", err) + return response.Error(http.StatusNotFound, "Organization not found", err) } - return response.Error(500, "Failed to get organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get organization", err) } result := models.OrgDetailsDTO{ Id: org.Id, @@ -60,9 +60,9 @@ func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Re if err := hs.SQLStore.GetOrgById(ctx, &query); err != nil { if errors.Is(err, models.ErrOrgNotFound) { - return response.Error(404, "Organization not found", err) + return response.Error(http.StatusNotFound, "Organization not found", err) } - return response.Error(500, "Failed to get organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get organization", err) } org := query.Result @@ -90,15 +90,15 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { } acEnabled := !hs.AccessControl.IsDisabled() if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) { - return response.Error(403, "Access denied", nil) + return response.Error(http.StatusForbidden, "Access denied", nil) } cmd.UserId = c.UserId if err := hs.SQLStore.CreateOrg(c.Req.Context(), &cmd); err != nil { if errors.Is(err, models.ErrOrgNameTaken) { - return response.Error(409, "Organization name taken", err) + return response.Error(http.StatusConflict, "Organization name taken", err) } - return response.Error(500, "Failed to create organization", err) + return response.Error(http.StatusInternalServerError, "Failed to create organization", err) } metrics.MApiOrgCreate.Inc() @@ -135,9 +135,9 @@ func (hs *HTTPServer) updateOrgHelper(ctx context.Context, form dtos.UpdateOrgFo cmd := models.UpdateOrgCommand{Name: form.Name, OrgId: orgID} if err := hs.SQLStore.UpdateOrg(ctx, &cmd); err != nil { if errors.Is(err, models.ErrOrgNameTaken) { - return response.Error(400, "Organization name taken", err) + return response.Error(http.StatusBadRequest, "Organization name taken", err) } - return response.Error(500, "Failed to update organization", err) + return response.Error(http.StatusInternalServerError, "Failed to update organization", err) } return response.Success("Organization updated") @@ -179,7 +179,7 @@ func (hs *HTTPServer) updateOrgAddressHelper(ctx context.Context, form dtos.Upda } if err := hs.SQLStore.UpdateOrgAddress(ctx, &cmd); err != nil { - return response.Error(500, "Failed to update org address", err) + return response.Error(http.StatusInternalServerError, "Failed to update org address", err) } return response.Success("Address updated") @@ -193,14 +193,14 @@ func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response { } // before deleting an org, check if user does not belong to the current org if c.OrgId == orgID { - return response.Error(400, "Can not delete org for current user", nil) + return response.Error(http.StatusBadRequest, "Can not delete org for current user", nil) } if err := hs.SQLStore.DeleteOrg(c.Req.Context(), &models.DeleteOrgCommand{Id: orgID}); err != nil { if errors.Is(err, models.ErrOrgNotFound) { - return response.Error(404, "Failed to delete organization. ID not found", nil) + return response.Error(http.StatusNotFound, "Failed to delete organization. ID not found", nil) } - return response.Error(500, "Failed to update organization", err) + return response.Error(http.StatusInternalServerError, "Failed to update organization", err) } return response.Success("Organization deleted") } @@ -221,7 +221,7 @@ func (hs *HTTPServer) SearchOrgs(c *models.ReqContext) response.Response { } if err := hs.SQLStore.SearchOrgs(c.Req.Context(), &query); err != nil { - return response.Error(500, "Failed to search orgs", err) + return response.Error(http.StatusInternalServerError, "Failed to search orgs", err) } return response.JSON(http.StatusOK, query.Result) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 2cd52088f16..c5ae8e49139 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -5,12 +5,14 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -50,9 +52,27 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { return response.Error(500, "Failed to query db for existing user check", err) } } else { + // Evaluate permissions for adding an existing user to the organization + userIDScope := ac.Scope("users", "id", strconv.Itoa(int(userQuery.Result.ID))) + hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionOrgUsersAdd, userIDScope)) + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err) + } + if !hasAccess { + return response.Error(http.StatusForbidden, "Permission denied: not permitted to add an existing user to this organisation", err) + } return hs.inviteExistingUserToOrg(c, userQuery.Result, &inviteDto) } + // Evaluate permissions for inviting a new user to Grafana + hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionUsersCreate)) + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err) + } + if !hasAccess { + return response.Error(http.StatusForbidden, "Permission denied: not permitted to create a new user", err) + } + if setting.DisableLoginForm { return response.Error(400, "Cannot invite when login is disabled.", nil) } @@ -63,7 +83,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { cmd.Name = inviteDto.Name cmd.Status = models.TmpUserInvitePending cmd.InvitedByUserId = c.UserId - var err error cmd.Code, err = util.GetRandomString(30) if err != nil { return response.Error(500, "Could not generate random string", err) diff --git a/pkg/api/org_invite_test.go b/pkg/api/org_invite_test.go new file mode 100644 index 00000000000..4f4cd04edf1 --- /dev/null +++ b/pkg/api/org_invite_test.go @@ -0,0 +1,86 @@ +package api + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func TestOrgInvitesAPIEndpointAccess(t *testing.T) { + type accessControlTestCase2 struct { + expectedCode int + desc string + url string + method string + permissions []accesscontrol.Permission + input string + } + tests := []accessControlTestCase2{ + { + expectedCode: http.StatusOK, + desc: "org viewer with the correct permissions can invite and existing user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + { + expectedCode: http.StatusForbidden, + desc: "org viewer with missing permissions cannot invite and existing user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{}, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + { + expectedCode: http.StatusForbidden, + desc: "org viewer with the wrong scope cannot invite and existing user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:id:100"}}, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + { + expectedCode: http.StatusForbidden, + desc: "org viewer with user add permission cannot invite a new user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, + input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + { + expectedCode: http.StatusOK, + desc: "org viewer with the correct permissions can invite a new user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}}, + input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + { + expectedCode: http.StatusForbidden, + desc: "org viewer with missing permissions cannot invite a new user to his org", + url: "/api/org/invites", + method: http.MethodPost, + permissions: []accesscontrol.Permission{}, + input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + sc := setupHTTPServer(t, true, true) + setInitCtxSignedInViewer(sc.initCtx) + setupOrgUsersDBForAccessControlTests(t, sc.db) + setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId) + + input := strings.NewReader(test.input) + response := callAPI(sc.server, test.method, test.url, input, t) + assert.Equal(t, test.expectedCode, response.Code) + }) + } +} diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bed48fa2ebb..ffe3101a016 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -119,7 +119,17 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) if !exists { - return response.Error(404, "Plugin not found, no installed plugin with that id", nil) + return response.Error(http.StatusNotFound, "Plugin not found, no installed plugin with that id", nil) + } + + // In a first iteration, we only have one permission for app plugins. + // We will need a different permission to allow users to configure the plugin without needing access to it. + if plugin.IsApp() { + hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) + if !hasAccess(accesscontrol.ReqSignedIn, + accesscontrol.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) { + return response.Error(http.StatusForbidden, "Access Denied", nil) + } } dto := &dtos.PluginSetting{ diff --git a/pkg/infra/filestorage/api.go b/pkg/infra/filestorage/api.go index 645ae462cf9..e9c3c1f344c 100644 --- a/pkg/infra/filestorage/api.go +++ b/pkg/infra/filestorage/api.go @@ -27,7 +27,7 @@ var ( ) func ValidatePath(path string) error { - if !filepath.IsAbs(path) { + if !strings.HasPrefix(path, Delimiter) { return ErrRelativePath } @@ -39,7 +39,8 @@ func ValidatePath(path string) error { return ErrPathEndsWithDelimiter } - if filepath.Clean(path) != path { + // apply `ToSlash` to replace OS-specific separators introduced by the Clean() function + if filepath.ToSlash(filepath.Clean(path)) != path { return ErrNonCanonicalPath } @@ -156,6 +157,16 @@ type ListOptions struct { Filter PathFilter } +type DeleteFolderOptions struct { + // Force if set to true, the `deleteFolder` operation will delete the selected folder together with all the nested files & folders + Force bool + + // AccessFilter must match all the nested files & folders in order for the `deleteFolder` operation to succeed + // The access check is not performed if `AccessFilter` is nil + AccessFilter PathFilter +} + +//go:generate mockery --name FileStorage --structname MockFileStorage --inpackage --filename file_storage_mock.go type FileStorage interface { Get(ctx context.Context, path string) (*File, error) Delete(ctx context.Context, path string) error @@ -165,7 +176,7 @@ type FileStorage interface { List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) CreateFolder(ctx context.Context, path string) error - DeleteFolder(ctx context.Context, path string) error + DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error close() error } diff --git a/pkg/infra/filestorage/api_test.go b/pkg/infra/filestorage/api_test.go index f22393bdd36..4cbf2906219 100644 --- a/pkg/infra/filestorage/api_test.go +++ b/pkg/infra/filestorage/api_test.go @@ -100,6 +100,9 @@ func TestFilestorageApi_ValidatePath(t *testing.T) { { path: "/myFile/file.jpg", }, + { + path: "/file.jpg", + }, } for _, tt := range tests { if tt.expectedError == nil { diff --git a/pkg/infra/filestorage/cdk_blob_filestorage.go b/pkg/infra/filestorage/cdk_blob_filestorage.go index 988b8749497..566aa90ad57 100644 --- a/pkg/infra/filestorage/cdk_blob_filestorage.go +++ b/pkg/infra/filestorage/cdk_blob_filestorage.go @@ -9,10 +9,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "gocloud.dev/blob" - "gocloud.dev/gcerrors" - _ "gocloud.dev/blob/fileblob" _ "gocloud.dev/blob/memblob" + "gocloud.dev/gcerrors" ) const ( @@ -215,20 +214,62 @@ func (c cdkBlobStorage) CreateFolder(ctx context.Context, path string) error { return nil } -func (c cdkBlobStorage) DeleteFolder(ctx context.Context, folderPath string) error { - directoryMarkerPath := fmt.Sprintf("%s%s%s", folderPath, Delimiter, directoryMarker) - exists, err := c.bucket.Exists(ctx, strings.ToLower(directoryMarkerPath)) - - if err != nil { - return err +func (c cdkBlobStorage) DeleteFolder(ctx context.Context, folderPath string, options *DeleteFolderOptions) error { + folderPrefix := strings.ToLower(c.convertFolderPathToPrefix(folderPath)) + directoryMarkerPath := folderPrefix + directoryMarker + if !options.Force { + return c.bucket.Delete(ctx, directoryMarkerPath) } - if !exists { - return nil + iterators := []*blob.ListIterator{c.bucket.List(&blob.ListOptions{ + Prefix: folderPrefix, + Delimiter: Delimiter, + })} + + var pathsToDelete []string + + for len(iterators) > 0 { + obj, err := iterators[0].Next(ctx) + if errors.Is(err, io.EOF) { + iterators = iterators[1:] + continue + } + + if err != nil { + c.log.Error("force folder delete: failed to retrieve next object", "err", err) + return err + } + + path := obj.Key + lowerPath := strings.ToLower(path) + if obj.IsDir { + iterators = append([]*blob.ListIterator{c.bucket.List(&blob.ListOptions{ + Prefix: lowerPath, + Delimiter: Delimiter, + })}, iterators...) + continue + } + + pathsToDelete = append(pathsToDelete, lowerPath) } - err = c.bucket.Delete(ctx, strings.ToLower(directoryMarkerPath)) - return err + for _, path := range pathsToDelete { + if !options.AccessFilter.IsAllowed(path) { + c.log.Error("force folder delete: unauthorized access", "path", path) + return fmt.Errorf("force folder delete error, unauthorized access to %s", path) + } + } + + var lastErr error + for _, path := range pathsToDelete { + if err := c.bucket.Delete(ctx, path); err != nil { + c.log.Error("force folder delete: failed while deleting a file", "err", err, "path", path) + lastErr = err + // keep going and delete remaining files + } + } + + return lastErr } //nolint: gocyclo diff --git a/pkg/infra/filestorage/db_filestorage.go b/pkg/infra/filestorage/db_filestorage.go index 43e12f62446..f84f146c08a 100644 --- a/pkg/infra/filestorage/db_filestorage.go +++ b/pkg/infra/filestorage/db_filestorage.go @@ -4,6 +4,7 @@ import ( "context" "crypto/md5" "encoding/hex" + "reflect" // can ignore because we don't need a cryptographically secure hash function // sha1 low chance of collisions and better performance than sha256 @@ -135,30 +136,23 @@ func (s dbFileStorage) Delete(ctx context.Context, filePath string) error { if err != nil { return err } - err = s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - table := &file{} - exists, innerErr := sess.Table("file").Where("path_hash = ?", pathHash).Get(table) - if innerErr != nil { - return innerErr + err = s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + deletedFilesCount, err := sess.Table("file").Where("path_hash = ?", pathHash).Delete(&file{}) + if err != nil { + return err } - if !exists { - return nil + deletedMetaCount, err := sess.Table("file_meta").Where("path_hash = ?", pathHash).Delete(&fileMeta{}) + if err != nil { + if rollErr := sess.Rollback(); rollErr != nil { + return fmt.Errorf("failed to roll back transaction due to error: %s: %w", rollErr, err) + } + + return err } - number, innerErr := sess.Table("file").Where("path_hash = ?", pathHash).Delete(table) - if innerErr != nil { - return innerErr - } - s.log.Info("Deleted file", "path", filePath, "affectedRecords", number) - - metaTable := &fileMeta{} - number, innerErr = sess.Table("file_meta").Where("path_hash = ?", pathHash).Delete(metaTable) - if innerErr != nil { - return innerErr - } - s.log.Info("Deleted metadata", "path", filePath, "affectedRecords", number) - return innerErr + s.log.Info("Deleted file", "path", filePath, "deletedMetaCount", deletedMetaCount, "deletedFilesCount", deletedFilesCount) + return err }) return err @@ -490,24 +484,87 @@ func (s dbFileStorage) CreateFolder(ctx context.Context, path string) error { return err } -func (s dbFileStorage) DeleteFolder(ctx context.Context, folderPath string) error { - err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - existing := &file{} - internalFolderPathHash, err := createPathHash(folderPath + Delimiter) - if err != nil { - return err - } - exists, err := sess.Table("file").Where("path_hash = ?", internalFolderPathHash).Get(existing) +func (s dbFileStorage) DeleteFolder(ctx context.Context, folderPath string, options *DeleteFolderOptions) error { + lowerFolderPath := strings.ToLower(folderPath) + if lowerFolderPath == "" || lowerFolderPath == Delimiter { + lowerFolderPath = Delimiter + } else if !strings.HasSuffix(lowerFolderPath, Delimiter) { + lowerFolderPath = lowerFolderPath + Delimiter + } + + if !options.Force { + return s.Delete(ctx, lowerFolderPath) + } + + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + var rawHashes []interface{} + + // xorm does not support `.Delete()` with `.Join()`, so we first have to retrieve all path_hashes and then use them to filter `file_meta` table + err := sess.Table("file"). + Cols("path_hash"). + Where("LOWER(path) LIKE ?", lowerFolderPath+"%"). + Find(&rawHashes) if err != nil { return err } - if !exists { + if len(rawHashes) == 0 { + s.log.Info("Force deleted folder", "path", lowerFolderPath, "deletedFilesCount", 0, "deletedMetaCount", 0) return nil } - _, err = sess.Table("file").Where("path_hash = ?", internalFolderPathHash).Delete(existing) - return err + accessFilter := options.AccessFilter.asSQLFilter() + accessibleFilesCount, err := sess.Table("file"). + Cols("path_hash"). + Where("LOWER(path) LIKE ?", lowerFolderPath+"%"). + Where(accessFilter.Where, accessFilter.Args...). + Count(&file{}) + if err != nil { + return err + } + + if int64(len(rawHashes)) != accessibleFilesCount { + s.log.Error("force folder delete: unauthorized access", "path", lowerFolderPath, "expectedAccessibleFilesCount", int64(len(rawHashes)), "actualAccessibleFilesCount", accessibleFilesCount) + return fmt.Errorf("force folder delete: unauthorized access for path %s", lowerFolderPath) + } + + var hashes []interface{} + for _, hash := range rawHashes { + if hashString, ok := hash.(string); ok { + hashes = append(hashes, hashString) + + // MySQL returns the `path_hash` field as []uint8 + } else if hashUint, ok := hash.([]uint8); ok { + hashes = append(hashes, string(hashUint)) + } else { + return fmt.Errorf("invalid hash type: %s", reflect.TypeOf(hash)) + } + } + + deletedFilesCount, err := sess. + Table("file"). + In("path_hash", hashes...). + Delete(&file{}) + + if err != nil { + return err + } + + deletedMetaCount, err := sess. + Table("file_meta"). + In("path_hash", hashes...). + Delete(&fileMeta{}) + + if err != nil { + if rollErr := sess.Rollback(); rollErr != nil { + return fmt.Errorf("failed to roll back transaction due to error: %s: %w", rollErr, err) + } + + return err + } + + s.log.Info("Force deleted folder", "path", folderPath, "deletedFilesCount", deletedFilesCount, "deletedMetaCount", deletedMetaCount) + return nil }) return err diff --git a/pkg/infra/filestorage/file_storage_mock.go b/pkg/infra/filestorage/file_storage_mock.go new file mode 100644 index 00000000000..c4b6bf70488 --- /dev/null +++ b/pkg/infra/filestorage/file_storage_mock.go @@ -0,0 +1,130 @@ +// Code generated by mockery v2.10.6. DO NOT EDIT. + +package filestorage + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockFileStorage is an autogenerated mock type for the FileStorage type +type MockFileStorage struct { + mock.Mock +} + +// CreateFolder provides a mock function with given fields: ctx, path +func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error { + ret := _m.Called(ctx, path) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, path) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: ctx, path +func (_m *MockFileStorage) Delete(ctx context.Context, path string) error { + ret := _m.Called(ctx, path) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, path) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteFolder provides a mock function with given fields: ctx, path, options +func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error { + ret := _m.Called(ctx, path, options) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *DeleteFolderOptions) error); ok { + r0 = rf(ctx, path, options) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, path +func (_m *MockFileStorage) Get(ctx context.Context, path string) (*File, error) { + ret := _m.Called(ctx, path) + + var r0 *File + if rf, ok := ret.Get(0).(func(context.Context, string) *File); ok { + r0 = rf(ctx, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*File) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, folderPath, paging, options +func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) { + ret := _m.Called(ctx, folderPath, paging, options) + + var r0 *ListResponse + if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) *ListResponse); ok { + r0 = rf(ctx, folderPath, paging, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ListResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *Paging, *ListOptions) error); ok { + r1 = rf(ctx, folderPath, paging, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Upsert provides a mock function with given fields: ctx, command +func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileCommand) error { + ret := _m.Called(ctx, command) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *UpsertFileCommand) error); ok { + r0 = rf(ctx, command) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// close provides a mock function with given fields: +func (_m *MockFileStorage) close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go index f2ea34e0479..89a152a962a 100644 --- a/pkg/infra/filestorage/fs_integration_test.go +++ b/pkg/infra/filestorage/fs_integration_test.go @@ -1183,6 +1183,123 @@ func TestIntegrationFsStorage(t *testing.T) { }, }, }, + { + name: "should be able to delete folders with files if using force", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/dashboards/myNewFolder/file.jpg", + Contents: emptyContents, + }, + }, + cmdDeleteFolder{ + path: "/folder/dashboards/myNewFolder", + options: &DeleteFolderOptions{ + Force: true, + }, + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder")), + checks(fPath("/folder/dashboards")), + }, + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder/file.jpg", + }, + }, + }, + }, + { + name: "should be able to delete root folder with force", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/dashboards/myNewFolder/file.jpg", + Contents: emptyContents, + }, + }, + cmdDeleteFolder{ + path: "/", + options: &DeleteFolderOptions{ + Force: true, + }, + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{}, + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder/file.jpg", + }, + }, + }, + }, + { + name: "should not be able to delete a folder unless have access to all nested files", + steps: []interface{}{ + cmdCreateFolder{ + path: "/folder/dashboards/myNewFolder", + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/dashboards/myNewFolder/file.jpg", + Contents: emptyContents, + }, + }, + cmdUpsert{ + cmd: UpsertFileCommand{ + Path: "/folder/dashboards/abc/file.jpg", + Contents: emptyContents, + }, + }, + cmdDeleteFolder{ + path: "/", + options: &DeleteFolderOptions{ + Force: true, + AccessFilter: NewPathFilter([]string{"/"}, nil, nil, []string{"/folder/dashboards/abc/file.jpg"}), + }, + error: &cmdErrorOutput{ + message: "force folder delete: unauthorized access for path %s", + args: []interface{}{"/"}, + }, + }, + queryListFolders{ + input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}}, + checks: [][]interface{}{ + checks(fPath("/folder")), + checks(fPath("/folder/dashboards")), + checks(fPath("/folder/dashboards/abc")), + checks(fPath("/folder/dashboards/myNewFolder")), + }, + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder/file.jpg", + }, + checks: checks( + fName("file.jpg"), + ), + }, + queryGet{ + input: queryGetInput{ + path: "/folder/dashboards/myNewFolder/file.jpg", + }, + checks: checks( + fName("file.jpg"), + ), + }, + }, + }, } } diff --git a/pkg/infra/filestorage/test_utils.go b/pkg/infra/filestorage/test_utils.go index 657e3cb9e0e..7ca19b6a631 100644 --- a/pkg/infra/filestorage/test_utils.go +++ b/pkg/infra/filestorage/test_utils.go @@ -32,8 +32,9 @@ type cmdCreateFolder struct { } type cmdDeleteFolder struct { - path string - error *cmdErrorOutput + path string + error *cmdErrorOutput + options *DeleteFolderOptions } type queryGetInput struct { @@ -175,7 +176,7 @@ func handleCommand(t *testing.T, ctx context.Context, cmd interface{}, cmdName s } expectedErr = c.error case cmdDeleteFolder: - err = fs.DeleteFolder(ctx, c.path) + err = fs.DeleteFolder(ctx, c.path, c.options) if c.error == nil { require.NoError(t, err, "%s: should be able to delete %s", cmdName, c.path) } diff --git a/pkg/infra/filestorage/wrapper.go b/pkg/infra/filestorage/wrapper.go index 4165053dc74..cfdb344091e 100644 --- a/pkg/infra/filestorage/wrapper.go +++ b/pkg/infra/filestorage/wrapper.go @@ -256,26 +256,58 @@ func (b wrapper) CreateFolder(ctx context.Context, path string) error { return b.wrapped.CreateFolder(ctx, rootedPath) } -func (b wrapper) DeleteFolder(ctx context.Context, path string) error { +func (b wrapper) deleteFolderOptionsWithDefaults(options *DeleteFolderOptions) *DeleteFolderOptions { + if options == nil { + return &DeleteFolderOptions{ + Force: false, + AccessFilter: b.filter, + } + } + + if options.AccessFilter == nil { + return &DeleteFolderOptions{ + Force: options.Force, + AccessFilter: b.filter, + } + } + + var filter PathFilter + if options.AccessFilter != nil { + filter = newAndPathFilter(b.filter, wrapPathFilter(options.AccessFilter, b.rootFolder)) + } else { + filter = b.filter + } + + return &DeleteFolderOptions{ + Force: options.Force, + AccessFilter: filter, + } +} + +func (b wrapper) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error { if err := b.validatePath(path); err != nil { return err } rootedPath := b.addRoot(path) - if !b.filter.IsAllowed(rootedPath) { - return nil + + optionsWithDefaults := b.deleteFolderOptionsWithDefaults(options) + if !optionsWithDefaults.AccessFilter.IsAllowed(rootedPath) { + return fmt.Errorf("delete folder unauthorized - no access to %s", rootedPath) } - isEmpty, err := b.isFolderEmpty(ctx, path) - if err != nil { - return err + if !optionsWithDefaults.Force { + isEmpty, err := b.isFolderEmpty(ctx, path) + if err != nil { + return err + } + + if !isEmpty { + return fmt.Errorf("folder %s is not empty - cant remove it", path) + } } - if !isEmpty { - return fmt.Errorf("folder %s is not empty - cant remove it", path) - } - - return b.wrapped.DeleteFolder(ctx, rootedPath) + return b.wrapped.DeleteFolder(ctx, rootedPath, optionsWithDefaults) } func (b wrapper) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) { diff --git a/pkg/infra/log/level/level.go b/pkg/infra/log/level/level.go deleted file mode 100644 index 5b1308d5a17..00000000000 --- a/pkg/infra/log/level/level.go +++ /dev/null @@ -1,264 +0,0 @@ -package level - -import ( - "github.com/go-kit/log" - gokitlevel "github.com/go-kit/log/level" -) - -// Error returns a logger that includes a Key/ErrorValue pair. -func Error(logger log.Logger) log.Logger { - return log.WithPrefix(logger, Key(), ErrorValue()) -} - -// Warn returns a logger that includes a Key/WarnValue pair. -func Warn(logger log.Logger) log.Logger { - return log.WithPrefix(logger, Key(), WarnValue()) -} - -// Info returns a logger that includes a Key/InfoValue pair. -func Info(logger log.Logger) log.Logger { - return log.WithPrefix(logger, Key(), InfoValue()) -} - -// Debug returns a logger that includes a Key/DebugValue pair. -func Debug(logger log.Logger) log.Logger { - return log.WithPrefix(logger, Key(), DebugValue()) -} - -// NewFilter wraps next and implements level filtering. See the commentary on -// the Option functions for a detailed description of how to configure levels. -// If no options are provided, all leveled log events created with Debug, -// Info, Warn or Error helper methods are squelched and non-leveled log -// events are passed to next unmodified. -func NewFilter(next log.Logger, options ...Option) log.Logger { - l := &logger{ - next: next, - } - for _, option := range options { - option(l) - } - return l -} - -type logger struct { - next log.Logger - allowed level - squelchNoLevel bool - errNotAllowed error - errNoLevel error -} - -func (l *logger) Log(keyvals ...interface{}) error { - var hasLevel, levelAllowed bool - for i := 1; i < len(keyvals); i += 2 { - if v, ok := keyvals[i].(*levelValue); ok { - hasLevel = true - levelAllowed = l.allowed&v.level != 0 - break - } - - if v, ok := keyvals[i].(gokitlevel.Value); ok { - hasLevel = true - levelAllowed = l.allowed&levelFromGokitLevel(v) != 0 - break - } - } - if !hasLevel && l.squelchNoLevel { - return l.errNoLevel - } - if hasLevel && !levelAllowed { - return l.errNotAllowed - } - return l.next.Log(keyvals...) -} - -// Option sets a parameter for the leveled logger. -type Option func(*logger) - -// AllowAll is an alias for AllowDebug. -func AllowAll() Option { - return AllowDebug() -} - -// AllowDebug allows error, warn, info and debug level log events to pass. -func AllowDebug() Option { - return allowed(levelError | levelWarn | levelInfo | levelDebug) -} - -// AllowInfo allows error, warn and info level log events to pass. -func AllowInfo() Option { - return allowed(levelError | levelWarn | levelInfo) -} - -// AllowWarn allows error and warn level log events to pass. -func AllowWarn() Option { - return allowed(levelError | levelWarn) -} - -// AllowError allows only error level log events to pass. -func AllowError() Option { - return allowed(levelError) -} - -// AllowNone allows no leveled log events to pass. -func AllowNone() Option { - return allowed(0) -} - -func allowed(allowed level) Option { - return func(l *logger) { l.allowed = allowed } -} - -// ErrNotAllowed sets the error to return from Log when it squelches a log -// event disallowed by the configured Allow[Level] option. By default, -// ErrNotAllowed is nil; in this case the log event is squelched with no -// error. -func ErrNotAllowed(err error) Option { - return func(l *logger) { l.errNotAllowed = err } -} - -// SquelchNoLevel instructs Log to squelch log events with no level, so that -// they don't proceed through to the wrapped logger. If SquelchNoLevel is set -// to true and a log event is squelched in this way, the error value -// configured with ErrNoLevel is returned to the caller. -func SquelchNoLevel(squelch bool) Option { - return func(l *logger) { l.squelchNoLevel = squelch } -} - -// ErrNoLevel sets the error to return from Log when it squelches a log event -// with no level. By default, ErrNoLevel is nil; in this case the log event is -// squelched with no error. -func ErrNoLevel(err error) Option { - return func(l *logger) { l.errNoLevel = err } -} - -// NewInjector wraps next and returns a logger that adds a Key/level pair to -// the beginning of log events that don't already contain a level. In effect, -// this gives a default level to logs without a level. -func NewInjector(next log.Logger, level Value) log.Logger { - return &injector{ - next: next, - level: level, - } -} - -type injector struct { - next log.Logger - level interface{} -} - -func (l *injector) Log(keyvals ...interface{}) error { - for i := 1; i < len(keyvals); i += 2 { - if _, ok := keyvals[i].(*levelValue); ok { - return l.next.Log(keyvals...) - } - } - kvs := make([]interface{}, len(keyvals)+2) - kvs[0], kvs[1] = key, l.level - copy(kvs[2:], keyvals) - return l.next.Log(kvs...) -} - -// Value is the interface that each of the canonical level values implement. -// It contains unexported methods that prevent types from other packages from -// implementing it and guaranteeing that NewFilter can distinguish the levels -// defined in this package from all other values. -type Value interface { - String() string - levelVal() -} - -// Key returns the unique key added to log events by the loggers in this -// package. -func Key() interface{} { return key } - -// ErrorValue returns the unique value added to log events by Error. -func ErrorValue() Value { return errorValue } - -// WarnValue returns the unique value added to log events by Warn. -func WarnValue() Value { return warnValue } - -// InfoValue returns the unique value added to log events by Info. -func InfoValue() Value { return infoValue } - -// DebugValue returns the unique value added to log events by Debug. -func DebugValue() Value { return debugValue } - -var ( - // key is of type interface{} so that it allocates once during package - // initialization and avoids allocating every time the value is added to a - // []interface{} later. - key interface{} = "lvl" - - errorValue = &levelValue{level: levelError, name: "eror"} - warnValue = &levelValue{level: levelWarn, name: "warn"} - infoValue = &levelValue{level: levelInfo, name: "info"} - debugValue = &levelValue{level: levelDebug, name: "dbug"} -) - -func SetLevelKeyAndValuesToGokitLog() { - key = "level" - errorValue = &levelValue{level: levelError, name: "error"} - warnValue = &levelValue{level: levelWarn, name: "warn"} - infoValue = &levelValue{level: levelInfo, name: "info"} - debugValue = &levelValue{level: levelDebug, name: "debug"} -} - -type level byte - -const ( - levelDebug level = 1 << iota - levelInfo - levelWarn - levelError -) - -func IsKey(v interface{}) bool { - return v != nil && (v == Key() || v == gokitlevel.Key()) -} - -func GetValue(v interface{}) Value { - if v == nil { - return nil - } - - if val, ok := v.(Value); ok { - return val - } - - if val, ok := v.(gokitlevel.Value); ok { - switch val { - case gokitlevel.InfoValue(): - return InfoValue() - case gokitlevel.WarnValue(): - return WarnValue() - case gokitlevel.ErrorValue(): - return ErrorValue() - case gokitlevel.DebugValue(): - return DebugValue() - } - } - - return nil -} - -type levelValue struct { - name string - level -} - -func (v *levelValue) String() string { return v.name } -func (v *levelValue) levelVal() {} - -func levelFromGokitLevel(l gokitlevel.Value) level { - switch l.String() { - case gokitlevel.ErrorValue().String(): - return levelError - case gokitlevel.WarnValue().String(): - return levelWarn - case gokitlevel.DebugValue().String(): - return levelDebug - } - - return levelInfo -} diff --git a/pkg/infra/log/level/level_test.go b/pkg/infra/log/level/level_test.go deleted file mode 100644 index 5b0f489420a..00000000000 --- a/pkg/infra/log/level/level_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package level_test - -import ( - "testing" - - gokitlog "github.com/go-kit/log" - gokitlevel "github.com/go-kit/log/level" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/log/level" -) - -func TestNewFilter(t *testing.T) { - newFilteredLoggerScenario(t, "Given all levels is allowed should log all messages", level.AllowAll(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 8) - require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string)) - require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string)) - require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String()) - - require.Equal(t, "level", ctx.loggedArgs[4][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[4][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[5][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[5][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[6][2].(string)) - require.Equal(t, "error", ctx.loggedArgs[6][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[7][2].(string)) - require.Equal(t, "debug", ctx.loggedArgs[7][3].(gokitlevel.Value).String()) - }) - - newFilteredLoggerScenario(t, "Given error, warnings, info, debug is allowed should log all messages", level.AllowDebug(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 8) - require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string)) - require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string)) - require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String()) - - require.Equal(t, "level", ctx.loggedArgs[4][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[4][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[5][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[5][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[6][2].(string)) - require.Equal(t, "error", ctx.loggedArgs[6][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[7][2].(string)) - require.Equal(t, "debug", ctx.loggedArgs[7][3].(gokitlevel.Value).String()) - }) - - newFilteredLoggerScenario(t, "Given error, warnings is allowed should log error and warning messages", level.AllowWarn(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 4) - require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[0][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string)) - require.Equal(t, "eror", ctx.loggedArgs[1][3].(level.Value).String()) - - require.Equal(t, "level", ctx.loggedArgs[2][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[2][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[3][2].(string)) - require.Equal(t, "error", ctx.loggedArgs[3][3].(gokitlevel.Value).String()) - }) - - newFilteredLoggerScenario(t, "Given error allowed should log error messages", level.AllowError(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 2) - require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string)) - require.Equal(t, "eror", ctx.loggedArgs[0][3].(level.Value).String()) - - require.Equal(t, "level", ctx.loggedArgs[1][2].(string)) - require.Equal(t, "error", ctx.loggedArgs[1][3].(gokitlevel.Value).String()) - }) - - newFilteredLoggerScenario(t, "Given error, warnings, info is allowed should log error, warning and info messages", level.AllowInfo(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 6) - require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String()) - require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string)) - require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String()) - - require.Equal(t, "level", ctx.loggedArgs[3][2].(string)) - require.Equal(t, "info", ctx.loggedArgs[3][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[4][2].(string)) - require.Equal(t, "warn", ctx.loggedArgs[4][3].(gokitlevel.Value).String()) - require.Equal(t, "level", ctx.loggedArgs[5][2].(string)) - require.Equal(t, "error", ctx.loggedArgs[5][3].(gokitlevel.Value).String()) - }) - - newFilteredLoggerScenario(t, "Given no levels is allowed should not log any messages", level.AllowNone(), func(t *testing.T, ctx *scenarioContext) { - logTestMessages(t, ctx) - - require.Len(t, ctx.loggedArgs, 0) - }) -} - -func logTestMessages(t *testing.T, ctx *scenarioContext) { - t.Helper() - - ctx.logger.Info("info msg") - ctx.logger.Warn("warn msg") - ctx.logger.Error("error msg") - ctx.logger.Debug("debug msg") - err := gokitlevel.Info(ctx.logger).Log("msg", "gokit info msg") - require.NoError(t, err) - err = gokitlevel.Warn(ctx.logger).Log("msg", "gokit warn msg") - require.NoError(t, err) - err = gokitlevel.Error(ctx.logger).Log("msg", "gokit error msg") - require.NoError(t, err) - err = gokitlevel.Debug(ctx.logger).Log("msg", "gokit debug msg") - require.NoError(t, err) -} - -type scenarioContext struct { - loggedArgs [][]interface{} - logger log.Logger -} - -func newFilteredLoggerScenario(t *testing.T, desc string, option level.Option, fn func(t *testing.T, ctx *scenarioContext)) { - t.Helper() - - ctx := &scenarioContext{ - loggedArgs: [][]interface{}{}, - } - - l := gokitlog.LoggerFunc(func(i ...interface{}) error { - ctx.loggedArgs = append(ctx.loggedArgs, i) - return nil - }) - filteredLogger := level.NewFilter(l, option) - testLogger := log.New("test") - testLogger.Swap(filteredLogger) - - ctx.logger = testLogger - - t.Run(desc, func(t *testing.T) { - fn(t, ctx) - }) -} diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index b03206b5d47..fc469f3716e 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -11,17 +11,16 @@ import ( "os" "path/filepath" "sort" - "strconv" "strings" "sync" "time" gokitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/go-stack/stack" "github.com/mattn/go-isatty" "gopkg.in/ini.v1" - "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/infra/log/term" "github.com/grafana/grafana/pkg/infra/log/text" "github.com/grafana/grafana/pkg/util" @@ -32,7 +31,7 @@ var ( loggersToReload []ReloadableHandler root *logManager now = time.Now - logTimeFormat = "2006-01-02T15:04:05.99-0700" + logTimeFormat = time.RFC3339Nano ) const ( @@ -56,10 +55,9 @@ func init() { // logManager manage loggers type logManager struct { *ConcreteLogger - loggersByName map[string]*ConcreteLogger - logFilters []logWithFilters - mutex sync.RWMutex - gokitLogActivated bool + loggersByName map[string]*ConcreteLogger + logFilters []logWithFilters + mutex sync.RWMutex } func newManager(logger gokitlog.Logger) *logManager { @@ -73,12 +71,6 @@ func (lm *logManager) initialize(loggers []logWithFilters) { lm.mutex.Lock() defer lm.mutex.Unlock() - if lm.gokitLogActivated { - level.SetLevelKeyAndValuesToGokitLog() - term.SetTimeFormatGokitLog() - logTimeFormat = time.RFC3339Nano - } - defaultLoggers := make([]gokitlog.Logger, len(loggers)) for index, logger := range loggers { defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) @@ -452,58 +444,9 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { handler.maxLevel = leveloption configLoggers = append(configLoggers, handler) } - - var err error - isOldLoggerActivated, err := isOldLoggerActivated(cfg) - root.gokitLogActivated = !isOldLoggerActivated - - if err != nil { - return err - } if len(configLoggers) > 0 { root.initialize(configLoggers) } return nil } - -// This would be removed eventually, no need to make a fancy design. -// For the sake of important cycle I just copied the function -func isOldLoggerActivated(cfg *ini.File) (bool, error) { - section := cfg.Section("feature_toggles") - toggles, err := readFeatureTogglesFromInitFile(section) - if err != nil { - return false, err - } - return toggles["oldlog"], nil -} - -func readFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) { - featureToggles := make(map[string]bool, 10) - - // parse the comma separated list in `enable`. - featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") - for _, feature := range util.SplitString(featuresTogglesStr) { - featureToggles[feature] = true - } - - // read all other settings under [feature_toggles]. If a toggle is - // present in both the value in `enable` is overridden. - for _, v := range featureTogglesSection.Keys() { - if v.Name() == "enable" { - continue - } - - b, err := strconv.ParseBool(v.Value()) - if err != nil { - return featureToggles, err - } - - featureToggles[v.Name()] = b - } - return featureToggles, nil -} - -func valueAsString(section *ini.Section, keyName string, defaultValue string) string { - return section.Key(keyName).MustString(defaultValue) -} diff --git a/pkg/infra/log/log_test.go b/pkg/infra/log/log_test.go index 830f148b60d..fd7dc4090c7 100644 --- a/pkg/infra/log/log_test.go +++ b/pkg/infra/log/log_test.go @@ -8,7 +8,7 @@ import ( gokitlog "github.com/go-kit/log" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/log/level" + "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/util" ) @@ -38,7 +38,7 @@ func TestLogger(t *testing.T) { require.Equal(t, "logger", ctx.loggedArgs[0][0].(string)) require.Equal(t, "one", ctx.loggedArgs[0][1].(string)) require.Equal(t, "t", ctx.loggedArgs[0][2].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[0][3].(fmt.Stringer).String()) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[0][3].(fmt.Stringer).String()) require.Equal(t, "msg", ctx.loggedArgs[0][4].(string)) require.Equal(t, "hello 1", ctx.loggedArgs[0][5].(string)) @@ -46,7 +46,7 @@ func TestLogger(t *testing.T) { require.Equal(t, "logger", ctx.loggedArgs[1][0].(string)) require.Equal(t, "two", ctx.loggedArgs[1][1].(string)) require.Equal(t, "t", ctx.loggedArgs[0][2].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[0][3].(fmt.Stringer).String()) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[0][3].(fmt.Stringer).String()) require.Equal(t, "msg", ctx.loggedArgs[1][4].(string)) require.Equal(t, "hello 2", ctx.loggedArgs[1][5].(string)) @@ -54,8 +54,8 @@ func TestLogger(t *testing.T) { require.Equal(t, "logger", ctx.loggedArgs[2][0].(string)) require.Equal(t, "three", ctx.loggedArgs[2][1].(string)) require.Equal(t, "t", ctx.loggedArgs[2][2].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[2][3].(fmt.Stringer).String()) - require.Equal(t, "lvl", ctx.loggedArgs[2][4].(string)) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[2][3].(fmt.Stringer).String()) + require.Equal(t, level.Key().(string), ctx.loggedArgs[2][4].(string)) require.Equal(t, level.ErrorValue(), ctx.loggedArgs[2][5].(level.Value)) require.Equal(t, "msg", ctx.loggedArgs[2][6].(string)) require.Equal(t, "hello 3", ctx.loggedArgs[2][7].(string)) @@ -66,7 +66,7 @@ func TestLogger(t *testing.T) { require.Equal(t, "key", ctx.loggedArgs[3][2].(string)) require.Equal(t, "value", ctx.loggedArgs[3][3].(string)) require.Equal(t, "t", ctx.loggedArgs[3][4].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[3][5].(fmt.Stringer).String()) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[3][5].(fmt.Stringer).String()) require.Equal(t, "msg", ctx.loggedArgs[3][6].(string)) require.Equal(t, "hello 4", ctx.loggedArgs[3][7].(string)) @@ -74,8 +74,8 @@ func TestLogger(t *testing.T) { require.Equal(t, "logger", ctx.loggedArgs[4][0].(string)) require.Equal(t, "three", ctx.loggedArgs[4][1].(string)) require.Equal(t, "t", ctx.loggedArgs[4][2].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[4][3].(fmt.Stringer).String()) - require.Equal(t, "lvl", ctx.loggedArgs[4][4].(string)) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[4][3].(fmt.Stringer).String()) + require.Equal(t, level.Key().(string), ctx.loggedArgs[4][4].(string)) require.Equal(t, level.ErrorValue(), ctx.loggedArgs[4][5].(level.Value)) require.Equal(t, "msg", ctx.loggedArgs[4][6].(string)) require.Equal(t, "hello 3 again", ctx.loggedArgs[4][7].(string)) @@ -124,8 +124,8 @@ func TestWithPrefix(t *testing.T) { require.Equal(t, "k1", args[2].(string)) require.Equal(t, "v1", args[3].(string)) require.Equal(t, "t", args[4].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), args[5].(fmt.Stringer).String()) - require.Equal(t, "lvl", args[6].(string)) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), args[5].(fmt.Stringer).String()) + require.Equal(t, level.Key().(string), args[6].(string)) require.Equal(t, level.InfoValue(), args[7].(level.Value)) require.Equal(t, "msg", args[8].(string)) require.Equal(t, "hello", args[9].(string)) @@ -145,8 +145,8 @@ func TestWithSuffix(t *testing.T) { require.Equal(t, "logger", args[0].(string)) require.Equal(t, "test", args[1].(string)) require.Equal(t, "t", args[2].(string)) - require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), args[3].(fmt.Stringer).String()) - require.Equal(t, "lvl", args[4].(string)) + require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), args[3].(fmt.Stringer).String()) + require.Equal(t, level.Key().(string), args[4].(string)) require.Equal(t, level.InfoValue(), args[5].(level.Value)) require.Equal(t, "msg", args[6].(string)) require.Equal(t, "hello", args[7].(string)) diff --git a/pkg/infra/log/syslog.go b/pkg/infra/log/syslog.go index 62564bcf3cf..09ff035a376 100644 --- a/pkg/infra/log/syslog.go +++ b/pkg/infra/log/syslog.go @@ -8,8 +8,8 @@ import ( "os" "github.com/go-kit/log" + "github.com/go-kit/log/level" gokitsyslog "github.com/go-kit/log/syslog" - "github.com/grafana/grafana/pkg/infra/log/level" "gopkg.in/ini.v1" ) @@ -25,8 +25,8 @@ type SysLogHandler struct { var selector = func(keyvals ...interface{}) syslog.Priority { for i := 0; i < len(keyvals); i += 2 { - if level.IsKey(keyvals[i]) { - val := level.GetValue(keyvals[i+1]) + if keyvals[i] == level.Key() { + val := keyvals[i+1] if val != nil { switch val { case level.ErrorValue(): @@ -39,11 +39,9 @@ var selector = func(keyvals ...interface{}) syslog.Priority { return syslog.LOG_DEBUG } } - break } } - return syslog.LOG_INFO } diff --git a/pkg/infra/log/term/terminal_logger.go b/pkg/infra/log/term/terminal_logger.go index b9aeac0e8a4..cc4080c90c4 100644 --- a/pkg/infra/log/term/terminal_logger.go +++ b/pkg/infra/log/term/terminal_logger.go @@ -11,11 +11,11 @@ import ( "time" gokitlog "github.com/go-kit/log" - "github.com/grafana/grafana/pkg/infra/log/level" + "github.com/go-kit/log/level" ) var ( - timeFormat = "2006-01-02T15:04:05-0700" + timeFormat = time.RFC3339Nano termTimeFormat = "01-02|15:04:05" ) @@ -25,10 +25,6 @@ const ( errorKey = "LOG15_ERROR" ) -func SetTimeFormatGokitLog() { - timeFormat = time.RFC3339Nano -} - type terminalLogger struct { w io.Writer } @@ -96,7 +92,6 @@ func getRecord(keyvals ...interface{}) *record { if len(keyvals)%2 == 1 { keyvals = append(keyvals, nil) } - for i := 0; i < len(keyvals); i += 2 { k, v := keyvals[i], keyvals[i+1] diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index 8d8e401c17d..e502b9350ba 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -5,8 +5,8 @@ import ( "net/http" "time" + "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/setting" "go.etcd.io/etcd/api/v3/version" jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index 878a4216293..bb7426f9f86 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" + "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/log/level" ) func newLogger(name string, lev string) log.Logger { diff --git a/pkg/models/dashboard_queries.go b/pkg/models/dashboard_queries.go index 7f27011e73c..f0b470acb19 100644 --- a/pkg/models/dashboard_queries.go +++ b/pkg/models/dashboard_queries.go @@ -11,9 +11,22 @@ func GetUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string { for _, panelObj := range dashboard.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) uid := panel.Get("datasource").Get("uid").MustString() - if _, ok := exists[uid]; !ok { - datasourceUids = append(datasourceUids, uid) - exists[uid] = true + + // if uid is for a mixed datasource, get the datasource uids from the targets + if uid == "-- Mixed --" { + for _, target := range panel.Get("targets").MustArray() { + target := simplejson.NewFromAny(target) + datasourceUid := target.Get("datasource").Get("uid").MustString() + if _, ok := exists[datasourceUid]; !ok { + datasourceUids = append(datasourceUids, datasourceUid) + exists[datasourceUid] = true + } + } + } else { + if _, ok := exists[uid]; !ok { + datasourceUids = append(datasourceUids, uid) + exists[uid] = true + } } } diff --git a/pkg/models/dashboard_queries_test.go b/pkg/models/dashboard_queries_test.go index ed02d11c88b..afa160515ab 100644 --- a/pkg/models/dashboard_queries_test.go +++ b/pkg/models/dashboard_queries_test.go @@ -56,6 +56,79 @@ const ( "schemaVersion": 35 }` + dashboardWithMixedDatasource = ` +{ + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "id": 1, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "abc123" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "id": 2, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "id": 3, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + dashboardWithDuplicateDatasources = ` { "panels": [ @@ -163,6 +236,16 @@ func TestGetUniqueDashboardDatasourceUids(t *testing.T) { require.Equal(t, "_yxMP8Ynk", uids[1]) }) + t.Run("can get unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) + require.NoError(t, err) + + uids := GetUniqueDashboardDatasourceUids(json) + require.Len(t, uids, 2) + require.Equal(t, "abc123", uids[0]) + require.Equal(t, "_yxMP8Ynk", uids[1]) + }) + t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) { json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) require.NoError(t, err) diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 0b91f064956..b92a165dc78 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -102,6 +102,9 @@ type AddOrgUserCommand struct { OrgId int64 `json:"-"` UserId int64 `json:"-"` + + // internal use: avoid adding service accounts to orgs via user routes + AllowAddingServiceAccount bool `json:"-"` } type UpdateOrgUserCommand struct { diff --git a/pkg/plugins/accesscontrol.go b/pkg/plugins/accesscontrol.go new file mode 100644 index 00000000000..be402538dc8 --- /dev/null +++ b/pkg/plugins/accesscontrol.go @@ -0,0 +1,30 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +const ( + ActionAppAccess = "plugins.app:access" +) + +var ( + ScopeProvider = ac.NewScopeProvider("plugins") +) + +func DeclareRBACRoles(acService ac.AccessControl) error { + AppPluginsReader := ac.RoleRegistration{ + Role: ac.RoleDTO{ + Name: ac.FixedRolePrefix + "plugins.app:reader", + DisplayName: "Application Plugins Access", + Description: "Access application plugins (still enforcing the organization role)", + Group: "Plugins", + Permissions: []ac.Permission{ + {Action: ActionAppAccess, Scope: ScopeProvider.GetResourceAllScope()}, + }, + }, + Grants: []string{string(models.ROLE_VIEWER)}, + } + return acService.DeclareFixedRoles(AppPluginsReader) +} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/generate.sh b/pkg/plugins/backendplugin/pluginextensionv2/generate.sh index dade69745c5..c7e2379cf79 100755 --- a/pkg/plugins/backendplugin/pluginextensionv2/generate.sh +++ b/pkg/plugins/backendplugin/pluginextensionv2/generate.sh @@ -13,4 +13,4 @@ DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" cd "$DIR" -protoc -I ./ rendererv2.proto --go_out=plugins=grpc:./ \ No newline at end of file +protoc -I ./ *.proto --go_out=plugins=grpc:./ diff --git a/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go b/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go index 6aef8a1ecd0..6dcc8e2e261 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go @@ -9,6 +9,7 @@ import ( type RendererPlugin interface { RendererClient + SanitizerClient } type RendererGRPCPlugin struct { @@ -20,11 +21,16 @@ func (p *RendererGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Serve } func (p *RendererGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { - return &RendererGRPCClient{NewRendererClient(c)}, nil + return &RendererGRPCClient{NewRendererClient(c), NewSanitizerClient(c)}, nil } type RendererGRPCClient struct { RendererClient + SanitizerClient +} + +func (m *RendererGRPCClient) Sanitize(ctx context.Context, req *SanitizeRequest, opts ...grpc.CallOption) (*SanitizeResponse, error) { + return m.SanitizerClient.Sanitize(ctx, req, opts...) } func (m *RendererGRPCClient) Render(ctx context.Context, req *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) { @@ -32,4 +38,5 @@ func (m *RendererGRPCClient) Render(ctx context.Context, req *RenderRequest, opt } var _ RendererClient = &RendererGRPCClient{} +var _ SanitizerClient = &RendererGRPCClient{} var _ plugin.GRPCPlugin = &RendererGRPCPlugin{} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go index 75e5f4e4a31..9194ea8dcb1 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go @@ -1,21 +1,20 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.15.8 +// protoc-gen-go v1.28.0 +// protoc v3.19.4 // source: rendererv2.proto package pluginextensionv2 import ( context "context" - reflect "reflect" - sync "sync" - grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go new file mode 100644 index 00000000000..b4cd30b7ab1 --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go @@ -0,0 +1,337 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.19.4 +// source: sanitizer.proto + +package pluginextensionv2 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SanitizeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + ConfigType string `protobuf:"bytes,3,opt,name=configType,proto3" json:"configType,omitempty"` // DOMPurify, ... + Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` +} + +func (x *SanitizeRequest) Reset() { + *x = SanitizeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sanitizer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SanitizeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SanitizeRequest) ProtoMessage() {} + +func (x *SanitizeRequest) ProtoReflect() protoreflect.Message { + mi := &file_sanitizer_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SanitizeRequest.ProtoReflect.Descriptor instead. +func (*SanitizeRequest) Descriptor() ([]byte, []int) { + return file_sanitizer_proto_rawDescGZIP(), []int{0} +} + +func (x *SanitizeRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *SanitizeRequest) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *SanitizeRequest) GetConfigType() string { + if x != nil { + return x.ConfigType + } + return "" +} + +func (x *SanitizeRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +type SanitizeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Sanitized []byte `protobuf:"bytes,2,opt,name=sanitized,proto3" json:"sanitized,omitempty"` +} + +func (x *SanitizeResponse) Reset() { + *x = SanitizeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_sanitizer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SanitizeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SanitizeResponse) ProtoMessage() {} + +func (x *SanitizeResponse) ProtoReflect() protoreflect.Message { + mi := &file_sanitizer_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SanitizeResponse.ProtoReflect.Descriptor instead. +func (*SanitizeResponse) Descriptor() ([]byte, []int) { + return file_sanitizer_proto_rawDescGZIP(), []int{1} +} + +func (x *SanitizeResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *SanitizeResponse) GetSanitized() []byte { + if x != nil { + return x.Sanitized + } + return nil +} + +var File_sanitizer_proto protoreflect.FileDescriptor + +var file_sanitizer_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x11, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, + 0x6f, 0x6e, 0x76, 0x32, 0x22, 0x7f, 0x0a, 0x0f, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, + 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x46, 0x0a, 0x10, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x32, 0x60, 0x0a, + 0x09, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x08, 0x53, 0x61, + 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x12, 0x22, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x53, 0x61, 0x6e, 0x69, 0x74, + 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x53, + 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x15, 0x5a, 0x13, 0x2e, 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sanitizer_proto_rawDescOnce sync.Once + file_sanitizer_proto_rawDescData = file_sanitizer_proto_rawDesc +) + +func file_sanitizer_proto_rawDescGZIP() []byte { + file_sanitizer_proto_rawDescOnce.Do(func() { + file_sanitizer_proto_rawDescData = protoimpl.X.CompressGZIP(file_sanitizer_proto_rawDescData) + }) + return file_sanitizer_proto_rawDescData +} + +var file_sanitizer_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sanitizer_proto_goTypes = []interface{}{ + (*SanitizeRequest)(nil), // 0: pluginextensionv2.SanitizeRequest + (*SanitizeResponse)(nil), // 1: pluginextensionv2.SanitizeResponse +} +var file_sanitizer_proto_depIdxs = []int32{ + 0, // 0: pluginextensionv2.Sanitizer.Sanitize:input_type -> pluginextensionv2.SanitizeRequest + 1, // 1: pluginextensionv2.Sanitizer.Sanitize:output_type -> pluginextensionv2.SanitizeResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sanitizer_proto_init() } +func file_sanitizer_proto_init() { + if File_sanitizer_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sanitizer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SanitizeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sanitizer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SanitizeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sanitizer_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sanitizer_proto_goTypes, + DependencyIndexes: file_sanitizer_proto_depIdxs, + MessageInfos: file_sanitizer_proto_msgTypes, + }.Build() + File_sanitizer_proto = out.File + file_sanitizer_proto_rawDesc = nil + file_sanitizer_proto_goTypes = nil + file_sanitizer_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// SanitizerClient is the client API for Sanitizer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type SanitizerClient interface { + Sanitize(ctx context.Context, in *SanitizeRequest, opts ...grpc.CallOption) (*SanitizeResponse, error) +} + +type sanitizerClient struct { + cc grpc.ClientConnInterface +} + +func NewSanitizerClient(cc grpc.ClientConnInterface) SanitizerClient { + return &sanitizerClient{cc} +} + +func (c *sanitizerClient) Sanitize(ctx context.Context, in *SanitizeRequest, opts ...grpc.CallOption) (*SanitizeResponse, error) { + out := new(SanitizeResponse) + err := c.cc.Invoke(ctx, "/pluginextensionv2.Sanitizer/Sanitize", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SanitizerServer is the server API for Sanitizer service. +type SanitizerServer interface { + Sanitize(context.Context, *SanitizeRequest) (*SanitizeResponse, error) +} + +// UnimplementedSanitizerServer can be embedded to have forward compatible implementations. +type UnimplementedSanitizerServer struct { +} + +func (*UnimplementedSanitizerServer) Sanitize(context.Context, *SanitizeRequest) (*SanitizeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Sanitize not implemented") +} + +func RegisterSanitizerServer(s *grpc.Server, srv SanitizerServer) { + s.RegisterService(&_Sanitizer_serviceDesc, srv) +} + +func _Sanitizer_Sanitize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SanitizeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SanitizerServer).Sanitize(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pluginextensionv2.Sanitizer/Sanitize", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SanitizerServer).Sanitize(ctx, req.(*SanitizeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Sanitizer_serviceDesc = grpc.ServiceDesc{ + ServiceName: "pluginextensionv2.Sanitizer", + HandlerType: (*SanitizerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Sanitize", + Handler: _Sanitizer_Sanitize_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sanitizer.proto", +} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.proto b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.proto new file mode 100644 index 00000000000..fc2606dab7c --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; +package pluginextensionv2; + +option go_package = ".;pluginextensionv2"; + +message SanitizeRequest { + string filename = 1; + bytes content = 2; + string configType = 3; // DOMPurify, ... + bytes config = 4; +} + +message SanitizeResponse { + string error = 1; + bytes sanitized = 2; +} + +service Sanitizer { + rpc Sanitize(SanitizeRequest) returns (SanitizeResponse); +} diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 308635bbb89..96c903a05a1 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -7,6 +7,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "gopkg.in/ini.v1" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -35,12 +40,6 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus" "github.com/grafana/grafana/pkg/tsdb/tempo" "github.com/grafana/grafana/pkg/tsdb/testdatasource" - "go.opentelemetry.io/otel/trace" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gopkg.in/ini.v1" ) func TestPluginManager_int_init(t *testing.T) { diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 22f1c3e233c..d29318cad33 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -7,12 +7,11 @@ import ( "testing" "time" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index c6970da787b..a46b4ab94ce 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -24,7 +24,9 @@ import ( "github.com/grafana/grafana/pkg/services/searchV2" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/serviceaccounts" + samanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/updatechecker" ) @@ -38,10 +40,11 @@ func ProvideBackgroundServiceRegistry( pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService, + saService *samanager.ServiceAccountsService, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider, - _ *plugindashboardsservice.DashboardUpdater, + _ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, @@ -67,6 +70,7 @@ func ProvideBackgroundServiceRegistry( thumbnailsService, searchService, entityEventsService, + saService, ) } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 8c3aafa4930..e7ce2a0a1b8 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -6,6 +6,7 @@ package server import ( "github.com/google/wire" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" @@ -93,6 +94,7 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" secretsMigrator "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/database" serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -237,6 +239,10 @@ var wireBasicSet = wire.NewSet( pluginSettings.ProvideService, wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), alerting.ProvideService, + database.ProvideServiceAccountsStore, + wire.Bind(new(serviceaccounts.Store), new(*database.ServiceAccountsStoreImpl)), + ossaccesscontrol.ProvideServiceAccountPermissions, + wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), serviceaccountsmanager.ProvideServiceAccountsService, wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)), expr.ProvideService, @@ -263,6 +269,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), comments.ProvideService, guardian.ProvideService, + sanitizer.ProvideService, secretsStore.ProvideService, avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 5a49c3ee927..c4c30ed4c69 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -62,6 +62,10 @@ type DatasourcePermissionsService interface { PermissionsService } +type ServiceAccountPermissionsService interface { + PermissionsService +} + type PermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index de8da4c50b4..5eed02aeecb 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -260,3 +261,46 @@ func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { return "" } + +type ServiceAccountPermissionsService struct { + *resourcepermissions.Service +} + +func ProvideServiceAccountPermissions( + cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, + ac accesscontrol.AccessControl, store resourcepermissions.Store, + license models.Licensing, serviceAccountStore serviceaccounts.Store, +) (*ServiceAccountPermissionsService, error) { + options := resourcepermissions.Options{ + Resource: "serviceaccounts", + ResourceAttribute: "id", + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return err + } + _, err = serviceAccountStore.RetrieveServiceAccount(ctx, orgID, id) + return err + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + ServiceAccounts: false, + }, + PermissionsToActions: map[string][]string{ + "View": {serviceaccounts.ActionRead}, + "Edit": {serviceaccounts.ActionRead, serviceaccounts.ActionWrite, serviceaccounts.ActionDelete}, + "Admin": {serviceaccounts.ActionRead, serviceaccounts.ActionWrite, serviceaccounts.ActionDelete, serviceaccounts.ActionPermissionsRead, serviceaccounts.ActionPermissionsWrite}, + }, + ReaderRoleName: "Service account permission reader", + WriterRoleName: "Service account permission writer", + RoleGroup: "Service accounts", + } + + srv, err := resourcepermissions.New(options, cfg, router, license, ac, store, sql) + if err != nil { + return nil, err + } + return &ServiceAccountPermissionsService{srv}, nil +} diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index b55e15870cc..29858fb65fa 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -339,6 +339,10 @@ type testRenderService struct { renderErrorImageProvider func(error error) (*rendering.RenderResult, error) } +func (s *testRenderService) SanitizeSVG(ctx context.Context, req *rendering.SanitizeSVGRequest) (*rendering.SanitizeSVGResponse, error) { + return &rendering.SanitizeSVGResponse{Sanitized: req.Content}, nil +} + func (s *testRenderService) HasCapability(feature rendering.CapabilityName) (rendering.CapabilitySupportRequestResult, error) { return rendering.CapabilitySupportRequestResult{}, nil } diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index cda763f2c37..f0bddf8d6f2 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -2,6 +2,7 @@ package contexthandler import ( "errors" + "net/http" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" @@ -23,7 +24,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) claims, err := h.JWTAuthService.Verify(ctx.Req.Context(), jwtToken) if err != nil { ctx.Logger.Debug("Failed to verify JWT", "error", err) - ctx.JsonApiErr(401, InvalidJWT, err) + ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } @@ -33,7 +34,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) if sub == "" { ctx.Logger.Warn("Got a JWT without the mandatory 'sub' claim", "error", err) - ctx.JsonApiErr(401, InvalidJWT, err) + ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } extUser := &models.ExternalUserInfo{ @@ -56,7 +57,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) if query.Login == "" && query.Email == "" { ctx.Logger.Debug("Failed to get an authentication claim from JWT") - ctx.JsonApiErr(401, InvalidJWT, err) + ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } @@ -80,10 +81,10 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) "username_claim", query.Login, ) err = login.ErrInvalidCredentials - ctx.JsonApiErr(401, UserNotFound, err) + ctx.JsonApiErr(http.StatusUnauthorized, UserNotFound, err) } else { ctx.Logger.Error("Failed to get signed in user", "error", err) - ctx.JsonApiErr(401, InvalidJWT, err) + ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) } return true } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 7794fcfcaba..94759f169b2 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -449,7 +449,7 @@ func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext return false } - _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithRenderAuth") + ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithRenderAuth") defer span.End() renderUser, exists := h.RenderService.GetRenderUser(reqContext.Req.Context(), key) @@ -458,12 +458,21 @@ func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext return true } - reqContext.IsSignedIn = true reqContext.SignedInUser = &models.SignedInUser{ OrgId: renderUser.OrgID, UserId: renderUser.UserID, OrgRole: models.RoleType(renderUser.OrgRole), } + + // UserID can be 0 for background tasks and, in this case, there is no user info to retrieve + if renderUser.UserID != 0 { + query := models.GetSignedInUserQuery{UserId: renderUser.UserID, OrgId: renderUser.OrgID} + if err := h.SQLStore.GetSignedInUserWithCacheCtx(ctx, &query); err == nil { + reqContext.SignedInUser = query.Result + } + } + + reqContext.IsSignedIn = true reqContext.IsRenderCall = true reqContext.LastSeenAt = time.Now() return true diff --git a/pkg/services/export/commit_helper.go b/pkg/services/export/commit_helper.go index b19749c6a03..ae5d05edfe2 100644 --- a/pkg/services/export/commit_helper.go +++ b/pkg/services/export/commit_helper.go @@ -17,13 +17,15 @@ import ( ) type commitHelper struct { - ctx context.Context - repo *git.Repository - work *git.Worktree - orgDir string // includes the orgID - workDir string // same as the worktree root - orgID int64 - users map[int64]*userInfo + ctx context.Context + repo *git.Repository + work *git.Worktree + orgDir string // includes the orgID + workDir string // same as the worktree root + orgID int64 + users map[int64]*userInfo + stopRequested bool + broadcast func(path string) } type commitBody struct { @@ -64,6 +66,26 @@ func (ch *commitHelper) initOrg(sql *sqlstore.SQLStore, orgID int64) error { } func (ch *commitHelper) add(opts commitOptions) error { + if ch.stopRequested { + return fmt.Errorf("stop requested") + } + + if len(opts.body) < 1 { + return nil // nothing to commit + } + + user, ok := ch.users[opts.userID] + if !ok { + user = &userInfo{ + Name: "admin", + Email: "admin@unknown.org", + } + } + sig := user.getAuthor() + if opts.when.Unix() > 100 { + sig.When = opts.when + } + for _, b := range opts.body { if !strings.HasPrefix(b.fpath, ch.orgDir) { return fmt.Errorf("invalid path, must be within the root folder") @@ -87,6 +109,10 @@ func (ch *commitHelper) add(opts commitOptions) error { if err != nil { return err } + err = os.Chtimes(b.fpath, sig.When, sig.When) + if err != nil { + return err + } sub := b.fpath[len(ch.workDir)+1:] _, err = ch.work.Add(sub) @@ -100,22 +126,11 @@ func (ch *commitHelper) add(opts commitOptions) error { } } - user, ok := ch.users[opts.userID] - if !ok { - user = &userInfo{ - Name: "admin", - Email: "admin@unknown.org", - } - } - sig := user.getAuthor() - if opts.when.Unix() > 10 { - sig.When = opts.when - } - copts := &git.CommitOptions{ Author: &sig, } + ch.broadcast(opts.body[0].fpath) _, err := ch.work.Commit(opts.comment, copts) return err } @@ -140,6 +155,7 @@ func (u *userInfo) getAuthor() object.Signature { return object.Signature{ Name: firstRealStringX(u.Name, u.Login, u.Email, "?"), Email: firstRealStringX(u.Email, u.Login, u.Name, "?"), + When: time.Now(), } } diff --git a/pkg/services/export/dummy_job.go b/pkg/services/export/dummy_job.go index f892b1cea6b..58a38889398 100644 --- a/pkg/services/export/dummy_job.go +++ b/pkg/services/export/dummy_job.go @@ -15,10 +15,11 @@ var _ Job = new(dummyExportJob) type dummyExportJob struct { logger log.Logger - statusMu sync.Mutex - status ExportStatus - cfg ExportConfig - broadcaster statusBroadcaster + statusMu sync.Mutex + status ExportStatus + cfg ExportConfig + broadcaster statusBroadcaster + stopRequested bool } func startDummyExportJob(cfg ExportConfig, broadcaster statusBroadcaster) (Job, error) { @@ -40,6 +41,10 @@ func startDummyExportJob(cfg ExportConfig, broadcaster statusBroadcaster) (Job, return job, nil } +func (e *dummyExportJob) requestStop() { + e.stopRequested = true +} + func (e *dummyExportJob) start() { defer func() { e.logger.Info("Finished dummy export job") @@ -74,7 +79,7 @@ func (e *dummyExportJob) start() { e.statusMu.Unlock() // Wait till we are done - shouldStop := e.status.Current >= e.status.Count + shouldStop := e.stopRequested || e.status.Current >= e.status.Count e.broadcaster(e.status) if shouldStop { diff --git a/pkg/services/export/export_dashboards.go b/pkg/services/export/export_dashboards.go index 212c31c1a38..b8deeb4a6f3 100644 --- a/pkg/services/export/export_dashboards.go +++ b/pkg/services/export/export_dashboards.go @@ -11,18 +11,24 @@ import ( "github.com/google/uuid" "github.com/grafana/grafana/pkg/infra/filestorage" + "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2/extract" "github.com/grafana/grafana/pkg/services/sqlstore" ) -func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) error { +func exportDashboards(helper *commitHelper, job *gitExportJob) error { alias := make(map[string]string, 100) ids := make(map[int64]string, 100) folders := make(map[int64]string, 100) // Should root files be at the root or in a subfolder called "general"? - if true { - folders[0] = "general" + if len(job.cfg.GeneralFolderPath) > 0 { + folders[0] = job.cfg.GeneralFolderPath // "general" + } + + lookup, err := searchV2.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql) + if err != nil { + return err } rootDir := path.Join(helper.orgDir, "root") @@ -31,7 +37,7 @@ func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) comment: "Exported folder structure", } - err := job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { + err = job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { type dashDataQueryResult struct { Id int64 UID string `xorm:"uid"` @@ -132,7 +138,7 @@ func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) // Now walk the history err = job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { type dashVersionResult struct { - DashId int64 `xorm:"dashboard_id"` + DashId int64 `xorm:"id"` Version int64 `xorm:"version"` Created time.Time `xorm:"created"` CreatedBy int64 `xorm:"created_by"` @@ -142,16 +148,27 @@ func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) rows := make([]*dashVersionResult, 0, len(ids)) - sess.Table("dashboard_version"). - Join("INNER", "dashboard", "dashboard.id = dashboard_version.dashboard_id"). - Where("org_id = ?", job.orgID). - Cols("dashboard_version.dashboard_id", - "dashboard_version.version", - "dashboard_version.created", - "dashboard_version.created_by", - "dashboard_version.message", - "dashboard_version.data"). - Asc("dashboard_version.created") + if job.cfg.KeepHistory { + sess.Table("dashboard_version"). + Join("INNER", "dashboard", "dashboard.id = dashboard_version.dashboard_id"). + Where("org_id = ?", job.orgID). + Cols("dashboard.id", + "dashboard_version.version", + "dashboard_version.created", + "dashboard_version.created_by", + "dashboard_version.message", + "dashboard_version.data"). + Asc("dashboard_version.created") + } else { + sess.Table("dashboard"). + Where("org_id = ?", job.orgID). + Cols("id", + "version", + "created", + "created_by", + "data"). + Asc("created") + } err := sess.Find(&rows) if err != nil { @@ -186,14 +203,8 @@ func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) if err != nil { return err } - count++ fmt.Printf("COMMIT: %d // %s (%d)\n", count, fpath, row.Version) - - job.status.Current = count - job.status.Last = fpath - job.status.Changed = time.Now().UnixMilli() - job.broadcaster(job.status) } return nil diff --git a/pkg/services/export/export_ds.go b/pkg/services/export/export_ds.go index 4dbe53c5553..e1aacd524aa 100644 --- a/pkg/services/export/export_ds.go +++ b/pkg/services/export/export_ds.go @@ -6,35 +6,27 @@ import ( "sort" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/searchV2/extract" ) -type dsLookup func(ref *extract.DataSourceRef) *extract.DataSourceRef - -func exportDataSources(helper *commitHelper, job *gitExportJob) (dsLookup, error) { +func exportDataSources(helper *commitHelper, job *gitExportJob) error { cmd := &datasources.GetDataSourcesQuery{ OrgId: job.orgID, } err := job.sql.GetDataSources(helper.ctx, cmd) if err != nil { - return nil, err + return nil } sort.SliceStable(cmd.Result, func(i, j int) bool { return cmd.Result[i].Created.After(cmd.Result[j].Created) }) - byUID := make(map[string]*extract.DataSourceRef, len(cmd.Result)) - byName := make(map[string]*extract.DataSourceRef, len(cmd.Result)) for _, ds := range cmd.Result { - ref := &extract.DataSourceRef{ - UID: ds.Uid, - Type: ds.Type, - } - byUID[ds.Uid] = ref - byName[ds.Name] = ref ds.OrgId = 0 ds.Version = 0 + ds.SecureJsonData = map[string][]byte{ + "TODO": []byte("XXX"), + } err := helper.add(commitOptions{ body: []commitBody{ @@ -47,26 +39,9 @@ func exportDataSources(helper *commitHelper, job *gitExportJob) (dsLookup, error comment: fmt.Sprintf("Add datasource: %s", ds.Name), }) if err != nil { - return nil, err + return err } } - // Return the lookup function - return func(ref *extract.DataSourceRef) *extract.DataSourceRef { - if ref == nil || ref.UID == "" { - return &extract.DataSourceRef{ - UID: "default.uid", - Type: "default.type", - } - } - v, ok := byUID[ref.UID] - if ok { - return v - } - v, ok = byName[ref.UID] - if ok { - return v - } - return nil - }, nil + return nil } diff --git a/pkg/services/export/export_files.go b/pkg/services/export/export_files.go new file mode 100644 index 00000000000..0530224626d --- /dev/null +++ b/pkg/services/export/export_files.go @@ -0,0 +1,48 @@ +package export + +import ( + "fmt" + "path" + + "github.com/grafana/grafana/pkg/infra/filestorage" + "github.com/grafana/grafana/pkg/infra/log" +) + +func exportFiles(helper *commitHelper, job *gitExportJob) error { + fs := filestorage.NewDbStorage(log.New("grafanaStorageLogger"), job.sql, nil, fmt.Sprintf("/%d/", helper.orgID)) + + paging := &filestorage.Paging{} + for { + rsp, err := fs.List(helper.ctx, "/resources", paging, &filestorage.ListOptions{ + WithFolders: false, // ???? + Recursive: true, + WithContents: true, + }) + if err != nil { + return err + } + + for _, f := range rsp.Files { + if f.Size < 1 { + continue + } + err = helper.add(commitOptions{ + body: []commitBody{{ + body: f.Contents, + fpath: path.Join(helper.orgDir, f.FullPath), + }}, + comment: fmt.Sprintf("Adding: %s", path.Base(f.FullPath)), + when: f.Created, + }) + if err != nil { + return err + } + } + + paging.After = rsp.LastPath + if !rsp.HasMore { + break + } + } + return nil +} diff --git a/pkg/services/export/export_kv_store.go b/pkg/services/export/export_kv_store.go new file mode 100644 index 00000000000..74fad5079db --- /dev/null +++ b/pkg/services/export/export_kv_store.go @@ -0,0 +1,46 @@ +package export + +import ( + "fmt" + "path" + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func exportKVStore(helper *commitHelper, job *gitExportJob) error { + kvdir := path.Join(helper.orgDir, "system", "kv_store") + + return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { + type kvResult struct { + Namespace string `xorm:"namespace"` + Key string `xorm:"key"` + Value string `xorm:"value"` + Updated time.Time `xorm:"updated"` + } + + rows := make([]*kvResult, 0) + + sess.Table("kv_store").Where("org_id = ? OR org_id = 0", helper.orgID) + + err := sess.Find(&rows) + if err != nil { + return err + } + + for _, row := range rows { + err = helper.add(commitOptions{ + body: []commitBody{{ + body: []byte(row.Value), + fpath: path.Join(kvdir, row.Namespace, row.Key), + }}, + comment: fmt.Sprintf("Exporting: %s/%s", row.Namespace, row.Key), + when: row.Updated, + }) + if err != nil { + return err + } + } + return err + }) +} diff --git a/pkg/services/export/export_live.go b/pkg/services/export/export_live.go new file mode 100644 index 00000000000..352ce24f56a --- /dev/null +++ b/pkg/services/export/export_live.go @@ -0,0 +1,47 @@ +package export + +import ( + "fmt" + "path" + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func exportLive(helper *commitHelper, job *gitExportJob) error { + messagedir := path.Join(helper.orgDir, "system", "live", "message") + + return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { + type msgResult struct { + Channel string `xorm:"channel"` + Data string `xorm:"data"` + CreatedBy int64 `xorm:"created_by"` + Created time.Time `xorm:"created"` + } + + rows := make([]*msgResult, 0) + + sess.Table("live_message").Where("org_id = ?", helper.orgID) + + err := sess.Find(&rows) + if err != nil { + return err + } + + for _, row := range rows { + err = helper.add(commitOptions{ + body: []commitBody{{ + body: []byte(row.Data), + fpath: path.Join(messagedir, row.Channel) + ".json", // must be JSON files + }}, + comment: fmt.Sprintf("Exporting: %s", row.Channel), + when: row.Created, + userID: row.CreatedBy, + }) + if err != nil { + return err + } + } + return err + }) +} diff --git a/pkg/services/export/export_snapshots.go b/pkg/services/export/export_snapshots.go index e9390cc8b79..fa2e05ce588 100644 --- a/pkg/services/export/export_snapshots.go +++ b/pkg/services/export/export_snapshots.go @@ -29,7 +29,7 @@ func exportSnapshots(helper *commitHelper, job *gitExportJob) error { gitcmd := commitOptions{ when: time.Now(), - comment: "Export playlists", + comment: "Export snapshots", } for _, snapshot := range cmd.Result { diff --git a/pkg/services/export/export_sys_preferences.go b/pkg/services/export/export_sys_preferences.go index 926240acc04..bcb48888a0a 100644 --- a/pkg/services/export/export_sys_preferences.go +++ b/pkg/services/export/export_sys_preferences.go @@ -58,11 +58,15 @@ func exportSystemPreferences(helper *commitHelper, job *gitExportJob) error { user, ok := users[row.UserID] if ok { delete(users, row.UserID) + if user.IsServiceAccount { + continue // don't write preferences for service account + } } else { user = &userInfo{ Login: fmt.Sprintf("__%d__", row.UserID), } } + fpath = filepath.Join(prefsDir, "user", fmt.Sprintf("%s.json", user.Login)) comment = fmt.Sprintf("User preferences: %s", user.getAuthor().Name) } @@ -105,6 +109,10 @@ func exportSystemPreferences(helper *commitHelper, job *gitExportJob) error { // add a file for all useres that may not be in the system for _, user := range users { + if user.IsServiceAccount { + continue + } + row := preferences{ Theme: user.Theme, // never set? } diff --git a/pkg/services/export/git_export_job.go b/pkg/services/export/git_export_job.go index 21243e05c52..55b6a5dc723 100644 --- a/pkg/services/export/git_export_job.go +++ b/pkg/services/export/git_export_job.go @@ -29,6 +29,7 @@ type gitExportJob struct { status ExportStatus cfg ExportConfig broadcaster statusBroadcaster + helper *commitHelper } type simpleExporter = func(helper *commitHelper, job *gitExportJob) error @@ -69,6 +70,10 @@ func (e *gitExportJob) getConfig() ExportConfig { return e.cfg } +func (e *gitExportJob) requestStop() { + e.helper.stopRequested = true // will error on the next write +} + // Utility function to export dashboards func (e *gitExportJob) start() { defer func() { @@ -119,16 +124,21 @@ func (e *gitExportJob) doExportWithHistory() error { if err != nil { return err } - helper := &commitHelper{ + e.helper = &commitHelper{ repo: r, work: w, ctx: context.Background(), workDir: e.rootDir, orgDir: e.rootDir, + broadcast: func(p string) { + e.status.Last = p[len(e.rootDir):] + e.status.Changed = time.Now().UnixMilli() + e.broadcaster(e.status) + }, } cmd := &models.SearchOrgsQuery{} - err = e.sql.SearchOrgs(helper.ctx, cmd) + err = e.sql.SearchOrgs(e.helper.ctx, cmd) if err != nil { return err } @@ -136,14 +146,14 @@ func (e *gitExportJob) doExportWithHistory() error { // Export each org for _, org := range cmd.Result { if len(cmd.Result) > 1 { - helper.orgDir = path.Join(e.rootDir, fmt.Sprintf("org_%d", org.Id)) + e.helper.orgDir = path.Join(e.rootDir, fmt.Sprintf("org_%d", org.Id)) } - err = helper.initOrg(e.sql, org.Id) + err = e.helper.initOrg(e.sql, org.Id) if err != nil { return err } - err = e.doOrgExportWithHistory(helper) + err = e.doOrgExportWithHistory(e.helper) if err != nil { return err } @@ -161,39 +171,45 @@ func (e *gitExportJob) doExportWithHistory() error { } func (e *gitExportJob) doOrgExportWithHistory(helper *commitHelper) error { - lookup, err := exportDataSources(helper, e) - if err != nil { - return err + include := e.cfg.Include + + exporters := []simpleExporter{} + if include.Dash { + exporters = append(exporters, exportDashboards) } - if true { - err = exportDashboards(helper, e, lookup) - if err != nil { - return err - } + if include.DS { + exporters = append(exporters, exportDataSources) } - // Run all the simple exporters - exporters := []simpleExporter{ - dumpAuthTables, - exportSystemPreferences, - exportSystemStars, - exportSystemPlaylists, - exportAnnotations, + if include.Auth { + exporters = append(exporters, dumpAuthTables) } - // This needs a real admin user to use the interfaces (and decrypt) - if false { + if include.Services { + exporters = append(exporters, exportFiles, + exportSystemPreferences, + exportSystemStars, + exportSystemPlaylists, + exportKVStore, + exportLive) + } + + if include.Anno { + exporters = append(exporters, exportAnnotations) + } + + if include.Snapshots { exporters = append(exporters, exportSnapshots) } for _, fn := range exporters { - err = fn(helper, e) + err := fn(helper, e) if err != nil { return err } } - return err + return nil } /** diff --git a/pkg/services/export/service.go b/pkg/services/export/service.go index 6551775a065..d39f5fc4fff 100644 --- a/pkg/services/export/service.go +++ b/pkg/services/export/service.go @@ -25,6 +25,9 @@ type ExportService interface { // Read raw file contents out of the store HandleRequestExport(c *models.ReqContext) response.Response + + // Cancel any running export + HandleRequestStop(c *models.ReqContext) response.Response } type StandardExport struct { @@ -63,6 +66,15 @@ func (ex *StandardExport) HandleGetStatus(c *models.ReqContext) response.Respons return response.JSON(http.StatusOK, ex.exportJob.getStatus()) } +func (ex *StandardExport) HandleRequestStop(c *models.ReqContext) response.Response { + ex.mutex.Lock() + defer ex.mutex.Unlock() + + ex.exportJob.requestStop() + + return response.JSON(http.StatusOK, ex.exportJob.getStatus()) +} + func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Response { var cfg ExportConfig err := json.NewDecoder(c.Req.Body).Decode(&cfg) diff --git a/pkg/services/export/stopped_job.go b/pkg/services/export/stopped_job.go index b9756f9d72f..2bd248d6a65 100644 --- a/pkg/services/export/stopped_job.go +++ b/pkg/services/export/stopped_job.go @@ -17,3 +17,5 @@ func (e *stoppedJob) getStatus() ExportStatus { func (e *stoppedJob) getConfig() ExportConfig { return ExportConfig{} } + +func (e *stoppedJob) requestStop() {} diff --git a/pkg/services/export/stub.go b/pkg/services/export/stub.go index 551bb730f5c..47e9267bb62 100644 --- a/pkg/services/export/stub.go +++ b/pkg/services/export/stub.go @@ -18,3 +18,7 @@ func (ex *StubExport) HandleGetStatus(c *models.ReqContext) response.Response { func (ex *StubExport) HandleRequestExport(c *models.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } + +func (ex *StubExport) HandleRequestStop(c *models.ReqContext) response.Response { + return response.Error(http.StatusForbidden, "feature not enabled", nil) +} diff --git a/pkg/services/export/types.go b/pkg/services/export/types.go index 20a1ec49e3d..ec80a12a469 100644 --- a/pkg/services/export/types.go +++ b/pkg/services/export/types.go @@ -15,21 +15,30 @@ type ExportStatus struct { // Basic export config (for now) type ExportConfig struct { - Format string `json:"format"` - Git GitExportConfig `json:"git"` + Format string `json:"format"` + GeneralFolderPath string `json:"generalFolderPath"` + KeepHistory bool `json:"history"` + + Include struct { + Auth bool `json:"auth"` + DS bool `json:"ds"` + Dash bool `json:"dash"` + Services bool `json:"services"` + Usage bool `json:"usage"` + Anno bool `json:"anno"` + Snapshots bool `json:"snapshots"` + } `json:"include"` + + // Depends on the format + Git GitExportConfig `json:"git"` } -type GitExportConfig struct { - // General folder is either at the root or as a subfolder - GeneralAtRoot bool `json:"generalAtRoot"` - - // Keeping all history is nice, but much slower - ExcludeHistory bool `json:"excludeHistory"` -} +type GitExportConfig struct{} type Job interface { getStatus() ExportStatus getConfig() ExportConfig + requestStop() } // Will broadcast the live status diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 8c3ff596e06..ad96139ff58 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -73,7 +73,8 @@ func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) { fm.update() } -func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool { +// meetsRequirements checks if grafana is able to run the given feature due to dev mode or licensing requirements +func (fm *FeatureManager) meetsRequirements(ff *FeatureFlag) bool { if ff.RequiresDevMode && !fm.isDevMod { return false } @@ -82,19 +83,22 @@ func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool { return false } - // TODO: CEL - expression - return ff.Expression == "true" + return true } // Update func (fm *FeatureManager) update() { enabled := make(map[string]bool) for _, flag := range fm.flags { - val := fm.evaluate(flag) + // if grafana cannot run the feature, omit metrics around it + if !fm.meetsRequirements(flag) { + continue + } // Update the registry track := 0.0 - if val { + // TODO: CEL - expression + if flag.Expression == "true" { track = 1 enabled[flag.Name] = true } diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 42b83741513..977f7f3db35 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -10,6 +10,7 @@ import ( "net" "strconv" "strings" + "time" "github.com/davecgh/go-spew/spew" "gopkg.in/ldap.v3" @@ -114,6 +115,9 @@ func (server *Server) Dial() error { return err } } + + timeout := time.Duration(server.Config.Timeout) * time.Second + for _, host := range strings.Split(server.Config.Host, " ") { // Remove any square brackets enclosing IPv6 addresses, a format we support for backwards compatibility host = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]") @@ -128,17 +132,17 @@ func (server *Server) Dial() error { tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) } if server.Config.StartTLS { - server.Connection, err = ldap.Dial("tcp", address) + server.Connection, err = dialWithTimeout("tcp", address, timeout) if err == nil { if err = server.Connection.StartTLS(tlsCfg); err == nil { return nil } } } else { - server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg) + server.Connection, err = dialTLSWithTimeout("tcp", address, tlsCfg, timeout) } } else { - server.Connection, err = ldap.Dial("tcp", address) + server.Connection, err = dialWithTimeout("tcp", address, timeout) } if err == nil { @@ -148,6 +152,30 @@ func (server *Server) Dial() error { return err } +// dialWithTimeout applies the specified timeout +// and connects to the given address on the given network using net.Dial +func dialWithTimeout(network, addr string, timeout time.Duration) (*ldap.Conn, error) { + c, err := net.DialTimeout(network, addr, timeout) + if err != nil { + return nil, err + } + conn := ldap.NewConn(c, false) + conn.Start() + return conn, nil +} + +// dialTLSWithTimeout applies the specified timeout +// connects to the given address on the given network using tls.Dial +func dialTLSWithTimeout(network, addr string, config *tls.Config, timeout time.Duration) (*ldap.Conn, error) { + c, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, network, addr, config) + if err != nil { + return nil, err + } + conn := ldap.NewConn(c, true) + conn.Start() + return conn, nil +} + // Close closes the LDAP connection // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 17b091c688a..54afa4bb544 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -12,6 +12,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +const defaultTimeout = 10 + // Config holds list of connections to LDAP type Config struct { Servers []*ServerConfig `toml:"servers"` @@ -29,6 +31,7 @@ type ServerConfig struct { ClientKey string `toml:"client_key"` BindDN string `toml:"bind_dn"` BindPassword string `toml:"bind_password"` + Timeout int `toml:"timeout"` Attr AttributeMap `toml:"attributes"` SearchFilter string `toml:"search_filter"` @@ -140,8 +143,8 @@ func readConfig(configFile string) (*Config, error) { return nil, fmt.Errorf("LDAP enabled but no LDAP servers defined in config file") } - // set default org id for _, server := range result.Servers { + // set default org id err = assertNotEmptyCfg(server.SearchFilter, "search_filter") if err != nil { return nil, fmt.Errorf("%v: %w", "Failed to validate SearchFilter section", err) @@ -160,6 +163,11 @@ func readConfig(configFile string) (*Config, error) { groupMap.OrgId = 1 } } + + // set default timeout if unspecified + if server.Timeout == 0 { + server.Timeout = defaultTimeout + } } return result, nil diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 7e5c06b665b..dd9328b2d91 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/go-kit/log" - "github.com/grafana/grafana/pkg/infra/log/level" + "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/quota" diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 2569d256b9f..5cc6c97f0d4 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -40,6 +40,7 @@ type TemplateService interface { type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance) error + ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) } type MuteTimingService interface { @@ -85,6 +86,14 @@ func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } +func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response.Response { + tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusAccepted, tree) +} + func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response { cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), c.OrgId) if err != nil { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 88574902f5f..e176402fcea 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -44,6 +44,15 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 202, response.Status()) }) + t.Run("successful DELETE returns 202", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + response := sut.RouteResetPolicyTree(&rc) + + require.Equal(t, 202, response.Status()) + }) + t.Run("when new policy tree is invalid", func(t *testing.T) { t.Run("PUT returns 400", func(t *testing.T) { sut := createProvisioningSrvSut(t) @@ -106,6 +115,18 @@ func TestProvisioningApi(t *testing.T) { require.NotEmpty(t, response.Body()) require.Contains(t, string(response.Body()), "something went wrong") }) + + t.Run("DELETE returns 500", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = &fakeFailingNotificationPolicyService{} + rc := createTestRequestCtx() + + response := sut.RouteResetPolicyTree(&rc) + + require.Equal(t, 500, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "something went wrong") + }) }) }) @@ -335,6 +356,11 @@ func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, or return nil } +func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + f.tree = definitions.Route{} // TODO + return f.tree, nil +} + type fakeFailingNotificationPolicyService struct{} func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { @@ -345,6 +371,10 @@ func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Cont return fmt.Errorf("something went wrong") } +func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, fmt.Errorf("something went wrong") +} + type fakeRejectingNotificationPolicyService struct{} func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { @@ -355,6 +385,10 @@ func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Co return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) } +func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, nil +} + func createInvalidContactPoint() definitions.EmbeddedContactPoint { settings, _ := simplejson.NewJson([]byte(`{}`)) return definitions.EmbeddedContactPoint{ diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 49f2ad16cd2..f5cd78b3140 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -191,6 +191,7 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope case http.MethodPut + "/api/v1/provisioning/policies", + http.MethodDelete + "/api/v1/provisioning/policies", http.MethodPost + "/api/v1/provisioning/contact-points", http.MethodPut + "/api/v1/provisioning/contact-points/{UID}", http.MethodDelete + "/api/v1/provisioning/contact-points/{UID}", diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 3339ab10d1f..2f61d026795 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -27,6 +27,10 @@ func (f *ForkedProvisioningApi) forkRoutePutPolicyTree(ctx *models.ReqContext, r return f.svc.RoutePutPolicyTree(ctx, route) } +func (f *ForkedProvisioningApi) forkRouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.svc.RouteResetPolicyTree(ctx) +} + func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response { return f.svc.RouteGetContactPoints(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 8fb66c89cc1..a0e9e6aedf7 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -40,6 +40,7 @@ type ProvisioningApiForkingService interface { RoutePutMuteTiming(*models.ReqContext) response.Response RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response + RouteResetPolicyTree(*models.ReqContext) response.Response } func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { @@ -156,6 +157,9 @@ func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) respons } return f.forkRoutePutTemplate(ctx, conf, nameParam) } +func (f *ForkedProvisioningApi) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.forkRouteResetPolicyTree(ctx) +} func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { @@ -369,5 +373,15 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Delete( + toMacaronPath("/api/v1/provisioning/policies"), + api.authorize(http.MethodDelete, "/api/v1/provisioning/policies"), + metrics.Instrument( + http.MethodDelete, + "/api/v1/provisioning/policies", + srv.RouteResetPolicyTree, + m, + ), + ) }, middleware.ReqSignedIn) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 60645a449f3..3078e893cf3 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -167,71 +167,25 @@ }, "AlertRule": { "properties": { - "annotations": { + "Annotations": { "additionalProperties": { "type": "string" }, - "example": { - "runbook_url": "https://supercoolrunbook.com/page/13" - }, "type": "object" }, - "condition": { - "example": "A", + "Condition": { "type": "string" }, - "data": { - "example": [ - { - "datasourceUid": "-100", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 0, - 0 - ], - "type": "gt" - }, - "operator": { - "type": "and" - }, - "query": { - "params": [] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "datasource": { - "type": "__expr__", - "uid": "__expr__" - }, - "expression": "1 == 1", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 43200, - "refId": "A", - "type": "math" - }, - "queryType": "", - "refId": "A", - "relativeTimeRange": { - "from": 0, - "to": 0 - } - } - ], + "DashboardUID": { + "type": "string" + }, + "Data": { "items": { "$ref": "#/definitions/AlertQuery" }, "type": "array" }, - "execErrState": { + "ExecErrState": { "enum": [ "Alerting", "Error", @@ -239,27 +193,27 @@ ], "type": "string" }, - "folderUID": { - "example": "project_x", - "type": "string" - }, - "for": { + "For": { "$ref": "#/definitions/Duration" }, - "id": { + "ID": { "format": "int64", "type": "integer" }, - "labels": { + "IntervalSeconds": { + "format": "int64", + "type": "integer" + }, + "Labels": { "additionalProperties": { "type": "string" }, - "example": { - "team": "sre-team-1" - }, "type": "object" }, - "noDataState": { + "NamespaceUID": { + "type": "string" + }, + "NoDataState": { "enum": [ "Alerting", "NoData", @@ -267,45 +221,58 @@ ], "type": "string" }, - "orgID": { + "OrgID": { "format": "int64", "type": "integer" }, - "provenance": { - "$ref": "#/definitions/Provenance" + "PanelID": { + "format": "int64", + "type": "integer" }, - "ruleGroup": { - "example": "eval_group_1", - "maxLength": 190, - "minLength": 1, + "RuleGroup": { "type": "string" }, + "RuleGroupIndex": { + "format": "int64", + "type": "integer" + }, + "Title": { + "type": "string" + }, + "UID": { + "type": "string" + }, + "Updated": { + "format": "date-time", + "type": "string" + }, + "Version": { + "format": "int64", + "type": "integer" + } + }, + "title": "AlertRule is the model for alert rules in unified alerting.", + "type": "object" + }, + "AlertRuleGroup": { + "properties": { + "folderUid": { + "type": "string" + }, + "interval": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRule" + }, + "type": "array" + }, "title": { - "example": "Always firing", - "maxLength": 190, - "minLength": 1, - "type": "string" - }, - "uid": { - "type": "string" - }, - "updated": { - "format": "date-time", - "readOnly": true, "type": "string" } }, - "required": [ - "orgID", - "folderUID", - "ruleGroup", - "title", - "condition", - "data", - "noDataState", - "execErrState", - "for" - ], "type": "object" }, "AlertRuleGroupMetadata": { @@ -2592,7 +2559,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2625,7 +2591,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "Userinfo": { @@ -2795,6 +2761,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -2818,6 +2785,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -2925,7 +2893,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -2981,12 +2948,14 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3035,7 +3004,6 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -3146,6 +3114,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3183,7 +3152,6 @@ "type": "object" }, "receiver": { - "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3550,7 +3518,10 @@ ], "responses": { "200": { - "$ref": "#/responses/AlertRuleGroup" + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } }, "404": { "description": " Not found." @@ -3748,6 +3719,24 @@ } }, "/api/v1/provisioning/policies": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + }, + "summary": "Clears the notification policy tree.", + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetPolicyTree", "responses": { diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index d54f27090bd..8ffd32b2ad4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -176,6 +176,7 @@ type AlertRuleGroupMetadata struct { Interval int64 `json:"interval"` } +// swagger:model type AlertRuleGroup struct { Title string `json:"title"` FolderUID string `json:"folderUid"` diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index e1f0f48859a..e68e73f06df 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -19,6 +19,16 @@ package definitions // 202: Ack // 400: ValidationError +// swagger:route DELETE /api/v1/provisioning/policies provisioning stable RouteResetPolicyTree +// +// Clears the notification policy tree. +// +// Consumes: +// - application/json +// +// Responses: +// 202: Ack + // swagger:parameters RoutePutPolicyTree type Policytree struct { // The new notification routing tree to use diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 05c7dbeb931..d5181930c97 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -167,71 +167,25 @@ }, "AlertRule": { "properties": { - "annotations": { + "Annotations": { "additionalProperties": { "type": "string" }, - "example": { - "runbook_url": "https://supercoolrunbook.com/page/13" - }, "type": "object" }, - "condition": { - "example": "A", + "Condition": { "type": "string" }, - "data": { - "example": [ - { - "datasourceUid": "-100", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 0, - 0 - ], - "type": "gt" - }, - "operator": { - "type": "and" - }, - "query": { - "params": [] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "datasource": { - "type": "__expr__", - "uid": "__expr__" - }, - "expression": "1 == 1", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 43200, - "refId": "A", - "type": "math" - }, - "queryType": "", - "refId": "A", - "relativeTimeRange": { - "from": 0, - "to": 0 - } - } - ], + "DashboardUID": { + "type": "string" + }, + "Data": { "items": { "$ref": "#/definitions/AlertQuery" }, "type": "array" }, - "execErrState": { + "ExecErrState": { "enum": [ "Alerting", "Error", @@ -239,27 +193,27 @@ ], "type": "string" }, - "folderUID": { - "example": "project_x", - "type": "string" - }, - "for": { + "For": { "$ref": "#/definitions/Duration" }, - "id": { + "ID": { "format": "int64", "type": "integer" }, - "labels": { + "IntervalSeconds": { + "format": "int64", + "type": "integer" + }, + "Labels": { "additionalProperties": { "type": "string" }, - "example": { - "team": "sre-team-1" - }, "type": "object" }, - "noDataState": { + "NamespaceUID": { + "type": "string" + }, + "NoDataState": { "enum": [ "Alerting", "NoData", @@ -267,45 +221,58 @@ ], "type": "string" }, - "orgID": { + "OrgID": { "format": "int64", "type": "integer" }, - "provenance": { - "$ref": "#/definitions/Provenance" + "PanelID": { + "format": "int64", + "type": "integer" }, - "ruleGroup": { - "example": "eval_group_1", - "maxLength": 190, - "minLength": 1, + "RuleGroup": { "type": "string" }, + "RuleGroupIndex": { + "format": "int64", + "type": "integer" + }, + "Title": { + "type": "string" + }, + "UID": { + "type": "string" + }, + "Updated": { + "format": "date-time", + "type": "string" + }, + "Version": { + "format": "int64", + "type": "integer" + } + }, + "title": "AlertRule is the model for alert rules in unified alerting.", + "type": "object" + }, + "AlertRuleGroup": { + "properties": { + "folderUid": { + "type": "string" + }, + "interval": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRule" + }, + "type": "array" + }, "title": { - "example": "Always firing", - "maxLength": 190, - "minLength": 1, - "type": "string" - }, - "uid": { - "type": "string" - }, - "updated": { - "format": "date-time", - "readOnly": true, "type": "string" } }, - "required": [ - "orgID", - "folderUID", - "ruleGroup", - "title", - "condition", - "data", - "noDataState", - "execErrState", - "for" - ], "type": "object" }, "AlertRuleGroupMetadata": { @@ -2592,7 +2559,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2625,7 +2591,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "Userinfo": { @@ -2819,7 +2785,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -2983,7 +2948,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, @@ -3039,6 +3003,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -3149,7 +3114,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3187,6 +3151,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -5179,7 +5144,10 @@ ], "responses": { "200": { - "$ref": "#/responses/AlertRuleGroup" + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } }, "404": { "description": " Not found." @@ -5377,6 +5345,24 @@ } }, "/api/v1/provisioning/policies": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + }, + "summary": "Clears the notification policy tree.", + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetPolicyTree", "responses": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 6878508f9b7..be46b5746e5 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1916,7 +1916,10 @@ ], "responses": { "200": { - "$ref": "#/responses/AlertRuleGroup" + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } }, "404": { "description": " Not found." @@ -2166,6 +2169,25 @@ } } } + }, + "delete": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Clears the notification policy tree.", + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + } } }, "/api/v1/provisioning/templates": { @@ -2518,83 +2540,27 @@ }, "AlertRule": { "type": "object", - "required": [ - "orgID", - "folderUID", - "ruleGroup", - "title", - "condition", - "data", - "noDataState", - "execErrState", - "for" - ], + "title": "AlertRule is the model for alert rules in unified alerting.", "properties": { - "annotations": { + "Annotations": { "type": "object", "additionalProperties": { "type": "string" - }, - "example": { - "runbook_url": "https://supercoolrunbook.com/page/13" } }, - "condition": { - "type": "string", - "example": "A" + "Condition": { + "type": "string" }, - "data": { + "DashboardUID": { + "type": "string" + }, + "Data": { "type": "array", "items": { "$ref": "#/definitions/AlertQuery" - }, - "example": [ - { - "datasourceUid": "-100", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 0, - 0 - ], - "type": "gt" - }, - "operator": { - "type": "and" - }, - "query": { - "params": [] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "datasource": { - "type": "__expr__", - "uid": "__expr__" - }, - "expression": "1 == 1", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 43200, - "refId": "A", - "type": "math" - }, - "queryType": "", - "refId": "A", - "relativeTimeRange": { - "from": 0, - "to": 0 - } - } - ] + } }, - "execErrState": { + "ExecErrState": { "type": "string", "enum": [ "Alerting", @@ -2602,27 +2568,27 @@ "OK" ] }, - "folderUID": { - "type": "string", - "example": "project_x" - }, - "for": { + "For": { "$ref": "#/definitions/Duration" }, - "id": { + "ID": { "type": "integer", "format": "int64" }, - "labels": { + "IntervalSeconds": { + "type": "integer", + "format": "int64" + }, + "Labels": { "type": "object", "additionalProperties": { "type": "string" - }, - "example": { - "team": "sre-team-1" } }, - "noDataState": { + "NamespaceUID": { + "type": "string" + }, + "NoDataState": { "type": "string", "enum": [ "Alerting", @@ -2630,32 +2596,55 @@ "OK" ] }, - "orgID": { + "OrgID": { "type": "integer", "format": "int64" }, - "provenance": { - "$ref": "#/definitions/Provenance" + "PanelID": { + "type": "integer", + "format": "int64" }, - "ruleGroup": { - "type": "string", - "maxLength": 190, - "minLength": 1, - "example": "eval_group_1" - }, - "title": { - "type": "string", - "maxLength": 190, - "minLength": 1, - "example": "Always firing" - }, - "uid": { + "RuleGroup": { "type": "string" }, - "updated": { + "RuleGroupIndex": { + "type": "integer", + "format": "int64" + }, + "Title": { + "type": "string" + }, + "UID": { + "type": "string" + }, + "Updated": { "type": "string", - "format": "date-time", - "readOnly": true + "format": "date-time" + }, + "Version": { + "type": "integer", + "format": "int64" + } + } + }, + "AlertRuleGroup": { + "type": "object", + "properties": { + "folderUid": { + "type": "string" + }, + "interval": { + "type": "integer", + "format": "int64" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRule" + } + }, + "title": { + "type": "string" } } }, @@ -4947,9 +4936,8 @@ } }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -5283,6 +5271,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5346,6 +5335,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5395,6 +5385,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -5506,6 +5497,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 1ad04d91cbb..fa95a519bbb 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -158,7 +158,7 @@ func (ng *AlertNG) init() error { ng.schedule = scheduler // Provisioning - policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Log) + policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Cfg.UnifiedAlerting, ng.Log) contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index 8597360fa03..cbd8d3d23c6 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" ) type NotificationPolicyService struct { @@ -14,15 +15,17 @@ type NotificationPolicyService struct { provenanceStore ProvisioningStore xact TransactionManager log log.Logger + settings setting.UnifiedAlertingSettings } func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, - xact TransactionManager, log log.Logger) *NotificationPolicyService { + xact TransactionManager, settings setting.UnifiedAlertingSettings, log log.Logger) *NotificationPolicyService { return &NotificationPolicyService{ amStore: am, provenanceStore: prov, xact: xact, log: log, + settings: settings, } } @@ -116,6 +119,49 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return nil } +func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + defaultCfg, err := deserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration)) + if err != nil { + nps.log.Error("failed to parse default alertmanager config: %w", err) + return definitions.Route{}, fmt.Errorf("failed to parse default alertmanager config: %w", err) + } + route := defaultCfg.AlertmanagerConfig.Route + + revision, err := getLastConfiguration(ctx, orgID, nps.amStore) + if err != nil { + return definitions.Route{}, err + } + revision.cfg.AlertmanagerConfig.Config.Route = route + + serialized, err := serializeAlertmanagerConfig(*revision.cfg) + if err != nil { + return definitions.Route{}, err + } + cmd := models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(serialized), + ConfigurationVersion: revision.version, + FetchedConfigurationHash: revision.concurrencyToken, + Default: false, + OrgID: orgID, + } + err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { + err := nps.amStore.UpdateAlertmanagerConfiguration(ctx, &cmd) + if err != nil { + return err + } + err = nps.provenanceStore.DeleteProvenance(ctx, route, orgID) + if err != nil { + return err + } + return nil + }) + if err != nil { + return definitions.Route{}, nil + } + + return *route, nil +} + func (nps *NotificationPolicyService) receiversToMap(records []*definitions.PostableApiReceiver) (map[string]struct{}, error) { receivers := map[string]struct{}{} for _, receiver := range records { diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index cff09e8899a..da7211791e4 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" @@ -213,6 +214,17 @@ func TestNotificationPolicyService(t *testing.T) { require.Error(t, err) require.ErrorIs(t, err, ErrValidation) }) + + t.Run("deleting route replaces with default", func(t *testing.T) { + sut := createNotificationPolicyServiceSut() + + tree, err := sut.ResetPolicyTree(context.Background(), 1) + + require.NoError(t, err) + require.Equal(t, "grafana-default-email", tree.Receiver) + require.Nil(t, tree.Routes) + require.Nil(t, tree.GroupBy) + }) } func createNotificationPolicyServiceSut() *NotificationPolicyService { @@ -221,6 +233,9 @@ func createNotificationPolicyServiceSut() *NotificationPolicyService { provenanceStore: NewFakeProvisioningStore(), xact: newNopTransactionManager(), log: log.NewNopLogger(), + settings: setting.UnifiedAlertingSettings{ + DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), + }, } } diff --git a/pkg/services/rendering/capabilities.go b/pkg/services/rendering/capabilities.go index 9339bfc4915..2087577ed04 100644 --- a/pkg/services/rendering/capabilities.go +++ b/pkg/services/rendering/capabilities.go @@ -16,6 +16,7 @@ type CapabilityName string const ( ScalingDownImages CapabilityName = "ScalingDownImages" FullHeightImages CapabilityName = "FullHeightImages" + SvgSanitization CapabilityName = "SvgSanitization" ) var ErrUnknownCapability = errors.New("unknown capability") diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 1cf6abbbc3e..6a7197f12e9 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -62,6 +62,15 @@ type ErrorOpts struct { ErrorRenderUnavailable bool } +type SanitizeSVGRequest struct { + Filename string + Content []byte +} + +type SanitizeSVGResponse struct { + Sanitized []byte +} + type CSVOpts struct { TimeoutOpts AuthOpts @@ -83,6 +92,7 @@ type RenderCSVResult struct { type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error) type renderCSVFunc func(ctx context.Context, renderKey string, options CSVOpts) (*RenderCSVResult, error) +type sanitizeFunc func(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) type renderKeyProvider interface { get(ctx context.Context, opts AuthOpts) (string, error) @@ -114,4 +124,5 @@ type Service interface { GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) HasCapability(capability CapabilityName) (CapabilitySupportRequestResult, error) CreateRenderingSession(ctx context.Context, authOpts AuthOpts, sessionOpts SessionOpts) (Session, error) + SanitizeSVG(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) } diff --git a/pkg/services/rendering/mock.go b/pkg/services/rendering/mock.go index d8a52a81520..705a05a1f9f 100644 --- a/pkg/services/rendering/mock.go +++ b/pkg/services/rendering/mock.go @@ -139,6 +139,21 @@ func (mr *MockServiceMockRecorder) RenderErrorImage(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenderErrorImage", reflect.TypeOf((*MockService)(nil).RenderErrorImage), arg0, arg1) } +// SanitizeSVG mocks base method. +func (m *MockService) SanitizeSVG(arg0 context.Context, arg1 *SanitizeSVGRequest) (*SanitizeSVGResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SanitizeSVG", arg0, arg1) + ret0, _ := ret[0].(*SanitizeSVGResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SanitizeSVG indicates an expected call of SanitizeSVG. +func (mr *MockServiceMockRecorder) SanitizeSVG(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SanitizeSVG", reflect.TypeOf((*MockService)(nil).SanitizeSVG), arg0, arg1) +} + // Version mocks base method. func (m *MockService) Version() string { m.ctrl.T.Helper() diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 694d93890f6..1bde374b7bb 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -27,18 +27,22 @@ func init() { remotecache.Register(&RenderUser{}) } +var _ Service = (*RenderingService)(nil) + const ServiceName = "RenderingService" type RenderingService struct { - log log.Logger - pluginInfo *plugins.Plugin - renderAction renderFunc - renderCSVAction renderCSVFunc - domain string - inProgressCount int32 - version string - versionMutex sync.RWMutex - capabilities []Capability + log log.Logger + pluginInfo *plugins.Plugin + renderAction renderFunc + renderCSVAction renderCSVFunc + sanitizeSVGAction sanitizeFunc + sanitizeURL string + domain string + inProgressCount int32 + version string + versionMutex sync.RWMutex + capabilities []Capability perRequestRenderKeyProvider renderKeyProvider Cfg *setting.Cfg @@ -59,8 +63,14 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p return nil, fmt.Errorf("failed to create CSVs directory %q: %w", cfg.CSVsDir, err) } + logger := log.New("rendering") + + // URL for HTTP sanitize API + var sanitizeURL string + + // value used for domain attribute of renderKey cookie var domain string - // set value used for domain attribute of renderKey cookie + switch { case cfg.RendererUrl != "": // RendererCallbackUrl has already been passed, it won't generate an error. @@ -69,6 +79,7 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p return nil, err } + sanitizeURL = getSanitizerURL(cfg.RendererUrl) domain = u.Hostname() case cfg.HTTPAddr != setting.DefaultHTTPAddr: domain = cfg.HTTPAddr @@ -76,7 +87,6 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p domain = "localhost" } - logger := log.New("rendering") s := &RenderingService{ perRequestRenderKeyProvider: &perRequestRenderKeyProvider{ cache: remoteCache, @@ -92,16 +102,26 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p name: ScalingDownImages, semverConstraint: ">= 3.4.0", }, + { + name: SvgSanitization, + semverConstraint: ">= 3.5.0", + }, }, Cfg: cfg, RemoteCacheService: remoteCache, RendererPluginManager: rm, log: logger, domain: domain, + sanitizeURL: sanitizeURL, } return s, nil } +func getSanitizerURL(rendererURL string) string { + rendererBaseURL := strings.TrimSuffix(rendererURL, "/render") + return rendererBaseURL + "/sanitize" +} + func (rs *RenderingService) Run(ctx context.Context) error { if rs.remoteAvailable() { rs.log = rs.log.New("renderer", "http") @@ -120,6 +140,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { }) rs.renderAction = rs.renderViaHTTP rs.renderCSVAction = rs.renderCSVViaHTTP + rs.sanitizeSVGAction = rs.sanitizeViaHTTP refreshTicker := time.NewTicker(remoteVersionRefreshInterval) @@ -146,6 +167,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { rs.version = rs.pluginInfo.Info.Version rs.renderAction = rs.renderViaPlugin rs.renderCSVAction = rs.renderCSVViaPlugin + rs.sanitizeSVGAction = rs.sanitizeSVGViaPlugin <-ctx.Done() // On Windows, Chromium is generating a debug.log file that breaks signature check on next restart @@ -293,6 +315,26 @@ func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session return result, err } +func (rs *RenderingService) SanitizeSVG(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) { + capability, err := rs.HasCapability(SvgSanitization) + if err != nil { + return nil, err + } + + if !capability.IsSupported { + return nil, fmt.Errorf("svg sanitization unsupported, requires image renderer version: %s", capability.SemverConstraint) + } + + start := time.Now() + + action, err := rs.sanitizeSVGAction(ctx, req) + if err != nil { + defer rs.log.Info("svg sanitization finished", "duration", time.Since(start), "filename", req.Filename, "isError", err != nil) + } + + return action, err +} + func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts, renderKeyProvider renderKeyProvider) (*RenderCSVResult, error) { if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit { return nil, ErrConcurrentLimitReached diff --git a/pkg/services/rendering/svgSanitizer.go b/pkg/services/rendering/svgSanitizer.go new file mode 100644 index 00000000000..b6b40038032 --- /dev/null +++ b/pkg/services/rendering/svgSanitizer.go @@ -0,0 +1,178 @@ +package rendering + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "net/url" + "time" + + "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" +) + +var ( + domPurifySvgConfig = map[string]interface{}{ + // domPurifyConfig is passed directly to DOMPurify https://github.com/cure53/DOMPurify#can-i-configure-dompurify + "domPurifyConfig": map[string]interface{}{ + "USE_PROFILES": map[string]bool{"svg": true, "svgFilters": true}, + "ADD_TAGS": []string{"use"}, + }, + // allowAllLinksInSvgUseTags will preserve all `use` tags. + // By default, we remove all non-self-referential `use` tags, i.e. those which `href` attribute does not start with `#` + "allowAllLinksInSvgUseTags": false, + } + domPurifyConfigType = "DOMPurify" +) + +type formFile struct { + fileName string + key string + contentType string + content io.Reader +} + +func createMultipartRequestBody(values []formFile) (bytes.Buffer, string, error) { + var b bytes.Buffer + w := multipart.NewWriter(&b) + for _, f := range values { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, f.key, f.fileName)) + h.Set("Content-Type", f.contentType) + formWriter, err := w.CreatePart(h) + + if err != nil { + return bytes.Buffer{}, "", err + } + + if _, err := io.Copy(formWriter, f.content); err != nil { + return bytes.Buffer{}, "", err + } + + if x, ok := f.content.(io.Closer); ok { + _ = x.Close() + } + } + + if err := w.Close(); err != nil { + return bytes.Buffer{}, "", err + } + + return b, w.FormDataContentType(), nil +} + +func (rs *RenderingService) sanitizeViaHTTP(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) { + sanitizerUrl, err := url.Parse(rs.sanitizeURL) + if err != nil { + return nil, err + } + + configJson, err := json.Marshal(map[string]interface{}{ + "config": domPurifySvgConfig, + "configType": domPurifyConfigType, + }) + if err != nil { + rs.log.Error("Sanitizer - HTTP: failed to create the request config", "error", err, "filename", req.Filename) + return nil, fmt.Errorf("config creation fail: %s", err) + } + + body, contentType, err := createMultipartRequestBody([]formFile{ + { + fileName: "config", + key: "config", + contentType: "application/json", + content: bytes.NewReader(configJson), + }, + { + fileName: req.Filename, + key: "file", + contentType: "image/svg+xml", + content: bytes.NewReader(req.Content), + }, + }) + if err != nil { + rs.log.Error("Sanitizer - HTTP: failed to create the request body", "error", err, "filename", req.Filename) + return nil, fmt.Errorf("body creation fail: %s", err) + } + + reqContext, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + httpReq, err := http.NewRequestWithContext(reqContext, "POST", sanitizerUrl.String(), &body) + if err != nil { + rs.log.Error("Sanitizer - HTTP: failed to create the HTTP request", "error", err, "filename", req.Filename) + return nil, err + } + + httpReq.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", rs.Cfg.BuildVersion)) + httpReq.Header.Set("Content-Type", contentType) + + rs.log.Debug("Sanitizer - HTTP: calling", "filename", req.Filename, "contentLength", len(req.Content), "url", sanitizerUrl) + // make request to renderer server + resp, err := netClient.Do(httpReq) + if err != nil { + rs.log.Error("Sanitizer - HTTP: failed to send request", "error", err) + return nil, fmt.Errorf("sanitizer - HTTP: failed to send request: %w", err) + } + + defer func() { + if err := resp.Body.Close(); err != nil { + rs.log.Error("Sanitizer - HTTP: failed to close response body", "statusCode", resp.StatusCode, "error", err) + } + }() + + if resp.StatusCode != http.StatusOK { + if body, err := io.ReadAll(resp.Body); body != nil { + rs.log.Error("Sanitizer - HTTP: failed to sanitize", "statusCode", resp.StatusCode, "error", err, "resp", string(body)) + } else { + rs.log.Error("Sanitizer - HTTP: failed to sanitize", "statusCode", resp.StatusCode, "error", err) + } + return nil, fmt.Errorf("sanitizer - HTTP: failed to sanitize %s", req.Filename) + } + + sanitized, err := io.ReadAll(resp.Body) + if err != nil { + rs.log.Error("Sanitizer - HTTP: failed to read response body", "error", err, "filename", req.Filename) + return nil, fmt.Errorf("sanitizer - HTTP: failed to read response body: %s", err) + } + + return &SanitizeSVGResponse{Sanitized: sanitized}, nil +} + +func (rs *RenderingService) sanitizeSVGViaPlugin(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) { + ctx, cancel := context.WithTimeout(ctx, time.Second*20) + defer cancel() + + domPurifyConfig, err := json.Marshal(domPurifySvgConfig) + if err != nil { + rs.log.Error("Sanitizer - plugin: failed to parse domPurifyConfig") + return nil, fmt.Errorf("sanitizer - plugin: failed to parse domPurifyConfig %s", err) + } + grpcReq := &pluginextensionv2.SanitizeRequest{ + Filename: req.Filename, + Content: req.Content, + ConfigType: domPurifyConfigType, + Config: domPurifyConfig, + } + rs.log.Debug("Sanitizer - plugin: calling", "filename", req.Filename, "contentLength", len(req.Content)) + + rsp, err := rs.pluginInfo.Renderer.Sanitize(ctx, grpcReq) + if err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + rs.log.Info("Sanitizer - plugin: time out") + return nil, ErrTimeout + } + + return nil, err + } + + if rsp.Error != "" { + return nil, fmt.Errorf("sanitizer - plugin: failed to sanitize: %s", rsp.Error) + } + + return &SanitizeSVGResponse{Sanitized: rsp.Sanitized}, nil +} diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index c65624c7f16..93658636fb0 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -429,7 +429,8 @@ func doSearchQuery( hasConstraints = true } - if q.Query == "*" || q.Query == "" { + isMatchAllQuery := q.Query == "*" || q.Query == "" + if isMatchAllQuery { if !hasConstraints { fullQuery.AddShould(bluge.NewMatchAllQuery()) } @@ -600,7 +601,11 @@ func doSearchQuery( } if q.Explain { - fScore.Append(match.Score) + if isMatchAllQuery { + fScore.Append(float64(fieldLen + q.From)) + } else { + fScore.Append(match.Score) + } if match.Explanation != nil { js, _ := json.Marshal(&match.Explanation) jsb := json.RawMessage(js) diff --git a/pkg/services/searchV2/extract/dashboard.go b/pkg/services/searchV2/extract/dashboard.go index 1b49a3a1f59..1ff4a6a5866 100644 --- a/pkg/services/searchV2/extract/dashboard.go +++ b/pkg/services/searchV2/extract/dashboard.go @@ -3,6 +3,7 @@ package extract import ( "io" "strconv" + "strings" jsoniter "github.com/json-iterator/go" ) @@ -11,6 +12,103 @@ func logf(format string, a ...interface{}) { //fmt.Printf(format, a...) } +type templateVariable struct { + current struct { + value interface{} + } + name string + query interface{} + variableType string +} + +type datasourceVariableLookup struct { + variableNameToRefs map[string][]DataSourceRef + dsLookup DatasourceLookup +} + +func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []DataSourceRef { + switch value { + case "default": + // can be the default DS, or a DS with UID="default" + candidateDs := d.dsLookup.ByRef(&DataSourceRef{UID: value}) + if candidateDs == nil { + // get the actual default DS + candidateDs = d.dsLookup.ByRef(nil) + } + + if candidateDs != nil { + return []DataSourceRef{*candidateDs} + } + return []DataSourceRef{} + case "$__all": + // TODO: filter datasources by template variable's regex + return d.dsLookup.ByType(datasourceType) + case "": + return []DataSourceRef{} + case "No data sources found": + return []DataSourceRef{} + default: + return []DataSourceRef{ + { + UID: value, + Type: datasourceType, + }, + } + } +} + +func (d *datasourceVariableLookup) add(templateVariable templateVariable) { + var refs []DataSourceRef + + datasourceType, isDataSourceTypeValid := templateVariable.query.(string) + if !isDataSourceTypeValid { + d.variableNameToRefs[templateVariable.name] = refs + return + } + + if values, multiValueVariable := templateVariable.current.value.([]interface{}); multiValueVariable { + for _, value := range values { + if valueAsString, ok := value.(string); ok { + refs = append(refs, d.getDsRefsByTemplateVariableValue(valueAsString, datasourceType)...) + } + } + } + + if value, stringValue := templateVariable.current.value.(string); stringValue { + refs = append(refs, d.getDsRefsByTemplateVariableValue(value, datasourceType)...) + } + + d.variableNameToRefs[templateVariable.name] = unique(refs) +} + +func unique(refs []DataSourceRef) []DataSourceRef { + var uniqueRefs []DataSourceRef + uidPresence := make(map[string]bool) + for _, ref := range refs { + if !uidPresence[ref.UID] { + uidPresence[ref.UID] = true + uniqueRefs = append(uniqueRefs, ref) + } + } + return uniqueRefs +} + +func (d *datasourceVariableLookup) getDatasourceRefs(name string) []DataSourceRef { + refs, ok := d.variableNameToRefs[name] + if ok { + return refs + } + + return []DataSourceRef{} +} + +func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableLookup { + return &datasourceVariableLookup{ + variableNameToRefs: make(map[string][]DataSourceRef), + dsLookup: dsLookup, + } +} + // nolint:gocyclo // ReadDashboard will take a byte stream and return dashboard info func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, error) { @@ -18,6 +116,8 @@ func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, e iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024) + datasourceVariablesLookup := newDatasourceVariableLookup(lookup) + for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { // Skip null values so we don't need special int handling if iter.WhatIsNext() == jsoniter.NilValue { @@ -112,13 +212,34 @@ func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, e for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { if sub == "list" { for iter.ReadArray() { + templateVariable := templateVariable{} + for k := iter.ReadObject(); k != ""; k = iter.ReadObject() { - if k == "name" { - dash.TemplateVars = append(dash.TemplateVars, iter.ReadString()) - } else { + switch k { + case "name": + name := iter.ReadString() + dash.TemplateVars = append(dash.TemplateVars, name) + templateVariable.name = name + case "type": + templateVariable.variableType = iter.ReadString() + case "query": + templateVariable.query = iter.Read() + case "current": + for c := iter.ReadObject(); c != ""; c = iter.ReadObject() { + if c == "value" { + templateVariable.current.value = iter.Read() + } else { + iter.Skip() + } + } + default: iter.Skip() } } + + if templateVariable.variableType == "datasource" { + datasourceVariablesLookup.add(templateVariable) + } } } else { iter.Skip() @@ -139,6 +260,8 @@ func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, e } } + replaceDatasourceVariables(dash, datasourceVariablesLookup) + targets := newTargetInfo(lookup) for _, panel := range dash.Panels { targets.addPanel(panel) @@ -148,6 +271,43 @@ func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, e return dash, iter.Error } +func replaceDatasourceVariables(dash *DashboardInfo, datasourceVariablesLookup *datasourceVariableLookup) { + for i, panel := range dash.Panels { + var dsVariableRefs []DataSourceRef + var dsRefs []DataSourceRef + + // partition into actual datasource references and variables + for i := range panel.Datasource { + isVariableRef := strings.HasPrefix(panel.Datasource[i].UID, "$") + if isVariableRef { + dsVariableRefs = append(dsVariableRefs, panel.Datasource[i]) + } else { + dsRefs = append(dsRefs, panel.Datasource[i]) + } + } + + dash.Panels[i].Datasource = append(dsRefs, findDatasourceRefsForVariables(dsVariableRefs, datasourceVariablesLookup)...) + } +} + +func getDataSourceVariableName(dsVariableRef DataSourceRef) string { + if strings.HasPrefix(dsVariableRef.UID, "${") { + return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${") + } + + return strings.TrimPrefix(dsVariableRef.UID, "$") +} + +func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []DataSourceRef { + var referencedDs []DataSourceRef + for _, dsVariableRef := range dsVariableRefs { + variableName := getDataSourceVariableName(dsVariableRef) + refs := datasourceVariablesLookup.getDatasourceRefs(variableName) + referencedDs = append(referencedDs, refs...) + } + return referencedDs +} + // will always return strings for now func readPanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelInfo { panel := PanelInfo{} diff --git a/pkg/services/searchV2/extract/dashboard_test.go b/pkg/services/searchV2/extract/dashboard_test.go index e017f9c672a..50ab7e8d9ea 100644 --- a/pkg/services/searchV2/extract/dashboard_test.go +++ b/pkg/services/searchV2/extract/dashboard_test.go @@ -4,28 +4,61 @@ import ( "encoding/json" "os" "path/filepath" + "sort" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type dsLookup struct { +} + +func (d *dsLookup) ByRef(ref *DataSourceRef) *DataSourceRef { + if ref == nil || ref.UID == "" { + return &DataSourceRef{ + UID: "default.uid", + Type: "default.type", + } + } + + if ref.UID == "default" { + return nil + } + return ref +} + +func (d *dsLookup) ByType(dsType string) []DataSourceRef { + if dsType == "sqlite-datasource" { + return []DataSourceRef{ + { + UID: "sqlite-1", + Type: "sqlite-datasource", + }, + { + UID: "sqlite-2", + Type: "sqlite-datasource", + }, + } + } + return make([]DataSourceRef, 0) +} + func TestReadDashboard(t *testing.T) { inputs := []string{ "check-string-datasource-id", "all-panels", "panel-graph/graph-shared-tooltips", - } - - // key will allow name or uid - ds := func(ref *DataSourceRef) *DataSourceRef { - if ref == nil || ref.UID == "" { - return &DataSourceRef{ - UID: "default.uid", - Type: "default.type", - } - } - return ref + "datasource-variable", + "default-datasource-variable", + "empty-datasource-variable", + "repeated-datasource-variables", + "string-datasource-variable", + "datasource-variable-no-curly-braces", + "all-selected-multi-datasource-variable", + "all-selected-single-datasource-variable", + "repeated-datasource-variables-with-default", } devdash := "../../../../devenv/dev-dashboards/" @@ -44,7 +77,9 @@ func TestReadDashboard(t *testing.T) { } require.NoError(t, err) - dash, err := ReadDashboard(f, ds) + dash, err := ReadDashboard(f, &dsLookup{}) + sortDatasources(dash) + require.NoError(t, err) out, err := json.MarshalIndent(dash, "", " ") require.NoError(t, err) @@ -64,3 +99,16 @@ func TestReadDashboard(t *testing.T) { } } } + +// assure consistent ordering of datasources to prevent random failures of `assert.JSONEq` +func sortDatasources(dash *DashboardInfo) { + sort.Slice(dash.Datasource, func(i, j int) bool { + return strings.Compare(dash.Datasource[i].UID, dash.Datasource[j].UID) > 0 + }) + + for panelId := range dash.Panels { + sort.Slice(dash.Panels[panelId].Datasource, func(i, j int) bool { + return strings.Compare(dash.Panels[panelId].Datasource[i].UID, dash.Panels[panelId].Datasource[j].UID) > 0 + }) + } +} diff --git a/pkg/services/searchV2/extract/targets.go b/pkg/services/searchV2/extract/targets.go index 6fa02622ecd..7714796e129 100644 --- a/pkg/services/searchV2/extract/targets.go +++ b/pkg/services/searchV2/extract/targets.go @@ -31,17 +31,17 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) { switch iter.WhatIsNext() { case jsoniter.StringValue: key := iter.ReadString() - ds := s.lookup(&DataSourceRef{UID: key}) + ds := s.lookup.ByRef(&DataSourceRef{UID: key}) s.addRef(ds) case jsoniter.NilValue: - s.addRef(s.lookup(nil)) + s.addRef(s.lookup.ByRef(nil)) iter.Skip() case jsoniter.ObjectValue: ref := &DataSourceRef{} iter.ReadVal(ref) - ds := s.lookup(ref) + ds := s.lookup.ByRef(ref) s.addRef(ds) default: diff --git a/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable-info.json new file mode 100644 index 00000000000..989ff83a686 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable-info.json @@ -0,0 +1,61 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "datasource": [ + { + "uid": "sqlite-2", + "type": "sqlite-datasource" + }, + { + "uid": "sqlite-1", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 7, + "title": "Panel Title", + "type": "timeseries", + "datasource": [ + { + "uid": "sqlite-2", + "type": "sqlite-datasource" + }, + { + "uid": "sqlite-1", + "type": "sqlite-datasource" + } + ] + }, + { + "id": 3, + "title": "Row title", + "type": "row" + }, + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "sqlite-2", + "type": "sqlite-datasource" + }, + { + "uid": "sqlite-1", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable.json b/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable.json new file mode 100644 index 00000000000..4f87ea84e50 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/all-selected-multi-datasource-variable.json @@ -0,0 +1,230 @@ +{ + "annotations": { + "list": [ + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656533909544, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "repeat": "sqllite", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "queryText": "\n SELECT CAST(strftime('%s', 'now', '-1 minute') as INTEGER) as time, 4 as value\n WHERE time >= 1234 and time < 134567\n ", + "queryType": "table", + "rawQueryText": "SELECT CAST(strftime('%s', 'now', '-1 minute') as INTEGER) as time, 4 as value \nWHERE time >= $__from / 1000 and time < $__to / 1000", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "Panel Title", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "panels": [], + "repeat": "sqllite", + "repeatDirection": "h", + "title": "Row title", + "type": "row" + }, + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 8, + "y": 9 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": "", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 36, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable-info.json new file mode 100644 index 00000000000..fb55df515b7 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable-info.json @@ -0,0 +1,41 @@ +{ + "id": 208, + "title": "new-dashboard-var-ds-test", + "tags": null, + "templateVars": [ + "dsVariable" + ], + "datasource": [ + { + "uid": "sqlite-2", + "type": "sqlite-datasource" + }, + { + "uid": "sqlite-1", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 2, + "title": "Panel Title", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "sqlite-2", + "type": "sqlite-datasource" + }, + { + "uid": "sqlite-1", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable.json b/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable.json new file mode 100644 index 00000000000..f25d804a491 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/all-selected-single-datasource-variable.json @@ -0,0 +1,133 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 208, + "iteration": 1657048248371, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${dsVariable}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "repeat": "dsVariable", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${dsVariable}" + }, + "refId": "A" + } + ], + "title": "Panel Title", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "hide": 0, + "includeAll": true, + "multi": false, + "name": "dsVariable", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "new-dashboard-var-ds-test", + "uid": "2HLX_B3nk", + "version": 21, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/datasource-variable-info.json new file mode 100644 index 00000000000..21f6bcc7e0f --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/datasource-variable-info.json @@ -0,0 +1,33 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces-info.json b/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces-info.json new file mode 100644 index 00000000000..21f6bcc7e0f --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces-info.json @@ -0,0 +1,33 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces.json b/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces.json new file mode 100644 index 00000000000..8c87724daef --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656508852566, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "$sqllite" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "$sqllite" + }, + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "SQLite Grafana", + "value": "SQLite Grafana" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 13, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/datasource-variable.json b/pkg/services/searchV2/extract/testdata/datasource-variable.json new file mode 100644 index 00000000000..f45f56ee76f --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/datasource-variable.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656508852566, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "SQLite Grafana", + "value": "SQLite Grafana" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 13, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/default-datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/default-datasource-variable-info.json new file mode 100644 index 00000000000..01e30f4ae57 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/default-datasource-variable-info.json @@ -0,0 +1,33 @@ +{ + "id": 208, + "title": "new-dashboard-var-ds-test", + "tags": null, + "templateVars": [ + "dsVariable" + ], + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ], + "panels": [ + { + "id": 2, + "title": "Panel Title", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/default-datasource-variable.json b/pkg/services/searchV2/extract/testdata/default-datasource-variable.json new file mode 100644 index 00000000000..07362994dc0 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/default-datasource-variable.json @@ -0,0 +1,133 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 208, + "iteration": 1657048248373, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "${dsVariable}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "repeat": "dsVariable", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "${dsVariable}" + }, + "refId": "A" + } + ], + "title": "Panel Title", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "dsVariable", + "options": [], + "query": "testdata", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "new-dashboard-var-ds-test", + "uid": "2HLX_B3nk", + "version": 22, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/empty-datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/empty-datasource-variable-info.json new file mode 100644 index 00000000000..371080d3786 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/empty-datasource-variable-info.json @@ -0,0 +1,21 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "panels": [ + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre" + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/empty-datasource-variable.json b/pkg/services/searchV2/extract/testdata/empty-datasource-variable.json new file mode 100644 index 00000000000..5daacfd0757 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/empty-datasource-variable.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656513278471, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "No data sources found", + "value": "" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "asdgasd", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 14, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-info.json b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-info.json new file mode 100644 index 00000000000..edb526ad04a --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-info.json @@ -0,0 +1,41 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "datasource": [ + { + "uid": "SQLite Grafana2", + "type": "sqlite-datasource" + }, + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "SQLite Grafana2", + "type": "sqlite-datasource" + }, + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default-info.json b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default-info.json new file mode 100644 index 00000000000..8f461ac4a74 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default-info.json @@ -0,0 +1,41 @@ +{ + "id": 208, + "title": "new-dashboard-var-ds-test", + "tags": null, + "templateVars": [ + "dsVariable" + ], + "datasource": [ + { + "uid": "gdev-testdata", + "type": "testdata" + }, + { + "uid": "default.uid", + "type": "default.type" + } + ], + "panels": [ + { + "id": 2, + "title": "Panel Title", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "gdev-testdata", + "type": "testdata" + }, + { + "uid": "default.uid", + "type": "default.type" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default.json b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default.json new file mode 100644 index 00000000000..d6941f5f48e --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default.json @@ -0,0 +1,139 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 208, + "iteration": 1657048248365, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "${dsVariable}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "repeat": "dsVariable", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "${dsVariable}" + }, + "refId": "A" + } + ], + "title": "Panel Title", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "gdev-testdata", + "default" + ], + "value": [ + "gdev-testdata", + "default" + ] + }, + "hide": 0, + "includeAll": false, + "multi": true, + "name": "dsVariable", + "options": [], + "query": "testdata", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "new-dashboard-var-ds-test", + "uid": "2HLX_B3nk", + "version": 19, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/repeated-datasource-variables.json b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables.json new file mode 100644 index 00000000000..abcd9589f5a --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/repeated-datasource-variables.json @@ -0,0 +1,143 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656507534777, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "repeat": "sqllite", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "key": "Q-7ab4ab1a-3c70-41f4-9a45-19919e1c2193-0", + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": [ + "SQLite Grafana", + "SQLite Grafana2" + ], + "value": [ + "SQLite Grafana", + "SQLite Grafana2" + ] + }, + "hide": 0, + "includeAll": false, + "multi": true, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 8, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/testdata/string-datasource-variable-info.json b/pkg/services/searchV2/extract/testdata/string-datasource-variable-info.json new file mode 100644 index 00000000000..21f6bcc7e0f --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/string-datasource-variable-info.json @@ -0,0 +1,33 @@ +{ + "id": 209, + "title": "ds-variables", + "tags": null, + "templateVars": [ + "sqllite" + ], + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ], + "panels": [ + { + "id": 1, + "title": "usersss!", + "type": "table", + "pluginVersion": "9.1.0-pre", + "datasource": [ + { + "uid": "SQLite Grafana", + "type": "sqlite-datasource" + } + ] + } + ], + "schemaVersion": 36, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/searchV2/extract/testdata/string-datasource-variable.json b/pkg/services/searchV2/extract/testdata/string-datasource-variable.json new file mode 100644 index 00000000000..c69379d9a00 --- /dev/null +++ b/pkg/services/searchV2/extract/testdata/string-datasource-variable.json @@ -0,0 +1,109 @@ +{ + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 209, + "iteration": 1656508852566, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": "${sqllite}", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.0-pre", + "targets": [ + { + "datasource": { + "type": "sqlite-datasource", + "uid": "${sqllite}" + }, + "queryText": "select * from user", + "queryType": "table", + "rawQueryText": "select * from user", + "refId": "A", + "timeColumns": [ + "time", + "ts" + ] + } + ], + "title": "usersss!", + "type": "table" + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "SQLite Grafana", + "value": "SQLite Grafana" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "sqllite", + "options": [], + "query": "sqlite-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ds-variables", + "uid": "7BzWolqnk", + "version": 13, + "weekStart": "" +} diff --git a/pkg/services/searchV2/extract/types.go b/pkg/services/searchV2/extract/types.go index 9b084cd2409..f149ba5d3c5 100644 --- a/pkg/services/searchV2/extract/types.go +++ b/pkg/services/searchV2/extract/types.go @@ -1,7 +1,10 @@ package extract -// empty everything will return the default -type DatasourceLookup = func(ref *DataSourceRef) *DataSourceRef +type DatasourceLookup interface { + // ByRef will return the default DS given empty reference (nil ref, or empty ref.uid and ref.type) + ByRef(ref *DataSourceRef) *DataSourceRef + ByType(dsType string) []DataSourceRef +} type DataSourceRef struct { UID string `json:"uid,omitempty"` diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 6cbe0916d0b..c9e98aeaf15 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -725,7 +725,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das } // key will allow name or uid - lookup, err := loadDatasourceLookup(ctx, orgID, l.sql) + lookup, err := LoadDatasourceLookup(ctx, orgID, l.sql) if err != nil { return dashboards, err } @@ -821,13 +821,79 @@ type datasourceQueryResult struct { IsDefault bool `xorm:"is_default"` } -func loadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (extract.DatasourceLookup, error) { +func createDatasourceLookup(rows []*datasourceQueryResult) extract.DatasourceLookup { byUID := make(map[string]*extract.DataSourceRef, 50) byName := make(map[string]*extract.DataSourceRef, 50) + byType := make(map[string][]extract.DataSourceRef, 50) var defaultDS *extract.DataSourceRef - err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - rows := make([]*datasourceQueryResult, 0) + for _, row := range rows { + ref := &extract.DataSourceRef{ + UID: row.UID, + Type: row.Type, + } + byUID[row.UID] = ref + byName[row.Name] = ref + if row.IsDefault { + defaultDS = ref + } + + if _, ok := byType[row.Type]; !ok { + byType[row.Type] = make([]extract.DataSourceRef, 5) + } + byType[row.Type] = append(byType[row.Type], *ref) + } + + return &dsLookup{ + byName: byName, + byUID: byUID, + byType: byType, + defaultDS: defaultDS, + } +} + +type dsLookup struct { + byName map[string]*extract.DataSourceRef + byUID map[string]*extract.DataSourceRef + byType map[string][]extract.DataSourceRef + defaultDS *extract.DataSourceRef +} + +func (d *dsLookup) ByRef(ref *extract.DataSourceRef) *extract.DataSourceRef { + if ref == nil { + return d.defaultDS + } + key := "" + if ref.UID != "" { + ds, ok := d.byUID[ref.UID] + if ok { + return ds + } + key = ref.UID + } + if key == "" { + return d.defaultDS + } + ds, ok := d.byUID[key] + if ok { + return ds + } + return d.byName[key] +} + +func (d *dsLookup) ByType(dsType string) []extract.DataSourceRef { + ds, ok := d.byType[dsType] + if !ok { + return make([]extract.DataSourceRef, 0) + } + + return ds +} + +func LoadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (extract.DatasourceLookup, error) { + rows := make([]*datasourceQueryResult, 0) + + if err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { sess.Table("data_source"). Where("org_id = ?", orgID). Cols("uid", "name", "type", "is_default") @@ -837,44 +903,10 @@ func loadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLSto return err } - for _, row := range rows { - ds := &extract.DataSourceRef{ - UID: row.UID, - Type: row.Type, - } - byUID[row.UID] = ds - byName[row.Name] = ds - if row.IsDefault { - defaultDS = ds - } - } - return nil - }) - if err != nil { + }); err != nil { return nil, err } - // Lookup by UID or name - return func(ref *extract.DataSourceRef) *extract.DataSourceRef { - if ref == nil { - return defaultDS - } - key := "" - if ref.UID != "" { - ds, ok := byUID[ref.UID] - if ok { - return ds - } - key = ref.UID - } - if key == "" { - return defaultDS - } - ds, ok := byUID[key] - if ok { - return ds - } - return byName[key] - }, err + return createDatasourceLookup(rows), nil } diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index ce185eae024..ba445fc1bfe 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -20,12 +20,13 @@ import ( ) type ServiceAccountsAPI struct { - cfg *setting.Cfg - service serviceaccounts.Service - accesscontrol accesscontrol.AccessControl - RouterRegister routing.RouteRegister - store serviceaccounts.Store - log log.Logger + cfg *setting.Cfg + service serviceaccounts.Service + accesscontrol accesscontrol.AccessControl + RouterRegister routing.RouteRegister + store serviceaccounts.Store + log log.Logger + permissionService accesscontrol.ServiceAccountPermissionsService } func NewServiceAccountsAPI( @@ -34,14 +35,16 @@ func NewServiceAccountsAPI( accesscontrol accesscontrol.AccessControl, routerRegister routing.RouteRegister, store serviceaccounts.Store, + permissionService accesscontrol.ServiceAccountPermissionsService, ) *ServiceAccountsAPI { return &ServiceAccountsAPI{ - cfg: cfg, - service: service, - accesscontrol: accesscontrol, - RouterRegister: routerRegister, - store: store, - log: log.New("serviceaccounts.api"), + cfg: cfg, + service: service, + accesscontrol: accesscontrol, + RouterRegister: routerRegister, + store: store, + log: log.New("serviceaccounts.api"), + permissionService: permissionService, } } @@ -84,7 +87,18 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon return response.Error(http.StatusBadRequest, "Bad request data", err) } - serviceAccount, err := api.store.CreateServiceAccount(c.Req.Context(), c.OrgId, cmd.Name) + if err := api.validateRole(cmd.Role, &c.OrgRole); err != nil { + switch { + case errors.Is(err, serviceaccounts.ErrServiceAccountInvalidRole): + return response.Error(http.StatusBadRequest, err.Error(), err) + case errors.Is(err, serviceaccounts.ErrServiceAccountRolePrivilegeDenied): + return response.Error(http.StatusForbidden, err.Error(), err) + default: + return response.Error(http.StatusInternalServerError, "failed to create service account", err) + } + } + + serviceAccount, err := api.store.CreateServiceAccount(c.Req.Context(), c.OrgId, &cmd) switch { case errors.Is(err, database.ErrServiceAccountAlreadyExists): return response.Error(http.StatusBadRequest, "Failed to create service account", err) @@ -92,6 +106,14 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon return response.Error(http.StatusInternalServerError, "Failed to create service account", err) } + if !api.accesscontrol.IsDisabled() { + if c.SignedInUser.IsRealUser() { + if _, err := api.permissionService.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: c.SignedInUser.UserId}, strconv.FormatInt(serviceAccount.Id, 10), "Admin"); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to set permissions for service account creator", err) + } + } + } + return response.JSON(http.StatusCreated, serviceAccount) } @@ -138,11 +160,15 @@ func (api *ServiceAccountsAPI) UpdateServiceAccount(c *models.ReqContext) respon return response.Error(http.StatusBadRequest, "Bad request data", err) } - if cmd.Role != nil && !cmd.Role.IsValid() { - return response.Error(http.StatusBadRequest, "Invalid role specified", nil) - } - if cmd.Role != nil && !c.OrgRole.Includes(*cmd.Role) { - return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil) + if err := api.validateRole(cmd.Role, &c.OrgRole); err != nil { + switch { + case errors.Is(err, serviceaccounts.ErrServiceAccountInvalidRole): + return response.Error(http.StatusBadRequest, err.Error(), err) + case errors.Is(err, serviceaccounts.ErrServiceAccountRolePrivilegeDenied): + return response.Error(http.StatusForbidden, err.Error(), err) + default: + return response.Error(http.StatusInternalServerError, "failed to update service account", err) + } } resp, err := api.store.UpdateServiceAccount(c.Req.Context(), c.OrgId, scopeID, &cmd) @@ -168,6 +194,16 @@ func (api *ServiceAccountsAPI) UpdateServiceAccount(c *models.ReqContext) respon }) } +func (api *ServiceAccountsAPI) validateRole(r *models.RoleType, orgRole *models.RoleType) error { + if r != nil && !r.IsValid() { + return serviceaccounts.ErrServiceAccountInvalidRole + } + if r != nil && !orgRole.Includes(*r) { + return serviceaccounts.ErrServiceAccountRolePrivilegeDenied + } + return nil +} + // DELETE /api/serviceaccounts/:serviceAccountId func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index 2f63ea3aa74..7bbe322cc2b 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/grafana/grafana/pkg/api/routing" @@ -15,8 +16,11 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + acDatabase "github.com/grafana/grafana/pkg/services/accesscontrol/database" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" @@ -35,7 +39,7 @@ var ( func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} autoAssignOrg := store.Cfg.AutoAssignOrg @@ -58,8 +62,8 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { } testCases := []testCreateSATestCase{ { - desc: "should be ok to create serviceaccount with permissions", - body: map[string]interface{}{"name": "New SA"}, + desc: "should be ok to create service account with permissions", + body: map[string]interface{}{"name": "New SA", "role": "Viewer", "is_disabled": "false"}, wantID: "sa-new-sa", acmock: tests.SetupMockAccesscontrol( t, @@ -70,6 +74,33 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { ), expectedCode: http.StatusCreated, }, + { + desc: "should fail to create a service account with higher privilege", + body: map[string]interface{}{"name": "New SA HP", "role": "Admin"}, + wantID: "sa-new-sa-hp", + acmock: tests.SetupMockAccesscontrol( + t, + func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { + return []accesscontrol.Permission{{Action: serviceaccounts.ActionCreate}}, nil + }, + false, + ), + expectedCode: http.StatusForbidden, + }, + { + desc: "should fail to create a service account with invalid role", + body: map[string]interface{}{"name": "New SA", "role": "Random"}, + wantID: "sa-new-sa", + wantError: "invalid role value: Random", + acmock: tests.SetupMockAccesscontrol( + t, + func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { + return []accesscontrol.Permission{{Action: serviceaccounts.ActionCreate}}, nil + }, + false, + ), + expectedCode: http.StatusBadRequest, + }, { desc: "not ok - duplicate name", body: map[string]interface{}{"name": "New SA"}, @@ -97,7 +128,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { expectedCode: http.StatusBadRequest, }, { - desc: "should be forbidden to create serviceaccount if no permissions", + desc: "should be forbidden to create service account if no permissions", body: map[string]interface{}{}, acmock: tests.SetupMockAccesscontrol( t, @@ -123,7 +154,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { serviceAccountRequestScenario(t, http.MethodPost, serviceAccountPath, testUser, func(httpmethod string, endpoint string, user *tests.TestUser) { - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, api := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) marshalled, err := json.Marshal(tc.body) require.NoError(t, err) @@ -139,9 +170,25 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { require.Equal(t, tc.expectedCode, actualCode, actualBody) if actualCode == http.StatusCreated { - assert.NotEmpty(t, actualBody["id"]) - assert.Equal(t, tc.body["name"], actualBody["name"].(string)) - assert.Equal(t, tc.wantID, actualBody["login"].(string)) + sa := serviceaccounts.ServiceAccountDTO{} + err = json.Unmarshal(actual.Body.Bytes(), &sa) + require.NoError(t, err) + assert.NotZero(t, sa.Id) + assert.Equal(t, tc.body["name"], sa.Name) + assert.Equal(t, tc.wantID, sa.Login) + tempUser := &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{ + 1: { + serviceaccounts.ActionRead: []string{serviceaccounts.ScopeAll}, + }, + }, + } + perms, err := api.permissionService.GetPermissions(context.Background(), tempUser, strconv.FormatInt(sa.Id, 10)) + assert.NoError(t, err) + assert.Equal(t, 1, len(perms), "should have added managed permissions for SA creator") + assert.Equal(t, int64(1), perms[0].ID) + assert.Equal(t, int64(1), perms[0].UserId) } else if actualCode == http.StatusBadRequest { assert.Contains(t, tc.wantError, actualBody["error"].(string)) } @@ -155,7 +202,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} var requestResponse = func(server *web.Mux, httpMethod, requestpath string) *httptest.ResponseRecorder { @@ -224,7 +271,11 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) { - a := NewServiceAccountsAPI(setting.NewCfg(), svc, acmock, routerRegister, saStore) + cfg := setting.NewCfg() + saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, acDatabase.ProvideService(sqlStore), &licensing.OSSLicensingService{}, saStore) + require.NoError(t, err) + + a := NewServiceAccountsAPI(cfg, svc, acmock, routerRegister, saStore, saPermissionService) a.RegisterAPIEndpoints() a.cfg.ApiKeyMaxSecondsToLive = -1 // disable api key expiration @@ -232,6 +283,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, m := web.New() signedUser := &models.SignedInUser{ OrgId: 1, + UserId: 1, OrgRole: models.ROLE_VIEWER, } @@ -251,7 +303,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} type testRetrieveSATestCase struct { desc string @@ -342,7 +394,7 @@ func newString(s string) *string { func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} type testUpdateSATestCase struct { desc string diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 93f46680378..2c9f5c3107a 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -52,7 +52,7 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string, func TestServiceAccountsAPI_CreateToken(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) @@ -169,7 +169,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) svcMock := &tests.ServiceAccountMock{} - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) type testCreateSAToken struct { diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 00d09b208fc..279401d6d7d 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -23,7 +23,7 @@ type ServiceAccountsStoreImpl struct { log log.Logger } -func NewServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl { +func ProvideServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl { return &ServiceAccountsStoreImpl{ sqlStore: store, kvStore: kvStore, @@ -32,30 +32,61 @@ func NewServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) } // CreateServiceAccount creates service account -func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, orgId int64, name string) (saDTO *serviceaccounts.ServiceAccountDTO, err error) { - generatedLogin := "sa-" + strings.ToLower(name) +func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, orgId int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + generatedLogin := "sa-" + strings.ToLower(saForm.Name) generatedLogin = strings.ReplaceAll(generatedLogin, " ", "-") - cmd := user.CreateUserCommand{ - Login: generatedLogin, - OrgID: orgId, - Name: name, - IsServiceAccount: true, + isDisabled := false + role := models.ROLE_VIEWER + if saForm.IsDisabled != nil { + isDisabled = *saForm.IsDisabled } + if saForm.Role != nil { + role = *saForm.Role + } + var newSA *user.User + createErr := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) (err error) { + var errUser error + newSA, errUser = s.sqlStore.CreateUser(ctx, user.CreateUserCommand{ + Login: generatedLogin, + OrgID: orgId, + Name: saForm.Name, + IsDisabled: isDisabled, + IsServiceAccount: true, + SkipOrgSetup: true, + }) + if errUser != nil { + return errUser + } - newuser, err := s.sqlStore.CreateUser(ctx, cmd) - if err != nil { - if errors.Is(err, models.ErrUserAlreadyExists) { + errAddOrgUser := s.sqlStore.AddOrgUser(ctx, &models.AddOrgUserCommand{ + Role: role, + OrgId: orgId, + UserId: newSA.ID, + AllowAddingServiceAccount: true, + }) + if errAddOrgUser != nil { + return errAddOrgUser + } + + return nil + }) + + if createErr != nil { + if errors.Is(createErr, models.ErrUserAlreadyExists) { return nil, ErrServiceAccountAlreadyExists } - return nil, fmt.Errorf("failed to create service account: %w", err) + + return nil, fmt.Errorf("failed to create service account: %w", createErr) } return &serviceaccounts.ServiceAccountDTO{ - Id: newuser.ID, - Name: newuser.Name, - Login: newuser.Login, - OrgId: newuser.OrgID, - Tokens: 0, + Id: newSA.ID, + Name: newSA.Name, + Login: newSA.Login, + OrgId: newSA.OrgID, + Tokens: 0, + Role: string(role), + IsDisabled: isDisabled, }, nil } diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index cb50a593196..10b896e3d48 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -13,13 +13,43 @@ import ( "github.com/stretchr/testify/require" ) -func TestStore_CreateServiceAccount(t *testing.T) { +// Service Account should not create an org on its own +func TestStore_CreateServiceAccountOrgNonExistant(t *testing.T) { _, store := setupTestDatabase(t) t.Run("create service account", func(t *testing.T) { serviceAccountName := "new Service Account" serviceAccountOrgId := int64(1) + serviceAccountRole := models.ROLE_ADMIN + isDisabled := true + saForm := serviceaccounts.CreateServiceAccountForm{ + Name: serviceAccountName, + Role: &serviceAccountRole, + IsDisabled: &isDisabled, + } - saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, serviceAccountName) + _, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) + require.Error(t, err) + }) +} + +func TestStore_CreateServiceAccount(t *testing.T) { + _, store := setupTestDatabase(t) + orgQuery := &models.CreateOrgCommand{Name: sqlstore.MainOrgName} + err := store.sqlStore.CreateOrg(context.Background(), orgQuery) + require.NoError(t, err) + + t.Run("create service account", func(t *testing.T) { + serviceAccountName := "new Service Account" + serviceAccountOrgId := orgQuery.Result.Id + serviceAccountRole := models.ROLE_ADMIN + isDisabled := true + saForm := serviceaccounts.CreateServiceAccountForm{ + Name: serviceAccountName, + Role: &serviceAccountRole, + IsDisabled: &isDisabled, + } + + saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) require.NoError(t, err) assert.Equal(t, "sa-new-service-account", saDTO.Login) assert.Equal(t, serviceAccountName, saDTO.Name) @@ -30,6 +60,8 @@ func TestStore_CreateServiceAccount(t *testing.T) { assert.Equal(t, "sa-new-service-account", retrieved.Login) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) + assert.Equal(t, string(serviceAccountRole), retrieved.Role) + assert.True(t, retrieved.IsDisabled) retrievedId, err := store.RetrieveServiceAccountIdByName(context.Background(), serviceAccountOrgId, serviceAccountName) require.NoError(t, err) @@ -73,7 +105,7 @@ func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreI t.Helper() db := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(db) - return db, NewServiceAccountsStore(db, kvStore) + return db, ProvideServiceAccountsStore(db, kvStore) } func TestStore_RetrieveServiceAccount(t *testing.T) { diff --git a/pkg/services/serviceaccounts/database/stats.go b/pkg/services/serviceaccounts/database/stats.go index edce5f15b44..2d47333c9c5 100644 --- a/pkg/services/serviceaccounts/database/stats.go +++ b/pkg/services/serviceaccounts/database/stats.go @@ -2,10 +2,69 @@ package database import ( "context" + "sync" + "time" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/prometheus/client_golang/prometheus" ) +const ( + ExporterName = "grafana" + metricsCollectionInterval = time.Minute * 30 +) + +var ( + // MStatTotalServiceAccounts is a metric gauge for total number of service accounts + MStatTotalServiceAccounts prometheus.Gauge + + // MStatTotalServiceAccountTokens is a metric gauge for total number of service account tokens + MStatTotalServiceAccountTokens prometheus.Gauge + + once sync.Once + Initialised bool = false +) + +func InitMetrics() { + once.Do(func() { + MStatTotalServiceAccounts = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_total_service_accounts", + Help: "total amount of service accounts", + Namespace: ExporterName, + }) + + MStatTotalServiceAccountTokens = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_total_service_account_tokens", + Help: "total amount of service account tokens", + Namespace: ExporterName, + }) + + prometheus.MustRegister( + MStatTotalServiceAccounts, + MStatTotalServiceAccountTokens, + ) + }) +} + +func (s *ServiceAccountsStoreImpl) RunMetricsCollection(ctx context.Context) error { + if _, err := s.GetUsageMetrics(ctx); err != nil { + s.log.Warn("Failed to get usage metrics", "error", err.Error()) + } + updateStatsTicker := time.NewTicker(metricsCollectionInterval) + defer updateStatsTicker.Stop() + + for { + select { + case <-updateStatsTicker.C: + if _, err := s.GetUsageMetrics(ctx); err != nil { + s.log.Warn("Failed to get usage metrics", "error", err.Error()) + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + func (s *ServiceAccountsStoreImpl) GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) { stats := map[string]interface{}{} @@ -39,5 +98,8 @@ func (s *ServiceAccountsStoreImpl) GetUsageMetrics(ctx context.Context) (map[str stats["stats.serviceaccounts.tokens.count"] = sqlStats.Tokens stats["stats.serviceaccounts.in_teams.count"] = sqlStats.InTeams + MStatTotalServiceAccountTokens.Set(float64(sqlStats.Tokens)) + MStatTotalServiceAccounts.Set(float64(sqlStats.ServiceAccounts)) + return stats, nil } diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go index d9aa95755ff..467c5e534d2 100644 --- a/pkg/services/serviceaccounts/database/stats_test.go +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -16,6 +16,7 @@ func TestStore_UsageStats(t *testing.T) { saToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} db, store := setupTestDatabase(t) sa := tests.SetupUserServiceAccount(t, db, saToCreate) + InitMetrics() keyName := t.Name() key, err := apikeygen.New(sa.OrgID, keyName) diff --git a/pkg/services/serviceaccounts/errors.go b/pkg/services/serviceaccounts/errors.go index 8538928be31..2ea8853d995 100644 --- a/pkg/services/serviceaccounts/errors.go +++ b/pkg/services/serviceaccounts/errors.go @@ -3,5 +3,7 @@ package serviceaccounts import "errors" var ( - ErrServiceAccountNotFound = errors.New("Service account not found") + ErrServiceAccountNotFound = errors.New("service account not found") + ErrServiceAccountInvalidRole = errors.New("invalid role specified") + ErrServiceAccountRolePrivilegeDenied = errors.New("can not assign a role higher than user's role") ) diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go index 6509c1e8236..6f317700c92 100644 --- a/pkg/services/serviceaccounts/manager/roles.go +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -23,11 +23,26 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { Grants: []string{string(models.ROLE_ADMIN)}, } + saCreator := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:serviceaccounts:creator", + DisplayName: "Service accounts creator", + Description: "Create service accounts.", + Group: "Service accounts", + Permissions: []accesscontrol.Permission{ + { + Action: serviceaccounts.ActionCreate, + }, + }, + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + saWriter := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: "fixed:serviceaccounts:writer", DisplayName: "Service accounts writer", - Description: "Create, delete, read, or query service accounts.", + Description: "Create, delete and read service accounts, manage service account permissions.", Group: "Service accounts", Permissions: accesscontrol.ConcatPermissions(saReader.Role.Permissions, []accesscontrol.Permission{ { @@ -41,12 +56,20 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { Action: serviceaccounts.ActionDelete, Scope: serviceaccounts.ScopeAll, }, + { + Action: serviceaccounts.ActionPermissionsRead, + Scope: serviceaccounts.ScopeAll, + }, + { + Action: serviceaccounts.ActionPermissionsWrite, + Scope: serviceaccounts.ScopeAll, + }, }), }, Grants: []string{string(models.ROLE_ADMIN)}, } - if err := ac.DeclareFixedRoles(saReader, saWriter); err != nil { + if err := ac.DeclareFixedRoles(saReader, saCreator, saWriter); err != nil { return err } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 5049a8c2b28..fd546c4fd00 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -4,21 +4,15 @@ import ( "context" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/api" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) -var ( - ServiceAccountFeatureToggleNotFound = "FeatureToggle serviceAccounts not found, try adding it to your custom.ini" -) - type ServiceAccountsService struct { store serviceaccounts.Store log log.Logger @@ -26,14 +20,15 @@ type ServiceAccountsService struct { func ProvideServiceAccountsService( cfg *setting.Cfg, - store *sqlstore.SQLStore, - kvStore kvstore.KVStore, ac accesscontrol.AccessControl, routeRegister routing.RouteRegister, usageStats usagestats.Service, + serviceAccountsStore serviceaccounts.Store, + permissionService accesscontrol.ServiceAccountPermissionsService, ) (*ServiceAccountsService, error) { + database.InitMetrics() s := &ServiceAccountsService{ - store: database.NewServiceAccountsStore(store, kvStore), + store: serviceAccountsStore, log: log.New("serviceaccounts"), } @@ -43,14 +38,19 @@ func ProvideServiceAccountsService( usageStats.RegisterMetricsFunc(s.store.GetUsageMetrics) - serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store) + serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store, permissionService) serviceaccountsAPI.RegisterAPIEndpoints() return s, nil } -func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, orgID int64, name string) (*serviceaccounts.ServiceAccountDTO, error) { - return sa.store.CreateServiceAccount(ctx, orgID, name) +func (sa *ServiceAccountsService) Run(ctx context.Context) error { + sa.log.Debug("Started Service Account Metrics collection service") + return sa.store.RunMetricsCollection(ctx) +} + +func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { + return sa.store.CreateServiceAccount(ctx, orgID, saForm) } func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 884e69a7910..1eab5a23b68 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -13,10 +13,12 @@ var ( ) const ( - ActionRead = "serviceaccounts:read" - ActionWrite = "serviceaccounts:write" - ActionCreate = "serviceaccounts:create" - ActionDelete = "serviceaccounts:delete" + ActionRead = "serviceaccounts:read" + ActionWrite = "serviceaccounts:write" + ActionCreate = "serviceaccounts:create" + ActionDelete = "serviceaccounts:delete" + ActionPermissionsRead = "serviceaccounts.permissions:read" + ActionPermissionsWrite = "serviceaccounts.permissions:write" ) type ServiceAccount struct { @@ -24,7 +26,9 @@ type ServiceAccount struct { } type CreateServiceAccountForm struct { - Name string `json:"name" binding:"Required"` + Name string `json:"name" binding:"Required"` + Role *models.RoleType `json:"role"` + IsDisabled *bool `json:"isDisabled"` } type UpdateServiceAccountForm struct { diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 71490383fb6..f166fadaf92 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -8,13 +8,13 @@ import ( // this should reflect the api type Service interface { - CreateServiceAccount(ctx context.Context, orgID int64, name string) (*ServiceAccountDTO, error) + CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) } type Store interface { - CreateServiceAccount(ctx context.Context, orgID int64, name string) (*ServiceAccountDTO, error) + CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, filter ServiceAccountFilter, page int, limit int, signedInUser *models.SignedInUser) (*SearchServiceAccountsResult, error) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, @@ -31,4 +31,5 @@ type Store interface { DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) error GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) + RunMetricsCollection(ctx context.Context) error } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index a697d64fbe5..fcdf1418f7e 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -86,7 +86,7 @@ func (s *ServiceAccountMock) RetrieveServiceAccountIdByName(ctx context.Context, return 0, nil } -func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, orgID int64, name string) (*serviceaccounts.ServiceAccountDTO, error) { +func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { return nil, nil } @@ -133,6 +133,7 @@ type Calls struct { } type ServiceAccountsStoreMock struct { + serviceaccounts.Store Calls Calls } @@ -141,9 +142,9 @@ func (s *ServiceAccountsStoreMock) RetrieveServiceAccountIdByName(ctx context.Co return 0, nil } -func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, orgID int64, name string) (*serviceaccounts.ServiceAccountDTO, error) { +func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { // now we can test that the mock has these calls when we call the function - s.Calls.CreateServiceAccount = append(s.Calls.CreateServiceAccount, []interface{}{ctx, orgID, name}) + s.Calls.CreateServiceAccount = append(s.Calls.CreateServiceAccount, []interface{}{ctx, orgID, saForm}) return nil, nil } diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index c7d2c0df7a3..f1fa782b18b 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -16,7 +16,12 @@ func (ss *SQLStore) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserComman return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { // check if user exists var user user.User - if exists, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Get(&user); err != nil { + session := sess.ID(cmd.UserId) + if !cmd.AllowAddingServiceAccount { + session = session.Where(notServiceAccountFilter(ss)) + } + + if exists, err := session.Get(&user); err != nil { return err } else if !exists { return models.ErrUserNotFound diff --git a/pkg/services/sqlstore/org_users_test.go b/pkg/services/sqlstore/org_users_test.go index c18a42eaa48..2b9e4ad9d3b 100644 --- a/pkg/services/sqlstore/org_users_test.go +++ b/pkg/services/sqlstore/org_users_test.go @@ -148,6 +148,61 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) { } } +func TestSQLStore_AddOrgUser(t *testing.T) { + var orgID int64 = 1 + store := InitTestDB(t) + + // create org and admin + _, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + Login: "admin", + OrgID: orgID, + }) + require.NoError(t, err) + + // create a service account with no org + sa, err := store.CreateUser(context.Background(), user.CreateUserCommand{ + Login: "sa-no-org", + IsServiceAccount: true, + SkipOrgSetup: true, + }) + + require.NoError(t, err) + require.Equal(t, int64(-1), sa.OrgID) + + // assign the sa to the org but without the override. should fail + err = store.AddOrgUser(context.Background(), &models.AddOrgUserCommand{ + Role: "Viewer", + OrgId: orgID, + UserId: sa.ID, + }) + require.Error(t, err) + + // assign the sa to the org with the override. should succeed + err = store.AddOrgUser(context.Background(), &models.AddOrgUserCommand{ + Role: "Viewer", + OrgId: orgID, + UserId: sa.ID, + AllowAddingServiceAccount: true, + }) + + require.NoError(t, err) + + // assert the org has been correctly set + saFound := new(user.User) + err = store.WithDbSession(context.Background(), func(sess *DBSession) error { + has, err := sess.ID(sa.ID).Get(saFound) + if err != nil { + return err + } else if !has { + return models.ErrUserNotFound + } + return nil + }) + + require.NoError(t, err) + require.Equal(t, saFound.OrgID, orgID) +} + func TestSQLStore_RemoveOrgUser(t *testing.T) { store := InitTestDB(t) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index ecca42cffaf..9b1cbddc47a 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -68,6 +68,11 @@ func (ss *SQLStore) userCaseInsensitiveLoginConflict(ctx context.Context, sess * } // createUser creates a user in the database +// if autoAssignOrg is enabled then args.OrgID will be used +// to add to an existing Org with id=args.OrgID +// if autoAssignOrg is disabled then args.OrgName will be used +// to create a new Org with name=args.OrgName. +// If a org already exists with that name, it will error func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.CreateUserCommand) (user.User, error) { var usr user.User var orgID int64 = -1 diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index bdbb634a161..a2fcb6db191 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -1,8 +1,10 @@ package store import ( + "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -20,6 +22,8 @@ type HTTPStorageService interface { List(c *models.ReqContext) response.Response Read(c *models.ReqContext) response.Response Delete(c *models.ReqContext) response.Response + DeleteFolder(c *models.ReqContext) response.Response + CreateFolder(c *models.ReqContext) response.Response Upload(c *models.ReqContext) response.Response } @@ -71,6 +75,14 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { }) } + folder, ok := c.Req.MultipartForm.Value["folder"] + if !ok || len(folder) != 1 { + return response.JSON(400, map[string]interface{}{ + "message": "please specify the upload folder", + "err": true, + }) + } + fileHeader := files[0] if fileHeader.Size > MAX_UPLOAD_SIZE { return errFileTooBig @@ -95,7 +107,7 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { return errFileTooBig } - path := RootResources + "/" + fileHeader.Filename + path := folder[0] + "/" + fileHeader.Filename mimeType := http.DetectContentType(data) @@ -126,6 +138,11 @@ func (s *httpStorage) Read(c *models.ReqContext) response.Response { if err != nil { return response.Error(400, "cannot call read", err) } + + if file == nil || file.Contents == nil { + return response.Error(404, "file does not exist", err) + } + // set the correct content type for svg if strings.HasSuffix(path, ".svg") { c.Resp.Header().Set("Content-Type", "image/svg+xml") @@ -135,17 +152,75 @@ func (s *httpStorage) Read(c *models.ReqContext) response.Response { func (s *httpStorage) Delete(c *models.ReqContext) response.Response { // full path is api/storage/delete/upload/example.jpg, but we only want the part after upload - _, path := getPathAndScope(c) - err := s.store.Delete(c.Req.Context(), c.SignedInUser, "/"+path) + scope, path := getPathAndScope(c) + + err := s.store.Delete(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { - return response.Error(400, "cannot call delete", err) + return response.Error(400, "failed to delete the file: "+err.Error(), err) } - return response.JSON(200, map[string]string{ + return response.JSON(200, map[string]interface{}{ "message": "Removed file from storage", + "success": true, "path": path, }) } +func (s *httpStorage) DeleteFolder(c *models.ReqContext) response.Response { + body, err := io.ReadAll(c.Req.Body) + if err != nil { + return response.Error(500, "error reading bytes", err) + } + + cmd := &DeleteFolderCmd{} + err = json.Unmarshal(body, cmd) + if err != nil { + return response.Error(400, "error parsing body", err) + } + + if cmd.Path == "" { + return response.Error(400, "empty path", err) + } + + // full path is api/storage/delete/upload/example.jpg, but we only want the part after upload + _, path := getPathAndScope(c) + if err := s.store.DeleteFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil { + return response.Error(400, "failed to delete the folder: "+err.Error(), err) + } + + return response.JSON(200, map[string]interface{}{ + "message": "Removed folder from storage", + "success": true, + "path": path, + }) +} + +func (s *httpStorage) CreateFolder(c *models.ReqContext) response.Response { + body, err := io.ReadAll(c.Req.Body) + if err != nil { + return response.Error(500, "error reading bytes", err) + } + + cmd := &CreateFolderCmd{} + err = json.Unmarshal(body, cmd) + if err != nil { + return response.Error(400, "error parsing body", err) + } + + if cmd.Path == "" { + return response.Error(400, "empty path", err) + } + + if err := s.store.CreateFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil { + return response.Error(400, "failed to create the folder: "+err.Error(), err) + } + + return response.JSON(200, map[string]interface{}{ + "message": "Folder created", + "success": true, + "path": cmd.Path, + }) +} + func (s *httpStorage) List(c *models.ReqContext) response.Response { params := web.Params(c.Req) path := params["*"] diff --git a/pkg/services/store/sanitize.go b/pkg/services/store/sanitize.go index 6634b77b7f5..df19e9ceaee 100644 --- a/pkg/services/store/sanitize.go +++ b/pkg/services/store/sanitize.go @@ -6,20 +6,44 @@ import ( "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/store/sanitizer" ) -func (s *standardStorageService) sanitizeUploadRequest(ctx context.Context, user *models.SignedInUser, req *UploadRequest, storagePath string) (*filestorage.UpsertFileCommand, error) { +func (s *standardStorageService) sanitizeContents(ctx context.Context, user *models.SignedInUser, req *UploadRequest, storagePath string) ([]byte, error) { if req.EntityType == EntityTypeImage { ext := filepath.Ext(req.Path) - //nolint: staticcheck if ext == ".svg" { - // TODO: sanitize svg + resp, err := sanitizer.SanitizeSVG(ctx, &rendering.SanitizeSVGRequest{ + Filename: storagePath, + Content: req.Contents, + }) + if err != nil { + if s.cfg.allowUnsanitizedSvgUpload { + grafanaStorageLogger.Debug("allowing unsanitized svg upload", "filename", req.Path, "sanitizationError", err) + return req.Contents, nil + } else { + grafanaStorageLogger.Debug("disallowing unsanitized svg upload", "filename", req.Path, "sanitizationError", err) + return nil, err + } + } + + return resp.Sanitized, nil } } + return req.Contents, nil +} + +func (s *standardStorageService) sanitizeUploadRequest(ctx context.Context, user *models.SignedInUser, req *UploadRequest, storagePath string) (*filestorage.UpsertFileCommand, error) { + contents, err := s.sanitizeContents(ctx, user, req, storagePath) + if err != nil { + return nil, err + } + return &filestorage.UpsertFileCommand{ Path: storagePath, - Contents: req.Contents, + Contents: contents, MimeType: req.MimeType, CacheControl: req.CacheControl, ContentDisposition: req.ContentDisposition, diff --git a/pkg/services/store/sanitizer/Provider.go b/pkg/services/store/sanitizer/Provider.go new file mode 100644 index 00000000000..9830b05c200 --- /dev/null +++ b/pkg/services/store/sanitizer/Provider.go @@ -0,0 +1,23 @@ +package sanitizer + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/services/rendering" +) + +// workaround for cyclic dep between the store and the renderer + +type Provider struct{} + +var SanitizeSVG = func(ctx context.Context, req *rendering.SanitizeSVGRequest) (*rendering.SanitizeSVGResponse, error) { + return nil, errors.New("not implemented") +} + +func ProvideService( + renderer rendering.Service, +) *Provider { + SanitizeSVG = renderer.SanitizeSVG + return &Provider{} +} diff --git a/pkg/services/store/service.go b/pkg/services/store/service.go index be879f00133..e096e8c89af 100644 --- a/pkg/services/store/service.go +++ b/pkg/services/store/service.go @@ -19,7 +19,7 @@ import ( var grafanaStorageLogger = log.New("grafanaStorageLogger") var ErrUploadFeatureDisabled = errors.New("upload feature is disabled") -var ErrUnsupportedStorage = errors.New("storage does not support upload operation") +var ErrUnsupportedStorage = errors.New("storage does not support this operation") var ErrUploadInternalError = errors.New("upload internal error") var ErrValidationFailed = errors.New("request validation failed") var ErrFileAlreadyExists = errors.New("file exists") @@ -29,6 +29,15 @@ const RootResources = "resources" const MAX_UPLOAD_SIZE = 3 * 1024 * 1024 // 3MB +type DeleteFolderCmd struct { + Path string `json:"path"` + Force bool `json:"force"` +} + +type CreateFolderCmd struct { + Path string `json:"path"` +} + type StorageService interface { registry.BackgroundService @@ -42,15 +51,24 @@ type StorageService interface { Delete(ctx context.Context, user *models.SignedInUser, path string) error + DeleteFolder(ctx context.Context, user *models.SignedInUser, cmd *DeleteFolderCmd) error + + CreateFolder(ctx context.Context, user *models.SignedInUser, cmd *CreateFolderCmd) error + validateUploadRequest(ctx context.Context, user *models.SignedInUser, req *UploadRequest, storagePath string) validationResult // sanitizeUploadRequest sanitizes the upload request and converts it into a command accepted by the FileStorage API sanitizeUploadRequest(ctx context.Context, user *models.SignedInUser, req *UploadRequest, storagePath string) (*filestorage.UpsertFileCommand, error) } +type storageServiceConfig struct { + allowUnsanitizedSvgUpload bool +} + type standardStorageService struct { sql *sqlstore.SQLStore tree *nestedTree + cfg storageServiceConfig } func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, cfg *setting.Cfg) StorageService { @@ -81,12 +99,10 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, return storages } - s := newStandardStorageService(globalRoots, initializeOrgStorages) - s.sql = sql - return s + return newStandardStorageService(sql, globalRoots, initializeOrgStorages) } -func newStandardStorageService(globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime) *standardStorageService { +func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime) *standardStorageService { rootsByOrgId := make(map[int64][]storageRuntime) rootsByOrgId[ac.GlobalOrgID] = globalRoots @@ -96,7 +112,11 @@ func newStandardStorageService(globalRoots []storageRuntime, initializeOrgStorag } res.init() return &standardStorageService{ + sql: sql, tree: res, + cfg: storageServiceConfig{ + allowUnsanitizedSvgUpload: false, + }, } } @@ -135,13 +155,18 @@ type UploadRequest struct { OverwriteExistingFile bool } +func storageSupportsMutatingOperations(path string) bool { + // TODO: this is temporary - make it rbac-driven + return strings.HasPrefix(path, RootResources+"/") || path == RootResources +} + func (s *standardStorageService) Upload(ctx context.Context, user *models.SignedInUser, req *UploadRequest) error { upload, _ := s.tree.getRoot(getOrgId(user), RootResources) if upload == nil { return ErrUploadFeatureDisabled } - if !strings.HasPrefix(req.Path, RootResources+"/") { + if !storageSupportsMutatingOperations(req.Path) { return ErrUnsupportedStorage } @@ -180,12 +205,53 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed return nil } -func (s *standardStorageService) Delete(ctx context.Context, user *models.SignedInUser, path string) error { - upload, _ := s.tree.getRoot(getOrgId(user), RootResources) - if upload == nil { - return fmt.Errorf("upload feature is not enabled") +func (s *standardStorageService) DeleteFolder(ctx context.Context, user *models.SignedInUser, cmd *DeleteFolderCmd) error { + resources, _ := s.tree.getRoot(getOrgId(user), RootResources) + if resources == nil { + return fmt.Errorf("resources storage is not enabled") } - err := upload.Delete(ctx, path) + + if !storageSupportsMutatingOperations(cmd.Path) { + return ErrUnsupportedStorage + } + + storagePath := strings.TrimPrefix(cmd.Path, RootResources) + if storagePath == "" { + storagePath = filestorage.Delimiter + } + return resources.DeleteFolder(ctx, storagePath, &filestorage.DeleteFolderOptions{Force: true}) +} + +func (s *standardStorageService) CreateFolder(ctx context.Context, user *models.SignedInUser, cmd *CreateFolderCmd) error { + if !storageSupportsMutatingOperations(cmd.Path) { + return ErrUnsupportedStorage + } + + resources, _ := s.tree.getRoot(getOrgId(user), RootResources) + if resources == nil { + return fmt.Errorf("resources storage is not enabled") + } + + storagePath := strings.TrimPrefix(cmd.Path, RootResources) + err := resources.CreateFolder(ctx, storagePath) + if err != nil { + return err + } + return nil +} + +func (s *standardStorageService) Delete(ctx context.Context, user *models.SignedInUser, path string) error { + if !storageSupportsMutatingOperations(path) { + return ErrUnsupportedStorage + } + + resources, _ := s.tree.getRoot(getOrgId(user), RootResources) + if resources == nil { + return fmt.Errorf("resources storage is not enabled") + } + + storagePath := strings.TrimPrefix(path, RootResources) + err := resources.Delete(ctx, storagePath) if err != nil { return err } diff --git a/pkg/services/store/service_test.go b/pkg/services/store/service_test.go index 9dba7282639..9c705621512 100644 --- a/pkg/services/store/service_test.go +++ b/pkg/services/store/service_test.go @@ -3,16 +3,15 @@ package store import ( "bytes" "context" - "os" "path/filepath" "testing" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/testdatasource" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -37,7 +36,7 @@ func TestListFiles(t *testing.T) { }).setReadOnly(true).setBuiltin(true), } - store := newStandardStorageService(roots, func(orgId int64) []storageRuntime { + store := newStandardStorageService(sqlstore.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) }) frame, err := store.List(context.Background(), dummyUser, "public/testdata") @@ -54,18 +53,75 @@ func TestListFiles(t *testing.T) { experimental.CheckGoldenJSONFrame(t, "testdata", "public_testdata_js_libraries.golden", testDsFrame, true) } -func TestUpload(t *testing.T) { - features := featuremgmt.WithFeatures(featuremgmt.FlagStorageLocalUpload) - path, err := os.Getwd() - require.NoError(t, err) - cfg := &setting.Cfg{AppURL: "http://localhost:3000/", DataPath: path} - s := ProvideService(sqlstore.InitTestDB(t), features, cfg) - request := UploadRequest{ +func setupUploadStore(t *testing.T) (StorageService, *filestorage.MockFileStorage, string) { + t.Helper() + storageName := "resources" + mockStorage := &filestorage.MockFileStorage{} + sqlStorage := newSQLStorage(storageName, "Testing upload", &StorageSQLConfig{orgId: 1}, sqlstore.InitTestDB(t)) + sqlStorage.store = mockStorage + + store := newStandardStorageService(sqlstore.InitTestDB(t), []storageRuntime{sqlStorage}, func(orgId int64) []storageRuntime { + return make([]storageRuntime, 0) + }) + + return store, mockStorage, storageName +} + +func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) { + service, mockStorage, storageName := setupUploadStore(t) + + mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(nil, nil) + mockStorage.On("Upsert", mock.Anything, mock.Anything).Return(nil) + + err := service.Upload(context.Background(), dummyUser, &UploadRequest{ EntityType: EntityTypeImage, Contents: make([]byte, 0), - Path: "resources/myFile.jpg", + Path: storageName + "/myFile.jpg", MimeType: "image/jpg", - } - err = s.Upload(context.Background(), dummyUser, &request) + }) + require.NoError(t, err) +} + +func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) { + service, mockStorage, storageName := setupUploadStore(t) + + mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(&filestorage.File{Contents: make([]byte, 0)}, nil) + + err := service.Upload(context.Background(), dummyUser, &UploadRequest{ + EntityType: EntityTypeImage, + Contents: make([]byte, 0), + Path: storageName + "/myFile.jpg", + MimeType: "image/jpg", + }) + require.ErrorIs(t, err, ErrFileAlreadyExists) +} + +func TestShouldDelegateFileDeletion(t *testing.T) { + service, mockStorage, storageName := setupUploadStore(t) + + mockStorage.On("Delete", mock.Anything, "/myFile.jpg").Return(nil) + + err := service.Delete(context.Background(), dummyUser, storageName+"/myFile.jpg") + require.NoError(t, err) +} + +func TestShouldDelegateFolderCreation(t *testing.T) { + service, mockStorage, storageName := setupUploadStore(t) + + mockStorage.On("CreateFolder", mock.Anything, "/nestedFolder/mostNestedFolder").Return(nil) + + err := service.CreateFolder(context.Background(), dummyUser, &CreateFolderCmd{Path: storageName + "/nestedFolder/mostNestedFolder"}) + require.NoError(t, err) +} + +func TestShouldDelegateFolderDeletion(t *testing.T) { + service, mockStorage, storageName := setupUploadStore(t) + + mockStorage.On("DeleteFolder", mock.Anything, "/", &filestorage.DeleteFolderOptions{Force: true}).Return(nil) + + err := service.DeleteFolder(context.Background(), dummyUser, &DeleteFolderCmd{ + Path: storageName, + Force: true, + }) require.NoError(t, err) } diff --git a/pkg/services/store/utils.go b/pkg/services/store/utils.go index 95777ace5e0..70f47349f39 100644 --- a/pkg/services/store/utils.go +++ b/pkg/services/store/utils.go @@ -1,7 +1,6 @@ package store import ( - "path/filepath" "strings" "github.com/grafana/grafana/pkg/models" @@ -27,5 +26,5 @@ func getPathAndScope(c *models.ReqContext) (string, string) { if path == "" { return "", "" } - return splitFirstSegment(filepath.Clean(path)) + return splitFirstSegment(path) } diff --git a/pkg/services/store/validate.go b/pkg/services/store/validate.go index b067e0216fd..7ad7cf391bf 100644 --- a/pkg/services/store/validate.go +++ b/pkg/services/store/validate.go @@ -13,6 +13,7 @@ var ( allowedImageExtensions = map[string]bool{ ".jpg": true, ".jpeg": true, + ".svg": true, ".gif": true, ".png": true, ".webp": true, @@ -23,6 +24,7 @@ var ( ".gif": {"image/gif": true}, ".png": {"image/png": true}, ".webp": {"image/webp": true}, + ".svg": {"text/xml; charset=utf-8": true, "text/plain; charset=utf-8": true, "image/svg+xml": true}, } ) @@ -68,7 +70,7 @@ func (s *standardStorageService) validateUploadRequest(ctx context.Context, user // TODO: validateProperties if err := filestorage.ValidatePath(storagePath); err != nil { - return fail("path validation failed: " + err.Error()) + return fail("path validation failed. error:" + err.Error() + ". path: " + storagePath) } switch req.EntityType { diff --git a/pkg/tsdb/cloudwatch/annotation_query.go b/pkg/tsdb/cloudwatch/annotation_query.go index 148f1167dbc..5a439f5cbcd 100644 --- a/pkg/tsdb/cloudwatch/annotation_query.go +++ b/pkg/tsdb/cloudwatch/annotation_query.go @@ -3,13 +3,13 @@ package cloudwatch import ( "errors" "fmt" + "strconv" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" ) type annotationEvent struct { @@ -19,29 +19,37 @@ type annotationEvent struct { Text string } -func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginContext, model *simplejson.Json, query backend.DataQuery) (*backend.QueryDataResponse, error) { +func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginContext, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() + statistic := "" - usePrefixMatch := model.Get("prefixMatching").MustBool(false) - region := model.Get("region").MustString("") - namespace := model.Get("namespace").MustString("") - metricName := model.Get("metricName").MustString("") - dimensions := model.Get("dimensions").MustMap() - statistic := model.Get("statistic").MustString() - period := int64(model.Get("period").MustInt(0)) - if period == 0 && !usePrefixMatch { + if model.Statistic != nil { + statistic = *model.Statistic + } + + var period int64 + if model.Period != "" { + p, err := strconv.ParseInt(model.Period, 10, 64) + if err != nil { + return nil, err + } + period = p + } + + if period == 0 && !model.PrefixMatching { period = 300 } - actionPrefix := model.Get("actionPrefix").MustString("") - alarmNamePrefix := model.Get("alarmNamePrefix").MustString("") - cli, err := e.getCWClient(pluginCtx, region) + actionPrefix := model.ActionPrefix + alarmNamePrefix := model.AlarmNamePrefix + + cli, err := e.getCWClient(pluginCtx, model.Region) if err != nil { return nil, err } var alarmNames []*string - if usePrefixMatch { + if model.PrefixMatching { params := &cloudwatch.DescribeAlarmsInput{ MaxRecords: aws.Int64(100), ActionPrefix: aws.String(actionPrefix), @@ -51,14 +59,14 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont if err != nil { return nil, fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarms", err) } - alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistic, period) + alarmNames = filterAlarms(resp, model.Namespace, model.MetricName, model.Dimensions, statistic, period) } else { - if region == "" || namespace == "" || metricName == "" || statistic == "" { + if model.Region == "" || model.Namespace == "" || model.MetricName == "" || statistic == "" { return result, errors.New("invalid annotations query") } var qd []*cloudwatch.Dimension - for k, v := range dimensions { + for k, v := range model.Dimensions { if vv, ok := v.([]interface{}); ok { for _, vvv := range vv { if vvvv, ok := vvv.(string); ok { @@ -71,8 +79,8 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont } } params := &cloudwatch.DescribeAlarmsForMetricInput{ - Namespace: aws.String(namespace), - MetricName: aws.String(metricName), + Namespace: aws.String(model.Namespace), + MetricName: aws.String(model.MetricName), Dimensions: qd, Statistic: aws.String(statistic), Period: aws.Int64(period), diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 10030172b06..1130d68a785 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -49,6 +48,20 @@ type datasourceInfo struct { HTTPClient *http.Client } +type DataQueryJson struct { + QueryType string `json:"type,omitempty"` + QueryMode string + PrefixMatching bool + Region string + Namespace string + MetricName string + Dimensions map[string]interface{} + Statistic *string + Period string + ActionPrefix string + AlarmNamePrefix string +} + const ( cloudWatchTSFormat = "2006-01-02 15:04:05.000" defaultRegion = "default" @@ -59,10 +72,12 @@ const ( alertMaxAttempts = 8 alertPollPeriod = 1000 * time.Millisecond + logsQueryMode = "Logs" ) var plog = log.New("tsdb.cloudwatch") var aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) +var baseLimit = int64(1) func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles) *CloudWatchService { plog.Debug("initing") @@ -188,7 +203,12 @@ func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx back if err != nil { return err } - _, err = e.handleDescribeLogGroups(ctx, logsClient, simplejson.NewFromAny(map[string]interface{}{"limit": "1"})) + + parameters := LogQueryJson{ + Limit: &baseLimit, + } + + _, err = e.handleDescribeLogGroups(ctx, logsClient, parameters) return err } @@ -282,16 +302,16 @@ func (e *cloudWatchExecutor) getRGTAClient(pluginCtx backend.PluginContext, regi } func (e *cloudWatchExecutor) alertQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - queryContext backend.DataQuery, model *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) { + queryContext backend.DataQuery, model LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) { startQueryOutput, err := e.executeStartQuery(ctx, logsClient, model, queryContext.TimeRange) if err != nil { return nil, err } - requestParams := simplejson.NewFromAny(map[string]interface{}{ - "region": model.Get("region").MustString(""), - "queryId": *startQueryOutput.QueryId, - }) + requestParams := LogQueryJson{ + Region: model.Region, + QueryId: *startQueryOutput.QueryId, + } ticker := time.NewTicker(alertPollPeriod) defer ticker.Stop() @@ -324,18 +344,19 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa frontend, but because alerts are executed on the backend the logic needs to be reimplemented here. */ q := req.Queries[0] - model, err := simplejson.NewJson(q.JSON) + var model DataQueryJson + err := json.Unmarshal(q.JSON, &model) if err != nil { return nil, err } _, fromAlert := req.Headers["FromAlert"] - isLogAlertQuery := fromAlert && model.Get("queryMode").MustString("") == "Logs" + isLogAlertQuery := fromAlert && model.QueryMode == logsQueryMode if isLogAlertQuery { return e.executeLogAlertQuery(ctx, req) } - queryType := model.Get("type").MustString("") + queryType := model.QueryType var result *backend.QueryDataResponse switch queryType { @@ -356,21 +377,22 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back resp := backend.NewQueryDataResponse() for _, q := range req.Queries { - model, err := simplejson.NewJson(q.JSON) + var model LogQueryJson + err := json.Unmarshal(q.JSON, &model) if err != nil { continue } - model.Set("subtype", "StartQuery") - model.Set("queryString", model.Get("expression").MustString("")) + model.Subtype = "StartQuery" + model.QueryString = model.Expression - region := model.Get("region").MustString(defaultRegion) - if region == defaultRegion { + region := model.Region + if model.Region == "" || region == defaultRegion { dsInfo, err := e.getDSInfo(req.PluginContext) if err != nil { return nil, err } - model.Set("region", dsInfo.region) + model.Region = dsInfo.region } logsClient, err := e.getCWLogsClient(req.PluginContext, region) @@ -389,10 +411,8 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back } var frames []*data.Frame - - statsGroups := model.Get("statsGroups").MustStringArray() - if len(statsGroups) > 0 && len(dataframe.Fields) > 0 { - frames, err = groupResults(dataframe, statsGroups) + if len(model.StatsGroups) > 0 && len(dataframe.Fields) > 0 { + frames, err = groupResults(dataframe, model.StatsGroups) if err != nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index 3b6e1a60fee..0e62c097e73 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -2,6 +2,7 @@ package cloudwatch import ( "context" + "encoding/json" "errors" "fmt" "math" @@ -13,13 +14,13 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "golang.org/x/sync/errgroup" ) const ( - LimitExceededException = "LimitExceededException" - defaultLimit = 10 + limitExceededException = "LimitExceededException" + defaultLimit = int64(10) + logGroupDefaultLimit = int64(50) ) type AWSError struct { @@ -28,6 +29,26 @@ type AWSError struct { Payload map[string]string } +type LogQueryJson struct { + LogType string `json:"type"` + SubType string + Limit *int64 + Time int64 + StartTime int64 + EndTime int64 + LogGroupName string + LogGroupNames []string + LogGroupNamePrefix string + LogStreamName string + StartFromHead bool + Region string + QueryString string + QueryId string + StatsGroups []string + Subtype string + Expression string +} + func (e *AWSError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } @@ -39,7 +60,8 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend eg, ectx := errgroup.WithContext(ctx) for _, query := range req.Queries { - model, err := simplejson.NewJson(query.JSON) + var model LogQueryJson + err := json.Unmarshal(query.JSON, &model) if err != nil { return nil, err } @@ -58,7 +80,7 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend return err } - groupedFrames, err := groupResponseFrame(dataframe, model.Get("statsGroups").MustStringArray()) + groupedFrames, err := groupResponseFrame(dataframe, model.StatsGroups) if err != nil { return err } @@ -86,25 +108,24 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend return resp, nil } -func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model *simplejson.Json, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) { - subType := model.Get("subtype").MustString() - +func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model LogQueryJson, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) { dsInfo, err := e.getDSInfo(pluginCtx) if err != nil { return nil, err } - defaultRegion := dsInfo.region + region := dsInfo.region + if model.Region != "" { + region = model.Region + } - region := model.Get("region").MustString(defaultRegion) logsClient, err := e.getCWLogsClient(pluginCtx, region) if err != nil { return nil, err } var data *data.Frame = nil - - switch subType { + switch model.SubType { case "DescribeLogGroups": data, err = e.handleDescribeLogGroups(ctx, logsClient, model) case "GetLogGroupFields": @@ -119,38 +140,36 @@ func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model *simple data, err = e.handleGetLogEvents(ctx, logsClient, model) } if err != nil { - return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", subType, err) + return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", model.SubType, err) } return data, nil } func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json) (*data.Frame, error) { - queryRequest := &cloudwatchlogs.GetLogEventsInput{ - Limit: aws.Int64(parameters.Get("limit").MustInt64(defaultLimit)), - StartFromHead: aws.Bool(parameters.Get("startFromHead").MustBool(false)), + parameters LogQueryJson) (*data.Frame, error) { + limit := defaultLimit + if parameters.Limit != nil && *parameters.Limit > 0 { + limit = *parameters.Limit } - logGroupName, err := parameters.Get("logGroupName").String() - if err != nil { + queryRequest := &cloudwatchlogs.GetLogEventsInput{ + Limit: aws.Int64(limit), + StartFromHead: aws.Bool(parameters.StartFromHead), + } + + if parameters.LogGroupName == "" { return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required") } - queryRequest.SetLogGroupName(logGroupName) + queryRequest.SetLogGroupName(parameters.LogGroupName) - logStreamName, err := parameters.Get("logStreamName").String() - if err != nil { - return nil, fmt.Errorf("Error: Parameter 'logStream' is required") + if parameters.LogStreamName == "" { + return nil, fmt.Errorf("Error: Parameter 'logStreamName' is required") } - queryRequest.SetLogStreamName(logStreamName) + queryRequest.SetLogStreamName(parameters.LogStreamName) - if startTime, err := parameters.Get("startTime").Int64(); err == nil { - queryRequest.SetStartTime(startTime) - } - - if endTime, err := parameters.Get("endTime").Int64(); err == nil { - queryRequest.SetEndTime(endTime) - } + queryRequest.SetStartTime(parameters.StartTime) + queryRequest.SetEndTime(parameters.EndTime) logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest) if err != nil { @@ -178,19 +197,22 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient } func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context, - logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) { - logGroupNamePrefix := parameters.Get("logGroupNamePrefix").MustString("") + logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters LogQueryJson) (*data.Frame, error) { + logGroupLimit := logGroupDefaultLimit + if parameters.Limit != nil && *parameters.Limit != 0 { + logGroupLimit = *parameters.Limit + } var response *cloudwatchlogs.DescribeLogGroupsOutput = nil var err error - if len(logGroupNamePrefix) == 0 { + if len(parameters.LogGroupNamePrefix) == 0 { response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ - Limit: aws.Int64(parameters.Get("limit").MustInt64(50)), + Limit: aws.Int64(logGroupLimit), }) } else { response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ - Limit: aws.Int64(parameters.Get("limit").MustInt64(50)), - LogGroupNamePrefix: aws.String(logGroupNamePrefix), + Limit: aws.Int64(logGroupLimit), + LogGroupNamePrefix: aws.String(parameters.LogGroupNamePrefix), }) } if err != nil || response == nil { @@ -209,7 +231,7 @@ func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context, } func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) { + parameters LogQueryJson, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) { startTime := timeRange.From endTime := timeRange.To @@ -222,7 +244,7 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c // The usage of ltrim around the @log/@logStream fields is a necessary workaround, as without it, // CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid. modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " + - logStreamIdentifierInternal + "|" + parameters.Get("queryString").MustString("") + logStreamIdentifierInternal + "|" + parameters.QueryString startQueryInput := &cloudwatchlogs.StartQueryInput{ StartTime: aws.Int64(startTime.Unix()), @@ -232,25 +254,25 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c // and also a little bit more but as CW logs accept only seconds as integers there is not much to do about // that. EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))), - LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()), + LogGroupNames: aws.StringSlice(parameters.LogGroupNames), QueryString: aws.String(modifiedQueryString), } - if resultsLimit, err := parameters.Get("limit").Int64(); err == nil { - startQueryInput.Limit = aws.Int64(resultsLimit) + if parameters.Limit != nil { + startQueryInput.Limit = aws.Int64(*parameters.Limit) } return logsClient.StartQueryWithContext(ctx, startQueryInput) } func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - model *simplejson.Json, timeRange backend.TimeRange, refID string) (*data.Frame, error) { + model LogQueryJson, timeRange backend.TimeRange, refID string) (*data.Frame, error) { startQueryResponse, err := e.executeStartQuery(ctx, logsClient, model, timeRange) if err != nil { var awsErr awserr.Error if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" { plog.Debug("executeStartQuery limit exceeded", "err", awsErr) - return nil, &AWSError{Code: LimitExceededException, Message: err.Error()} + return nil, &AWSError{Code: limitExceededException, Message: err.Error()} } return nil, err } @@ -258,11 +280,14 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl dataFrame := data.NewFrame(refID, data.NewField("queryId", nil, []string{*startQueryResponse.QueryId})) dataFrame.RefID = refID - clientRegion := model.Get("region").MustString("default") + region := "default" + if model.Region != "" { + region = model.Region + } dataFrame.Meta = &data.FrameMeta{ Custom: map[string]interface{}{ - "Region": clientRegion, + "Region": region, }, } @@ -270,9 +295,9 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl } func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json) (*cloudwatchlogs.StopQueryOutput, error) { + parameters LogQueryJson) (*cloudwatchlogs.StopQueryOutput, error) { queryInput := &cloudwatchlogs.StopQueryInput{ - QueryId: aws.String(parameters.Get("queryId").MustString()), + QueryId: aws.String(parameters.QueryId), } response, err := logsClient.StopQueryWithContext(ctx, queryInput) @@ -291,7 +316,7 @@ func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cl } func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json) (*data.Frame, error) { + parameters LogQueryJson) (*data.Frame, error) { response, err := e.executeStopQuery(ctx, logsClient, parameters) if err != nil { return nil, err @@ -302,16 +327,16 @@ func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient clo } func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) { + parameters LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) { queryInput := &cloudwatchlogs.GetQueryResultsInput{ - QueryId: aws.String(parameters.Get("queryId").MustString()), + QueryId: aws.String(parameters.QueryId), } return logsClient.GetQueryResultsWithContext(ctx, queryInput) } func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json, refID string) (*data.Frame, error) { + parameters LogQueryJson, refID string) (*data.Frame, error) { getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters) if err != nil { return nil, err @@ -329,10 +354,10 @@ func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClie } func (e *cloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters *simplejson.Json, refID string) (*data.Frame, error) { + parameters LogQueryJson, refID string) (*data.Frame, error) { queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{ - LogGroupName: aws.String(parameters.Get("logGroupName").MustString()), - Time: aws.Int64(parameters.Get("time").MustInt64()), + LogGroupName: aws.String(parameters.LogGroupName), + Time: aws.Int64(parameters.Time), } getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput) diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 709338a5d1d..e94f062644e 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -406,33 +406,6 @@ func Test_executeStartQuery(t *testing.T) { }, cli.calls.startQueryWithContext) }) - t.Run("cannot parse limit as float", func(t *testing.T) { - cli = fakeCWLogsClient{} - im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return datasourceInfo{}, nil - }) - executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) - - _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, - Queries: []backend.DataQuery{ - { - RefID: "A", - TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, - JSON: json.RawMessage(`{ - "type": "logAction", - "subtype": "StartQuery", - "limit": 12.0 - }`), - }, - }, - }) - - assert.NoError(t, err) - require.Len(t, cli.calls.startQueryWithContext, 1) - assert.Nil(t, cli.calls.startQueryWithContext[0].Limit) - }) - t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { diff --git a/pkg/tsdb/cloudwatch/request_parser.go b/pkg/tsdb/cloudwatch/request_parser.go index 6ec134a92da..39d6b651555 100644 --- a/pkg/tsdb/cloudwatch/request_parser.go +++ b/pkg/tsdb/cloudwatch/request_parser.go @@ -1,6 +1,7 @@ package cloudwatch import ( + "encoding/json" "errors" "fmt" "math" @@ -12,32 +13,60 @@ import ( "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/featuremgmt" ) var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`) +type QueryJson struct { + Datasource map[string]string `json:"datasource,omitempty"` + Dimensions map[string]interface{} `json:"dimensions,omitempty"` + Expression string `json:"expression,omitempty"` + Id string `json:"id,omitempty"` + Label *string `json:"label,omitempty"` + MatchExact *bool `json:"matchExact,omitempty"` + MaxDataPoints int `json:"maxDataPoints,omitempty"` + MetricEditorMode *int `json:"metricEditorMode,omitempty"` + MetricName string `json:"metricName,omitempty"` + MetricQueryType metricQueryType `json:"metricQueryType,omitempty"` + Namespace string `json:"namespace,omitempty"` + Period string `json:"period,omitempty"` + RefId string `json:"refId,omitempty"` + Region string `json:"region,omitempty"` + SqlExpression string `json:"sqlExpression,omitempty"` + Statistic *string `json:"statistic,omitempty"` + Statistics []*string `json:"statistics,omitempty"` + TimezoneUTCOffset string `json:"timezoneUTCOffset,omitempty"` + QueryType string `json:"queryType,omitempty"` + Hide *bool `json:"hide,omitempty"` + Alias *string `json:"alias,omitempty"` +} + // parseQueries parses the json queries and returns a map of cloudWatchQueries by region. The cloudWatchQuery has a 1 to 1 mapping to a query editor row func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time) (map[string][]*cloudWatchQuery, error) { requestQueries := make(map[string][]*cloudWatchQuery) - migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels)) if err != nil { return nil, err } for _, query := range migratedQueries { - model, err := simplejson.NewJson(query.JSON) + var model QueryJson + err := json.Unmarshal(query.JSON, &model) if err != nil { return nil, &queryError{err: err, RefID: query.RefID} } - queryType := model.Get("type").MustString() + queryType := model.QueryType if queryType != "timeSeriesQuery" && queryType != "" { continue } + if model.MatchExact == nil { + trueBooleanValue := true + model.MatchExact = &trueBooleanValue + } + refID := query.RefID query, err := parseRequestQuery(model, refID, startTime, endTime) if err != nil { @@ -58,7 +87,8 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool) migratedQueries := []*backend.DataQuery{} for _, q := range queries { query := q - queryJson, err := simplejson.NewJson(query.JSON) + var queryJson *QueryJson + err := json.Unmarshal(query.JSON, &queryJson) if err != nil { return nil, err } @@ -67,12 +97,10 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool) return nil, err } - _, labelExists := queryJson.CheckGet("label") - if !labelExists && dynamicLabelsEnabled { + if queryJson.Label == nil && dynamicLabelsEnabled { migrateAliasToDynamicLabel(queryJson) } - - query.JSON, err = queryJson.MarshalJSON() + query.JSON, err = json.Marshal(queryJson) if err != nil { return nil, err } @@ -86,16 +114,15 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool) // migrateStatisticsToStatistic migrates queries that has a `statistics` field to use the `statistic` field instead. // In case the query used more than one stat, the first stat in the slice will be used in the statistic field // Read more here https://github.com/grafana/grafana/issues/30629 -func migrateStatisticsToStatistic(queryJson *simplejson.Json) error { - _, err := queryJson.Get("statistic").String() +func migrateStatisticsToStatistic(queryJson *QueryJson) error { // If there's not a statistic property in the json, we know it's the legacy format and then it has to be migrated - if err != nil { - stats, err := queryJson.Get("statistics").StringArray() - if err != nil { + if queryJson.Statistic == nil { + if queryJson.Statistics == nil { return fmt.Errorf("query must have either statistic or statistics field") } - queryJson.Del("statistics") - queryJson.Set("statistic", stats[0]) + + queryJson.Statistic = queryJson.Statistics[0] + queryJson.Statistics = nil } return nil @@ -112,10 +139,13 @@ var aliasPatterns = map[string]string{ var legacyAliasRegexp = regexp.MustCompile(`{{\s*(.+?)\s*}}`) -func migrateAliasToDynamicLabel(queryJson *simplejson.Json) { - fullAliasField := queryJson.Get("alias").MustString() - if fullAliasField != "" { - matches := legacyAliasRegexp.FindAllStringSubmatch(fullAliasField, -1) +func migrateAliasToDynamicLabel(queryJson *QueryJson) { + fullAliasField := "" + + if queryJson.Alias != nil && *queryJson.Alias != "" { + matches := legacyAliasRegexp.FindAllStringSubmatch(*queryJson.Alias, -1) + fullAliasField = *queryJson.Alias + for _, groups := range matches { fullMatch := groups[0] subgroup := groups[1] @@ -126,36 +156,36 @@ func migrateAliasToDynamicLabel(queryJson *simplejson.Json) { } } } - - queryJson.Set("label", fullAliasField) + queryJson.Label = &fullAliasField } -func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) { +func parseRequestQuery(model QueryJson, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) { plog.Debug("Parsing request query", "query", model) + cloudWatchQuery := cloudWatchQuery{ + Alias: "", + Label: "", + MatchExact: true, + Statistic: "", + ReturnData: false, + UsedExpression: "", + RefId: refId, + Id: model.Id, + Region: model.Region, + Namespace: model.Namespace, + MetricName: model.MetricName, + MetricQueryType: model.MetricQueryType, + SqlExpression: model.SqlExpression, + TimezoneUTCOffset: model.TimezoneUTCOffset, + Expression: model.Expression, + } reNumber := regexp.MustCompile(`^\d+$`) - region, err := model.Get("region").String() - if err != nil { - return nil, err - } - namespace, err := model.Get("namespace").String() - if err != nil { - return nil, fmt.Errorf("failed to get namespace: %v", err) - } - metricName, err := model.Get("metricName").String() - if err != nil { - return nil, fmt.Errorf("failed to get metricName: %v", err) - } - dimensions, err := parseDimensions(model) + dimensions, err := parseDimensions(model.Dimensions) if err != nil { return nil, fmt.Errorf("failed to parse dimensions: %v", err) } + cloudWatchQuery.Dimensions = dimensions - statistic, err := model.Get("statistic").String() - if err != nil { - return nil, fmt.Errorf("failed to parse statistic: %v", err) - } - - p := model.Get("period").MustString("") + p := model.Period var period int if strings.ToLower(p) == "auto" || p == "" { deltaInSeconds := endTime.Sub(startTime).Seconds() @@ -182,9 +212,9 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time period = int(d.Seconds()) } } + cloudWatchQuery.Period = period - id := model.Get("id").MustString("") - if id == "" { + if model.Id == "" { // Why not just use refId if id is not specified in the frontend? When specifying an id in the editor, // and alphabetical must be used. The id must be unique, so if an id like for example a, b or c would be used, // it would likely collide with some ref id. That's why the `query` prefix is used. @@ -193,55 +223,48 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time uuid := uuid.NewString() suffix = strings.Replace(uuid, "-", "", -1) } - id = fmt.Sprintf("query%s", suffix) + cloudWatchQuery.Id = fmt.Sprintf("query%s", suffix) } - expression := model.Get("expression").MustString("") - sqlExpression := model.Get("sqlExpression").MustString("") - alias := model.Get("alias").MustString() - label := model.Get("label").MustString() - returnData := !model.Get("hide").MustBool(false) - queryType := model.Get("type").MustString() - timezoneUTCOffset := model.Get("timezoneUTCOffset").MustString("") - if queryType == "" { + if model.Hide != nil { + cloudWatchQuery.ReturnData = !*model.Hide + } + + if model.QueryType == "" { // If no type is provided we assume we are called by alerting service, which requires to return data! // Note, this is sort of a hack, but the official Grafana interfaces do not carry the information // who (which service) called the TsdbQueryEndpoint.Query(...) function. - returnData = true + cloudWatchQuery.ReturnData = true } - matchExact := model.Get("matchExact").MustBool(true) - metricQueryType := metricQueryType(model.Get("metricQueryType").MustInt(0)) - - var metricEditorModeValue metricEditorMode - memv, err := model.Get("metricEditorMode").Int() - if err != nil && len(expression) > 0 { + if model.MetricEditorMode == nil && len(model.Expression) > 0 { // this should only ever happen if this is an alerting query that has not yet been migrated in the frontend - metricEditorModeValue = MetricEditorModeRaw + cloudWatchQuery.MetricEditorMode = MetricEditorModeRaw } else { - metricEditorModeValue = metricEditorMode(memv) + if model.MetricEditorMode != nil { + cloudWatchQuery.MetricEditorMode = metricEditorMode(*model.MetricEditorMode) + } else { + cloudWatchQuery.MetricEditorMode = metricEditorMode(0) + } } - return &cloudWatchQuery{ - RefId: refId, - Region: region, - Id: id, - Namespace: namespace, - MetricName: metricName, - Statistic: statistic, - Expression: expression, - ReturnData: returnData, - Dimensions: dimensions, - Period: period, - Alias: alias, - Label: label, - MatchExact: matchExact, - UsedExpression: "", - MetricQueryType: metricQueryType, - MetricEditorMode: metricEditorModeValue, - SqlExpression: sqlExpression, - TimezoneUTCOffset: timezoneUTCOffset, - }, nil + if model.Statistic != nil { + cloudWatchQuery.Statistic = *model.Statistic + } + + if model.MatchExact != nil { + cloudWatchQuery.MatchExact = *model.MatchExact + } + + if model.Alias != nil { + cloudWatchQuery.Alias = *model.Alias + } + + if model.Label != nil { + cloudWatchQuery.Label = *model.Label + } + + return &cloudWatchQuery, nil } func getRetainedPeriods(timeSince time.Duration) []int { @@ -257,9 +280,9 @@ func getRetainedPeriods(timeSince time.Duration) []int { } } -func parseDimensions(model *simplejson.Json) (map[string][]string, error) { +func parseDimensions(dimensions map[string]interface{}) (map[string][]string, error) { parsedDimensions := make(map[string][]string) - for k, v := range model.Get("dimensions").MustMap() { + for k, v := range dimensions { // This is for backwards compatibility. Before 6.5 dimensions values were stored as strings and not arrays if value, ok := v.(string); ok { parsedDimensions[k] = []string{value} diff --git a/pkg/tsdb/cloudwatch/request_parser_test.go b/pkg/tsdb/cloudwatch/request_parser_test.go index 65d3ee9edc9..bb28ed205d5 100644 --- a/pkg/tsdb/cloudwatch/request_parser_test.go +++ b/pkg/tsdb/cloudwatch/request_parser_test.go @@ -1,12 +1,12 @@ package cloudwatch import ( + "encoding/json" "fmt" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,32 +37,33 @@ func TestRequestParser(t *testing.T) { migratedQuery := migratedQueries[0] assert.Equal(t, "A", migratedQuery.RefID) - model, err := simplejson.NewJson(migratedQuery.JSON) + var model QueryJson + err = json.Unmarshal(migratedQuery.JSON, &model) require.NoError(t, err) - assert.Equal(t, "Average", model.Get("statistic").MustString()) - res, err := model.Get("statistic").Array() - assert.Error(t, err) - assert.Nil(t, res) + assert.Equal(t, "Average", *model.Statistic) }) }) t.Run("New dimensions structure", func(t *testing.T) { - query := simplejson.NewFromAny(map[string]interface{}{ + fixtureJSON := []byte(`{ "refId": "ref1", "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", "id": "", "expression": "", - "dimensions": map[string]interface{}{ - "InstanceId": []interface{}{"test"}, - "InstanceType": []interface{}{"test2", "test3"}, + "dimensions": { + "InstanceId": ["test"], + "InstanceType": ["test2", "test3"] }, "statistic": "Average", "period": "600", - "hide": false, - }) + "hide": false + }`) + var query QueryJson + err := json.Unmarshal(fixtureJSON, &query) + require.NoError(t, err) res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) assert.Equal(t, "us-east-1", res.Region) @@ -81,21 +82,25 @@ func TestRequestParser(t *testing.T) { }) t.Run("Old dimensions structure (backwards compatibility)", func(t *testing.T) { - query := simplejson.NewFromAny(map[string]interface{}{ + fixtureJSON := []byte(`{ "refId": "ref1", "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", "id": "", "expression": "", - "dimensions": map[string]interface{}{ - "InstanceId": "test", - "InstanceType": "test2", + "dimensions": { + "InstanceId": ["test"], + "InstanceType": ["test2"] }, "statistic": "Average", "period": "600", - "hide": false, - }) + "hide": false + }`) + + var query QueryJson + err := json.Unmarshal(fixtureJSON, &query) + require.NoError(t, err) res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) @@ -115,21 +120,25 @@ func TestRequestParser(t *testing.T) { }) t.Run("Period defined in the editor by the user is being used when time range is short", func(t *testing.T) { - query := simplejson.NewFromAny(map[string]interface{}{ + fixtureJSON := []byte(`{ "refId": "ref1", "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", "id": "", "expression": "", - "dimensions": map[string]interface{}{ - "InstanceId": "test", - "InstanceType": "test2", + "dimensions": { + "InstanceId": ["test"], + "InstanceType": ["test2"] }, "statistic": "Average", - "hide": false, - }) - query.Set("period", "900") + "hide": false + }`) + + var query QueryJson + err := json.Unmarshal(fixtureJSON, &query) + require.NoError(t, err) + query.Period = "900" res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) @@ -137,24 +146,28 @@ func TestRequestParser(t *testing.T) { }) t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) { - query := simplejson.NewFromAny(map[string]interface{}{ + fixtureJSON := []byte(`{ "refId": "ref1", "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", "id": "", "expression": "", - "dimensions": map[string]interface{}{ - "InstanceId": "test", - "InstanceType": "test2", + "dimensions": { + "InstanceId": ["test"], + "InstanceType": ["test2"] }, "statistic": "Average", "hide": false, - "period": "auto", - }) + "period": "auto" + }`) + + var query QueryJson + err := json.Unmarshal(fixtureJSON, &query) + require.NoError(t, err) t.Run("Time range is 5 minutes", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.Local().Add(time.Minute * time.Duration(5)) @@ -164,7 +177,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 1 day", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(0, 0, -1) @@ -174,7 +187,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 2 days", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(0, 0, -2) res, err := parseRequestQuery(query, "ref1", from, to) @@ -183,7 +196,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 7 days", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(0, 0, -7) @@ -193,7 +206,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 30 days", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(0, 0, -30) @@ -203,7 +216,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 90 days", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(0, 0, -90) @@ -213,7 +226,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 1 year", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(-1, 0, 0) @@ -223,7 +236,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 2 years", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now() from := to.AddDate(-2, 0, 0) @@ -233,7 +246,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now().AddDate(0, 0, -14) from := to.AddDate(0, 0, -2) res, err := parseRequestQuery(query, "ref1", from, to) @@ -242,7 +255,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now().AddDate(0, 0, -88) from := to.AddDate(0, 0, -2) res, err := parseRequestQuery(query, "ref1", from, to) @@ -251,7 +264,7 @@ func TestRequestParser(t *testing.T) { }) t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) { - query.Set("period", "auto") + query.Period = "auto" to := time.Now().AddDate(0, 0, -454) from := to.AddDate(0, 0, -2) res, err := parseRequestQuery(query, "ref1", from, to) @@ -273,7 +286,7 @@ func TestRequestParser(t *testing.T) { t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) { query := getBaseJsonQuery() - query.Set("expression", "SUM(a)") + query.Expression = "SUM(a)" res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType) @@ -284,7 +297,7 @@ func TestRequestParser(t *testing.T) { t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) { query := getBaseJsonQuery() - query.Set("expression", "SUM(a)") + query.Expression = "SUM(a)" res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType) @@ -303,7 +316,7 @@ func TestRequestParser(t *testing.T) { t.Run("Valid id is generated if ID is not provided and refId is not a valid MetricData ID", func(t *testing.T) { query := getBaseJsonQuery() - query.Set("refId", "$$") + query.RefId = "$$" res, err := parseRequestQuery(query, "$$", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) require.NoError(t, err) assert.Equal(t, "$$", res.RefId) @@ -312,8 +325,11 @@ func TestRequestParser(t *testing.T) { t.Run("parseRequestQuery sets label when label is present in json query", func(t *testing.T) { query := getBaseJsonQuery() - query.Set("alias", "some alias") - query.Set("label", "some label") + alias := "some alias" + query.Alias = &alias + + label := "some label" + query.Label = &label res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) @@ -323,15 +339,22 @@ func TestRequestParser(t *testing.T) { }) } -func getBaseJsonQuery() *simplejson.Json { - return simplejson.NewFromAny(map[string]interface{}{ +func getBaseJsonQuery() QueryJson { + fixtureJSON := []byte(`{ "refId": "ref1", "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", "statistic": "Average", - "period": "900", - }) + "period": "900" + }`) + + var query QueryJson + err := json.Unmarshal(fixtureJSON, &query) + if err != nil { + panic(err) + } + return query } func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_creates_new_label(t *testing.T) { @@ -352,7 +375,7 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - queryJson, err := simplejson.NewJson([]byte(fmt.Sprintf(`{ + queryJson := []byte(fmt.Sprintf(`{ "region": "us-east-1", "namespace": "ec2", "metricName": "CPUUtilization", @@ -363,26 +386,35 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create "statistic": "Average", "period": "600", "hide": false - }`, tc.inputAlias))) + }`, tc.inputAlias)) + + var query QueryJson + err := json.Unmarshal(queryJson, &query) require.NoError(t, err) - migrateAliasToDynamicLabel(queryJson) + migrateAliasToDynamicLabel(&query) - assert.Equal(t, simplejson.NewFromAny( - map[string]interface{}{ - "alias": tc.inputAlias, - "dimensions": map[string]interface{}{"InstanceId": []interface{}{"test"}}, - "hide": false, - "label": tc.expectedLabel, - "metricName": "CPUUtilization", - "namespace": "ec2", - "period": "600", - "region": "us-east-1", - "statistic": "Average"}), queryJson) + matchedJson := []byte(fmt.Sprintf(`{ + "alias": "%s", + "dimensions": { + "InstanceId": ["test"] + }, + "hide": false, + "label": "%s", + "metricName": "CPUUtilization", + "namespace": "ec2", + "period": "600", + "region": "us-east-1", + "statistic": "Average" + }`, tc.inputAlias, tc.expectedLabel)) + + result, err := json.Marshal(query) + require.NoError(t, err) + + assert.JSONEq(t, string(matchedJson), string(result)) }) } } - func Test_Test_migrateLegacyQuery(t *testing.T) { t.Run("migrates alias to label when label does not already exist and feature toggle enabled", func(t *testing.T) { migratedQueries, err := migrateLegacyQuery( diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index 41a24c2148e..37d5627de11 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -10,7 +10,6 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -297,9 +296,9 @@ func createDataLinks(link string) []data.DataLink { func createMeta(query *cloudWatchQuery) *data.FrameMeta { return &data.FrameMeta{ ExecutedQueryString: query.UsedExpression, - Custom: simplejson.NewFromAny(map[string]interface{}{ - "period": query.Period, - "id": query.Id, - }), + Custom: fmt.Sprintf(`{ + "period": %d, + "id": %s, + }`, query.Period, query.Id), } } diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 1d815e8fd80..a1e288c7bd0 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -173,7 +173,7 @@ func (s *Service) createRequest(ctx context.Context, dsInfo *models.DatasourceIn params := req.URL.Query() params.Set("db", dsInfo.Database) - params.Set("epoch", "s") + params.Set("epoch", "ms") if httpMode == "GET" { params.Set("q", query) diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 6169085ee7c..7193198a9f8 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -85,7 +85,7 @@ func (query *Query) renderTags() []string { func (query *Query) renderTimeFilter(queryContext *backend.QueryDataRequest) string { from, to := epochMStoInfluxTime(&queryContext.Queries[0].TimeRange) - return fmt.Sprintf("time > %s and time < %s", from, to) + return fmt.Sprintf("time >= %s and time <= %s", from, to) } func (query *Query) renderSelectors(queryContext *backend.QueryDataRequest) string { diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index d35d9b05024..e767748bff4 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -49,7 +49,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") FROM "policy"."cpu" WHERE time > 1596240000000ms and time < 1596240300000ms GROUP BY time(10s) fill(null)`) + require.Equal(t, rawQuery, `SELECT mean("value") FROM "policy"."cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms GROUP BY time(10s) fill(null)`) }) t.Run("can build query with tz", func(t *testing.T) { @@ -63,7 +63,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") FROM "cpu" WHERE time > 1596240000000ms and time < 1596240300000ms GROUP BY time(5s) tz('Europe/Paris')`) + require.Equal(t, rawQuery, + `SELECT mean("value") FROM "cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms GROUP BY time(5s) tz('Europe/Paris')`) }) t.Run("can build query with tz, limit, slimit, orderByTime and puts them in the correct order", func(t *testing.T) { @@ -80,7 +81,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") FROM "cpu" WHERE time > 1596240000000ms and time < 1596240300000ms GROUP BY time(5s) ORDER BY time ASC limit 1 slimit 1 tz('Europe/Paris')`) + require.Equal(t, rawQuery, + `SELECT mean("value") FROM "cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms GROUP BY time(5s) ORDER BY time ASC limit 1 slimit 1 tz('Europe/Paris')`) }) t.Run("can build query with group bys", func(t *testing.T) { @@ -94,7 +96,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time > 1596240000000ms and time < 1596240300000ms GROUP BY time(5s), "datacenter" fill(null)`) + require.Equal(t, rawQuery, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time >= 1596240000000ms and time <= 1596240300000ms GROUP BY time(5s), "datacenter" fill(null)`) }) t.Run("can build query with math part", func(t *testing.T) { @@ -106,7 +108,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") / 100 FROM "cpu" WHERE time > 1596240000000ms and time < 1596240300000ms`) + require.Equal(t, rawQuery, + `SELECT mean("value") / 100 FROM "cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms`) }) t.Run("can build query with math part using $__interval_ms variable", func(t *testing.T) { @@ -118,7 +121,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") / 5000 FROM "cpu" WHERE time > 1596240000000ms and time < 1596240300000ms`) + require.Equal(t, rawQuery, + `SELECT mean("value") / 5000 FROM "cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms`) }) t.Run("can build query with old $interval variable", func(t *testing.T) { @@ -132,7 +136,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { rawQuery, err := query.Build(queryContext) require.NoError(t, err) - require.Equal(t, rawQuery, `SELECT mean("value") FROM "cpu" WHERE time > 1596240000000ms and time < 1596240300000ms GROUP BY time(200ms)`) + require.Equal(t, rawQuery, + `SELECT mean("value") FROM "cpu" WHERE time >= 1596240000000ms and time <= 1596240300000ms GROUP BY time(200ms)`) }) t.Run("can render time range", func(t *testing.T) { @@ -150,7 +155,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { }, }, } - require.Equal(t, query.renderTimeFilter(queryContext), "time > 1596240000000ms and time < 1596243600000ms") + require.Equal(t, query.renderTimeFilter(queryContext), + "time >= 1596240000000ms and time <= 1596243600000ms") }) t.Run("render from: 10m", func(t *testing.T) { @@ -165,7 +171,8 @@ func TestInfluxdbQueryBuilder(t *testing.T) { }, }, } - require.Equal(t, query.renderTimeFilter(queryContext), "time > 1596240000000ms and time < 1596240600000ms") + require.Equal(t, query.renderTimeFilter(queryContext), + "time >= 1596240000000ms and time <= 1596240600000ms") }) }) diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go index baae4314241..a0b1755f32c 100644 --- a/pkg/tsdb/influxdb/response_parser.go +++ b/pkg/tsdb/influxdb/response_parser.go @@ -207,14 +207,14 @@ func parseTimestamp(value interface{}) (time.Time, error) { if !ok { return time.Time{}, fmt.Errorf("timestamp-value has invalid type: %#v", value) } - timestampFloat, err := timestampNumber.Float64() + timestampInMilliseconds, err := timestampNumber.Int64() if err != nil { return time.Time{}, err } // currently in the code the influxdb-timestamps are requested with - // seconds-precision, meaning these values are seconds - t := time.Unix(int64(timestampFloat), 0).UTC() + // milliseconds-precision, meaning these values are milliseconds + t := time.UnixMilli(timestampInMilliseconds).UTC() return t, nil } diff --git a/pkg/tsdb/influxdb/response_parser_test.go b/pkg/tsdb/influxdb/response_parser_test.go index 600f62c8797..430f1bde2c3 100644 --- a/pkg/tsdb/influxdb/response_parser_test.go +++ b/pkg/tsdb/influxdb/response_parser_test.go @@ -77,9 +77,9 @@ func TestInfluxdbResponseParser(t *testing.T) { floatFrame := data.NewFrame("cpu.mean { datacenter: America }", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), }), floatField, ) @@ -92,9 +92,9 @@ func TestInfluxdbResponseParser(t *testing.T) { stringFrame := data.NewFrame("cpu.path { datacenter: America }", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), }), stringField, ) @@ -107,9 +107,9 @@ func TestInfluxdbResponseParser(t *testing.T) { boolFrame := data.NewFrame("cpu.isActive { datacenter: America }", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), }), boolField, ) @@ -300,9 +300,9 @@ func TestInfluxdbResponseParser(t *testing.T) { testFrame := data.NewFrame("cpu.mean", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 41, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC), }), newField, ) @@ -349,8 +349,8 @@ func TestInfluxdbResponseParser(t *testing.T) { testFrame := data.NewFrame("cpu.mean", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC), }), newField, ) @@ -402,7 +402,7 @@ func TestInfluxdbResponseParser(t *testing.T) { testFrame := data.NewFrame("series alias", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), }), newField, ) @@ -667,9 +667,9 @@ func TestInfluxdbResponseParser(t *testing.T) { testFrame := data.NewFrame("cpu.mean { datacenter: America }", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC), }), newField, ) @@ -718,9 +718,9 @@ func TestInfluxdbResponseParser(t *testing.T) { }) t.Run("Influxdb response parser parseTimestamp valid JSON.number", func(t *testing.T) { - // currently we use seconds-precision with influxdb, so the test works with that. - // if we change this to for example milliseconds-precision, the tests will have to change. - timestamp, err := parseTimestamp(json.Number("1609556645")) + // currently we use milliseconds-precision with influxdb, so the test works with that. + // if we change this to for example nanoseconds-precision, the tests will have to change. + timestamp, err := parseTimestamp(json.Number("1609556645000")) require.NoError(t, err) require.Equal(t, timestamp.Format(time.RFC3339), "2021-01-02T03:04:05Z") }) @@ -754,9 +754,9 @@ func TestResponseParser_Parse(t *testing.T) { testFrame := data.NewFrame("cpu.mean", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 41, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC), }), newField, ) @@ -781,9 +781,9 @@ func TestResponseParser_Parse(t *testing.T) { testFrame := data.NewFrame("cpu.mean", data.NewField("time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 41, 0, time.UTC), - time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC), + time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC), }), newField, ) diff --git a/public/app/core/components/OptionsUI/NumberInput.tsx b/public/app/core/components/OptionsUI/NumberInput.tsx index 5a95524cb44..0631853b5d0 100644 --- a/public/app/core/components/OptionsUI/NumberInput.tsx +++ b/public/app/core/components/OptionsUI/NumberInput.tsx @@ -126,7 +126,12 @@ export class NumberInput extends PureComponent { range = `> ${max}`; } return ( - + {this.renderInput()} ); diff --git a/public/app/core/components/OptionsUI/slider.tsx b/public/app/core/components/OptionsUI/slider.tsx index 2d1c3f0bb2e..079a4464593 100644 --- a/public/app/core/components/OptionsUI/slider.tsx +++ b/public/app/core/components/OptionsUI/slider.tsx @@ -1,26 +1,144 @@ -import React from 'react'; +import { css, cx } from '@emotion/css'; +import { Global } from '@emotion/react'; +import SliderComponent from 'rc-slider'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { FieldConfigEditorProps, SliderFieldConfigSettings } from '@grafana/data'; -import { Slider } from '@grafana/ui'; +import { FieldConfigEditorProps, GrafanaTheme2, SliderFieldConfigSettings } from '@grafana/data'; +import { useTheme2 } from '@grafana/ui'; +import { getStyles } from '@grafana/ui/src/components/Slider/styles'; + +import { NumberInput } from './NumberInput'; export const SliderValueEditor: React.FC> = ({ value, onChange, item, }) => { + // Input reference + const inputRef = useRef(null); + + // Settings const { settings } = item; - const initialValue = typeof value === 'number' ? value : typeof value === 'string' ? +value : 0; + const min = settings?.min || 0; + const max = settings?.max || 100; + const step = settings?.step; + const marks = settings?.marks || { [min]: min, [max]: max }; + const included = settings?.included; + const ariaLabelForHandle = settings?.ariaLabelForHandle; + + // Core slider specific parameters and state + const inputWidthDefault = 75; + const isHorizontal = true; + const theme = useTheme2(); + const SliderWithTooltip = SliderComponent; + const [sliderValue, setSliderValue] = useState(value ?? min); + const [inputWidth, setInputWidth] = useState(inputWidthDefault); + + // Check for a difference between prop value and internal state + useEffect(() => { + if (value != null && value !== sliderValue) { + setSliderValue(value); + } + }, [value, sliderValue]); + + // Using input font and expected maximum number of digits, set input width + useEffect(() => { + const inputElement = getComputedStyle(inputRef.current!); + const fontWeight = inputElement.getPropertyValue('font-weight') || 'normal'; + const fontSize = inputElement.getPropertyValue('font-size') || '16px'; + const fontFamily = inputElement.getPropertyValue('font-family') || 'Arial'; + const wideNumericalCharacter = '0'; + const marginDigits = 4; // extra digits to account for things like negative, exponential, and controls + const inputPadding = 8; // TODO: base this on input styling + const maxDigits = + Math.max((max + (step || 0)).toString().length, (max - (step || 0)).toString().length) + marginDigits; + const refString = wideNumericalCharacter.repeat(maxDigits); + const calculatedTextWidth = getTextWidth(refString, `${fontWeight} ${fontSize} ${fontFamily}`); + if (calculatedTextWidth) { + setInputWidth(calculatedTextWidth + inputPadding * 2); + } + }, [max, step]); + + const onSliderChange = useCallback( + (v: number) => { + setSliderValue(v); + + if (onChange) { + onChange(v); + } + }, + [setSliderValue, onChange] + ); + + const onSliderInputChange = useCallback( + (value?: number) => { + let v = value; + + if (Number.isNaN(v) || !v) { + v = 0; + } + + setSliderValue(v); + + if (onChange) { + onChange(v); + } + }, + [onChange] + ); + + // Styles + const styles = getStyles(theme, isHorizontal, Boolean(marks)); + const stylesSlider = getStylesSlider(theme, inputWidth); + const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : []; return ( - +
+ {/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */} + + +
); }; + +// Calculate width of string with given font +function getTextWidth(text: string, font: string): number | null { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (context) { + context.font = font; + const metrics = context.measureText(text); + return metrics.width; + } + return null; +} + +const getStylesSlider = (theme: GrafanaTheme2, width: number) => { + return { + numberInputWrapper: css` + margin-left: 10px; + max-height: 32px; + max-width: ${width}px; + min-width: ${width}px; + overflow: visible; + width: 100%; + `, + }; +}; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 19982cc9110..301be37ad76 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -166,7 +166,7 @@ export class ContextSrv { return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } - hasAccess(action: string, fallBack: boolean) { + hasAccess(action: string, fallBack: boolean): boolean { if (!this.accessControlEnabled()) { return fallBack; } diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index d481950b742..a167a3992eb 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -63,6 +63,8 @@ export class Scene { isPanelEditing = locationService.getSearchObject().editPanel !== undefined; + inlineEditingCallback?: () => void; + constructor(cfg: CanvasFrameOptions, enableEditing: boolean, public onSave: (cfg: CanvasFrameOptions) => void) { this.root = this.load(cfg, enableEditing); } diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index a22a3e4a280..a12ceca2bdd 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -61,7 +61,6 @@ class SubMenuUnConnected extends PureComponent { />
{dashboard && } -
); } diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.ts index 52e89d55fba..f3a595e3383 100644 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.ts +++ b/public/app/features/dashboard/services/PublicDashboardDataSource.ts @@ -5,20 +5,27 @@ import { DataQueryRequest, DataQueryResponse, DataSourceApi, + DataSourcePluginMeta, DataSourceRef, - PluginMeta, } from '@grafana/data'; import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; +import { MIXED_DATASOURCE_NAME } from '../../../plugins/datasource/mixed/MixedDataSource'; + export const PUBLIC_DATASOURCE = '-- Public --'; export class PublicDashboardDataSource extends DataSourceApi { constructor(datasource: DataSourceRef | string | DataSourceApi | null) { + let meta = {} as DataSourcePluginMeta; + if (PublicDashboardDataSource.isMixedDatasource(datasource)) { + meta.mixed = true; + } + super({ name: 'public-ds', id: 0, type: 'public-ds', - meta: {} as PluginMeta, + meta, uid: PublicDashboardDataSource.resolveUid(datasource), jsonData: {}, access: 'proxy', @@ -38,6 +45,14 @@ export class PublicDashboardDataSource extends DataSourceApi { return datasource?.uid ?? PUBLIC_DATASOURCE; } + private static isMixedDatasource(datasource: DataSourceRef | string | DataSourceApi | null): boolean { + if (typeof datasource === 'string' || datasource === null) { + return false; + } + + return datasource?.uid === MIXED_DATASOURCE_NAME; + } + /** * Ideally final -- any other implementation may not work as expected */ diff --git a/public/app/features/data-connections/DataConnectionsPage.test.tsx b/public/app/features/data-connections/DataConnectionsPage.test.tsx index 515b3dd0e47..ba0bcddeca8 100644 --- a/public/app/features/data-connections/DataConnectionsPage.test.tsx +++ b/public/app/features/data-connections/DataConnectionsPage.test.tsx @@ -7,7 +7,7 @@ import { locationService } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import DataConnectionsPage from './DataConnectionsPage'; -import navIndex from './__mocks__/store.navIndex.mock'; +import { navIndex } from './__mocks__/store.navIndex.mock'; import { ROUTE_BASE_ID, ROUTES } from './constants'; const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => { diff --git a/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts index 4546a29ec7c..a6093868d83 100644 --- a/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts +++ b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts @@ -1,8 +1,10 @@ -export default { +import { NavIndex, NavSection } from '@grafana/data'; + +export const navIndex: NavIndex = { dashboards: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -74,7 +76,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -147,7 +149,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -220,7 +222,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -293,7 +295,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -366,7 +368,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -436,7 +438,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -512,7 +514,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -588,7 +590,7 @@ export default { parentItem: { id: 'dashboards', text: 'Dashboards', - section: 'core', + section: NavSection.Core, subTitle: 'Manage dashboards and folders', icon: 'apps', url: '/dashboards', @@ -661,7 +663,7 @@ export default { explore: { id: 'explore', text: 'Explore', - section: 'core', + section: NavSection.Core, subTitle: 'Explore your data', icon: 'compass', url: '/explore', @@ -670,7 +672,7 @@ export default { alerting: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -737,7 +739,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -805,7 +807,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -873,7 +875,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -941,7 +943,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -1009,7 +1011,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -1077,7 +1079,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -1148,7 +1150,7 @@ export default { parentItem: { id: 'alerting', text: 'Alerting', - section: 'core', + section: NavSection.Core, subTitle: 'Alert rules and notifications', icon: 'bell', url: '/alerting/list', @@ -1211,7 +1213,7 @@ export default { 'data-connections': { id: 'data-connections', text: 'Data Connections', - section: 'core', + section: NavSection.Core, icon: 'link', url: '/data-connections', sortWeight: -1500, @@ -1259,7 +1261,7 @@ export default { parentItem: { id: 'data-connections', text: 'Data Connections', - section: 'core', + section: NavSection.Core, icon: 'link', url: '/data-connections', sortWeight: -1500, @@ -1304,7 +1306,7 @@ export default { parentItem: { id: 'data-connections', text: 'Data Connections', - section: 'core', + section: NavSection.Core, icon: 'link', url: '/data-connections', sortWeight: -1500, @@ -1349,7 +1351,7 @@ export default { parentItem: { id: 'data-connections', text: 'Data Connections', - section: 'core', + section: NavSection.Core, icon: 'link', url: '/data-connections', sortWeight: -1500, @@ -1394,7 +1396,7 @@ export default { parentItem: { id: 'data-connections', text: 'Data Connections', - section: 'core', + section: NavSection.Core, icon: 'link', url: '/data-connections', sortWeight: -1500, @@ -1433,7 +1435,7 @@ export default { 'plugin-page-basic-app': { id: 'plugin-page-basic-app', text: 'Basic App', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/basic-app/img/logo.svg', url: '/a/basic-app/one', sortWeight: -1400, @@ -1467,7 +1469,7 @@ export default { parentItem: { id: 'plugin-page-grafana-synthetic-monitoring-app', text: 'Synthetic Monitoring', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', url: '/a/grafana-synthetic-monitoring-app/home', sortWeight: -1400, @@ -1502,7 +1504,7 @@ export default { 'plugin-page-cloudflare-app': { id: 'plugin-page-cloudflare-app', text: 'Cloudflare Grafana App', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/cloudflare-app/img/cf_icon.png', sortWeight: -1400, children: [ @@ -1519,7 +1521,7 @@ export default { 'plugin-page-grafana-easystart-app': { id: 'plugin-page-grafana-easystart-app', text: 'Integrations and Connections', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/grafana-easystart-app/img/logo.svg', url: '/a/grafana-easystart-app', sortWeight: -1400, @@ -1527,7 +1529,7 @@ export default { 'plugin-page-redis-explorer-app': { id: 'plugin-page-redis-explorer-app', text: 'Redis Explorer', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/redis-explorer-app/img/logo.svg', url: '/a/redis-explorer-app/', sortWeight: -1400, @@ -1567,7 +1569,7 @@ export default { 'plugin-page-grafana-synthetic-monitoring-app': { id: 'plugin-page-grafana-synthetic-monitoring-app', text: 'Synthetic Monitoring', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', url: '/a/grafana-synthetic-monitoring-app/home', sortWeight: -1400, @@ -1601,7 +1603,7 @@ export default { 'plugin-page-grafana-k6-app': { id: 'plugin-page-grafana-k6-app', text: 'k6 Cloud App', - section: 'plugin', + section: NavSection.Plugin, img: 'public/plugins/grafana-k6-app/img/logo.svg', url: '/a/grafana-k6-app', sortWeight: -1400, @@ -1609,7 +1611,7 @@ export default { cfg: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1668,7 +1670,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1728,7 +1730,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1788,7 +1790,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1848,7 +1850,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1908,7 +1910,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -1968,7 +1970,7 @@ export default { parentItem: { id: 'cfg', text: 'Configuration', - section: 'config', + section: NavSection.Config, subTitle: 'Organization: Main Org.', icon: 'cog', url: '/datasources', @@ -2022,7 +2024,7 @@ export default { admin: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2069,7 +2071,7 @@ export default { parentItem: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2117,7 +2119,7 @@ export default { parentItem: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2165,7 +2167,7 @@ export default { parentItem: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2213,7 +2215,7 @@ export default { parentItem: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2261,7 +2263,7 @@ export default { parentItem: { id: 'admin', text: 'Server Admin', - section: 'config', + section: NavSection.Config, subTitle: 'Manage all users and orgs', icon: 'shield', url: '/admin/users', @@ -2304,7 +2306,7 @@ export default { profile: { id: 'profile', text: 'admin', - section: 'config', + section: NavSection.Config, img: '/avatar/46d229b033af06a191ff2267bca9ae56', url: '/profile', sortWeight: -1100, @@ -2345,7 +2347,7 @@ export default { parentItem: { id: 'profile', text: 'admin', - section: 'config', + section: NavSection.Config, img: '/avatar/46d229b033af06a191ff2267bca9ae56', url: '/profile', sortWeight: -1100, @@ -2387,7 +2389,7 @@ export default { parentItem: { id: 'profile', text: 'admin', - section: 'config', + section: NavSection.Config, img: '/avatar/46d229b033af06a191ff2267bca9ae56', url: '/profile', sortWeight: -1100, @@ -2429,7 +2431,7 @@ export default { parentItem: { id: 'profile', text: 'admin', - section: 'config', + section: NavSection.Config, img: '/avatar/46d229b033af06a191ff2267bca9ae56', url: '/profile', sortWeight: -1100, @@ -2473,7 +2475,7 @@ export default { parentItem: { id: 'profile', text: 'admin', - section: 'config', + section: NavSection.Config, img: '/avatar/46d229b033af06a191ff2267bca9ae56', url: '/profile', sortWeight: -1100, @@ -2510,7 +2512,7 @@ export default { help: { id: 'help', text: 'Help', - section: 'config', + section: NavSection.Config, subTitle: 'Grafana v9.0.0-pre (abb5c6109a)', icon: 'question-circle', url: '#', diff --git a/public/app/features/datasources/DataSourceList.test.tsx b/public/app/features/datasources/DataSourcesList.test.tsx similarity index 56% rename from public/app/features/datasources/DataSourceList.test.tsx rename to public/app/features/datasources/DataSourcesList.test.tsx index 3110b35a6e8..f09b772431f 100644 --- a/public/app/features/datasources/DataSourceList.test.tsx +++ b/public/app/features/datasources/DataSourcesList.test.tsx @@ -1,18 +1,30 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { Provider } from 'react-redux'; import { LayoutModes } from '@grafana/data'; +import { configureStore } from 'app/store/configureStore'; +import { DataSourcesState } from 'app/types'; import DataSourcesList from './DataSourcesList'; import { getMockDataSources } from './__mocks__/dataSourcesMocks'; +import { initialState } from './state/reducers'; -const setup = () => { - const props = { - dataSources: getMockDataSources(3), - layoutMode: LayoutModes.Grid, - }; +const setup = (stateOverride?: Partial) => { + const store = configureStore({ + dataSources: { + ...initialState, + dataSources: getMockDataSources(3), + layoutMode: LayoutModes.Grid, + ...stateOverride, + }, + }); - return render(); + return render( + + + + ); }; describe('DataSourcesList', () => { diff --git a/public/app/features/datasources/DataSourcesList.tsx b/public/app/features/datasources/DataSourcesList.tsx index 9bcca5d543c..dbdf2971194 100644 --- a/public/app/features/datasources/DataSourcesList.tsx +++ b/public/app/features/datasources/DataSourcesList.tsx @@ -1,17 +1,16 @@ // Libraries import { css } from '@emotion/css'; -import React, { FC } from 'react'; +import React from 'react'; // Types -import { DataSourceSettings, LayoutMode } from '@grafana/data'; +import { DataSourceSettings } from '@grafana/data'; import { Card, Tag, useStyles } from '@grafana/ui'; -export interface Props { +export type Props = { dataSources: DataSourceSettings[]; - layoutMode: LayoutMode; -} +}; -export const DataSourcesList: FC = ({ dataSources, layoutMode }) => { +export const DataSourcesList = ({ dataSources }: Props) => { const styles = useStyles(getStyles); return ( diff --git a/public/app/features/datasources/DataSourcesListHeader.tsx b/public/app/features/datasources/DataSourcesListHeader.tsx new file mode 100644 index 00000000000..612bb3fdfb3 --- /dev/null +++ b/public/app/features/datasources/DataSourcesListHeader.tsx @@ -0,0 +1,26 @@ +import React, { useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; +import { contextSrv } from 'app/core/core'; +import { AccessControlAction, StoreState } from 'app/types'; + +import { setDataSourcesSearchQuery } from './state/reducers'; +import { getDataSourcesSearchQuery } from './state/selectors'; + +export const DataSourcesListHeader = () => { + const dispatch = useDispatch(); + const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]); + const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources)); + const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); + + const linkButton = { + href: 'datasources/new', + title: 'Add data source', + disabled: !canCreateDataSource, + }; + + return ( + + ); +}; diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 34ee0dc69ac..fe992512b95 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -1,11 +1,15 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { Provider } from 'react-redux'; -import { DataSourceSettings, NavModel, LayoutModes } from '@grafana/data'; +import { DataSourceSettings, LayoutModes } from '@grafana/data'; +import { configureStore } from 'app/store/configureStore'; +import { DataSourcesState } from 'app/types'; -import { DataSourcesListPage, Props } from './DataSourcesListPage'; +import { DataSourcesListPage } from './DataSourcesListPage'; import { getMockDataSources } from './__mocks__/dataSourcesMocks'; -import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers'; +import navIndex from './__mocks__/store.navIndex.mock'; +import { initialState } from './state/reducers'; jest.mock('app/core/core', () => { return { @@ -15,29 +19,30 @@ jest.mock('app/core/core', () => { }; }); -const setup = (propOverrides?: object) => { - const props: Props = { - dataSources: [] as DataSourceSettings[], - layoutMode: LayoutModes.Grid, - loadDataSources: jest.fn(), - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Data Sources', - }, - } as NavModel, - dataSourcesCount: 0, - searchQuery: '', - setDataSourcesSearchQuery, - setDataSourcesLayoutMode, - hasFetched: false, - }; +const getMock = jest.fn().mockResolvedValue([]); - Object.assign(props, propOverrides); +jest.mock('app/core/services/backend_srv', () => ({ + ...jest.requireActual('app/core/services/backend_srv'), + getBackendSrv: () => ({ get: getMock }), +})); - return render(); +const setup = (stateOverride?: Partial) => { + const store = configureStore({ + dataSources: { + ...initialState, + dataSources: [] as DataSourceSettings[], + layoutMode: LayoutModes.Grid, + hasFetched: false, + ...stateOverride, + }, + navIndex, + }); + + return render( + + + + ); }; describe('Render', () => { diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index 4535c5b0f76..b9bf34c4c1f 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -1,98 +1,22 @@ -import React, { PureComponent } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React from 'react'; +import { useSelector } from 'react-redux'; -import { IconName } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; -import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; -import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; -import { StoreState, AccessControlAction } from 'app/types'; +import { StoreState } from 'app/types'; -import DataSourcesList from './DataSourcesList'; -import { loadDataSources } from './state/actions'; -import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers'; -import { - getDataSources, - getDataSourcesCount, - getDataSourcesLayoutMode, - getDataSourcesSearchQuery, -} from './state/selectors'; +import { DataSourcesListPageContent } from './DataSourcesListPageContent'; -function mapStateToProps(state: StoreState) { - return { - navModel: getNavModel(state.navIndex, 'datasources'), - dataSources: getDataSources(state.dataSources), - layoutMode: getDataSourcesLayoutMode(state.dataSources), - dataSourcesCount: getDataSourcesCount(state.dataSources), - searchQuery: getDataSourcesSearchQuery(state.dataSources), - hasFetched: state.dataSources.hasFetched, - }; -} +export const DataSourcesListPage = () => { + const navModel = useSelector(({ navIndex }: StoreState) => getNavModel(navIndex, 'datasources')); -const mapDispatchToProps = { - loadDataSources, - setDataSourcesSearchQuery, - setDataSourcesLayoutMode, + return ( + + + + + + ); }; -const connector = connect(mapStateToProps, mapDispatchToProps); - -export type Props = ConnectedProps; - -const emptyListModel = { - title: 'No data sources defined', - buttonIcon: 'database' as IconName, - buttonLink: 'datasources/new', - buttonTitle: 'Add data source', - proTip: 'You can also define data sources through configuration files.', - proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list', - proTipLinkTitle: 'Learn more', - proTipTarget: '_blank', -}; - -export class DataSourcesListPage extends PureComponent { - componentDidMount() { - this.props.loadDataSources(); - } - - render() { - const { dataSources, dataSourcesCount, navModel, layoutMode, searchQuery, setDataSourcesSearchQuery, hasFetched } = - this.props; - - const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); - - const linkButton = { - href: 'datasources/new', - title: 'Add data source', - disabled: !canCreateDataSource, - }; - - const emptyList = { - ...emptyListModel, - buttonDisabled: !canCreateDataSource, - }; - - return ( - - - <> - {hasFetched && dataSourcesCount === 0 && } - {hasFetched && - dataSourcesCount > 0 && [ - setDataSourcesSearchQuery(query)} - linkButton={linkButton} - key="action-bar" - />, - , - ]} - - - - ); - } -} - -export default connector(DataSourcesListPage); +export default DataSourcesListPage; diff --git a/public/app/features/datasources/DataSourcesListPageContent.tsx b/public/app/features/datasources/DataSourcesListPageContent.tsx new file mode 100644 index 00000000000..aebe1de2c33 --- /dev/null +++ b/public/app/features/datasources/DataSourcesListPageContent.tsx @@ -0,0 +1,58 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { IconName } from '@grafana/ui'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import PageLoader from 'app/core/components/PageLoader/PageLoader'; +import { contextSrv } from 'app/core/core'; +import { StoreState, AccessControlAction } from 'app/types'; + +import DataSourcesList from './DataSourcesList'; +import { DataSourcesListHeader } from './DataSourcesListHeader'; +import { loadDataSources } from './state/actions'; +import { getDataSourcesCount, getDataSources } from './state/selectors'; + +const buttonIcon: IconName = 'database'; +const emptyListModel = { + title: 'No data sources defined', + buttonIcon, + buttonLink: 'datasources/new', + buttonTitle: 'Add data source', + proTip: 'You can also define data sources through configuration files.', + proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list', + proTipLinkTitle: 'Learn more', + proTipTarget: '_blank', +}; + +export const DataSourcesListPageContent = () => { + const dispatch = useDispatch(); + const dataSources = useSelector((state: StoreState) => getDataSources(state.dataSources)); + const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources)); + const hasFetched = useSelector(({ dataSources }: StoreState) => dataSources.hasFetched); + const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); + const emptyList = { + ...emptyListModel, + buttonDisabled: !canCreateDataSource, + }; + + useEffect(() => { + if (!hasFetched) { + dispatch(loadDataSources()); + } + }, [dispatch, hasFetched]); + + if (!hasFetched) { + return ; + } + + if (dataSourcesCount === 0) { + return ; + } + + return ( + <> + + + + ); +}; diff --git a/public/app/features/datasources/__mocks__/store.navIndex.mock.ts b/public/app/features/datasources/__mocks__/store.navIndex.mock.ts new file mode 100644 index 00000000000..17569b14120 --- /dev/null +++ b/public/app/features/datasources/__mocks__/store.navIndex.mock.ts @@ -0,0 +1,2633 @@ +import { NavSection } from '@grafana/data'; + +export default { + dashboards: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + 'manage-dashboards': { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + playlists: { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + snapshots: { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'library-panels': { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + divider: { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'new-dashboard': { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'new-folder': { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + import: { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: NavSection.Core, + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'not-found': { + text: 'Page not found', + subTitle: '404 Error', + icon: 'exclamation-triangle', + }, + explore: { + id: 'explore', + text: 'Explore', + section: NavSection.Core, + subTitle: 'Explore your data', + icon: 'compass', + url: '/explore', + sortWeight: -1700, + }, + alerting: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + 'alert-list': { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + receivers: { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'am-routes': { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + silences: { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + groups: { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'alerting-admin': { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + alert: { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'alerting', + text: 'Alerting', + section: NavSection.Core, + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'data-connections': { + id: 'data-connections', + text: 'Data Connections', + section: NavSection.Core, + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + 'data-connections-datasources': { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: NavSection.Core, + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-plugins': { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: NavSection.Core, + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-cloud-integrations': { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: NavSection.Core, + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-recorded-queries': { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: NavSection.Core, + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'plugin-page-basic-app': { + id: 'plugin-page-basic-app', + text: 'Basic App', + section: NavSection.Plugin, + img: 'public/plugins/basic-app/img/logo.svg', + url: '/a/basic-app/one', + sortWeight: -1400, + children: [ + { + text: 'Page One', + url: '/a/basic-app/one', + }, + { + text: 'Page Two', + url: '/a/basic-app/two', + }, + { + text: 'Page Three', + url: '/a/basic-app/three', + }, + { + text: 'Page Four', + url: '/a/basic-app/four', + }, + { + text: 'Configuration', + icon: 'fa fa-cog', + url: '/plugins/basic-app', + }, + ], + }, + undefined: { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + parentItem: { + id: 'plugin-page-grafana-synthetic-monitoring-app', + text: 'Synthetic Monitoring', + section: NavSection.Plugin, + img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', + url: '/a/grafana-synthetic-monitoring-app/home', + sortWeight: -1400, + children: [ + { + text: 'Home', + url: '/a/grafana-synthetic-monitoring-app/home', + }, + { + text: 'Summary', + url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary', + }, + { + text: 'Checks', + url: '/a/grafana-synthetic-monitoring-app/checks', + }, + { + text: 'Probes', + url: '/a/grafana-synthetic-monitoring-app/probes', + }, + { + text: 'Alerts', + url: '/a/grafana-synthetic-monitoring-app/alerts', + }, + { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + }, + ], + }, + }, + 'plugin-page-cloudflare-app': { + id: 'plugin-page-cloudflare-app', + text: 'Cloudflare Grafana App', + section: NavSection.Plugin, + img: 'public/plugins/cloudflare-app/img/cf_icon.png', + sortWeight: -1400, + children: [ + { + text: 'Zones', + url: '/d/KAVdMAw9k', + }, + { + text: 'DNS Firewall', + url: '/d/QrKttDVqu', + }, + ], + }, + 'plugin-page-grafana-easystart-app': { + id: 'plugin-page-grafana-easystart-app', + text: 'Integrations and Connections', + section: NavSection.Plugin, + img: 'public/plugins/grafana-easystart-app/img/logo.svg', + url: '/a/grafana-easystart-app', + sortWeight: -1400, + }, + 'plugin-page-redis-explorer-app': { + id: 'plugin-page-redis-explorer-app', + text: 'Redis Explorer', + section: NavSection.Plugin, + img: 'public/plugins/redis-explorer-app/img/logo.svg', + url: '/a/redis-explorer-app/', + sortWeight: -1400, + children: [ + { + text: 'Home', + icon: 'home-alt', + url: '/a/redis-explorer-app/', + }, + { + text: 'Enterprise Clusters', + icon: 'apps', + url: '/d/1dKhTjtGk', + }, + { + text: 'Cluster Overview', + icon: 'monitor', + url: '/d/viroIzSGz', + }, + { + text: 'Cluster Nodes', + icon: 'sitemap', + url: '/d/hqze6rtGz', + }, + { + text: 'Cluster Databases', + icon: 'database', + url: '/d/k_A8MjtMk', + }, + { + text: 'Cluster Alerts', + icon: 'info-circle', + url: '/d/xESAiFcnk', + }, + ], + }, + 'plugin-page-grafana-synthetic-monitoring-app': { + id: 'plugin-page-grafana-synthetic-monitoring-app', + text: 'Synthetic Monitoring', + section: NavSection.Plugin, + img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', + url: '/a/grafana-synthetic-monitoring-app/home', + sortWeight: -1400, + children: [ + { + text: 'Home', + url: '/a/grafana-synthetic-monitoring-app/home', + }, + { + text: 'Summary', + url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary', + }, + { + text: 'Checks', + url: '/a/grafana-synthetic-monitoring-app/checks', + }, + { + text: 'Probes', + url: '/a/grafana-synthetic-monitoring-app/probes', + }, + { + text: 'Alerts', + url: '/a/grafana-synthetic-monitoring-app/alerts', + }, + { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + }, + ], + }, + 'plugin-page-grafana-k6-app': { + id: 'plugin-page-grafana-k6-app', + text: 'k6 Cloud App', + section: NavSection.Plugin, + img: 'public/plugins/grafana-k6-app/img/logo.svg', + url: '/a/grafana-k6-app', + sortWeight: -1400, + }, + cfg: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + datasources: { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + users: { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + teams: { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + plugins: { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + 'org-settings': { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + apikeys: { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + serviceaccounts: { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: NavSection.Config, + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + { + id: 'serviceaccounts', + text: 'Service accounts', + description: 'Manage service accounts', + icon: 'gf-service-account', + url: '/org/serviceaccounts', + }, + ], + }, + }, + admin: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + 'global-users': { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'global-orgs': { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'server-settings': { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'admin-plugins': { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + upgrading: { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: NavSection.Config, + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + profile: { + id: 'profile', + text: 'admin', + section: NavSection.Config, + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + 'profile-settings': { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + parentItem: { + id: 'profile', + text: 'admin', + section: NavSection.Config, + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + notifications: { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + parentItem: { + id: 'profile', + text: 'admin', + section: NavSection.Config, + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + 'change-password': { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + parentItem: { + id: 'profile', + text: 'admin', + section: NavSection.Config, + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + 'sign-out': { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + parentItem: { + id: 'profile', + text: 'admin', + section: NavSection.Config, + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + help: { + id: 'help', + text: 'Help', + section: NavSection.Config, + subTitle: 'Grafana v9.0.0-pre (cd35366222)', + icon: 'question-circle', + url: '#', + sortWeight: -1000, + }, +}; diff --git a/public/app/features/scenes/components/Scene.tsx b/public/app/features/scenes/components/Scene.tsx index 7bf37c758b5..a5e52ad0612 100644 --- a/public/app/features/scenes/components/Scene.tsx +++ b/public/app/features/scenes/components/Scene.tsx @@ -17,13 +17,13 @@ export class Scene extends SceneObjectBase { static Component = SceneRenderer; urlSyncManager?: UrlSyncManager; - onMount() { - super.onMount(); + activate() { + super.activate(); this.urlSyncManager = new UrlSyncManager(this); } - onUnmount() { - super.onUnmount(); + deactivate() { + super.deactivate(); this.urlSyncManager!.cleanUp(); } } @@ -31,8 +31,6 @@ export class Scene extends SceneObjectBase { function SceneRenderer({ model }: SceneComponentProps) { const { title, layout, actions = [], isEditing, $editor } = model.useState(); - console.log('render scene'); - return (
diff --git a/public/app/features/scenes/components/ScenePanelRepeater.tsx b/public/app/features/scenes/components/ScenePanelRepeater.tsx index 6d2ba7569e6..81c3d3441e4 100644 --- a/public/app/features/scenes/components/ScenePanelRepeater.tsx +++ b/public/app/features/scenes/components/ScenePanelRepeater.tsx @@ -11,8 +11,8 @@ interface RepeatOptions extends SceneObjectState { } export class ScenePanelRepeater extends SceneObjectBase { - onMount() { - super.onMount(); + activate(): void { + super.activate(); this.subs.add( this.getData().subscribe({ diff --git a/public/app/features/scenes/core/SceneComponentWrapper.tsx b/public/app/features/scenes/core/SceneComponentWrapper.tsx new file mode 100644 index 00000000000..74cfb834d6b --- /dev/null +++ b/public/app/features/scenes/core/SceneComponentWrapper.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from 'react'; + +import { SceneComponentEditingWrapper } from '../editor/SceneComponentEditWrapper'; + +import { SceneComponentProps, SceneObject } from './types'; + +export function SceneComponentWrapper({ model, isEditing }: SceneComponentProps) { + const Component = (model as any).constructor['Component'] ?? EmptyRenderer; + const inner = ; + + // Handle component activation state state + useEffect(() => { + if (!model.isActive) { + model.activate(); + } + return () => { + if (model.isActive) { + model.deactivate(); + } + }; + }, [model]); + + if (!isEditing) { + return inner; + } + + return {inner}; +} + +function EmptyRenderer(_: SceneComponentProps): React.ReactElement | null { + return null; +} diff --git a/public/app/features/scenes/core/SceneObjectBase.test.ts b/public/app/features/scenes/core/SceneObjectBase.test.ts index 2ac12132c81..2cfc82c73f1 100644 --- a/public/app/features/scenes/core/SceneObjectBase.test.ts +++ b/public/app/features/scenes/core/SceneObjectBase.test.ts @@ -23,12 +23,12 @@ describe('SceneObject', () => { ], }); - scene.state.nested?.onMount(); + scene.state.nested?.activate(); const clone = scene.clone(); expect(clone).not.toBe(scene); expect(clone.state.nested).not.toBe(scene.state.nested); - expect(clone.state.nested?.isMounted).toBe(undefined); + expect(clone.state.nested?.isActive).toBe(undefined); expect(clone.state.children![0]).not.toBe(scene.state.children![0]); }); diff --git a/public/app/features/scenes/core/SceneObjectBase.tsx b/public/app/features/scenes/core/SceneObjectBase.tsx index 89c08b11bdd..56eca2e0b0e 100644 --- a/public/app/features/scenes/core/SceneObjectBase.tsx +++ b/public/app/features/scenes/core/SceneObjectBase.tsx @@ -1,11 +1,10 @@ -import { useEffect } from 'react'; import { useObservable } from 'react-use'; import { Observer, Subject, Subscription } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; import { EventBusSrv } from '@grafana/data'; -import { SceneComponentEditWrapper } from './SceneComponentEditWrapper'; +import { SceneComponentWrapper } from './SceneComponentWrapper'; import { SceneObjectStateChangedEvent } from './events'; import { SceneDataState, @@ -23,7 +22,7 @@ export abstract class SceneObjectBase impl state: TState; parent?: SceneObjectBase; subs = new Subscription(); - isMounted?: boolean; + isActive?: boolean; events = new EventBusSrv(); constructor(state: TState) { @@ -41,7 +40,7 @@ export abstract class SceneObjectBase impl * Wraps the component in an EditWrapper that handles edit mode */ get Component(): SceneComponent { - return SceneComponentEditWrapper; + return SceneComponentWrapper; } /** @@ -96,46 +95,27 @@ export abstract class SceneObjectBase impl return !this.parent ? this : this.parent.getRoot(); } - onMount() { - this.isMounted = true; + activate() { + this.isActive = true; const { $data } = this.state; - if ($data && !$data.isMounted) { - $data.onMount(); + if ($data && !$data.isActive) { + $data.activate(); } } - onUnmount() { - this.isMounted = false; + deactivate(): void { + this.isActive = false; const { $data } = this.state; - if ($data && $data.isMounted) { - $data.onUnmount(); + if ($data && $data.isActive) { + $data.deactivate(); } this.subs.unsubscribe(); this.subs = new Subscription(); } - /** - * The scene object needs to know when the react component is mounted to trigger query and other lazy actions - */ - useMount() { - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (!this.isMounted) { - this.onMount(); - } - return () => { - if (this.isMounted) { - this.onUnmount(); - } - }; - }, []); - - return this; - } - useState() { // eslint-disable-next-line react-hooks/rules-of-hooks return useObservable(this.subject, this.state); diff --git a/public/app/features/scenes/core/types.ts b/public/app/features/scenes/core/types.ts index f44bfee7b21..bb32a56b38f 100644 --- a/public/app/features/scenes/core/types.ts +++ b/public/app/features/scenes/core/types.ts @@ -41,7 +41,7 @@ export interface SceneObject state: TState; /** True when there is a React component mounted for this Object */ - isMounted?: boolean; + isActive?: boolean; /** SceneObject parent */ parent?: SceneObject; @@ -55,14 +55,11 @@ export interface SceneObject /** How to modify state */ setState(state: Partial): void; - /** Utility hook for main component so that object knows when it's mounted */ - useMount(): this; - - /** Called when component mounts. A place to register event listeners add subscribe to state changes */ - onMount(): void; + /** Called when the Component is mounted. A place to register event listeners add subscribe to state changes */ + activate(): void; /** Called when component unmounts. Unsubscribe to events */ - onUnmount(): void; + deactivate(): void; /** Get the scene editor */ getSceneEditor(): SceneEditor; diff --git a/public/app/features/scenes/core/SceneComponentEditWrapper.tsx b/public/app/features/scenes/editor/SceneComponentEditWrapper.tsx similarity index 69% rename from public/app/features/scenes/core/SceneComponentEditWrapper.tsx rename to public/app/features/scenes/editor/SceneComponentEditWrapper.tsx index 9bbde1c2511..5049e06f893 100644 --- a/public/app/features/scenes/core/SceneComponentEditWrapper.tsx +++ b/public/app/features/scenes/editor/SceneComponentEditWrapper.tsx @@ -4,26 +4,9 @@ import React, { CSSProperties } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -import { SceneObjectBase } from './SceneObjectBase'; -import { SceneComponentProps } from './types'; +import { SceneObject } from '../core/types'; -export function SceneComponentEditWrapper>({ - model, - isEditing, -}: SceneComponentProps) { - const Component = (model as any).constructor['Component'] ?? EmptyRenderer; - const inner = ; - - model.useMount(); - - if (!isEditing) { - return inner; - } - - return {inner}; -} - -export function SceneComponentEditingWrapper>({ +export function SceneComponentEditingWrapper({ model, children, }: { @@ -76,7 +59,3 @@ const getStyles = (theme: GrafanaTheme2) => { }), }; }; - -function EmptyRenderer(_: SceneComponentProps): React.ReactElement | null { - return null; -} diff --git a/public/app/features/scenes/querying/SceneQueryRunner.ts b/public/app/features/scenes/querying/SceneQueryRunner.ts index fb76b1c5088..3d006885d3b 100644 --- a/public/app/features/scenes/querying/SceneQueryRunner.ts +++ b/public/app/features/scenes/querying/SceneQueryRunner.ts @@ -31,8 +31,8 @@ export interface DataQueryExtended extends DataQuery { export class SceneQueryRunner extends SceneObjectBase { private querySub?: Unsubscribable; - onMount() { - super.onMount(); + activate() { + super.activate(); const timeRange = this.getTimeRange(); @@ -49,12 +49,9 @@ export class SceneQueryRunner extends SceneObjectBase { } } - onUnmount() { - super.onUnmount(); - this.cleanUp(); - } + deactivate(): void { + super.deactivate(); - cleanUp() { if (this.querySub) { this.querySub.unsubscribe(); this.querySub = undefined; @@ -108,8 +105,6 @@ export class SceneQueryRunner extends SceneObjectBase { request.interval = norm.interval; request.intervalMs = norm.intervalMs; - console.log('Query runner run'); - this.querySub = runRequest(ds, request).subscribe({ next: (data) => { console.log('set data', data, data.state); diff --git a/public/app/features/search/page/components/ExplainScorePopup.tsx b/public/app/features/search/page/components/ExplainScorePopup.tsx new file mode 100644 index 00000000000..5f14e7f7947 --- /dev/null +++ b/public/app/features/search/page/components/ExplainScorePopup.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +import { DataFrame } from '@grafana/data'; +import { CodeEditor, Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; +import { DataHoverView } from 'app/plugins/panel/geomap/components/DataHoverView'; + +export interface Props { + name: string; + explain: {}; + frame: DataFrame; + row: number; +} + +const tabs = [ + { label: 'Score', value: 'score' }, + { label: 'Fields', value: 'fields' }, +]; + +export function ExplainScorePopup({ name, explain, frame, row }: Props) { + const [isOpen, setOpen] = useState(true); + const [activeTab, setActiveTab] = useState('score'); + + const modalHeader = ( + { + setActiveTab(t.value); + }} + /> + ); + + return ( + setOpen(false)} closeOnBackdropClick closeOnEscape> + + {activeTab === tabs[0].value && ( + + )} + {activeTab === tabs[1].value && ( +
+ +
+ )} +
+
+ ); +} diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index eb88361da35..8f30e9c1671 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -282,6 +282,11 @@ const getColumnStyles = (theme: GrafanaTheme2) => { text-align: right; padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)}; `, + explainItem: css` + text-align: right; + padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)}; + cursor: pointer; + `, locationCellStyle: css` padding-top: ${theme.spacing(1)}; padding-right: ${theme.spacing(1)}; diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index ce7377a15b2..adddc05973b 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -64,6 +64,7 @@ export const SearchView = ({ ds_uid: query.datasource as string, location: folderDTO?.uid, // This will scope all results to the prefix sort: query.sort?.value, + explain: query.explain, }; // Only dashboards have additional properties diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 12219de34ab..9381df6b15b 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -5,11 +5,14 @@ import SVG from 'react-inlinesvg'; import { Field, FieldType, formattedValueToString, getDisplayProcessor, getFieldDisplayName } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; import { PluginIconName } from 'app/features/plugins/admin/types'; +import { ShowModalReactEvent } from 'app/types/events'; import { QueryResponse, SearchResultMeta } from '../../service'; import { SelectionChecker, SelectionToggle } from '../selection'; +import { ExplainScorePopup } from './ExplainScorePopup'; import { TableColumn } from './SearchResultsTable'; const TYPE_COLUMN_WIDTH = 175; @@ -40,6 +43,10 @@ export const generateColumns = ( availableWidth -= sortFieldWith; // pre-allocate the space for the last column } + if (access.explain && access.score) { + availableWidth -= 100; // pre-allocate the space for the last column + } + let width = 50; if (selection && selectionToggle) { width = 30; @@ -107,7 +114,8 @@ export const generateColumns = ( let classNames = cx(styles.nameCellStyle); let name = access.name.values.get(p.row.index); if (!name?.length) { - name = 'Missing title'; // normal for panels + const loading = p.row.index >= response.view.dataFrame.length; + name = loading ? 'Loading...' : 'Missing title'; // normal for panels classNames += ' ' + styles.missingTitleText; } return ( @@ -196,6 +204,37 @@ export const generateColumns = ( }); } + if (access.explain && access.score) { + const vals = access.score.values; + const showExplainPopup = (row: number) => { + appEvents.publish( + new ShowModalReactEvent({ + component: ExplainScorePopup, + props: { + name: access.name.values.get(row), + explain: access.explain.values.get(row), + frame: response.view.dataFrame, + row: row, + }, + }) + ); + }; + + columns.push({ + Header: () =>
Score
, + Cell: (p) => { + return ( +
showExplainPopup(p.row.index)}> + {vals.get(p.row.index)} +
+ ); + }, + id: `column-score-field`, + field: access.score, + width: 100, + }); + } + return columns; }; diff --git a/public/app/features/search/reducers/searchQueryReducer.ts b/public/app/features/search/reducers/searchQueryReducer.ts index 9def0ce0de1..84b42beed07 100644 --- a/public/app/features/search/reducers/searchQueryReducer.ts +++ b/public/app/features/search/reducers/searchQueryReducer.ts @@ -17,9 +17,6 @@ export const defaultQuery: DashboardQuery = { query: '', tag: [], starred: false, - skipRecent: false, - skipStarred: false, - folderIds: [], sort: null, layout: SearchLayout.Folders, prevSort: null, diff --git a/public/app/features/search/service/bluge.ts b/public/app/features/search/service/bluge.ts index 6d7e252fec7..e66f73d45a7 100644 --- a/public/app/features/search/service/bluge.ts +++ b/public/app/features/search/service/bluge.ts @@ -121,15 +121,12 @@ async function doSearchQuery(query: SearchQuery): Promise { } } - const view = new DataFrameView(first); - return { - totalRows: meta.count ?? first.length, - view, - loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { - console.log('LOAD NEXT PAGE', { startIndex, stopIndex, length: view.dataFrame.length }); + let loadMax = 0; + let pending: Promise | undefined = undefined; + const getNextPage = async () => { + while (loadMax > view.dataFrame.length) { const from = view.dataFrame.length; - const limit = stopIndex - from; - if (limit < 0) { + if (from >= meta.count) { return; } const frame = ( @@ -141,7 +138,7 @@ async function doSearchQuery(query: SearchQuery): Promise { search: { ...(target?.search ?? {}), from, - limit: Math.max(limit, nextPageSizes), + limit: nextPageSizes, }, refId: 'Page', facet: undefined, @@ -175,7 +172,20 @@ async function doSearchQuery(query: SearchQuery): Promise { meta.locationInfo[key] = value; } } - return; + } + pending = undefined; + }; + + const view = new DataFrameView(first); + return { + totalRows: meta.count ?? first.length, + view, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { + loadMax = Math.max(loadMax, stopIndex); + if (!pending) { + pending = getNextPage(); + } + return pending; }, isItemLoaded: (index: number): boolean => { return index < view.dataFrame.length; diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index e0a9eaf6aa9..adea9ba357d 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -33,6 +33,10 @@ export interface DashboardQueryResult { tags: string[]; location: string; // url that can be split ds_uid: string[]; + + // debugging fields + score: number; + explain: {}; } export interface LocationInfo { diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 5d16446bb9d..47068c37f34 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -61,9 +61,7 @@ export interface DashboardQuery { query: string; tag: string[]; starred: boolean; - skipRecent: boolean; - skipStarred: boolean; - folderIds: number[]; + explain?: boolean; // adds debug info datasource?: string; sort: SelectableValue | null; // Save sorting data between layouts diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index 5d2faca95f7..a2d3427990b 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -13,7 +13,11 @@ import { OrgRolePicker } from '../admin/OrgRolePicker'; export interface Props {} -const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa); +const createServiceAccount = async (sa: ServiceAccountDTO) => { + const result = await getBackendSrv().post('/api/serviceaccounts/', sa); + await contextSrv.fetchUserPermissions(); + return result; +}; const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) => getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa); @@ -44,7 +48,10 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { setRoleOptions(options); } - if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { + if ( + contextSrv.accessControlBuiltInRoleAssignmentEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) + ) { const builtInRoles = await fetchBuiltinRoles(currentOrgId); setBuiltinRoles(builtInRoles); } @@ -75,7 +82,11 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { tokens: response.tokens, }; await updateServiceAccount(response.id, data); - if (contextSrv.licensedAccessControlEnabled()) { + if ( + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove) + ) { await updateUserRoles(pendingRoles, newAccount.id, newAccount.orgId); } } catch (e) { diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index d13d484cdc4..a654a5c7278 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -11,6 +11,7 @@ jest.mock('app/core/core', () => ({ licensedAccessControlEnabled: () => false, hasPermission: () => true, hasPermissionInMetadata: () => true, + hasAccessInMetadata: () => false, }, })); diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 75d4d9add87..aaeae3bc4ae 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types'; +import { ServiceAccountPermissions } from './ServiceAccountPermissions'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; import { ServiceAccountProfile } from './components/ServiceAccountProfile'; import { ServiceAccountTokensTable } from './components/ServiceAccountTokensTable'; @@ -79,6 +80,11 @@ export const ServiceAccountPageUnconnected = ({ !contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled; const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite); + const canReadPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ServiceAccountsPermissionsRead, + serviceAccount!, + false + ); useEffect(() => { loadServiceAccount(serviceAccountId); @@ -186,7 +192,7 @@ export const ServiceAccountPageUnconnected = ({ /> )}
-

Tokens

+

Tokens

@@ -199,6 +205,7 @@ export const ServiceAccountPageUnconnected = ({ tokenActionsDisabled={tokenActionsDisabled} /> )} + {canReadPermissions && }
{ + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ServiceAccountsPermissionsWrite, + props.serviceAccount + ); + + return ( + + ); +}; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 1d87856501a..04ad61af137 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -230,23 +230,22 @@ export const ServiceAccountsListPageUnconnected = ({ )} - <> -
- - - - - - - - - - - - {!isLoading && - serviceAccounts.length !== 0 && - serviceAccounts.map((serviceAccount: ServiceAccountDTO) => ( + {!isLoading && serviceAccounts.length !== 0 && ( + <> +
+
AccountIDRolesTokens -
+ + + + + + + + + + + {serviceAccounts.map((serviceAccount: ServiceAccountDTO) => ( ))} - -
AccountIDRolesTokens +
-
- + + +
+ + )} {currentServiceAccount && ( <> -

Information

+

Information

+
@@ -124,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ neverExpire: css` color: ${theme.colors.text.secondary}; `, + section: css` + margin-bottom: ${theme.spacing(4)}; + `, }); diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 39129f7a8e7..3058878eddc 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -47,8 +47,10 @@ export function fetchACOptions(): ThunkResult { export function getApiKeysMigrationStatus(): ThunkResult { return async (dispatch) => { - const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); - dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); + if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) { + const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); + dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); + } }; } @@ -61,20 +63,22 @@ export function fetchServiceAccounts( ): ThunkResult { return async (dispatch, getState) => { try { - if (withLoadingIndicator) { - dispatch(serviceAccountsFetchBegin()); + if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) { + if (withLoadingIndicator) { + dispatch(serviceAccountsFetchBegin()); + } + const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts; + const result = await getBackendSrv().get( + `/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter( + serviceAccountStateFilter + )}&accesscontrol=true` + ); + dispatch(serviceAccountsFetched(result)); } - const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts; - const result = await getBackendSrv().get( - `/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter( - serviceAccountStateFilter - )}&accesscontrol=true` - ); - dispatch(serviceAccountsFetched(result)); } catch (error) { console.error(error); } finally { - serviceAccountsFetchEnd(); + dispatch(serviceAccountsFetchEnd()); } }; } diff --git a/public/app/features/storage/CreateNewFolderModal.tsx b/public/app/features/storage/CreateNewFolderModal.tsx new file mode 100644 index 00000000000..152e7cf55ca --- /dev/null +++ b/public/app/features/storage/CreateNewFolderModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { SubmitHandler, Validate } from 'react-hook-form'; + +import { Button, Field, Form, Input, Modal } from '@grafana/ui'; + +type FormModel = { folderName: string }; + +interface Props { + onSubmit: SubmitHandler; + onDismiss: () => void; + validate: Validate; +} + +const initialFormModel = { folderName: '' }; + +export function CreateNewFolderModal({ validate, onDismiss, onSubmit }: Props) { + return ( + +
+ {({ register, errors }) => ( + <> + + + + + + + + + )} + +
+ ); +} diff --git a/public/app/features/storage/ExportView.tsx b/public/app/features/storage/ExportView.tsx index ed983e1639c..77bec1490c5 100644 --- a/public/app/features/storage/ExportView.tsx +++ b/public/app/features/storage/ExportView.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useState } from 'react'; +import { useLocalStorage } from 'react-use'; import { isLiveChannelMessageEvent, isLiveChannelStatusEvent, LiveChannelScope } from '@grafana/data'; import { getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime'; -import { Button, CodeEditor, Modal } from '@grafana/ui'; +import { Button, CodeEditor, HorizontalGroup, LinkButton } from '@grafana/ui'; import { StorageView } from './types'; +export const EXPORT_LOCAL_STORAGE_KEY = 'grafana.export.config'; + interface ExportStatusMessage { running: boolean; target: string; @@ -18,25 +21,57 @@ interface ExportStatusMessage { status: string; } +interface ExportInclude { + auth: boolean; + ds: boolean; + dash: boolean; + services: boolean; + usage: boolean; + anno: boolean; + snapshots: boolean; +} + +interface ExportJob { + format: 'git'; + generalFolderPath: string; + history: boolean; + include: ExportInclude; + + git?: {}; +} + +const includAll: ExportInclude = { + auth: true, + ds: true, + dash: true, + services: true, + usage: true, + anno: true, + snapshots: false, // will fail until we have a real user +}; + +const defaultJob: ExportJob = { + format: 'git', + generalFolderPath: 'general', + history: true, + include: includAll, + git: {}, +}; + interface Props { onPathChange: (p: string, v?: StorageView) => void; } export const ExportView = ({ onPathChange }: Props) => { const [status, setStatus] = useState(); + const [rawBody, setBody] = useLocalStorage(EXPORT_LOCAL_STORAGE_KEY, defaultJob); + const body = { ...defaultJob, ...rawBody, include: { ...includAll, ...rawBody?.include } }; - const [open, setOpen] = useState(false); - const [body, setBody] = useState({ - format: 'git', - git: {}, - }); - const onDismiss = () => setOpen(false); const doStart = () => { - getBackendSrv() - .post('/api/admin/export', body) - .then((v) => { - onDismiss(); - }); + getBackendSrv().post('/api/admin/export', body); + }; + const doStop = () => { + getBackendSrv().post('/api/admin/export/stop'); }; useEffect(() => { @@ -56,71 +91,53 @@ export const ExportView = ({ onPathChange }: Props) => { }, }); - // if not running, open the thread - setTimeout(() => { - if (!status) { - setOpen(true); - } - }, 500); - return () => { subscription.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const renderButton = () => { - return ( - <> - -
- { - setBody(JSON.parse(text)); // force JSON? - }} - /> -
- - - - -
- - - - - ); - }; - - if (!status) { - return
{renderButton()}
; - } - return (
-
{JSON.stringify(status, null, 2)}
- {Boolean(!status.running) && renderButton()} - {Boolean(status.running) && ( - + {status && ( +
+

Status

+
{JSON.stringify(status, null, 2)}
+ {status.running && ( +
+ +
+ )} +
+ )} + + {!Boolean(status?.running) && ( +
+

Export grafana instance

+ { + setBody(JSON.parse(text)); // force JSON? + }} + /> +
+ + + + + Cancel + + +
)}
); diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index 4d53c6f1b2c..64639923997 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -1,16 +1,19 @@ import { css } from '@emotion/css'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; +import { ShowConfirmModalEvent } from 'app/types/events'; import { AddRootView } from './AddRootView'; import { Breadcrumb } from './Breadcrumb'; +import { CreateNewFolderModal } from './CreateNewFolderModal'; import { ExportView } from './ExportView'; import { FileView } from './FileView'; import { FolderView } from './FolderView'; @@ -26,8 +29,20 @@ interface QueryParams { view: StorageView; } +const folderNameRegex = /^[a-z\d!\-_.*'() ]+$/; +const folderNameMaxLength = 256; + interface Props extends GrafanaRouteComponentProps {} +const getParentPath = (path: string) => { + const lastSlashIdx = path.lastIndexOf('/'); + if (lastSlashIdx < 1) { + return ''; + } + + return path.substring(0, lastSlashIdx); +}; + export default function StoragePage(props: Props) { const styles = useStyles2(getStyles); const navModel = useNavModel('storage'); @@ -41,6 +56,8 @@ export default function StoragePage(props: Props) { locationService.push(url); }; + const [isAddingNewFolder, setIsAddingNewFolder] = useState(false); + const listing = useAsync((): Promise => { return getGrafanaStorage() .list(path) @@ -74,17 +91,26 @@ export default function StoragePage(props: Props) { let isFolder = path?.indexOf('/') < 0; if (listing.value) { const length = listing.value.length; - if (length > 1) { - isFolder = true; - } if (length === 1) { const first = listing.value.fields[0].values.get(0) as string; isFolder = !path.endsWith(first); + } else { + // TODO: handle files/folders which do not exist + isFolder = true; } } return isFolder; }, [path, listing]); + const fileNames = useMemo(() => { + return ( + listing.value?.fields + ?.find((f) => f.name === 'name') + ?.values?.toArray() + ?.filter((v) => typeof v === 'string') ?? [] + ); + }, [listing]); + const renderView = () => { const isRoot = !path?.length || path === '/'; switch (view) { @@ -135,20 +161,43 @@ export default function StoragePage(props: Props) { }); } const canAddFolder = isFolder && path.startsWith('resources'); - const canDelete = !isFolder && path.startsWith('resources/'); + const canDelete = path.startsWith('resources/'); return (
- + -
- {canAddFolder && } + + {canAddFolder && } {canDelete && ( - )} -
+
@@ -166,6 +215,41 @@ export default function StoragePage(props: Props) { ) : ( )} + + {isAddingNewFolder && ( + { + const folderPath = `${path}/${folderName}`; + const res = await getGrafanaStorage().createFolder(folderPath); + if (typeof res?.error !== 'string') { + setPath(folderPath); + setIsAddingNewFolder(false); + } + }} + onDismiss={() => { + setIsAddingNewFolder(false); + }} + validate={(folderName) => { + const lowerCase = folderName.toLowerCase(); + const trimmedLowerCase = lowerCase.trim(); + const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase()); + + if (existingTrimmedLowerCaseNames.includes(trimmedLowerCase)) { + return 'A file or a folder with the same name already exists'; + } + + if (!folderNameRegex.test(lowerCase)) { + return 'Name contains illegal characters'; + } + + if (folderName.length > folderNameMaxLength) { + return `Name is too long, maximum length: ${folderNameMaxLength} characters`; + } + + return true; + }} + /> + )}
); }; diff --git a/public/app/features/storage/helper.ts b/public/app/features/storage/helper.ts index f93c15d841c..366fe02867a 100644 --- a/public/app/features/storage/helper.ts +++ b/public/app/features/storage/helper.ts @@ -8,6 +8,8 @@ export interface GrafanaStorage { get: (path: string) => Promise; list: (path: string) => Promise; upload: (folder: string, file: File) => Promise; + createFolder: (path: string) => Promise<{ error?: string }>; + delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>; } class SimpleStorage implements GrafanaStorage { @@ -34,6 +36,52 @@ class SimpleStorage implements GrafanaStorage { return undefined; } + async createFolder(path: string): Promise<{ error?: string }> { + const res = await getBackendSrv().post<{ success: boolean; message: string }>( + '/api/storage/createFolder', + JSON.stringify({ path }) + ); + + if (!res.success) { + return { + error: res.message ?? 'unknown error', + }; + } + + return {}; + } + + async deleteFolder(req: { path: string; force: boolean }): Promise<{ error?: string }> { + const res = await getBackendSrv().post<{ success: boolean; message: string }>( + `/api/storage/deleteFolder`, + JSON.stringify(req) + ); + + if (!res.success) { + return { + error: res.message ?? 'unknown error', + }; + } + + return {}; + } + + async deleteFile(req: { path: string }): Promise<{ error?: string }> { + const res = await getBackendSrv().post<{ success: boolean; message: string }>(`/api/storage/delete/${req.path}`); + + if (!res.success) { + return { + error: res.message ?? 'unknown error', + }; + } + + return {}; + } + + async delete(req: { isFolder: boolean; path: string }): Promise<{ error?: string }> { + return req.isFolder ? this.deleteFolder({ path: req.path, force: true }) : this.deleteFile({ path: req.path }); + } + async upload(folder: string, file: File): Promise { const formData = new FormData(); formData.append('folder', folder); diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 55fe3b046f3..0d69e835bf3 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { RadioButtonGroup, LinkButton, FilterInput } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction, StoreState } from 'app/types'; import { selectTotal } from '../invites/state/selectors'; @@ -37,7 +37,9 @@ export class UsersActionBar extends PureComponent { { label: 'Users', value: 'users' }, { label: `Pending Invites (${pendingInvitesCount})`, value: 'invites' }, ]; - const canAddToOrg = contextSrv.hasAccess(AccessControlAction.UsersCreate, canInvite); + const canAddToOrg: boolean = + contextSrv.hasAccess(AccessControlAction.UsersCreate, canInvite) || + contextSrv.hasAccess(AccessControlAction.OrgUsersAdd, canInvite); return (
@@ -64,7 +66,7 @@ export class UsersActionBar extends PureComponent { } } -function mapStateToProps(state: any) { +function mapStateToProps(state: StoreState) { return { searchQuery: getUsersSearchQuery(state.users), pendingInvitesCount: selectTotal(state.invites), diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index bcb8d3a7584..d662e226a76 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -52,6 +52,7 @@ export function setupMockedDataSource({ datasource.getNamespaces = jest.fn().mockResolvedValue([]); datasource.getRegions = jest.fn().mockResolvedValue([]); + datasource.defaultLogGroups = []; const fetchMock = jest.fn().mockReturnValue(of({ data })); setBackendSrv({ fetch: fetchMock } as any); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index ac4ca34f1e1..26b998f6514 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -1,71 +1,92 @@ +import { render, screen } from '@testing-library/react'; import { shallow } from 'enzyme'; import React from 'react'; +import selectEvent from 'react-select-event'; import { AwsAuthType } from '@grafana/aws-sdk'; +import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; + import { ConfigEditor, Props } from './ConfigEditor'; +const ds = setupMockedDataSource(); + jest.mock('app/features/plugins/datasource_srv', () => ({ getDatasourceSrv: () => ({ - loadDatasource: jest.fn().mockImplementation(() => - Promise.resolve({ - getRegions: jest.fn().mockReturnValue([ - { - label: 'ap-east-1', - value: 'ap-east-1', - }, - ]), - }) - ), + loadDatasource: jest.fn().mockResolvedValue({ + getRegions: jest.fn().mockResolvedValue([ + { + label: 'ap-east-1', + value: 'ap-east-1', + }, + ]), + describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar']), + getActualRegion: jest.fn().mockReturnValue('ap-east-1'), + getVariables: jest.fn().mockReturnValue([]), + }), }), })); -const setup = (propOverrides?: object) => { - const props: Props = { - options: { - id: 1, - uid: 'z', - orgId: 1, - typeLogoUrl: '', - name: 'CloudWatch', - access: 'proxy', - url: '', - database: '', - type: 'cloudwatch', - typeName: 'Cloudwatch', - user: '', - basicAuth: false, - basicAuthUser: '', - isDefault: true, - readOnly: false, - withCredentials: false, - secureJsonFields: { - accessKey: false, - secretKey: false, - }, - jsonData: { - assumeRoleArn: '', - externalId: '', - database: '', - customMetricsNamespaces: '', - authType: AwsAuthType.Keys, - defaultRegion: 'us-east-2', - timeField: '@timestamp', - }, - secureJsonData: { - secretKey: '', - accessKey: '', - }, +jest.mock('./XrayLinkConfig', () => ({ + XrayLinkConfig: () => <>, +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => ({ + put: jest.fn().mockResolvedValue({ datasource: ds.datasource }), + }), +})); + +const props: Props = { + options: { + id: 1, + uid: 'z', + orgId: 1, + typeLogoUrl: '', + name: 'CloudWatch', + access: 'proxy', + url: '', + database: '', + type: 'cloudwatch', + typeName: 'Cloudwatch', + user: '', + basicAuth: false, + basicAuthUser: '', + isDefault: true, + readOnly: false, + withCredentials: false, + secureJsonFields: { + accessKey: false, + secretKey: false, }, - onOptionsChange: jest.fn(), - }; + jsonData: { + assumeRoleArn: '', + externalId: '', + database: '', + customMetricsNamespaces: '', + authType: AwsAuthType.Keys, + defaultRegion: 'us-east-2', + timeField: '@timestamp', + }, + secureJsonData: { + secretKey: '', + accessKey: '', + }, + }, + onOptionsChange: jest.fn(), +}; - Object.assign(props, propOverrides); +const setup = (propOverrides?: object) => { + const newProps = { ...props, ...propOverrides }; - return shallow(); + return shallow(); }; describe('Render', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); it('should render component', () => { const wrapper = setup(); @@ -107,4 +128,15 @@ describe('Render', () => { }); expect(wrapper).toMatchSnapshot(); }); + + it('should load log groups when multiselect is opened', async () => { + (window as any).grafanaBootData = { + settings: {}, + }; + + render(); + const multiselect = await screen.findByLabelText('Log Groups'); + selectEvent.openMenu(multiselect); + expect(await screen.findByText('logGroup-foo')).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index 7d54e625375..7eb6a4567cd 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -7,7 +7,9 @@ import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOption, updateDatasourcePluginJsonDataOption, + updateDatasourcePluginOption, } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; import { Input, InlineField } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createWarningNotification } from 'app/core/copy/appNotification'; @@ -17,16 +19,43 @@ import { store } from 'app/store/store'; import { CloudWatchDatasource } from '../datasource'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types'; +import { LogGroupSelector } from './LogGroupSelector'; import { XrayLinkConfig } from './XrayLinkConfig'; export type Props = DataSourcePluginOptionsEditorProps; export const ConfigEditor: FC = (props: Props) => { const { options } = props; + const { defaultLogGroups, logsTimeout, defaultRegion } = options.jsonData; + const [saved, setSaved] = useState(!!options.version && options.version > 1); - const datasource = useDatasource(options.name); + const datasource = useDatasource(options.name, saved); useAuthenticationWarning(options.jsonData); - const logsTimeoutError = useTimoutValidation(props.options.jsonData.logsTimeout); + const logsTimeoutError = useTimoutValidation(logsTimeout); + useEffect(() => { + setSaved(false); + }, [ + props.options.jsonData.assumeRoleArn, + props.options.jsonData.authType, + props.options.jsonData.defaultRegion, + props.options.jsonData.endpoint, + props.options.jsonData.externalId, + props.options.jsonData.profile, + props.options.secureJsonData?.accessKey, + props.options.secureJsonData?.secretKey, + ]); + + const saveOptions = async (): Promise => { + if (saved) { + return; + } + await getBackendSrv() + .put(`/api/datasources/${options.id}`, options) + .then((result: { datasource: any }) => { + updateDatasourcePluginOption(props, 'version', result.datasource.version); + }); + setSaved(true); + }; return ( <> @@ -52,7 +81,7 @@ export const ConfigEditor: FC = (props: Props) => { = (props: Props) => { title={'The timeout must be a valid duration string, such as "15m" "30s" "2000ms" etc.'} /> + + { + updateDatasourcePluginJsonDataOption(props, 'defaultLogGroups', logGroups); + }} + onOpenMenu={saveOptions} + width={60} + saved={saved} + /> +
(); useEffect(() => { + // reload the datasource when it's saved + if (!saved) { + return; + } getDatasourceSrv() .loadDatasource(datasourceName) .then((datasource) => { @@ -102,7 +152,7 @@ function useDatasource(datasourceName: string) { // So a "as" type assertion here is a necessary evil. setDatasource(datasource as CloudWatchDatasource); }); - }, [datasourceName]); + }, [datasourceName, saved]); return datasource; } diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx index 519b4ebed3c..91ebf4d32eb 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.test.tsx @@ -85,7 +85,6 @@ describe('LogGroupSelector', () => { 'DeliciousGroup', 'DeliciousGroup2', 'DeliciousGroup3', - 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3', diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx index b3dc5a0654a..5ad1dcbdad7 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import React from 'react'; import { act } from 'react-dom/test-utils'; @@ -34,4 +34,25 @@ describe('CloudWatchLogsQueryField', () => { }); expect(onRunQuery).toHaveBeenCalled(); }); + + it('loads defaultLogGroups', async () => { + const onRunQuery = jest.fn(); + const ds = setupMockedDataSource(); + ds.datasource.defaultLogGroups = ['foo']; + + render( + {}} + /> + ); + + await waitFor(() => { + expect(screen.getByText('foo')).toBeInTheDocument(); + }); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index ab1815eda45..259b9aeb6b5 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -66,10 +66,10 @@ export class CloudWatchLogsQueryField extends React.PureComponent { - const { query, onChange } = this.props; + const { query, datasource, onChange } = this.props; if (onChange) { - onChange({ ...query, logGroupNames: query.logGroupNames ?? [] }); + onChange({ ...query, logGroupNames: query.logGroupNames ?? datasource.defaultLogGroups }); } }; @@ -135,7 +135,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent + + + + + + + + + + + + + + + ): Observable => { - const validLogQueries = logQueries.filter((item) => item.logGroupNames?.length); + const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({ + queryString: target.expression || '', + refId: target.refId, + logGroupNames: target.logGroupNames || this.defaultLogGroups, + region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'), + })); + + const validLogQueries = queryParams.filter((item) => item.logGroupNames?.length); if (logQueries.length > validLogQueries.length) { return of({ data: [], error: { message: 'Log group is required' } }); } @@ -186,13 +195,6 @@ export class CloudWatchDatasource return of({ data: [], state: LoadingState.Done }); } - const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({ - queryString: target.expression || '', - refId: target.refId, - logGroupNames: target.logGroupNames, - region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'), - })); - const startTime = new Date(); const timeoutFunc = () => { return Date.now() >= startTime.valueOf() + rangeUtil.intervalToMs(this.logsTimeout); diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index d9428d8581a..668dad3e861 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -121,6 +121,7 @@ export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData { logsTimeout?: string; // Used to create links if logs contain traceId. tracingDatasourceUid?: string; + defaultLogGroups?: string[]; } export interface CloudWatchSecureJsonData extends AwsAuthDataSourceSecureJsonData { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 0f7273eda6d..f1c06ce1a70 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -1,7 +1,7 @@ import { find, startsWith } from 'lodash'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { resourceTypeDisplayNames, supportedMetricNamespaces } from '../azureMetadata'; @@ -42,11 +42,13 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend) { super(instanceSettings); this.timeSrv = getTimeSrv(); + this.templateSrv = getTemplateSrv(); this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId; const cloud = getAzureCloud(instanceSettings); @@ -244,7 +246,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { @@ -273,7 +276,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value'); @@ -285,7 +289,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { return ResponseParser.parseMetadata(result, metricName); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts index 0e3a01293ef..a7103ef1046 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts @@ -1,33 +1,96 @@ +import { getTemplateSrv } from '@grafana/runtime'; + import UrlBuilder from './url_builder'; +let replaceMock = jest.fn().mockImplementation((s: string) => s); + +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + getTemplateSrv: () => ({ + replace: replaceMock, + }), + }; +}); + describe('AzureMonitorUrlBuilder', () => { + let templateSrv = getTemplateSrv(); describe('buildResourceUri', () => { it('builds a resource uri when the required properties are provided', () => { - expect(UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.NetApp/netAppAccounts', 'name')).toEqual( - '/subscriptions/sub/resourceGroups/group/providers/Microsoft.NetApp/netAppAccounts/name' - ); + expect( + UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.NetApp/netAppAccounts', 'name', templateSrv) + ).toEqual('/subscriptions/sub/resourceGroups/group/providers/Microsoft.NetApp/netAppAccounts/name'); }); it('builds a resource uri correctly when a template variable is used as namespace', () => { - expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns', 'name')).toEqual( + expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns', 'name', templateSrv)).toEqual( '/subscriptions/sub/resourceGroups/group/providers/$ns/name' ); }); it('builds a resource uri correctly when the namespace includes a storage sub-resource', () => { expect( - UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.Storage/storageAccounts/tableServices', 'name') + UrlBuilder.buildResourceUri( + 'sub', + 'group', + 'Microsoft.Storage/storageAccounts/tableServices', + 'name', + templateSrv + ) ).toEqual( '/subscriptions/sub/resourceGroups/group/providers/Microsoft.Storage/storageAccounts/name/tableServices/default' ); }); + + describe('when using template variables', () => { + replaceMock = jest + .fn() + .mockImplementation((s: string) => + s + .replace('$ns', 'Microsoft.Storage/storageAccounts') + .replace('$ns2', 'tableServices') + .replace('$rs', 'name') + .replace('$rs2', 'default') + ); + templateSrv = getTemplateSrv(); + + it('builds a resource uri without specifying a subresource (default)', () => { + expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', 'name', templateSrv)).toEqual( + '/subscriptions/sub/resourceGroups/group/providers/$ns/name/tableServices/default' + ); + }); + + it('builds a resource uri specifying a subresource (default)', () => { + expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', 'name/default', templateSrv)).toEqual( + '/subscriptions/sub/resourceGroups/group/providers/$ns/name/tableServices/default' + ); + }); + + it('builds a resource uri specifying a resource template variable', () => { + expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', '$rs/default', templateSrv)).toEqual( + '/subscriptions/sub/resourceGroups/group/providers/$ns/$rs/tableServices/default' + ); + }); + + it('builds a resource uri specifying multiple template variables', () => { + expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/$ns2', '$rs/$rs2', templateSrv)).toEqual( + '/subscriptions/sub/resourceGroups/group/providers/$ns/$rs/$ns2/$rs2' + ); + }); + }); }); describe('when a resource uri is provided', () => { it('builds a getMetricNamesnamespace url', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { - resourceUri: '/subscriptions/sub/resource-uri/resource', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( + '', + '2017-05-01-preview', + { + resourceUri: '/subscriptions/sub/resource-uri/resource', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' ); @@ -36,10 +99,15 @@ describe('AzureMonitorUrlBuilder', () => { describe('when a resource uri and metric namespace is provided', () => { it('builds a getMetricNames url', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - resourceUri: '/subscriptions/sub/resource-uri/resource', - metricNamespace: 'Microsoft.Sql/servers', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + resourceUri: '/subscriptions/sub/resource-uri/resource', + metricNamespace: 'Microsoft.Sql/servers', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers' ); @@ -49,12 +117,17 @@ describe('AzureMonitorUrlBuilder', () => { describe('Legacy query object', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => { it('should build the getMetricNamespaces url in the even longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', - resourceName: 'rn1/rn2/rn3', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', + resourceName: 'rn1/rn2/rn3', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' @@ -64,12 +137,17 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Sql/servers/databases', () => { it('should build the getMetricNamespaces url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Sql/servers/databases', - resourceName: 'rn1/rn2', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Sql/servers/databases', + resourceName: 'rn1/rn2', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' @@ -79,12 +157,17 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Sql/servers', () => { it('should build the getMetricNamespaces url in the shorter format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Sql/servers', - resourceName: 'rn', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Sql/servers', + resourceName: 'rn', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' @@ -94,13 +177,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => { it('should build the getMetricNames url in the even longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', - resourceName: 'rn1/rn2/rn3', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', + resourceName: 'rn1/rn2/rn3', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -110,13 +198,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => { it('should build the getMetricNames url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Sql/servers/databases', - resourceName: 'rn1/rn2', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Sql/servers/databases', + resourceName: 'rn1/rn2', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -126,13 +219,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => { it('should build the getMetricNames url in the shorter format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Sql/servers', - resourceName: 'rn', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Sql/servers', + resourceName: 'rn', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -142,13 +240,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => { it('should build the getMetricNames url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Storage/storageAccounts/blobServices', - resourceName: 'rn1/default', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Storage/storageAccounts/blobServices', + resourceName: 'rn1/default', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -158,13 +261,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => { it('should build the getMetricNames url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Storage/storageAccounts/fileServices', - resourceName: 'rn1/default', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Storage/storageAccounts/fileServices', + resourceName: 'rn1/default', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -174,13 +282,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => { it('should build the getMetricNames url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Storage/storageAccounts/tableServices', - resourceName: 'rn1/default', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Storage/storageAccounts/tableServices', + resourceName: 'rn1/default', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' @@ -190,13 +303,18 @@ describe('AzureMonitorUrlBuilder', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => { it('should build the getMetricNames url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { - subscription: 'sub1', - resourceGroup: 'rg', - metricDefinition: 'Microsoft.Storage/storageAccounts/queueServices', - resourceName: 'rn1/default', - metricNamespace: 'default', - }); + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'Microsoft.Storage/storageAccounts/queueServices', + resourceName: 'rn1/default', + metricNamespace: 'default', + }, + templateSrv + ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' + 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts index 51dd5a80b35..d52df58b96e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts @@ -1,3 +1,5 @@ +import { TemplateSrv } from '@grafana/runtime'; + import { GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types'; export default class UrlBuilder { @@ -5,40 +7,65 @@ export default class UrlBuilder { subscriptionId: string, resourceGroup: string, metricDefinition: string, - resourceName: string + resourceName: string, + templateSrv: TemplateSrv ) { + const metricDefinitionProcessed = templateSrv.replace(metricDefinition); const metricDefinitionArray = metricDefinition.split('/'); + const resourceNameProcessed = templateSrv.replace(resourceName); const resourceNameArray = resourceName.split('/'); const provider = metricDefinitionArray.shift(); const urlArray = ['/subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider]; - if (metricDefinition.startsWith('Microsoft.Storage/storageAccounts/') && resourceNameArray.at(-1) !== 'default') { + + if ( + metricDefinitionProcessed.startsWith('Microsoft.Storage/storageAccounts/') && + !resourceNameProcessed.endsWith('default') + ) { resourceNameArray.push('default'); } - if (metricDefinitionArray.length > 0) { - for (const i in metricDefinitionArray) { - urlArray.push(metricDefinitionArray[i]); - urlArray.push(resourceNameArray[i]); - } - } else { - urlArray.push(resourceNameArray[0]); + + if (resourceNameArray.length > metricDefinitionArray.length) { + const parentResource = resourceNameArray.shift(); + urlArray.push(parentResource); + } + + for (const i in metricDefinitionArray) { + urlArray.push(metricDefinitionArray[i]); + urlArray.push(resourceNameArray[i]); } return urlArray.join('/'); } - static buildAzureMonitorGetMetricNamespacesUrl(baseUrl: string, apiVersion: string, query: GetMetricNamespacesQuery) { + static buildAzureMonitorGetMetricNamespacesUrl( + baseUrl: string, + apiVersion: string, + query: GetMetricNamespacesQuery, + templateSrv: TemplateSrv + ) { let resourceUri: string; if ('resourceUri' in query) { resourceUri = query.resourceUri; } else { const { subscription, resourceGroup, metricDefinition, resourceName } = query; - resourceUri = UrlBuilder.buildResourceUri(subscription, resourceGroup, metricDefinition, resourceName); + resourceUri = UrlBuilder.buildResourceUri( + subscription, + resourceGroup, + metricDefinition, + resourceName, + templateSrv + ); } return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`; } - static buildAzureMonitorGetMetricNamesUrl(baseUrl: string, apiVersion: string, query: GetMetricNamesQuery) { + static buildAzureMonitorGetMetricNamesUrl( + baseUrl: string, + apiVersion: string, + query: GetMetricNamesQuery, + templateSrv: TemplateSrv + ) { let resourceUri: string; const { metricNamespace } = query; @@ -46,7 +73,13 @@ export default class UrlBuilder { resourceUri = query.resourceUri; } else { const { subscription, resourceGroup, metricDefinition, resourceName } = query; - resourceUri = UrlBuilder.buildResourceUri(subscription, resourceGroup, metricDefinition, resourceName); + resourceUri = UrlBuilder.buildResourceUri( + subscription, + resourceGroup, + metricDefinition, + resourceName, + templateSrv + ); } return ( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/usePreparedQuery.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/usePreparedQuery.ts index 560d734cbf6..9ed96bb1103 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/usePreparedQuery.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/usePreparedQuery.ts @@ -2,6 +2,8 @@ import deepEqual from 'fast-deep-equal'; import { defaults } from 'lodash'; import { useEffect, useMemo } from 'react'; +import { getTemplateSrv } from '@grafana/runtime'; + import { AzureMonitorQuery, AzureQueryType } from '../../types'; import migrateQuery from '../../utils/migrateQuery'; @@ -12,7 +14,7 @@ const DEFAULT_QUERY = { const prepareQuery = (query: AzureMonitorQuery) => { // Note: _.defaults does not apply default values deeply. const withDefaults = defaults({}, query, DEFAULT_QUERY); - const migratedQuery = migrateQuery(withDefaults); + const migratedQuery = migrateQuery(withDefaults, getTemplateSrv()); // If we didn't make any changes to the object, then return the original object to keep the // identity the same, and not trigger any other useEffects or anything. diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index 6110e9d2ae3..9906180e042 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -70,7 +70,7 @@ export default class Datasource extends DataSourceWithBackend
Name