From f3a3ce76916b57266b6b94a85b845a00cd941ae2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:27:15 +0000 Subject: [PATCH 01/55] Update dependency react-hook-form to v7.53.2 (#96301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 458faf9ef15..6d4af9977c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27278,11 +27278,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.53.0 - resolution: "react-hook-form@npm:7.53.0" + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/b7d73696b7c10e042f6ea6fcec01f951091146bfbc89d1378327a970bcd724b968e93fae1657bddada75caf648cfaf8693c5ba03c25e96816b755079d29f65da + checksum: 10/172589ac5871bd41717e8c6bc4c825d149db88eb02e356a8fef4115615a56b21bf66ad16954575b1d6302ed265b8780f1975a2730d5aa10a53e412b56d9f72ca languageName: node linkType: hard From 095be20f8ea6f3129e72d0389fb2dc835bcd725b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:30:09 +0000 Subject: [PATCH 02/55] Update dependency react-select to v5.8.3 (#96302) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 30 +++++++++---------- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index b98d85a4cf4..61d23688535 100644 --- a/package.json +++ b/package.json @@ -380,7 +380,7 @@ "react-router": "5.3.3", "react-router-dom": "5.3.3", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 964787d4819..577a6adcd29 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "11.4.0-pre", "@grafana/schema": "11.4.0-pre", "@grafana/ui": "11.4.0-pre", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index cb9b5d14e41..209390e75d3 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -65,7 +65,7 @@ "pluralize": "8.0.0", "prismjs": "1.29.0", "react-highlight-words": "0.20.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "react-window": "1.8.10", "rxjs": "7.8.1", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 32105e354ee..611de96aad4 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -25,7 +25,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "react-virtualized-auto-sizer": "1.0.24", "rxjs": "7.8.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f7f631e3d05..a04da9f920f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -99,7 +99,7 @@ "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.5.0", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.5.1", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index acb1e3ad106..638e8ab640f 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -19,7 +19,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 38d7ca2d9bd..05cc3443b98 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -20,7 +20,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 1362fc24caa..59a588ad818 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -15,7 +15,7 @@ "micro-memoize": "^4.1.2", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 2ad68c2bf4f..30c32a5e00e 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -15,7 +15,7 @@ "logfmt": "^1.3.2", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-window": "1.8.10", "rxjs": "7.8.1", "stream-browserify": "3.0.0", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 1bb877f9529..7bac25d8881 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -29,7 +29,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "semver": "7.6.3", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 84bf1925c04..86fb5a2eb05 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -14,7 +14,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/yarn.lock b/yarn.lock index 6d4af9977c7..bc10b07814d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3079,7 +3079,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" @@ -3190,7 +3190,7 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -3231,7 +3231,7 @@ __metadata: logfmt: "npm:^1.3.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-window: "npm:1.8.10" rxjs: "npm:7.8.1" stream-browserify: "npm:3.0.0" @@ -3372,7 +3372,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.1" @@ -3429,7 +3429,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" @@ -3469,7 +3469,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -3819,7 +3819,7 @@ __metadata: "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" react: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-jest: "npm:29.2.5" @@ -3948,7 +3948,7 @@ __metadata: react: "npm:18.2.0" react-dom: "npm:18.2.0" react-highlight-words: "npm:0.20.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.1" @@ -4143,7 +4143,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" react-virtualized-auto-sizer: "npm:1.0.24" rxjs: "npm:7.8.1" @@ -4277,7 +4277,7 @@ __metadata: react-inlinesvg: "npm:3.0.2" react-loading-skeleton: "npm:3.5.0" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:^5.1.0" react-table: "npm:7.8.0" react-test-renderer: "npm:18.2.0" @@ -18996,7 +18996,7 @@ __metadata: react-router: "npm:5.3.3" react-router-dom: "npm:5.3.3" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" @@ -27662,9 +27662,9 @@ __metadata: languageName: node linkType: hard -"react-select@npm:5.8.2": - version: 5.8.2 - resolution: "react-select@npm:5.8.2" +"react-select@npm:5.8.3": + version: 5.8.3 + resolution: "react-select@npm:5.8.3" dependencies: "@babel/runtime": "npm:^7.12.0" "@emotion/cache": "npm:^11.4.0" @@ -27678,7 +27678,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/143f3974829d6abc79a80ab79e1daaa054ad5120064c8f840d2d224cc60a853896a68ef3451f90b9997751ba17e7d558bd8875bed7be19ea3166a491d10783c7 + checksum: 10/16a4543b1d9ffbf54dd2f749cc5ff765254ad96186fbdac164c1e20e3c40a7c64b726e020050775b3d54d10b9ac6e60ff9eb7fd443010982496dad814958e5fa languageName: node linkType: hard From ae95a6158fad6c2b8de5a05cd830012584079ae0 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 12 Nov 2024 15:30:34 +0100 Subject: [PATCH 03/55] Zanana: Remove opefga from client and implement compile (#96275) Zanana: Remove opefga from client and implement Compile --- pkg/services/authz/zanzana/client/client.go | 35 +++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 01e7ed8e60b..819cf2b91a5 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/authlib/authz" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/claims" - openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel" "google.golang.org/grpc" @@ -37,7 +36,6 @@ func WithLogger(logger log.Logger) ClientOption { type Client struct { logger log.Logger - openfga openfgav1.OpenFGAServiceClient authz authzv1.AuthzServiceClient authzext authzextv1.AuthzExtentionServiceClient tenantID string @@ -59,7 +57,6 @@ func NewClient(ctx context.Context, cc grpc.ClientConnInterface, cfg *setting.Cf func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) (*Client, error) { c := &Client{ - openfga: openfgav1.NewOpenFGAServiceClient(cc), authz: authzv1.NewAuthzServiceClient(cc), authzext: authzextv1.NewAuthzExtentionServiceClient(cc), } @@ -103,7 +100,7 @@ func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.List ctx, span := tracer.Start(ctx, "authz.zanzana.client.Compile") defer span.End() - _, err := c.authzext.List(ctx, &authzextv1.ListRequest{ + res, err := c.authzext.List(ctx, &authzextv1.ListRequest{ Subject: id.GetUID(), Group: req.Group, Verb: utils.VerbList, @@ -115,8 +112,34 @@ func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.List return nil, err } - // FIXME: implement checker - return func(namespace, name, folder string) bool { return false }, nil + return newItemChecker(res), nil +} + +func newItemChecker(res *authzextv1.ListResponse) authz.ItemChecker { + // if we can see all resource of this type we can just return a function that always return true + if res.GetAll() { + return func(_, _, _ string) bool { return true } + } + + folders := make(map[string]struct{}, len(res.Folders)) + for _, f := range res.Folders { + folders[f] = struct{}{} + } + + items := make(map[string]struct{}, len(res.Items)) + for _, i := range res.Items { + items[i] = struct{}{} + } + + return func(_, name, folder string) bool { + if _, ok := items[name]; ok { + return true + } + if _, ok := folders[folder]; ok { + return true + } + return false + } } func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) { From 6e7de36a67a97c6970724c2103183b249ebe754b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=B1or=20Performo=20-=20Leandro=20Melendez?= <54183040+srperf@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:51:57 -0600 Subject: [PATCH 04/55] docs: Fixed title wording from bar gauge to canvas (#96312) --- .../panels-visualizations/visualizations/canvas/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 0131e1688ae..5e4cdaab205 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -43,7 +43,7 @@ With all of these dynamic elements, there's almost no limit to what a canvas can We'd love your feedback on the canvas visualization. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fcanvas) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fcanvas&title=Canvas:&projects=grafana-dataviz&template=1-feature_requests.md) as needed. {{< /admonition >}} -## Configure a bar gauge visualization +## Configure a canvas visualization The following video shows you how to create and configure a canvas visualization: From 3a6858cf2602207da6e351f7fa6beb1d18fd35ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Tue, 12 Nov 2024 16:07:27 +0100 Subject: [PATCH 05/55] Frontend o11y: Report browser crashes to Faro (#95772) * Report browser crashes to Faro * Fix linting * Change context log context prefix * Update types * Update crash detection library to report stale tabs * Post merge fixes --- .../feature-toggles/index.md | 1 + package.json | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 15 +++- public/app/app.ts | 5 ++ public/app/core/crash/client.worker.ts | 7 ++ public/app/core/crash/crash.utils.ts | 58 +++++++++++++ public/app/core/crash/detector.worker.ts | 8 ++ public/app/core/crash/index.ts | 81 +++++++++++++++++++ yarn.lock | 8 ++ 13 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 public/app/core/crash/client.worker.ts create mode 100644 public/app/core/crash/crash.utils.ts create mode 100644 public/app/core/crash/detector.worker.ts create mode 100644 public/app/core/crash/index.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 80e8e20257d..61dee43370e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -222,6 +222,7 @@ Experimental features might be changed or removed without prior notice. | `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | | `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy | | `enableSCIM` | Enables SCIM support for user and group management | +| `crashDetection` | Enables browser crash detection reporting to Faro. | ## Development feature toggles diff --git a/package.json b/package.json index 61d23688535..830d19cde25 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "codeowners": "^5.1.1", "copy-webpack-plugin": "12.0.2", "core-js": "3.38.1", + "crashme": "0.0.15", "css-loader": "7.1.2", "css-minimizer-webpack-plugin": "6.0.0", "cypress": "13.10.0", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0081e6fc721..d3dbd6c2c7f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -236,4 +236,5 @@ export interface FeatureToggles { enableExtensionsAdminPage?: boolean; zipkinBackendMigration?: boolean; enableSCIM?: boolean; + crashDetection?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 836bed5b50b..687326cefb8 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1628,6 +1628,13 @@ var ( Stage: FeatureStageExperimental, Owner: identityAccessTeam, }, + { + Name: "crashDetection", + Description: "Enables browser crash detection reporting to Faro.", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityTracesAndProfilingSquad, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 68295315b36..cac9fde8832 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -217,3 +217,4 @@ exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,fals enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false zipkinBackendMigration,experimental,@grafana/oss-big-tent,false,false,false enableSCIM,experimental,@grafana/identity-access-team,false,false,false +crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 70c0813d282..1656aa935c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -878,4 +878,8 @@ const ( // FlagEnableSCIM // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" + + // FlagCrashDetection + // Enables browser crash detection reporting to Faro. + FlagCrashDetection = "crashDetection" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bde889b2f00..34c1e5e5f78 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -855,6 +855,19 @@ "expression": "true" } }, + { + "metadata": { + "name": "crashDetection", + "resourceVersion": "1730381712885", + "creationTimestamp": "2024-10-31T13:35:12Z" + }, + "spec": { + "description": "Enables browser crash detection reporting to Faro.", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, { "metadata": { "name": "dashboardNewLayouts", @@ -3470,4 +3483,4 @@ } } ] -} \ No newline at end of file +} diff --git a/public/app/app.ts b/public/app/app.ts index 052a3382a58..dad577603d8 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -56,6 +56,7 @@ import { AppChromeService } from './core/components/AppChrome/AppChromeService'; import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry'; import { PluginPage } from './core/components/Page/PluginPage'; import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext'; +import { initializeCrashDetection } from './core/crash'; import { initIconCache } from './core/icons/iconBundle'; import { initializeI18n } from './core/internationalization'; import { setMonacoEnv } from './core/monacoEnv'; @@ -267,6 +268,10 @@ export class GrafanaApp { initializeScopes(); + if (config.featureToggles.crashDetection) { + initializeCrashDetection(); + } + const root = createRoot(document.getElementById('reactRoot')!); root.render( createElement(AppWrapper, { diff --git a/public/app/core/crash/client.worker.ts b/public/app/core/crash/client.worker.ts new file mode 100644 index 00000000000..e3afa5526a4 --- /dev/null +++ b/public/app/core/crash/client.worker.ts @@ -0,0 +1,7 @@ +import { initClientWorker } from 'crashme'; + +initClientWorker({ + dbName: 'grafana.crashes', + // How often the tab will report its state + pingInterval: 1000, +}); diff --git a/public/app/core/crash/crash.utils.ts b/public/app/core/crash/crash.utils.ts new file mode 100644 index 00000000000..ba9367a8923 --- /dev/null +++ b/public/app/core/crash/crash.utils.ts @@ -0,0 +1,58 @@ +import { LogContext } from '@grafana/faro-core/dist/types/api/logs/types'; + +export interface ChromePerformanceMemory { + totalJSHeapSize: number; + usedJSHeapSize: number; + jsHeapSizeLimit: number; +} + +export interface ChromePerformance { + memory: ChromePerformanceMemory; +} + +function isChromePerformanceMemory(memory: unknown): memory is ChromePerformanceMemory { + if (!memory || typeof memory !== 'object') { + return false; + } + + return 'totalJSHeapSize' in memory && 'usedJSHeapSize' in memory && 'jsHeapSizeLimit' in memory; +} + +export function isChromePerformance(performance: unknown): performance is ChromePerformance { + if (!performance || typeof performance !== 'object') { + return false; + } + + return 'memory' in performance && isChromePerformanceMemory(performance.memory); +} + +/** + * Ensures the context is a flat object with strings (required by Faro) + */ +export function prepareContext(context: Object): LogContext { + const preparedContext: LogContext = {}; + function prepare(value: object | string | number, propertyName: string) { + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + throw new Error('Array values are not supported.'); + } else { + for (const key in value) { + if (value.hasOwnProperty(key)) { + // @ts-ignore + prepare(value[key], propertyName ? `${propertyName}_${key}` : key); + } + } + } + } else if (typeof value === 'string') { + preparedContext[propertyName] = value; + } else if (typeof value === 'number') { + if (Number.isInteger(value)) { + preparedContext[propertyName] = value.toString(); + } else { + preparedContext[propertyName] = value.toFixed(4); + } + } + } + prepare(context, 'crash'); + return preparedContext; +} diff --git a/public/app/core/crash/detector.worker.ts b/public/app/core/crash/detector.worker.ts new file mode 100644 index 00000000000..58411a3d534 --- /dev/null +++ b/public/app/core/crash/detector.worker.ts @@ -0,0 +1,8 @@ +import { initDetectorWorker } from 'crashme'; + +initDetectorWorker({ + dbName: 'grafana.crashes', + interval: 5000, + crashThreshold: 5000, + staleThreshold: 5000, +}); diff --git a/public/app/core/crash/index.ts b/public/app/core/crash/index.ts new file mode 100644 index 00000000000..a2f54a9d866 --- /dev/null +++ b/public/app/core/crash/index.ts @@ -0,0 +1,81 @@ +import { initCrashDetection } from 'crashme'; +import { BaseStateReport } from 'crashme/dist/types'; +import { nanoid } from 'nanoid'; + +import { config, createMonitoringLogger } from '@grafana/runtime'; + +import { contextSrv } from '../services/context_srv'; + +import { isChromePerformance, prepareContext } from './crash.utils'; + +const logger = createMonitoringLogger('core.crash-detection'); + +interface GrafanaCrashReport extends BaseStateReport { + app: { + version: string; + url: string; + }; + user: { + email: string; + login: string; + name: string; + }; + memory?: { + heapUtilization: number; + limitUtilization: number; + usedJSHeapSize: number; + totalJSHeapSize: number; + jsHeapSizeLimit: number; + }; +} + +export function initializeCrashDetection() { + initCrashDetection({ + id: nanoid(5), + + dbName: 'grafana.crashes', + + createClientWorker(): Worker { + return new Worker(new URL('./client.worker', import.meta.url)); + }, + + createDetectorWorker(): SharedWorker { + return new SharedWorker(new URL('./detector.worker', import.meta.url)); + }, + + reportCrash: async (report) => { + const preparedContext = prepareContext(report); + logger.logWarning('browser crash detected', preparedContext); + return true; + }, + + reportStaleTab: async (report) => { + const preparedContext = prepareContext(report); + logger.logWarning('stale browser tab detected', preparedContext); + return true; + }, + + updateInfo: (info) => { + info.app = { + version: config.buildInfo.version, + url: window.location.href, + }; + + info.user = { + email: contextSrv.user.email, + login: contextSrv.user.login, + name: contextSrv.user.name, + }; + + if (isChromePerformance(performance)) { + info.memory = { + heapUtilization: performance.memory.usedJSHeapSize / performance.memory.totalJSHeapSize, + limitUtilization: performance.memory.totalJSHeapSize / performance.memory.jsHeapSizeLimit, + usedJSHeapSize: performance.memory.usedJSHeapSize, + totalJSHeapSize: performance.memory.totalJSHeapSize, + jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, + }; + } + }, + }); +} diff --git a/yarn.lock b/yarn.lock index bc10b07814d..43186bbfe74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14324,6 +14324,13 @@ __metadata: languageName: node linkType: hard +"crashme@npm:0.0.15": + version: 0.0.15 + resolution: "crashme@npm:0.0.15" + checksum: 10/576110b3d61f996869b993c2f6b8dca33ac82d07ea708b39217b6349653e38a9894c1af838136bc60ba61f6c69bd68f1c60e3da772cb2fa1fe0b64c2fbc53b79 + languageName: node + linkType: hard + "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -18878,6 +18885,7 @@ __metadata: common-tags: "npm:1.8.2" copy-webpack-plugin: "npm:12.0.2" core-js: "npm:3.38.1" + crashme: "npm:0.0.15" css-loader: "npm:7.1.2" css-minimizer-webpack-plugin: "npm:6.0.0" cypress: "npm:13.10.0" From 2e687a10907a6d2dc16d809fb6f3112c9b242ef3 Mon Sep 17 00:00:00 2001 From: "Alyssa (Bull) Joyner" <58453566+alyssabull@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:07:43 -0700 Subject: [PATCH 06/55] Azure Monitor: Fix JSON parsing error (#96004) --- .../azuremonitor-resource-handler.go | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go index c52aa3a1670..454f6830172 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go @@ -1,10 +1,12 @@ package azuremonitor import ( + "encoding/json" "fmt" "io" "net/http" "net/url" + "regexp" "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -27,16 +29,46 @@ type httpServiceProxy struct { logger log.Logger } +func (s *httpServiceProxy) writeErrorResponse(rw http.ResponseWriter, statusCode int, message string) error { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(statusCode) + + // Attempt to locate JSON portion in error message + re := regexp.MustCompile(`\{.*?\}`) + jsonPart := re.FindString(message) + + var jsonData map[string]interface{} + if unmarshalErr := json.Unmarshal([]byte(jsonPart), &jsonData); unmarshalErr != nil { + errorMsg, _ := json.Marshal(map[string]string{"error": "Invalid JSON format in error message"}) + _, err := rw.Write(errorMsg) + if err != nil { + return fmt.Errorf("unable to write HTTP response: %v", err) + } + return unmarshalErr + } + + // Extract relevant fields for a formatted error message + errorType, _ := jsonData["error"].(string) + errorDescription, _ := jsonData["error_description"].(string) + if errorType == "" { + errorType = "UnknownError" + } + formattedError := fmt.Sprintf("%s: %s", errorType, errorDescription) + + errorMsg, _ := json.Marshal(map[string]string{"error": formattedError}) + _, err := rw.Write(errorMsg) + if err != nil { + return fmt.Errorf("unable to write HTTP response: %v", err) + } + return nil +} + func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { res, err := cli.Do(req) if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - _, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err))) - if err != nil { - return nil, fmt.Errorf("unable to write HTTP response: %v", err) - } - return nil, err + return nil, s.writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error: %v", err)) } + defer func() { if err := res.Body.Close(); err != nil { s.logger.Warn("Failed to close response body", "err", err) @@ -52,6 +84,7 @@ func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *ht } return nil, err } + rw.WriteHeader(res.StatusCode) _, err = rw.Write(body) if err != nil { @@ -64,7 +97,8 @@ func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *ht rw.Header().Add(k, v) } } - // Returning the response write for testing purposes + + // Return the ResponseWriter for testing purposes return rw, nil } From ec32f47db3cc617c0dd46a4a38d9e3ffcdfeccb5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:32:26 +0000 Subject: [PATCH 07/55] Update dependency rudder-sdk-js to v2.48.26 (#96321) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 830d19cde25..2c19a1c22a9 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,7 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.5", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.23", + "rudder-sdk-js": "2.48.26", "sass": "1.79.5", "sass-loader": "16.0.2", "smtp-tester": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 43186bbfe74..19886a11cc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19023,7 +19023,7 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:5.1.1" rimraf: "npm:6.0.1" - rudder-sdk-js: "npm:2.48.23" + rudder-sdk-js: "npm:2.48.26" rxjs: "npm:7.8.1" sass: "npm:1.79.5" sass-loader: "npm:16.0.2" @@ -28915,10 +28915,10 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.23": - version: 2.48.23 - resolution: "rudder-sdk-js@npm:2.48.23" - checksum: 10/520d92172f6aa870a75d7caa0a8e763c7aa20b73b82decc9e565aa17028f834891816091080e5db12442c18a25bfd6a744d88bca4fbc685c3dd7f2a6068f5205 +"rudder-sdk-js@npm:2.48.26": + version: 2.48.26 + resolution: "rudder-sdk-js@npm:2.48.26" + checksum: 10/1f0bb65fce1b764bd40ac224be7a6b6ed8e16b6bbcc35e92337b850e20771de94e3f8e6142147a6b247afc3b704243e1aecaeeec873e2aad64578d14549e579d languageName: node linkType: hard From 108d611205a9f9bbb9d26f0a3605c48baf86f11f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:34:09 +0000 Subject: [PATCH 08/55] Update dependency sass-loader to v16.0.3 (#96322) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 2c19a1c22a9..1f7065b217e 100644 --- a/package.json +++ b/package.json @@ -226,7 +226,7 @@ "rimraf": "6.0.1", "rudder-sdk-js": "2.48.26", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", "stylelint": "16.9.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 209390e75d3..12b94fd5674 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -131,7 +131,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a04da9f920f..f1aa33a945a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -179,7 +179,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "rollup-plugin-svg-import": "3.0.0", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "storybook": "^8.1.6", "storybook-dark-mode": "^4.0.1", "style-loader": "4.0.0", diff --git a/yarn.lock b/yarn.lock index 19886a11cc0..701ac60aac4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3959,7 +3959,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" sass: "npm:1.79.5" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" semver: "npm:7.6.3" style-loader: "npm:4.0.0" testing-library-selector: "npm:0.3.1" @@ -4292,7 +4292,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rollup-plugin-svg-import: "npm:3.0.0" rxjs: "npm:7.8.1" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" @@ -19026,7 +19026,7 @@ __metadata: rudder-sdk-js: "npm:2.48.26" rxjs: "npm:7.8.1" sass: "npm:1.79.5" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" selecto: "npm:1.26.3" semver: "npm:7.6.3" slate: "npm:0.47.9" @@ -29005,9 +29005,9 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:16.0.2": - version: 16.0.2 - resolution: "sass-loader@npm:16.0.2" +"sass-loader@npm:16.0.3": + version: 16.0.3 + resolution: "sass-loader@npm:16.0.3" dependencies: neo-async: "npm:^2.6.2" peerDependencies: @@ -29027,7 +29027,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10/a24ae7f6b7ef2159274b47c0a7e35cb3bfb56367a192d362ff7674f2b85b2c7bfd021e83e9351d3af36b66524efd85ebf70e242971c0b807af6622ba4ee8e298 + checksum: 10/cd49635013efae80b3a24b7e655a6f2334078006f270d50ac9e6ed19a44a80d689b402b5584b64ca737c3f948366a47e2896d07cead19015de7b2bd03f778d2b languageName: node linkType: hard From 1e001451bdbbf3a3af2619da9557f93349ca3260 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 12 Nov 2024 15:43:04 +0000 Subject: [PATCH 09/55] ScrollContainer: Replace `CustomScrollbar` usage (#96229) * replace CustomScrollbar * use ScrollContainer in RolePickerMenu and PanelDataPane --- .../querybuilder/components/MetricSelect.tsx | 12 +-- .../components/RolePicker/RolePickerMenu.tsx | 14 ++- .../RolePicker/RolePickerSubMenu.tsx | 7 +- .../PanelDataPane/PanelDataPane.tsx | 10 +-- .../AddWidgetModal/AddWidgetModal.tsx | 85 ------------------- .../SubMenu/DashboardLinksDashboard.tsx | 8 +- .../features/query/components/QueryGroup.tsx | 7 +- .../scopes/internal/ScopesTreeItem.tsx | 11 +-- .../suggestionsInput/SuggestionsInput.tsx | 18 ++-- .../LogsQueryEditor/AzureCheatSheet.tsx | 21 ++--- 10 files changed, 49 insertions(+), 144 deletions(-) delete mode 100644 public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx index bdfdd52f3f0..4aa698fa7cb 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx @@ -12,7 +12,6 @@ import { config } from '@grafana/runtime'; import { AsyncSelect, Button, - CustomScrollbar, FormatOptionLabelMeta, getSelectStyles, Icon, @@ -22,6 +21,7 @@ import { useStyles2, useTheme2, } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { PrometheusDatasource } from '../../datasource'; import { truncateResult } from '../../language_utils'; @@ -245,15 +245,9 @@ export function MetricSelect({ style={{ maxHeight: Math.round(maxHeight * 0.9) }} aria-label="Select options menu" > - + {children} - + {optionsLoaded && (
diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index ed68be79f07..193a5062a8f 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,8 +1,9 @@ import { css, cx } from '@emotion/css'; import { useEffect, useRef, useState } from 'react'; -import { Button, CustomScrollbar, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; +import { Button, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { OrgRole, Role } from 'app/types'; import { BuiltinRoleSelector } from './BuiltinRoleSelector'; @@ -217,14 +218,11 @@ export const RolePickerMenu = ({ )} >
- {showBasicRole && (
@@ -254,7 +252,7 @@ export const RolePickerMenu = ({ showOnLeftSubMenu={menuLeft} /> ))} - +
); } @@ -128,8 +129,5 @@ function getStyles(theme: GrafanaTheme2) { flexShrink: 0, paddingLeft: theme.spacing(2), }), - scroll: css({ - background: theme.colors.background.primary, - }), }; } diff --git a/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx b/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx deleted file mode 100644 index 85572494816..00000000000 --- a/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { css } from '@emotion/css'; -import { useMemo, useState } from 'react'; - -import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; -import { CustomScrollbar, Icon, Input, Modal, useStyles2 } from '@grafana/ui'; -import { onCreateNewWidgetPanel } from 'app/features/dashboard/utils/dashboard'; -import { VizTypePickerPlugin } from 'app/features/panel/components/VizTypePicker/VizTypePickerPlugin'; -import { filterPluginList, getWidgetPluginMeta } from 'app/features/panel/state/util'; -import { useSelector } from 'app/types'; - -export const AddWidgetModal = () => { - const styles = useStyles2(getStyles); - const [searchQuery, setSearchQuery] = useState(''); - const dashboard = useSelector((state) => state.dashboard.getModel()); - - const widgetsList: PanelPluginMeta[] = useMemo(() => { - return getWidgetPluginMeta(); - }, []); - - const filteredWidgetsTypes = useMemo((): PanelPluginMeta[] => { - return filterPluginList(widgetsList, searchQuery); - }, [widgetsList, searchQuery]); - - const onDismiss = () => { - locationService.partial({ addWidget: null }); - }; - - return ( - - } - placeholder="Search widget" - onChange={(e) => { - setSearchQuery(e.currentTarget.value); - }} - /> - -
- {filteredWidgetsTypes.map((plugin, index) => ( - { - const id = onCreateNewWidgetPanel(dashboard!, plugin.id); - locationService.partial({ editPanel: id, addWidget: null }); - }} - /> - ))} -
-
-
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - modal: css({ - width: '65%', - maxWidth: '960px', - [theme.breakpoints.down('md')]: { - width: '100%', - }, - }), - searchInput: css({ - marginBottom: theme.spacing(2), - }), - grid: css({ - display: 'grid', - gridGap: theme.spacing(1), - }), -}); diff --git a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx index 76946da2697..8de5f5c24cf 100644 --- a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx +++ b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx @@ -6,8 +6,9 @@ import { GrafanaTheme2, ScopedVars } from '@grafana/data'; import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize'; import { selectors } from '@grafana/e2e-selectors'; import { DashboardLink } from '@grafana/schema'; -import { CustomScrollbar, Dropdown, Icon, Button, Menu, useStyles2 } from '@grafana/ui'; +import { Dropdown, Icon, Button, Menu, useStyles2 } from '@grafana/ui'; import { ButtonLinkProps, LinkButton } from '@grafana/ui/src/components/Button'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchItem } from 'app/features/search/types'; @@ -36,7 +37,7 @@ function DashboardLinksMenu({ dashboardUID, link }: DashboardLinksMenuProps) { return (
- + {resolvedLinks.map((resolvedLink, index) => { return ( ); })} - +
); @@ -157,7 +158,6 @@ function getStyles(theme: GrafanaTheme2) { dropdown: css({ maxWidth: 'max(30vw, 300px)', maxHeight: '70vh', - overflowY: 'auto', }), button: css({ color: theme.colors.text.primary, diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index affb0328aaf..e37977e3a45 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -16,7 +16,8 @@ import { import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; -import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui'; +import { Button, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; @@ -331,7 +332,7 @@ export class QueryGroup extends PureComponent { const styles = getStyles(); return ( - +
{this.renderTopSection(styles)} {dsSettings && ( @@ -346,7 +347,7 @@ export class QueryGroup extends PureComponent { )}
-
+ ); } } diff --git a/public/app/features/scopes/internal/ScopesTreeItem.tsx b/public/app/features/scopes/internal/ScopesTreeItem.tsx index 6d310688e74..54dbfdd2cdb 100644 --- a/public/app/features/scopes/internal/ScopesTreeItem.tsx +++ b/public/app/features/scopes/internal/ScopesTreeItem.tsx @@ -2,7 +2,8 @@ import { css, cx } from '@emotion/css'; import { Dictionary } from 'lodash'; import { GrafanaTheme2 } from '@grafana/data'; -import { Checkbox, CustomScrollbar, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { t } from 'app/core/internationalization'; import { ScopesTree } from './ScopesTree'; @@ -129,12 +130,12 @@ export function ScopesTreeItem({ if (isLastExpandedNode) { return ( - {children} - + ); } diff --git a/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx b/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx index 09c9066e9cb..055f58b4731 100644 --- a/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx +++ b/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx @@ -4,9 +4,10 @@ import { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2, VariableSuggestion } from '@grafana/data'; -import { CustomScrollbar, FieldValidationMessage, Portal, TextArea, useTheme2 } from '@grafana/ui'; +import { FieldValidationMessage, Portal, TextArea, useTheme2 } from '@grafana/ui'; import { DataLinkSuggestions } from '@grafana/ui/src/components/DataLinks/DataLinkSuggestions'; import { Input } from '@grafana/ui/src/components/Input/Input'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; const modulo = (a: number, n: number) => a - n * Math.floor(a / n); const ERROR_TOOLTIP_OFFSET = 8; @@ -61,6 +62,7 @@ export const SuggestionsInput = ({ const [showingSuggestions, setShowingSuggestions] = useState(false); const [suggestionsIndex, setSuggestionsIndex] = useState(0); const [variableValue, setVariableValue] = useState(value.toString()); + const scrollRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); const [inputHeight, setInputHeight] = useState(0); const [startPos, setStartPos] = useState(0); @@ -70,6 +72,10 @@ export const SuggestionsInput = ({ const inputRef = useRef(); + useEffect(() => { + scrollRef.current?.scrollTo(0, scrollTop); + }, [scrollTop]); + // the order of middleware is important! const middleware = [ flip({ @@ -189,10 +195,10 @@ export const SuggestionsInput = ({ {showingSuggestions && (
- setScrollTop(scrollTop)} + setScrollTop(event.currentTarget.scrollTop ?? 0)} + ref={scrollRef} > {/* This suggestion component has a specialized name, but is rather generalistic in implementation, @@ -205,7 +211,7 @@ export const SuggestionsInput = ({ onClose={() => setShowingSuggestions(false)} activeIndex={suggestionsIndex} /> - +
)} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx index f415eb7fa56..da536d30376 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx @@ -3,17 +3,8 @@ import { useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { - Button, - Card, - Collapse, - CustomScrollbar, - Field, - Input, - LoadingPlaceholder, - Select, - useStyles2, -} from '@grafana/ui'; +import { Button, Card, Collapse, Field, Input, LoadingPlaceholder, Select, useStyles2 } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; import { @@ -171,7 +162,7 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => { return totalQueries; }, 0)}
- + {Object.keys(visibleQueries).map((category: string) => { if (visibleQueries[category]!.length) { return ( @@ -186,14 +177,14 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => { return ( {query.displayName} - + - +
) : ( From 9e1812dc3eafe290baa7255fe3282b0d2fc32d48 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:01:22 +0000 Subject: [PATCH 10/55] Release: update changelog for 11.2.3+security-01 (#96264) * Update changelog * add relevant CVE fix to changelog * remove irrelevant changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Sergej-Vlasov --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5206c9da37..ca59058b11b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ + + +# 11.2.3+security-01 (2024-11-12) + +### Bug fixes + +- **MigrationAssistant:** Fix Migration Assistant issue [CVE-2024-9476] + + # 10.4.12 (2024-11-08) From eaa6ba81cf643f89f2b461037bb95c9914fcfceb Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:11:34 +0000 Subject: [PATCH 11/55] Release: update changelog for 11.3.0+security-01 (#96294) * Update changelog * add relevant CVE fix to the changelog --------- Co-authored-by: github-actions[bot] Co-authored-by: Sergej-Vlasov --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca59058b11b..af808ed98a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ + + +# 11.3.0+security-01 (2024-11-12) + +### Bug fixes + +- **MigrationAssistant:** Fix Migration Assistant issue [CVE-2024-9476] + + # 11.2.3+security-01 (2024-11-12) -### Bug fixes - - **MigrationAssistant:** Fix Migration Assistant issue [CVE-2024-9476] From 7b3364cbc98e9832c92be6f90f10d0f3992e604b Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 12 Nov 2024 17:15:32 +0100 Subject: [PATCH 12/55] Chore: Rename the sorting option in explore metrics (#96160) rename the option --- public/app/features/trails/Breakdown/SortByScene.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/trails/Breakdown/SortByScene.tsx b/public/app/features/trails/Breakdown/SortByScene.tsx index cd9498ff2ed..6282c758f5b 100644 --- a/public/app/features/trails/Breakdown/SortByScene.tsx +++ b/public/app/features/trails/Breakdown/SortByScene.tsx @@ -31,7 +31,7 @@ export class SortByScene extends SceneObjectBase { options: [ { value: 'outliers', - label: 'Outlying Values', + label: 'Outlying series', description: 'Prioritizes values that show distinct behavior from others within the same label', }, { From 1c25d2ac2b2bb82b13cadbbd3d8c2a6ea98ce3be Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 12 Nov 2024 17:17:08 +0100 Subject: [PATCH 13/55] Explore Metrics: Hide errors when fetching loki rules (#96328) --- public/app/features/trails/Integrations/logsIntegration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/trails/Integrations/logsIntegration.ts b/public/app/features/trails/Integrations/logsIntegration.ts index f86e685bbb1..59f19e22b1c 100644 --- a/public/app/features/trails/Integrations/logsIntegration.ts +++ b/public/app/features/trails/Integrations/logsIntegration.ts @@ -33,7 +33,7 @@ export type ExtractedRecordingRules = { */ async function fetchRecordingRuleGroups(datasourceSettings: DataSourceInstanceSettings) { const recordingRuleUrl = `api/prometheus/${datasourceSettings.uid}/api/v1/rules`; - const recordingRules: BackendSrvRequest = { url: recordingRuleUrl }; + const recordingRules: BackendSrvRequest = { url: recordingRuleUrl, showErrorAlert: false, showSuccessAlert: false }; const { data } = await lastValueFrom< FetchResponse<{ data: { groups: RecordingRuleGroup[] }; From e1533ff39b30be976c29e24cc09e04912ffba954 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:21:48 +0200 Subject: [PATCH 14/55] Update dependency @faker-js/faker to v9.2.0 (#96334) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 701ac60aac4..0ac100a90dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2848,9 +2848,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^9.0.0": - version: 9.0.3 - resolution: "@faker-js/faker@npm:9.0.3" - checksum: 10/60ea71c31d6bd6317184736de17daee415e229cab5e10a4146a19622f3eb22f49f42345ff339dc9261f0214ca1f9b019c63fd9aac842a2d359f135102788ec90 + version: 9.2.0 + resolution: "@faker-js/faker@npm:9.2.0" + checksum: 10/c233aaa41ebc4f4bed36c1304c399c424b9e5abca9cb8fe54ce73d4162ec7acd66f294b90c85ed7a5df774400e44f0a10364bdd41f49e72cb519a48b5948939f languageName: node linkType: hard From 29bcf7dbe9463f8b1c93c052d4f4df9538c81509 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:23:05 +0200 Subject: [PATCH 15/55] Update babel monorepo to v7.26.0 (#96333) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 +- packages/grafana-flamegraph/package.json | 4 +- packages/grafana-icons/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 226 +++++++++-------------- 5 files changed, 95 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index 1f7065b217e..b02911aabbf 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,9 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { - "@babel/core": "7.25.9", - "@babel/preset-env": "7.25.9", - "@babel/runtime": "7.25.9", + "@babel/core": "7.26.0", + "@babel/preset-env": "7.26.0", + "@babel/runtime": "7.26.0", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index e3eb85bba5d..4de626ec320 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -56,8 +56,8 @@ "tslib": "2.7.0" }, "devDependencies": { - "@babel/core": "7.25.9", - "@babel/preset-env": "7.25.9", + "@babel/core": "7.26.0", + "@babel/preset-env": "7.26.0", "@babel/preset-react": "7.25.9", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index b3a753cce6c..caba46ada87 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -34,7 +34,7 @@ "build": "yarn generate && rollup -c rollup.config.ts --configPlugin esbuild" }, "devDependencies": { - "@babel/core": "7.25.9", + "@babel/core": "7.26.0", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-typescript": "^12.1.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f1aa33a945a..d02364677ae 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -114,7 +114,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@babel/core": "7.25.9", + "@babel/core": "7.26.0", "@faker-js/faker": "^9.0.0", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", diff --git a/yarn.lock b/yarn.lock index 0ac100a90dd..8e2d783dec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,55 +82,57 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/code-frame@npm:7.25.9" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" dependencies: - "@babel/highlight": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 10/96d69a570d0df82daedeb3d26ca508970bb31de83580c36c9605e7e7c0aae307ae17bc42609363016f0bdab12e991cebca3c02bf10765036b136bfe7281aee9a + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/compat-data@npm:7.25.9" - checksum: 10/76d06c56e1d1ab661dc90870d70d950c7df5514d2abfb115387ea0790ceeb1924ee3a88c959345f235aad219cfb13ff03c4458081ac350d47fc135a7ba2d49d3 +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9, @babel/compat-data@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/compat-data@npm:7.26.2" + checksum: 10/ed9eed6b62ce803ef4a320b1dac76b0302abbb29c49dddf96f3e3207d9717eb34e299a8651bb1582e9c3346ead74b6d595ffced5b3dae718afa08b18741f8402 languageName: node linkType: hard -"@babel/core@npm:7.25.9, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.24.4": - version: 7.25.9 - resolution: "@babel/core@npm:7.25.9" +"@babel/core@npm:7.26.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.24.4": + version: 7.26.0 + resolution: "@babel/core@npm:7.26.0" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.25.9" - "@babel/generator": "npm:^7.25.9" + "@babel/code-frame": "npm:^7.26.0" + "@babel/generator": "npm:^7.26.0" "@babel/helper-compilation-targets": "npm:^7.25.9" - "@babel/helper-module-transforms": "npm:^7.25.9" - "@babel/helpers": "npm:^7.25.9" - "@babel/parser": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.26.0" + "@babel/helpers": "npm:^7.26.0" + "@babel/parser": "npm:^7.26.0" "@babel/template": "npm:^7.25.9" "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/92cc69d9d59a5eb057527e69c41db46f05d0a8eeeb5ebab3f34e5ad040b74f34f20a4d97c3f3ede6476537cac93d2b46e3915b572269d2a039301dab068fd2e8 + checksum: 10/65767bfdb1f02e80d3af4f138066670ef8fdd12293de85ef151758a901c191c797e86d2e99b11c4cdfca33c72385ecaf38bbd7fa692791ec44c77763496b9b93 languageName: node linkType: hard -"@babel/generator@npm:^7.22.9, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/generator@npm:7.25.9" +"@babel/generator@npm:^7.22.9, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0, @babel/generator@npm:^7.7.2": + version: 7.26.2 + resolution: "@babel/generator@npm:7.26.2" dependencies: - "@babel/types": "npm:^7.25.9" + "@babel/parser": "npm:^7.26.2" + "@babel/types": "npm:^7.26.0" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^3.0.2" - checksum: 10/eb36706c62ea77a09604077b84fae4e25d103cce58a15926d9d8b62d90c5fa69e35962515c05e78b5a975848ef772406dd79e2d4e83851bf9f7517b197a1b19d + checksum: 10/71ace82b5b07a554846a003624bfab93275ccf73cdb9f1a37a4c1094bf9dc94bb677c67e8b8c939dbd6c5f0eda2e8f268aa2b0d9c3b9511072565660e717e045 languageName: node linkType: hard @@ -241,17 +243,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-module-transforms@npm:7.25.9" +"@babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" dependencies: "@babel/helper-module-imports": "npm:^7.25.9" - "@babel/helper-simple-access": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" "@babel/traverse": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/6a9dc7da67f901a511ef26b99fd1b395946d466495159cbf80c092345ef3238306296ee76b204aea5f2675713130c58760c46b1ff3b6f6b19f7f8afcaa19d8ca + checksum: 10/9841d2a62f61ad52b66a72d08264f23052d533afc4ce07aec2a6202adac0bfe43014c312f94feacb3291f4c5aafe681955610041ece2c276271adce3f570f2f5 languageName: node linkType: hard @@ -349,36 +350,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helpers@npm:7.25.9" +"@babel/helpers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helpers@npm:7.26.0" dependencies: "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/83c0df8f45850c5621be660b69c33d93c02832162a9109bb9a03de32a2b6477fbbd1e2c8c5c19fadb0d48f07066ff20d2d2da32de62dfda5c0a6a1036cebeb00 + "@babel/types": "npm:^7.26.0" + checksum: 10/fd4757f65d10b64cfdbf4b3adb7ea6ffff9497c53e0786452f495d1f7794da7e0898261b4db65e1c62bbb9a360d7d78a1085635c23dfc3af2ab6dcba06585f86 languageName: node linkType: hard -"@babel/highlight@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/highlight@npm:7.25.9" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" dependencies: - "@babel/helper-validator-identifier": "npm:^7.25.9" - chalk: "npm:^2.4.2" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.0.0" - checksum: 10/0d165283dd4eb312292cea8fec3ae0d376874b1885f476014f0136784ed5b564b2c2ba2d270587ed546ee92505056dab56493f7960c01c4e6394d71d1b2e7db6 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/parser@npm:7.25.9" - dependencies: - "@babel/types": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" bin: parser: ./bin/babel-parser.js - checksum: 10/702af8c40bb1236e3e3e6187b99e1290bd4bc1500aa53593ea63df8fe99f07ff1efef147b1d58886b264aff0972c4b9440ace442c8db9a6e079f318d46773421 + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d languageName: node linkType: hard @@ -528,25 +517,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.25.9" +"@babel/plugin-syntax-import-assertions@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.26.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2f0c70bb379135ee205402caa42c0dda4d5d8fb64ff4ad163cab94bd8291c1a63b9dc9cf293758cecee223080d2d61e83f92c6d2a264621e24a07258c48968db + checksum: 10/b58f2306df4a690ca90b763d832ec05202c50af787158ff8b50cdf3354359710bce2e1eb2b5135fcabf284756ac8eadf09ca74764aa7e76d12a5cac5f6b21e67 languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.25.9" +"@babel/plugin-syntax-import-attributes@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.26.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0ea00b9100d383680c7142e61e3aa7101e3657ec5e1bfa990871eee4ae17e2c4a0da084e8f611d349bb9612908e911e1400418eb59caa5184226b08f513c1a0a + checksum: 10/c122aa577166c80ee67f75aebebeef4150a132c4d3109d25d7fc058bf802946f883e330f20b78c1d3e3a5ada631c8780c263d2d01b5dbaecc69efefeedd42916 languageName: node linkType: hard @@ -754,15 +743,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-class-static-block@npm:7.25.9" +"@babel/plugin-transform-class-static-block@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-class-static-block@npm:7.26.0" dependencies: "@babel/helper-create-class-features-plugin": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/01dbb32e443086cf98e986a0d3532f74916ed85ef4823139d155d69c6fbf4ae80ae862f83abba57d815bb6aebd349b18cdec93ef08be42ead4248f3a294c8a08 + checksum: 10/60cba3f125a7bc4f90706af0a011697c7ffd2eddfba336ed6f84c5f358c44c3161af18b0202475241a96dee7964d96dd3a342f46dbf85b75b38bb789326e1766 languageName: node linkType: hard @@ -1207,6 +1196,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regexp-modifiers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.26.0" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/726deca486bbd4b176f8a966eb0f4aabc19d9def3b8dabb8b3a656778eca0df1fda3f3c92b213aa5a184232fdafd5b7bd73b4e24ca4345c498ef6baff2bda4e1 + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-reserved-words@npm:7.25.9" @@ -1345,11 +1346,11 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.25.9, @babel/preset-env@npm:^7.24.4": - version: 7.25.9 - resolution: "@babel/preset-env@npm:7.25.9" +"@babel/preset-env@npm:7.26.0, @babel/preset-env@npm:^7.24.4": + version: 7.26.0 + resolution: "@babel/preset-env@npm:7.26.0" dependencies: - "@babel/compat-data": "npm:^7.25.9" + "@babel/compat-data": "npm:^7.26.0" "@babel/helper-compilation-targets": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" "@babel/helper-validator-option": "npm:^7.25.9" @@ -1359,8 +1360,8 @@ __metadata: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.25.9" "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.25.9" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions": "npm:^7.25.9" - "@babel/plugin-syntax-import-attributes": "npm:^7.25.9" + "@babel/plugin-syntax-import-assertions": "npm:^7.26.0" + "@babel/plugin-syntax-import-attributes": "npm:^7.26.0" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.25.9" "@babel/plugin-transform-async-generator-functions": "npm:^7.25.9" @@ -1368,7 +1369,7 @@ __metadata: "@babel/plugin-transform-block-scoped-functions": "npm:^7.25.9" "@babel/plugin-transform-block-scoping": "npm:^7.25.9" "@babel/plugin-transform-class-properties": "npm:^7.25.9" - "@babel/plugin-transform-class-static-block": "npm:^7.25.9" + "@babel/plugin-transform-class-static-block": "npm:^7.26.0" "@babel/plugin-transform-classes": "npm:^7.25.9" "@babel/plugin-transform-computed-properties": "npm:^7.25.9" "@babel/plugin-transform-destructuring": "npm:^7.25.9" @@ -1401,6 +1402,7 @@ __metadata: "@babel/plugin-transform-private-property-in-object": "npm:^7.25.9" "@babel/plugin-transform-property-literals": "npm:^7.25.9" "@babel/plugin-transform-regenerator": "npm:^7.25.9" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.26.0" "@babel/plugin-transform-reserved-words": "npm:^7.25.9" "@babel/plugin-transform-shorthand-properties": "npm:^7.25.9" "@babel/plugin-transform-spread": "npm:^7.25.9" @@ -1419,7 +1421,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7a9ca1a19949426498fdffc37eed919f9581226ea45b18be764d29ce81bbf8c8f2f37152300c3524b7a3861831d33f10d481810f3011dff702f780d3f79aa789 + checksum: 10/a7a80314f845deea713985a6316361c476621c76cfe5c6c28e8b9558f01634b49bbfdd3581ef94b5d6cff5c2b8830468aa53a73f5b5c1224db2dfea5db7e676f languageName: node linkType: hard @@ -1505,12 +1507,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.25.9, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": - version: 7.25.9 - resolution: "@babel/runtime@npm:7.25.9" +"@babel/runtime@npm:7.26.0, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/8d904cfcb433374b3bb90369452751c94ae69547cdd3679950de4527ac5d04195b9c4a1840482a6f3a84694cb22a6403a7f98b826d60cd945918223a4a6b479c + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 languageName: node linkType: hard @@ -1540,13 +1542,13 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.25.9 - resolution: "@babel/types@npm:7.25.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/dd0f2874b10048aa230a5633ab440bbee8c3905f254ef26223b5321ddb824b057b9404d24a87556c6a9f7430198fa6311473778d147ed8ed7845428aee2ebc34 + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e languageName: node linkType: hard @@ -3714,8 +3716,8 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" dependencies: - "@babel/core": "npm:7.25.9" - "@babel/preset-env": "npm:7.25.9" + "@babel/core": "npm:7.26.0" + "@babel/preset-env": "npm:7.26.0" "@babel/preset-react": "npm:7.25.9" "@emotion/css": "npm:11.13.4" "@grafana/data": "npm:11.4.0-pre" @@ -4023,7 +4025,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/saga-icons@workspace:packages/grafana-icons" dependencies: - "@babel/core": "npm:7.25.9" + "@babel/core": "npm:7.26.0" "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-typescript": "npm:^12.1.0" @@ -4169,7 +4171,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: - "@babel/core": "npm:7.25.9" + "@babel/core": "npm:7.26.0" "@emotion/css": "npm:11.13.4" "@emotion/react": "npm:11.13.3" "@emotion/serialize": "npm:1.3.2" @@ -12018,15 +12020,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -13307,17 +13300,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.4.2": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10/3d1d103433166f6bfe82ac75724951b33769675252d8417317363ef9d54699b7c3b2d46671b772b893a8e50c3ece70c4b933c73c01e81bc60ea4df9b55afa303 - languageName: node - linkType: hard - "chalk@npm:^3.0.0": version: 3.0.0 resolution: "chalk@npm:3.0.0" @@ -13737,15 +13719,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -13762,13 +13735,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - "color-name@npm:^1.1.4, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -18721,9 +18687,9 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: - "@babel/core": "npm:7.25.9" - "@babel/preset-env": "npm:7.25.9" - "@babel/runtime": "npm:7.25.9" + "@babel/core": "npm:7.26.0" + "@babel/preset-env": "npm:7.26.0" + "@babel/runtime": "npm:7.26.0" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" "@betterer/eslint": "npm:5.4.0" @@ -19173,13 +19139,6 @@ __metadata: languageName: node linkType: hard -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -30626,15 +30585,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b - languageName: node - linkType: hard - "supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" From 1fcc0a8454029a3d6dc49d0f43e37f52f3f538ce Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 12 Nov 2024 18:24:51 +0200 Subject: [PATCH 16/55] Alerting: Remove history patches (#95407) --- .../history-npm-4.10.1-ee217563ae.patch | 63 ------------------- package.json | 2 - yarn.lock | 16 +---- 3 files changed, 1 insertion(+), 80 deletions(-) delete mode 100644 .yarn/patches/history-npm-4.10.1-ee217563ae.patch diff --git a/.yarn/patches/history-npm-4.10.1-ee217563ae.patch b/.yarn/patches/history-npm-4.10.1-ee217563ae.patch deleted file mode 100644 index 6cd9f7f4b42..00000000000 --- a/.yarn/patches/history-npm-4.10.1-ee217563ae.patch +++ /dev/null @@ -1,63 +0,0 @@ -diff --git a/cjs/history.js b/cjs/history.js -index fcd8ebab613c6d87b9ac824feb30ab1080cf0ef2..4df20d5cb2f9ba5fc8777899aada53f49399560b 100644 ---- a/cjs/history.js -+++ b/cjs/history.js -@@ -103,16 +103,6 @@ function createLocation(path, state, key, currentLocation) { - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { -diff --git a/esm/history.js b/esm/history.js -index df67820fe3eed558c44fca07a82b0cd409d46720..e0e0d4f69a407e8de782b3fdf8297d42708e110a 100644 ---- a/esm/history.js -+++ b/esm/history.js -@@ -80,16 +80,6 @@ function createLocation(path, state, key, currentLocation) { - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { -diff --git a/umd/history.js b/umd/history.js -index 80e4ff66c44a2a71d4f842cc05a252e48dd18e9a..f8f4901be95e48c66f5626fbf051747a2ffbe41d 100644 ---- a/umd/history.js -+++ b/umd/history.js -@@ -207,16 +207,6 @@ - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { diff --git a/package.json b/package.json index b02911aabbf..0e4184c06c7 100644 --- a/package.json +++ b/package.json @@ -427,8 +427,6 @@ "debug@npm:^0.7.4": "2.6.9", "slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch", "react-split-pane@0.1.92": "patch:react-split-pane@npm:0.1.92#.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch", - "history@4.10.1": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch", - "history@^4.9.0": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch", "redux": "^5.0.0", "@storybook/blocks@npm:8.1.6": "patch:@storybook/blocks@npm%3A8.1.6#~/.yarn/patches/@storybook-blocks-npm-8.1.6-892f57a6d7.patch", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", diff --git a/yarn.lock b/yarn.lock index 8e2d783dec3..9ee71611dfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19290,7 +19290,7 @@ __metadata: languageName: node linkType: hard -"history@npm:4.10.1": +"history@npm:4.10.1, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" dependencies: @@ -19313,20 +19313,6 @@ __metadata: languageName: node linkType: hard -"history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::locator=grafana%40workspace%3A.": - version: 4.10.1 - resolution: "history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::version=4.10.1&hash=b8466f&locator=grafana%40workspace%3A." - dependencies: - "@babel/runtime": "npm:^7.1.2" - loose-envify: "npm:^1.2.0" - resolve-pathname: "npm:^3.0.0" - tiny-invariant: "npm:^1.0.2" - tiny-warning: "npm:^1.0.0" - value-equal: "npm:^1.0.1" - checksum: 10/15ef2925dc665d7671308625a03e43b0c2e5b7d7a28c41b04c3ca68c805b6c4b6c6e0c217168ebf356c753ccdce6236ad717b58e6dae92be0501a0bfd44c0423 - languageName: node - linkType: hard - "hoist-non-react-statics@npm:3.3.2, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" From 6414bcb6aa92d274b035b165681a5bfcae56b5cd Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:39:49 +0000 Subject: [PATCH 17/55] Combobox: Increase min height of the menu (#96327) --- packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index bedebb4b749..e2f7e3ac851 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -41,7 +41,7 @@ export const useComboboxFloat = ( const preferredMaxHeight = availableHeight - POPOVER_PADDING; const width = Math.max(preferredMaxWidth, 0); - const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT), POPOVER_MAX_HEIGHT); + const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT * 6), POPOVER_MAX_HEIGHT); setPopoverMaxSize({ width, height }); }, From 7f99e943a6a266270dfb0a58d382fac28b0dd51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Tue, 12 Nov 2024 17:55:53 +0100 Subject: [PATCH 18/55] Do not use the span recorder in test (#96324) --- .../cloudmigration/cloudmigrationimpl/cloudmigration_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 18b28f7ad2a..ddbf3a4d22f 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -48,7 +48,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func Test_NoopServiceDoesNothing(t *testing.T) { @@ -784,8 +783,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi sqlStore := db.InitTestDB(t) secretsService := secretsfakes.NewFakeSecretsService() rr := routing.NewRouteRegister() - spanRecorder := tracetest.NewSpanRecorder() - tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) + tracer := tracing.InitializeTracerForTest() mockFolder := &foldertest.FakeService{ ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"}, } From e9956f2345080a0947bb4ba2cd0e5a41dfa8f4ed Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 12 Nov 2024 19:58:32 +0300 Subject: [PATCH 19/55] Storage: Always set a UID in the apiserver (#96086) --- pkg/storage/unified/apistore/prepare.go | 9 ++++++++- pkg/storage/unified/resource/server.go | 2 ++ pkg/storage/unified/resource/server_test.go | 9 ++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index ed379fd73c9..9cf7739ad81 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -7,7 +7,9 @@ import ( "math" "time" + "github.com/google/uuid" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/storage" "k8s.io/klog/v2" @@ -50,6 +52,9 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime if obj.GetResourceVersion() != "" { return nil, storage.ErrResourceVersionSetOnCreate } + if obj.GetUID() == "" { + obj.SetUID(types.UID(uuid.NewString())) + } obj.SetGenerateName("") // Clear the random name field obj.SetResourceVersion("") @@ -95,7 +100,9 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti if previous.GetUID() == "" { klog.Errorf("object is missing UID: %s, %s", obj.GetGroupVersionKind().String(), obj.GetName()) } else if obj.GetUID() != previous.GetUID() { - klog.Errorf("object UID mismatch: %s, was:%s, now: %s", obj.GetGroupVersionKind().String(), previous.GetName(), obj.GetUID()) + if obj.GetUID() != "" { + klog.Errorf("object UID mismatch: %s, was:%s, now: %s", obj.GetGroupVersionKind().String(), previous.GetName(), obj.GetUID()) + } obj.SetUID(previous.GetUID()) } diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 316bd75e471..ff6f3b5e838 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -303,6 +303,8 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour } if obj.GetUID() == "" { + // TODO! once https://github.com/grafana/grafana/pull/96086 is deployed everywhere + // return nil, NewBadRequestError("object is missing UID") s.log.Error("object is missing UID", "key", key) } diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 4d93e18bc1b..cc54f4ab6cc 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -8,13 +8,14 @@ import ( "testing" "time" - "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/stretchr/testify/require" "gocloud.dev/blob/fileblob" "gocloud.dev/blob/memblob" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) func TestSimpleServer(t *testing.T) { @@ -56,6 +57,7 @@ func TestSimpleServer(t *testing.T) { "kind": "Playlist", "metadata": { "name": "fdgsv37qslr0ga", + "uid": "xyz", "namespace": "default", "annotations": { "grafana.app/originName": "elsewhere", @@ -172,6 +174,7 @@ func TestSimpleServer(t *testing.T) { "metadata": { "name": "fdgsv37qslr0ga", "namespace": "default", + "uid": "xyz", "annotations": { "grafana.app/originName": "elsewhere", "grafana.app/originPath": "path/to/item", From 01d2376782de145971ec1b1b0bed9914c474420b Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 12 Nov 2024 17:46:25 +0000 Subject: [PATCH 20/55] Shard query splitting: stop querying on failure (#96284) Shard query splitting: stop querying on final errors --- public/app/plugins/datasource/loki/querySplitting.test.ts | 5 +++++ public/app/plugins/datasource/loki/responseUtils.ts | 8 ++++---- .../plugins/datasource/loki/shardQuerySplitting.test.ts | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index 9c8326eb767..57f9859ea6c 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -18,6 +18,10 @@ jest.mock('uuid', () => ({ })); const originalShardingFlagState = config.featureToggles.lokiShardSplitting; +const originalErr = console.error; +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); beforeAll(() => { // @ts-expect-error jest.spyOn(global, 'setTimeout').mockImplementation((callback) => { @@ -28,6 +32,7 @@ beforeAll(() => { afterAll(() => { jest.mocked(global.setTimeout).mockReset(); config.featureToggles.lokiShardSplitting = originalShardingFlagState; + console.error = originalErr; }); describe('runSplitQuery()', () => { diff --git a/public/app/plugins/datasource/loki/responseUtils.ts b/public/app/plugins/datasource/loki/responseUtils.ts index 3701ad0cb35..dcfafdc670a 100644 --- a/public/app/plugins/datasource/loki/responseUtils.ts +++ b/public/app/plugins/datasource/loki/responseUtils.ts @@ -138,9 +138,9 @@ export function isRetriableError(errorResponse: DataQueryResponse) { : (errorResponse.error?.message ?? ''); if (message.includes('timeout')) { return true; - } else if (message.includes('parse error')) { - // If the error is a parse error, we want to signal to stop querying. - throw new Error(message); + } else if (errorResponse.data.length > 0 && errorResponse.data[0].fields.length > 0) { + // Error response but we're receiving data, continue querying. + return false; } - return false; + throw new Error(message); } diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts index 39dd75ca4f1..06637829cf6 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts @@ -14,6 +14,7 @@ jest.mock('uuid', () => ({ const originalLog = console.log; const originalWarn = console.warn; +const originalErr = console.error; beforeEach(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'warn').mockImplementation(() => {}); @@ -22,6 +23,7 @@ beforeEach(() => { afterAll(() => { console.log = originalLog; console.warn = originalWarn; + console.error = originalErr; }); describe('runShardSplitQuery()', () => { @@ -203,7 +205,7 @@ describe('runShardSplitQuery()', () => { callback(); }); await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { - expect(datasource.runQuery).toHaveBeenCalledTimes(2); + expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); }); From d4d9d56e9de8f6d8586c42f1b703c8c5133bb668 Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 12 Nov 2024 18:05:46 +0000 Subject: [PATCH 21/55] GAS: Add docs for group attribute sync (#96090) * add docs describing what GAS is and how it can be configured * Update configure-group-attribute-sync.md * Update configure-group-attribute-sync.md --------- Co-authored-by: Aaron Godin --- .../configure-group-attribute-sync.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md diff --git a/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md b/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md new file mode 100644 index 00000000000..1377f885397 --- /dev/null +++ b/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md @@ -0,0 +1,78 @@ +--- +description: Learn how to use group attribute sync to synchronize between groups in your authentication provider and Grafana RBAC roles. +labels: + products: + - cloud + - enterprise +title: Configure group attribute sync +weight: 1000 +--- + +# Configure group attribute sync + +Group attribute sync allows you to manage user permissions in Grafana based on group membership sourced from the user's identity provider (IdP). +Groups are mapped to [fixed](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#fixed-roles) and [custom](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#custom-roles) role-based access control roles in Grafana. + +> **Note:** Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). + +{{% admonition type="note" %}} +This feature is behind the `groupAttributeSync` feature toggle. +You can enable feature toggles through configuration file or environment variables. See configuration [docs](/docs/grafana//setup-grafana/configure-grafana/#feature_toggles) for details. +{{% /admonition %}} + +When a user logs in, Grafana checks the user's external group memberships and the configured group to role mappings to assign the corresponding roles to the user. +If the user's group memberships change or a new mapping is created, the user's role assignments are updated the next time the user logs in. +If a group mapping is removed, the role assignment to users for the group mapping is revoked immediately. + +Role mappings are tied to organizations, so you can have different mappings for different organizations. + +## Create role mappings for a new group + +For information about creating group mappings via the API, refer to [create group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#create-group-mappings). + +### Before you begin + +Ensure you have permission to create and update group mappings. By default, the organization administrator role is required to create and edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To create mappings between an external group and RBAC roles + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Click **New**. +1. Insert the group identifier for the group that you want to map. +1. Use the role picker to select the roles that you want to map to the group and click **Update**. +1. Click **Save**. + +## Update role mappings for a group + +For information about updating group mappings via the API, refer to [update group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#update-group-mappings). + +### Before you begin + +Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To update role mappings for an external group + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Find the group whose mappings you want to update. +1. Click on the role picker corresponding to the group and select the roles that you want to map. +1. Click **Apply**. + +## Remove role mappings for a group + +For information about deleting group mappings via the API, refer to [delete group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#delete-group-mappings). + +### Before you begin + +Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To remove role mappings for an external group + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Find the group whose mappings you want to remove. +1. Click on the trash bin icon corresponding to the group mappings you want to remove. From 580d073b969d58dc31d2995cd1418e6a789601ec Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 12 Nov 2024 19:41:05 +0100 Subject: [PATCH 22/55] Alerting: Add context to the logger in state manager Warm (#96228) --- pkg/services/ngalert/state/manager.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index a2eb863fddd..76beadd09fc 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -124,16 +124,19 @@ func (st *Manager) Run(ctx context.Context) error { } func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceReader InstanceReader) { + logger := st.log.FromContext(ctx) + if st.instanceStore == nil { - st.log.Info("Skip warming the state because instance store is not configured") + logger.Info("Skip warming the state because instance store is not configured") return } + startTime := time.Now() - st.log.Info("Warming state cache for startup") + logger.Info("Warming state cache for startup") orgIds, err := instanceReader.FetchOrgIds(ctx) if err != nil { - st.log.Error("Unable to fetch orgIds", "error", err) + logger.Error("Unable to fetch orgIds", "error", err) } statesCount := 0 @@ -144,7 +147,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea } alertRules, err := rulesReader.ListAlertRules(ctx, &ruleCmd) if err != nil { - st.log.Error("Unable to fetch previous state", "error", err) + logger.Error("Unable to fetch previous state", "error", err) } ruleByUID := make(map[string]*ngModels.AlertRule, len(alertRules)) @@ -158,7 +161,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea // We will not enforce this here, but it's convenient to emit the warning here as we load up all the rules. for name, size := range groupSizes { if st.rulesPerRuleGroupLimit > 0 && size > st.rulesPerRuleGroupLimit { - st.log.Warn( + logger.Warn( "Large rule group was loaded. Large groups are discouraged and changes to them may be disallowed in the future.", "limit", st.rulesPerRuleGroupLimit, "actual", size, @@ -173,7 +176,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea } alertInstances, err := instanceReader.ListAlertInstances(ctx, &cmd) if err != nil { - st.log.Error("Unable to fetch previous state", "error", err) + logger.Error("Unable to fetch previous state", "error", err) } for _, entry := range alertInstances { @@ -195,7 +198,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea if entry.ResultFingerprint != "" { fp, err := strconv.ParseUint(entry.ResultFingerprint, 16, 64) if err != nil { - st.log.Error("Failed to parse result fingerprint of alert instance", "error", err, "ruleUID", entry.RuleUID) + logger.Error("Failed to parse result fingerprint of alert instance", "error", err, "ruleUID", entry.RuleUID) } resultFp = data.Fingerprint(fp) } @@ -215,12 +218,12 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea ResolvedAt: entry.ResolvedAt, LastSentAt: entry.LastSentAt, } - st.cache.getOrAdd(state, st.log) + st.cache.getOrAdd(state, logger) statesCount++ } } - st.log.Info("State cache has been initialized", "states", statesCount, "duration", time.Since(startTime)) + logger.Info("State cache has been initialized", "states", statesCount, "duration", time.Since(startTime)) } func (st *Manager) Get(orgID int64, alertRuleUID string, stateId data.Fingerprint) *State { From 68db1c6e6822ab50467423b79fb2ba24a5ff4b54 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 12 Nov 2024 18:44:06 +0000 Subject: [PATCH 23/55] Legacy annotations: add dataplane support (#96226) --- .../datasource/loki/datasource.test.ts | 65 +++++++++++++++++++ .../app/plugins/datasource/loki/datasource.ts | 14 ++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 4f9f6ca3372..1e5158630aa 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -442,9 +442,74 @@ describe('LokiDatasource', () => { expect(res.length).toBe(2); expect(res[0].text).toBe('hello'); expect(res[0].tags).toEqual(['value']); + expect(res[0].time).toEqual(1); expect(res[1].text).toBe('hello 2'); expect(res[1].tags).toEqual(['value2']); + expect(res[1].time).toEqual(2); + }); + + it('should transform the loki dataplane data to annotation response', async () => { + const originalDataplaneState = config.featureToggles.lokiLogsDataplane; + config.featureToggles.lokiLogsDataplane = true; + const testFrame: DataFrame = { + refId: 'A', + fields: [ + { + name: 'timestamp', + type: FieldType.time, + config: {}, + values: [1, 2], + }, + { + name: 'body', + type: FieldType.string, + config: {}, + values: ['hello', 'hello 2'], + }, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: [ + { + label: 'value', + label2: 'value ', + }, + { + label: '', + label2: 'value2', + label3: ' ', + }, + ], + }, + { + name: 'tsNs', + type: FieldType.string, + config: {}, + values: ['1000000', '2000000'], + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: ['id1', 'id2'], + }, + ], + length: 2, + }; + const res = await getTestContext(testFrame, { stepInterval: '15s' }); + + expect(res.length).toBe(2); + expect(res[0].text).toBe('hello'); + expect(res[0].tags).toEqual(['value']); + expect(res[0].time).toEqual(1); + + expect(res[1].text).toBe('hello 2'); + expect(res[1].tags).toEqual(['value2']); + expect(res[1].time).toEqual(2); + + config.featureToggles.lokiLogsDataplane = originalDataplaneState; }); describe('Formatting', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index f661d8d5f39..e146973ccf7 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1042,8 +1042,12 @@ export class LokiDatasource const annotations: AnnotationEvent[] = []; const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== ''); + const isDataplaneLog = config.featureToggles.lokiLogsDataplane; + for (const frame of data) { - const view = new DataFrameView<{ Time: string; Line: string; labels: Labels }>(frame); + const view = new DataFrameView<{ timestamp: string; Time: string; body: string; Line: string; labels: Labels }>( + frame + ); view.forEach((row) => { const { labels } = row; @@ -1063,15 +1067,17 @@ export class LokiDatasource return true; }) - .map(([key, val]) => val); // keep only the label-value + .map(([_, val]) => val); // keep only the label-value // remove duplicates const tags = Array.from(new Set(maybeDuplicatedTags)); + const logLine = isDataplaneLog ? row.body : row.Line; + annotations.push({ - time: new Date(row.Time).valueOf(), + time: isDataplaneLog ? new Date(row.timestamp).valueOf() : new Date(row.Time).valueOf(), title: renderLegendFormat(titleFormat, labels), - text: renderLegendFormat(textFormat, labels) || row.Line, + text: renderLegendFormat(textFormat, labels) || logLine, tags, }); }); From 9f02acd4cab3cf8ea07e4b7e29d034fca277291c Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 12 Nov 2024 12:50:38 -0600 Subject: [PATCH 24/55] Transformations: Window calculation should use 0 if value in window does not exist (#95625) * Use 0 for window functions if value does not exist * Simplify logic, add test * Fix tests, have first value count as 0 if null as well * evaluate entire array instead of individual values * Adjust logic, move prior results back --- .../transformers/calculateField.test.ts | 29 +++++++++++++++++++ .../transformers/calculateField.ts | 7 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index c5e5911ce88..59872c2019b 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -728,6 +728,35 @@ describe('calculateField transformer w/ timeseries', () => { }); }); + it('calculates fixed, trailing moving average with missing values', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 2, + windowSizeMode: WindowSizeMode.Fixed, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, undefined, 3, 4, 5] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + //console.log(data.fields); + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values).toEqual([1, 1, 3, 3.5, 4.5]); + }); + }); + it('throws error when calculating moving average if window size < 1', async () => { const cfg = { id: DataTransformerID.calculateField, diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 1a1ada7a5b8..80f41cc985e 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -357,8 +357,11 @@ function getTrailingWindowValues(frame: DataFrame, reducer: ReducerID, selectedF sum += currentValue; if (i > window - 1) { - sum -= selectedField.values[i - window]; - count--; + const value = selectedField.values[i - window]; + if (value != null) { + sum -= value; + count--; + } } } vals.push(count === 0 ? 0 : sum / count); From 750a0bed711273f2e8582740180f46f09de8cc0b Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:03:16 -0300 Subject: [PATCH 25/55] MigrationAssistant: Restrict dashboards, folders and datasources by the org id of the signed in user (#96339) apply security patch: main/206-202410241510.patch --- pkg/services/cloudmigration/api/api.go | 11 +-- pkg/services/cloudmigration/cloudmigration.go | 8 +-- .../cloudmigrationimpl/cloudmigration.go | 29 ++++---- .../cloudmigrationimpl/cloudmigration_noop.go | 8 +-- .../cloudmigrationimpl/cloudmigration_test.go | 37 ++++++---- .../fake/cloudmigration_fake.go | 8 +-- .../cloudmigrationimpl/snapshot_mgmt.go | 29 ++++---- .../cloudmigrationimpl/store.go | 23 ++++--- .../cloudmigrationimpl/xorm_store.go | 67 +++++++++++++------ .../cloudmigrationimpl/xorm_store_test.go | 66 ++++++++++-------- pkg/services/cloudmigration/model.go | 8 ++- .../sqlstore/migrations/cloud_migrations.go | 9 ++- 12 files changed, 188 insertions(+), 115 deletions(-) diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index fde10066063..0fb47d737fa 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -175,7 +175,7 @@ func (cma *CloudMigrationAPI) GetSessionList(c *contextmodel.ReqContext) respons ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.GetSessionList") defer span.End() - sl, err := cma.cloudMigrationService.GetSessionList(ctx) + sl, err := cma.cloudMigrationService.GetSessionList(ctx, c.OrgID) if err != nil { span.SetStatus(codes.Error, "session list error") span.RecordError(err) @@ -208,7 +208,7 @@ func (cma *CloudMigrationAPI) GetSession(c *contextmodel.ReqContext) response.Re return response.Error(http.StatusBadRequest, "invalid session uid", err) } - s, err := cma.cloudMigrationService.GetSession(ctx, uid) + s, err := cma.cloudMigrationService.GetSession(ctx, c.OrgID, uid) if err != nil { span.SetStatus(codes.Error, "session not found") span.RecordError(err) @@ -247,6 +247,7 @@ func (cma *CloudMigrationAPI) CreateSession(c *contextmodel.ReqContext) response } s, err := cma.cloudMigrationService.CreateSession(ctx, cloudmigration.CloudMigrationSessionRequest{ AuthToken: cmd.AuthToken, + OrgID: c.SignedInUser.OrgID, }) if err != nil { span.SetStatus(codes.Error, "session creation error") @@ -285,7 +286,7 @@ func (cma *CloudMigrationAPI) DeleteSession(c *contextmodel.ReqContext) response return response.ErrOrFallback(http.StatusBadRequest, "invalid session uid", err) } - _, err := cma.cloudMigrationService.DeleteSession(ctx, uid) + _, err := cma.cloudMigrationService.DeleteSession(ctx, c.OrgID, uid) if err != nil { span.SetStatus(codes.Error, "session delete error") span.RecordError(err) @@ -365,6 +366,7 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R SessionUID: sessUid, ResultPage: c.QueryInt("resultPage"), ResultLimit: c.QueryInt("resultLimit"), + OrgID: c.SignedInUser.OrgID, } if q.ResultLimit == 0 { q.ResultLimit = 100 @@ -448,6 +450,7 @@ func (cma *CloudMigrationAPI) GetSnapshotList(c *contextmodel.ReqContext) respon Limit: c.QueryInt("limit"), Page: c.QueryInt("page"), Sort: c.Query("sort"), + OrgID: c.SignedInUser.OrgID, } if q.Limit == 0 { q.Limit = 100 @@ -508,7 +511,7 @@ func (cma *CloudMigrationAPI) UploadSnapshot(c *contextmodel.ReqContext) respons return response.ErrOrFallback(http.StatusBadRequest, "invalid snapshot uid", err) } - if err := cma.cloudMigrationService.UploadSnapshot(ctx, sessUid, snapshotUid); err != nil { + if err := cma.cloudMigrationService.UploadSnapshot(ctx, c.OrgID, sessUid, snapshotUid); err != nil { span.SetStatus(codes.Error, "error uploading snapshot") span.RecordError(err) diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index 8e93df6d024..4ee8c433979 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -17,13 +17,13 @@ type Service interface { DeleteToken(ctx context.Context, uid string) error CreateSession(ctx context.Context, req CloudMigrationSessionRequest) (*CloudMigrationSessionResponse, error) - GetSession(ctx context.Context, migUID string) (*CloudMigrationSession, error) - DeleteSession(ctx context.Context, migUID string) (*CloudMigrationSession, error) - GetSessionList(context.Context) (*CloudMigrationSessionListResponse, error) + GetSession(ctx context.Context, orgID int64, migUID string) (*CloudMigrationSession, error) + DeleteSession(ctx context.Context, orgID int64, migUID string) (*CloudMigrationSession, error) + GetSessionList(ctx context.Context, orgID int64) (*CloudMigrationSessionListResponse, error) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*CloudMigrationSnapshot, error) GetSnapshot(ctx context.Context, query GetSnapshotsQuery) (*CloudMigrationSnapshot, error) GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error) - UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error + UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index e7b58fa5ebf..d749c1d6428 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -362,10 +362,10 @@ func (s *Service) DeleteToken(ctx context.Context, tokenID string) error { return nil } -func (s *Service) GetSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *Service) GetSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSession") defer span.End() - migration, err := s.store.GetMigrationSessionByUID(ctx, uid) + migration, err := s.store.GetMigrationSessionByUID(ctx, orgID, uid) if err != nil { return nil, err } @@ -373,11 +373,11 @@ func (s *Service) GetSession(ctx context.Context, uid string) (*cloudmigration.C return migration, nil } -func (s *Service) GetSessionList(ctx context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (s *Service) GetSessionList(ctx context.Context, orgID int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSessionList") defer span.End() - values, err := s.store.GetCloudMigrationSessionList(ctx) + values, err := s.store.GetCloudMigrationSessionList(ctx, orgID) if err != nil { return nil, fmt.Errorf("retrieving session list from store: %w", err) } @@ -408,7 +408,7 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig return nil, cloudmigration.ErrTokenInvalid.Errorf("token could not be decoded") // don't want to leak info here } - migration := token.ToMigration() + migration := token.ToMigration(cmd.OrgID) // validate token against GMS before saving if err := s.ValidateToken(ctx, migration); err != nil { return nil, err @@ -429,11 +429,11 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig }, nil } -func (s *Service) DeleteSession(ctx context.Context, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { +func (s *Service) DeleteSession(ctx context.Context, orgID int64, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.DeleteSession") defer span.End() - session, snapshots, err := s.store.DeleteMigrationSessionByUID(ctx, sessionUID) + session, snapshots, err := s.store.DeleteMigrationSessionByUID(ctx, orgID, sessionUID) if err != nil { s.report(ctx, session, gmsclient.EventDisconnect, 0, err) return nil, fmt.Errorf("deleting migration from db for session %v: %w", sessionUID, err) @@ -451,7 +451,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI defer span.End() // fetch session for the gms auth token - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, signedInUser.GetOrgID(), sessionUid) if err != nil { return nil, fmt.Errorf("fetching migration session for uid %s: %w", sessionUid, err) } @@ -538,13 +538,13 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSnapshot") defer span.End() - sessionUid, snapshotUid := query.SessionUID, query.SnapshotUID - snapshot, err := s.store.GetSnapshotByUID(ctx, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) + orgID, sessionUid, snapshotUid := query.OrgID, query.SessionUID, query.SnapshotUID + snapshot, err := s.store.GetSnapshotByUID(ctx, orgID, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) if err != nil { return nil, fmt.Errorf("fetching snapshot for uid %s: %w", snapshotUid, err) } - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, orgID, sessionUid) if err != nil { return nil, fmt.Errorf("fetching session for uid %s: %w", sessionUid, err) } @@ -594,7 +594,7 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps } // Refresh the snapshot after the update - snapshot, err = s.store.GetSnapshotByUID(ctx, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) + snapshot, err = s.store.GetSnapshotByUID(ctx, orgID, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) if err != nil { return nil, fmt.Errorf("fetching snapshot for uid %s: %w", snapshotUid, err) } @@ -685,7 +685,7 @@ func (s *Service) GetSnapshotList(ctx context.Context, query cloudmigration.List return snapshotList, nil } -func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.UploadSnapshot", trace.WithAttributes( attribute.String("sessionUid", sessionUid), @@ -695,7 +695,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho defer span.End() // fetch session for the gms auth token - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, orgID, sessionUid) if err != nil { return fmt.Errorf("fetching migration session for uid %s: %w", sessionUid, err) } @@ -703,6 +703,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho snapshot, err := s.GetSnapshot(ctx, cloudmigration.GetSnapshotsQuery{ SnapshotUID: snapshotUid, SessionUID: sessionUid, + OrgID: orgID, }) if err != nil { return fmt.Errorf("fetching snapshot with uid %s: %w", snapshotUid, err) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 5db6e03304e..50da4fa57c0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -29,11 +29,11 @@ func (s *NoopServiceImpl) ValidateToken(ctx context.Context, cm cloudmigration.C return cloudmigration.ErrMigrationDisabled } -func (s *NoopServiceImpl) GetSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *NoopServiceImpl) GetSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) GetSessionList(ctx context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (s *NoopServiceImpl) GetSessionList(ctx context.Context, orgID int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { return nil, cloudmigration.ErrFeatureDisabledError } @@ -41,7 +41,7 @@ func (s *NoopServiceImpl) CreateSession(ctx context.Context, cm cloudmigration.C return nil, cloudmigration.ErrMigrationDisabled } -func (s *NoopServiceImpl) DeleteSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *NoopServiceImpl) DeleteSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { return nil, cloudmigration.ErrFeatureDisabledError } @@ -57,7 +57,7 @@ func (s *NoopServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrat return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error { return cloudmigration.ErrFeatureDisabledError } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index ddbf3a4d22f..50c0bfb6095 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -357,22 +357,19 @@ func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) { func Test_DeletedDashboardsNotMigrated(t *testing.T) { s := setUpServiceTest(t, false).(*Service) + + /** NOTE: this is not used at the moment since we changed the service + // modify what the mock returns for just this test case dashMock := s.dashboardService.(*dashboards.FakeDashboardService) dashMock.On("GetAllDashboards", mock.Anything).Return( []*dashboards.Dashboard{ - { - UID: "1", - Data: simplejson.New(), - }, - { - UID: "2", - Data: simplejson.New(), - Deleted: time.Now(), - }, + {UID: "1", OrgID: 1, Data: simplejson.New()}, + {UID: "2", OrgID: 1, Data: simplejson.New(), Deleted: time.Now()}, }, nil, ) + */ data, err := s.getMigrationDataJSON(context.TODO(), &user.SignedInUser{OrgID: 1}) assert.NoError(t, err) @@ -555,7 +552,7 @@ func TestDeleteSession(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - session, err := s.DeleteSession(ctx, "invalid-session-uid") + session, err := s.DeleteSession(ctx, 2, "invalid-session-uid") require.Nil(t, session) require.Error(t, err) }) @@ -570,6 +567,7 @@ func TestDeleteSession(t *testing.T) { cmd := cloudmigration.CloudMigrationSessionRequest{ AuthToken: createTokenResp.Token, + OrgID: 3, } createResp, err := s.CreateSession(ctx, cmd) @@ -577,12 +575,12 @@ func TestDeleteSession(t *testing.T) { require.NotEmpty(t, createResp.UID) require.NotEmpty(t, createResp.Slug) - deletedSession, err := s.DeleteSession(ctx, createResp.UID) + deletedSession, err := s.DeleteSession(ctx, cmd.OrgID, createResp.UID) require.NoError(t, err) require.NotNil(t, deletedSession) require.Equal(t, deletedSession.UID, createResp.UID) - notFoundSession, err := s.GetSession(ctx, deletedSession.UID) + notFoundSession, err := s.GetSession(ctx, cmd.OrgID, deletedSession.UID) require.ErrorIs(t, err, cloudmigration.ErrMigrationNotFound) require.Nil(t, notFoundSession) }) @@ -868,6 +866,21 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi LastApplied: time.Now().Unix(), })) + // Insert test data for dashboard test, should be removed later when we move GetAllDashboardsByOrgId() to the dashboard service + _, err = sqlStore.GetSqlxSession().Exec(context.Background(), ` + INSERT INTO + dashboard (id, org_id, data, deleted, slug, title, created, version, updated ) + VALUES + (1, 1, '{}', null, 'asdf', 'ghjk', '2024-03-27 15:30:43.000' , '1','2024-03-27 15:30:43.000' ), + (2, 1, '{}', '2024-03-27 15:30:43.000','qwert', 'yuio', '2024-03-27 15:30:43.000' , '2','2024-03-27 15:30:43.000'), + (3, 2, '{}', null, 'asdf', 'ghjk', '2024-03-27 15:30:43.000' , '1','2024-03-27 15:30:43.000' ), + (4, 2, '{}', '2024-03-27 15:30:43.000','qwert', 'yuio', '2024-03-27 15:30:43.000' , '2','2024-03-27 15:30:43.000'); + `, + ) + if err != nil { + require.NoError(t, err) + } + s, err := ProvideService( cfg, httpclient.NewProvider(), diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index a9d5838113c..37d2d8498eb 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -57,21 +57,21 @@ func (m FakeServiceImpl) CreateSession(_ context.Context, _ cloudmigration.Cloud }, nil } -func (m FakeServiceImpl) GetSession(_ context.Context, _ string) (*cloudmigration.CloudMigrationSession, error) { +func (m FakeServiceImpl) GetSession(_ context.Context, _ int64, _ string) (*cloudmigration.CloudMigrationSession, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } return &cloudmigration.CloudMigrationSession{UID: "fake"}, nil } -func (m FakeServiceImpl) DeleteSession(_ context.Context, _ string) (*cloudmigration.CloudMigrationSession, error) { +func (m FakeServiceImpl) DeleteSession(_ context.Context, _ int64, _ string) (*cloudmigration.CloudMigrationSession, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } return &cloudmigration.CloudMigrationSession{UID: "fake"}, nil } -func (m FakeServiceImpl) GetSessionList(_ context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (m FakeServiceImpl) GetSessionList(_ context.Context, _ int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } @@ -154,7 +154,7 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati return cloudSnapshots, nil } -func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, _ int64, sessionUid string, snapshotUid string) error { if m.ReturnError { return fmt.Errorf("mock error") } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index df57e1f2fc1..1722eb213ce 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -44,7 +44,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S defer span.End() // Data sources - dataSources, err := s.getDataSourceCommands(ctx) + dataSources, err := s.getDataSourceCommands(ctx, signedInUser) if err != nil { s.log.Error("Failed to get datasources", "err", err) return nil, err @@ -208,17 +208,17 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return migrationData, nil } -func (s *Service) getDataSourceCommands(ctx context.Context) ([]datasources.AddDataSourceCommand, error) { +func (s *Service) getDataSourceCommands(ctx context.Context, signedInUser *user.SignedInUser) ([]datasources.AddDataSourceCommand, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getDataSourceCommands") defer span.End() - dataSources, err := s.dsService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + dataSources, err := s.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{OrgID: signedInUser.GetOrgID()}) if err != nil { s.log.Error("Failed to get all datasources", "err", err) return nil, err } - result := []datasources.AddDataSourceCommand{} + result := make([]datasources.AddDataSourceCommand, 0, len(dataSources)) for _, dataSource := range dataSources { // Decrypt secure json to send raw credentials decryptedData, err := s.secretsService.DecryptJsonData(ctx, dataSource.SecureJsonData) @@ -253,7 +253,7 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getDashboardAndFolderCommands") defer span.End() - dashs, err := s.dashboardService.GetAllDashboards(ctx) + dashs, err := s.store.GetAllDashboardsByOrgId(ctx, signedInUser.GetOrgID()) if err != nil { return nil, nil, err } @@ -279,20 +279,21 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{ UIDs: folderUids, SignedInUser: signedInUser, + OrgID: signedInUser.GetOrgID(), WithFullpathUIDs: true, }) if err != nil { return nil, nil, err } - folderCmds := make([]folder.CreateFolderCommand, len(folders)) - for i, f := range folders { - folderCmds[i] = folder.CreateFolderCommand{ + folderCmds := make([]folder.CreateFolderCommand, 0, len(folders)) + for _, f := range folders { + folderCmds = append(folderCmds, folder.CreateFolderCommand{ UID: f.UID, Title: f.Title, Description: f.Description, ParentUID: f.ParentUID, - } + }) } return dashboardCmds, folderCmds, nil @@ -641,6 +642,7 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{ UIDs: folderUIDs, SignedInUser: signedInUser, + OrgID: signedInUser.GetOrgID(), WithFullpathUIDs: true, }) if err != nil { @@ -661,15 +663,18 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser // getParentNames finds the parent names for resources and returns a map of data type: {data UID : parentName} // for dashboards, folders and library elements - the parent is the parent folder func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedInUser, dashboards []dashboards.Dashboard, folders []folder.CreateFolderCommand, libraryElements []libraryElement) (map[cloudmigration.MigrateDataType]map[string](string), error) { - parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string](string)) + parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string]string) for _, dataType := range currentMigrationTypes { parentNamesByType[dataType] = make(map[string]string) } // Obtain list of unique folderUIDs - parentFolderUIDsSet := make(map[string]struct{}, len(dashboards)+len(folders)+len(libraryElements)) + parentFolderUIDsSet := make(map[string]struct{}) for _, dashboard := range dashboards { - parentFolderUIDsSet[dashboard.FolderUID] = struct{}{} + // we dont need the root folder + if dashboard.FolderUID != "" { + parentFolderUIDsSet[dashboard.FolderUID] = struct{}{} + } } for _, f := range folders { parentFolderUIDsSet[f.ParentUID] = struct{}{} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/store.go b/pkg/services/cloudmigration/cloudmigrationimpl/store.go index cbc4b7c4d99..2a36c1acfdc 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/store.go @@ -4,24 +4,29 @@ import ( "context" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/dashboards" ) type store interface { CreateMigrationSession(ctx context.Context, session cloudmigration.CloudMigrationSession) (*cloudmigration.CloudMigrationSession, error) - GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) - GetCloudMigrationSessionList(ctx context.Context) ([]*cloudmigration.CloudMigrationSession, error) + GetMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) + GetCloudMigrationSessionList(ctx context.Context, orgID int64) ([]*cloudmigration.CloudMigrationSession, error) // DeleteMigrationSessionByUID deletes the migration session, and all the related snapshot and resources. // the work is done in a transaction. - DeleteMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) + DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error) UpdateSnapshot(ctx context.Context, snapshot cloudmigration.UpdateSnapshotCmd) error - GetSnapshotByUID(ctx context.Context, sessUid, id string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) + GetSnapshotByUID(ctx context.Context, orgID int64, sessUid, id string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) - DeleteSnapshot(ctx context.Context, snapshotUid string) error - CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error - GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) - GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) - DeleteSnapshotResources(ctx context.Context, snapshotUid string) error + // Deleted because were not used externally + // - DeleteSnapshot(ctx context.Context, snapshotUid string) error + // - CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error + // - GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) + // - GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) + // - DeleteSnapshotResources(ctx context.Context, snapshotUid string) error + + // TODO move this function dashboards/databases/databases.go + GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index b5189fffcff..a540beb10c0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/secrets" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -29,10 +30,10 @@ const ( GetSnapshotListSortingLatest = "latest" ) -func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { var cm cloudmigration.CloudMigrationSession err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - exist, err := sess.Where("uid=?", uid).Get(&cm) + exist, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&cm) if err != nil { return err } @@ -74,11 +75,10 @@ func (ss *sqlStore) CreateMigrationSession(ctx context.Context, migration cloudm return &migration, nil } -func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context) ([]*cloudmigration.CloudMigrationSession, error) { +func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context, orgID int64) ([]*cloudmigration.CloudMigrationSession, error) { var migrations = make([]*cloudmigration.CloudMigrationSession, 0) err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - sess.OrderBy("created DESC") - return sess.Find(&migrations) + return sess.Where("org_id=?", orgID).OrderBy("created DESC").Find(&migrations) }) if err != nil { return nil, err @@ -95,10 +95,10 @@ func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context) ([]*cloudm return migrations, nil } -func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) { +func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) { var c cloudmigration.CloudMigrationSession err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - exist, err := sess.Where("uid=?", uid).Get(&c) + exist, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&c) if err != nil { return err } @@ -124,11 +124,11 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, uid string) err = ss.db.InTransaction(ctx, func(ctx context.Context) error { for _, snapshot := range snapshots { - err := ss.DeleteSnapshotResources(ctx, snapshot.UID) + err := ss.deleteSnapshotResources(ctx, snapshot.UID) if err != nil { return fmt.Errorf("deleting snapshot resource from db: %w", err) } - err = ss.DeleteSnapshot(ctx, snapshot.UID) + err = ss.deleteSnapshot(ctx, orgID, snapshot.UID) if err != nil { return fmt.Errorf("deleting snapshot from db: %w", err) } @@ -214,7 +214,7 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up // Update resources if set if len(update.Resources) > 0 { - if err := ss.CreateUpdateSnapshotResources(ctx, update.UID, update.Resources); err != nil { + if err := ss.createUpdateSnapshotResources(ctx, update.UID, update.Resources); err != nil { return err } } @@ -224,7 +224,7 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up return err } -func (ss *sqlStore) DeleteSnapshot(ctx context.Context, snapshotUid string) error { +func (ss *sqlStore) deleteSnapshot(ctx context.Context, orgID int64, snapshotUid string) error { return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Delete(cloudmigration.CloudMigrationSnapshot{ UID: snapshotUid, @@ -233,9 +233,16 @@ func (ss *sqlStore) DeleteSnapshot(ctx context.Context, snapshotUid string) erro }) } -func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) { +func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, orgID int64, sessionUid, uid string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) { + // first we check if the session exists, using orgId and sessionUid + session, err := ss.GetMigrationSessionByUID(ctx, orgID, sessionUid) + if err != nil || session == nil { + return nil, err + } + + // now we get the snapshot var snapshot cloudmigration.CloudMigrationSnapshot - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + err = ss.db.WithDbSession(ctx, func(sess *db.Session) error { exist, err := sess.Where("session_uid=? AND uid=?", sessionUid, uid).Get(&snapshot) if err != nil { return err @@ -257,11 +264,11 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string snapshot.EncryptionKey = []byte(secret) } - resources, err := ss.GetSnapshotResources(ctx, uid, resultPage, resultLimit) + resources, err := ss.getSnapshotResources(ctx, uid, resultPage, resultLimit) if err == nil { snapshot.Resources = resources } - stats, err := ss.GetSnapshotResourceStats(ctx, uid) + stats, err := ss.getSnapshotResourceStats(ctx, uid) if err == nil { snapshot.StatsRollup = *stats } @@ -274,7 +281,9 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) { var snapshots = make([]cloudmigration.CloudMigrationSnapshot, 0) err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - sess.Join("INNER", "cloud_migration_session", "cloud_migration_session.uid = cloud_migration_snapshot.session_uid") + sess.Join("INNER", "cloud_migration_session", + "cloud_migration_session.uid = cloud_migration_snapshot.session_uid AND cloud_migration_session.org_id = ?", query.OrgID, + ) if query.Limit != GetAllSnapshots { offset := (query.Page - 1) * query.Limit sess.Limit(query.Limit, offset) @@ -298,7 +307,7 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li snapshot.EncryptionKey = []byte(secret) } - if stats, err := ss.GetSnapshotResourceStats(ctx, snapshot.UID); err != nil { + if stats, err := ss.getSnapshotResourceStats(ctx, snapshot.UID); err != nil { return nil, err } else { snapshot.StatsRollup = *stats @@ -310,7 +319,7 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li // CreateUpdateSnapshotResources either updates a migration resource for a snapshot, or creates it if it does not exist // If the uid is not known, it uses snapshot_uid + resource_uid as a lookup -func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error { +func (ss *sqlStore) createUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error { return ss.db.InTransaction(ctx, func(ctx context.Context) error { sql := "UPDATE cloud_migration_resource SET status=?, error_string=?, error_code=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)" err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { @@ -344,7 +353,7 @@ func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotU }) } -func (ss *sqlStore) GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) { +func (ss *sqlStore) getSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) { if page < 1 { page = 1 } @@ -366,7 +375,7 @@ func (ss *sqlStore) GetSnapshotResources(ctx context.Context, snapshotUid string return resources, nil } -func (ss *sqlStore) GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) { +func (ss *sqlStore) getSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) { typeCounts := make([]struct { Count int `json:"count"` Type string `json:"type"` @@ -413,7 +422,7 @@ func (ss *sqlStore) GetSnapshotResourceStats(ctx context.Context, snapshotUid st return stats, nil } -func (ss *sqlStore) DeleteSnapshotResources(ctx context.Context, snapshotUid string) error { +func (ss *sqlStore) deleteSnapshotResources(ctx context.Context, snapshotUid string) error { return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Delete(cloudmigration.CloudMigrationResource{ SnapshotUID: snapshotUid, @@ -456,3 +465,19 @@ func (ss *sqlStore) decryptToken(ctx context.Context, cm *cloudmigration.CloudMi return nil } + +// TODO move this function dashboards/databases/databases.go +func (ss *sqlStore) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) { + //ctx, span := tracer.Start(ctx, "dashboards.database.GetAllDashboardsByOrgId") + //defer span.End() + + var dashs = make([]*dashboards.Dashboard, 0) + err := ss.db.WithDbSession(ctx, func(session *db.Session) error { + // "deleted IS NULL" is to avoid deleted dashboards + return session.Where("org_id = ? AND deleted IS NULL", orgID).Find(&dashs) + }) + if err != nil { + return nil, err + } + return dashs, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 58b98f6c1ad..c9ae1163eba 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -25,7 +25,7 @@ func Test_GetAllCloudMigrationSessions(t *testing.T) { ctx := context.Background() t.Run("get all cloud_migration_session entries", func(t *testing.T) { - value, err := s.GetCloudMigrationSessionList(ctx) + value, err := s.GetCloudMigrationSessionList(ctx, 1) require.NoError(t, err) require.Equal(t, 3, len(value)) for _, m := range value { @@ -54,6 +54,7 @@ func Test_CreateMigrationSession(t *testing.T) { cm := cloudmigration.CloudMigrationSession{ AuthToken: encodeToken("token"), Slug: "fake_stack", + OrgID: 3, StackID: 1234, RegionSlug: "fake_slug", ClusterSlug: "fake_cluster_slug", @@ -63,7 +64,7 @@ func Test_CreateMigrationSession(t *testing.T) { require.NotEmpty(t, sess.ID) require.NotEmpty(t, sess.UID) - getRes, err := s.GetMigrationSessionByUID(ctx, sess.UID) + getRes, err := s.GetMigrationSessionByUID(ctx, 3, sess.UID) require.NoError(t, err) require.Equal(t, sess.ID, getRes.ID) require.Equal(t, sess.UID, getRes.UID) @@ -80,13 +81,15 @@ func Test_GetMigrationSessionByUID(t *testing.T) { ctx := context.Background() t.Run("find session by uid", func(t *testing.T) { uid := "qwerty" - mig, err := s.GetMigrationSessionByUID(ctx, uid) + orgId := int64(1) + mig, err := s.GetMigrationSessionByUID(ctx, orgId, uid) require.NoError(t, err) require.Equal(t, uid, mig.UID) + require.Equal(t, orgId, mig.OrgID) }) t.Run("returns error if session is not found by uid", func(t *testing.T) { - _, err := s.GetMigrationSessionByUID(ctx, "fake_uid_1234") + _, err := s.GetMigrationSessionByUID(ctx, 1, "fake_uid_1234") require.ErrorIs(t, cloudmigration.ErrMigrationNotFound, err) }) } @@ -115,7 +118,10 @@ func Test_SnapshotManagement(t *testing.T) { ctx := context.Background() t.Run("tests the snapshot lifecycle", func(t *testing.T) { - session, err := s.CreateMigrationSession(ctx, cloudmigration.CloudMigrationSession{}) + session, err := s.CreateMigrationSession(ctx, cloudmigration.CloudMigrationSession{ + OrgID: 1, + AuthToken: encodeToken("token"), + }) require.NoError(t, err) // create a snapshot @@ -129,7 +135,7 @@ func Test_SnapshotManagement(t *testing.T) { require.NotEmpty(t, snapshotUid) //retrieve it from the db - snapshot, err := s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.NoError(t, err) require.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) @@ -138,22 +144,22 @@ func Test_SnapshotManagement(t *testing.T) { require.NoError(t, err) //retrieve it again - snapshot, err = s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.NoError(t, err) require.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) // lists snapshots and ensures it's in there - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: session.UID, Page: 1, Limit: 100}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: session.UID, OrgID: 1, Page: 1, Limit: 100}) require.NoError(t, err) require.Len(t, snapshots, 1) require.Equal(t, *snapshot, snapshots[0]) // delete snapshot - err = s.DeleteSnapshot(ctx, snapshotUid) + err = s.deleteSnapshot(ctx, 1, snapshotUid) require.NoError(t, err) // now we expect not to find the snapshot - snapshot, err = s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.ErrorIs(t, err, cloudmigration.ErrSnapshotNotFound) require.Nil(t, snapshot) }) @@ -165,12 +171,12 @@ func Test_SnapshotResources(t *testing.T) { t.Run("tests CRUD of snapshot resources", func(t *testing.T) { // Get the default rows from the test - resources, err := s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err := s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 3) // create a new resource and update an existing resource - err = s.CreateUpdateSnapshotResources(ctx, "poiuy", []cloudmigration.CloudMigrationResource{ + err = s.createUpdateSnapshotResources(ctx, "poiuy", []cloudmigration.CloudMigrationResource{ { Type: cloudmigration.DatasourceDataType, RefID: "mi39fj", @@ -184,7 +190,7 @@ func Test_SnapshotResources(t *testing.T) { assert.NoError(t, err) // Get resources again - resources, err = s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err = s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 4) // ensure existing resource was updated @@ -203,7 +209,7 @@ func Test_SnapshotResources(t *testing.T) { } // check stats - stats, err := s.GetSnapshotResourceStats(ctx, "poiuy") + stats, err := s.getSnapshotResourceStats(ctx, "poiuy") assert.NoError(t, err) assert.Equal(t, map[cloudmigration.MigrateDataType]int{ cloudmigration.DatasourceDataType: 2, @@ -217,10 +223,10 @@ func Test_SnapshotResources(t *testing.T) { assert.Equal(t, 4, stats.Total) // delete snapshot resources - err = s.DeleteSnapshotResources(ctx, "poiuy") + err = s.deleteSnapshotResources(ctx, "poiuy") assert.NoError(t, err) // make sure they're gone - resources, err = s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err = s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 0) }) @@ -233,7 +239,7 @@ func TestGetSnapshotList(t *testing.T) { ctx := context.Background() t.Run("returns list of snapshots that belong to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 100}) require.NoError(t, err) ids := make([]string, 0) @@ -246,7 +252,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns only one snapshot that belongs to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 1}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 1}) require.NoError(t, err) assert.Len(t, snapshots, 1) }) @@ -258,7 +264,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns paginated snapshot that belongs to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 2, Limit: 1}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 2, Limit: 1}) require.NoError(t, err) ids := make([]string, 0) @@ -271,7 +277,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns desc sorted list of snapshots that belong to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100, Sort: "latest"}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 100, Sort: "latest"}) require.NoError(t, err) ids := make([]string, 0) @@ -291,7 +297,7 @@ func TestGetSnapshotList(t *testing.T) { t.Run("if the session is deleted, snapshots can't be retrieved anymore", func(t *testing.T) { // Delete the session. - _, _, err := s.DeleteMigrationSessionByUID(ctx, sessionUID) + _, _, err := s.DeleteMigrationSessionByUID(ctx, 1, sessionUID) require.NoError(t, err) // Fetch the snapshots that belong to the deleted session. @@ -363,15 +369,17 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { // insert cloud migration test data _, err := testDB.GetSqlxSession().Exec(ctx, ` INSERT INTO - cloud_migration_session (id, uid, auth_token, slug, stack_id, region_slug, cluster_slug, created, updated) + cloud_migration_session (id, uid, org_id, auth_token, slug, stack_id, region_slug, cluster_slug, created, updated) VALUES - (1,'qwerty', ?, '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), - (2,'asdfgh', ?, '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), - (3,'zxcvbn', ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); + (1,'qwerty', 1, ?, '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (2,'asdfgh', 1, ?, '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (3,'zxcvbn', 1, ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (4,'zxcvbn_org2', 2, ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); `, encodeToken("12345"), encodeToken("6789"), encodeToken("777"), + encodeToken("0987"), ) require.NoError(t, err) @@ -380,9 +388,10 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { INSERT INTO cloud_migration_snapshot (session_uid, uid, created, updated, finished, status) VALUES - ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), ('qwerty', 'lkjhg', '2024-03-26 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), - ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); + ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('zxcvbn_org2', 'mnbvvc_org2', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); `, ) require.NoError(t, err) @@ -400,7 +409,8 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { ('mnbvde', 'poiuy', 'DATASOURCE', 'jf38gh', 'OK', ''), ('qwerty', 'poiuy', 'DASHBOARD', 'ejcx4d', 'ERROR', 'fake error'), ('zxcvbn', 'poiuy', 'FOLDER', 'fi39fj', 'PENDING', ''), - ('4fi9sd', '39fi39', 'FOLDER', 'fi39fj', 'OK', ''); + ('4fi9sd', '39fi39', 'FOLDER', 'fi39fj', 'OK', ''), + ('4fi9ee', 'mnbvvc_org2', 'DATASOURCE', 'fi39asd', 'OK', ''); `, ) require.NoError(t, err) diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 6fb86ac9e77..635ae12572a 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -21,6 +21,7 @@ var ( // CloudMigrationSession represents a configured migration token type CloudMigrationSession struct { ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` UID string `xorm:"uid"` AuthToken string Slug string @@ -144,6 +145,8 @@ type CloudMigrationRunList struct { type CloudMigrationSessionRequest struct { AuthToken string + // OrgId in the on prem instance + OrgID int64 } type CloudMigrationSessionResponse struct { @@ -159,6 +162,7 @@ type CloudMigrationSessionListResponse struct { type GetSnapshotsQuery struct { SnapshotUID string + OrgID int64 SessionUID string ResultPage int ResultLimit int @@ -166,6 +170,7 @@ type GetSnapshotsQuery struct { type ListSnapshotsQuery struct { SessionUID string + OrgID int64 Page int Limit int Sort string @@ -189,13 +194,14 @@ type Base64EncodedTokenPayload struct { Instance Base64HGInstance } -func (p Base64EncodedTokenPayload) ToMigration() CloudMigrationSession { +func (p Base64EncodedTokenPayload) ToMigration(orgID int64) CloudMigrationSession { return CloudMigrationSession{ AuthToken: p.Token, Slug: p.Instance.Slug, StackID: p.Instance.StackID, RegionSlug: p.Instance.RegionSlug, ClusterSlug: p.Instance.ClusterSlug, + OrgID: orgID, } } diff --git a/pkg/services/sqlstore/migrations/cloud_migrations.go b/pkg/services/sqlstore/migrations/cloud_migrations.go index b7d5a84d6bb..18dc3590570 100644 --- a/pkg/services/sqlstore/migrations/cloud_migrations.go +++ b/pkg/services/sqlstore/migrations/cloud_migrations.go @@ -66,7 +66,7 @@ func addCloudMigrationsMigrations(mg *Migrator) { })) // --- v2 - asynchronous workflow refactor - sessionTable := Table{ + migrationSessionTable := Table{ Name: "cloud_migration_session", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, @@ -99,7 +99,7 @@ func addCloudMigrationsMigrations(mg *Migrator) { }, } - addTableReplaceMigrations(mg, migrationTable, sessionTable, 2, map[string]string{ + addTableReplaceMigrations(mg, migrationTable, migrationSessionTable, 2, map[string]string{ "id": "id", "uid": "uid", "auth_token": "auth_token", @@ -171,6 +171,11 @@ func addCloudMigrationsMigrations(mg *Migrator) { Nullable: true, })) + // -- Adds org_id column for for all elements - defaults to 1 (default org) + mg.AddMigration("add cloud_migration_session.org_id column", NewAddColumnMigration(migrationSessionTable, &Column{ + Name: "org_id", Type: DB_BigInt, Nullable: false, Default: "1", + })) + mg.AddMigration("add cloud_migration_resource.error_code column", NewAddColumnMigration(migrationResourceTable, &Column{ Name: "error_code", Type: DB_Text, From f83f7332a1fb1a1ebf480db61c74ca07ae7f7b7c Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 12 Nov 2024 14:55:05 -0600 Subject: [PATCH 26/55] Docs/removing outdated versioning info in OSS data source docs (#96351) docs/removing outdated versioning info in oss data source docs --- docs/sources/datasources/postgres/_index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/datasources/postgres/_index.md b/docs/sources/datasources/postgres/_index.md index 223c47868ed..3489b760f70 100644 --- a/docs/sources/datasources/postgres/_index.md +++ b/docs/sources/datasources/postgres/_index.md @@ -428,9 +428,7 @@ SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter' ### Using Variables in Queries -From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses. - -From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. +Template variable values are only quoted when the template variable is a `multi-value`. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. From acf57aa556fdba98782976f48d5d89a5c0a288ae Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 13 Nov 2024 09:28:19 +0100 Subject: [PATCH 27/55] Frontend Sandbox: Do not consider internal plugins as elegible (#96325) --- .../sandbox/sandbox_plugin_loader_registry.test.ts | 8 ++++++++ .../plugins/sandbox/sandbox_plugin_loader_registry.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts index 9ff282baef8..a0c0121ee0f 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts @@ -7,6 +7,7 @@ import { shouldLoadPluginInFrontendSandbox, setSandboxEnabledCheck, isPluginFrontendSandboxEnabled, + isPluginFrontendSandboxEligible, } from './sandbox_plugin_loader_registry'; jest.mock('@grafana/runtime', () => ({ @@ -89,4 +90,11 @@ describe('Sandbox eligibility checks', () => { expect(customCheck).toHaveBeenCalledWith({ pluginId: 'test-plugin' }); expect(result).toBe(false); }); + + test('isPluginFrontendSandboxEligible returns false for plugins with internal signature', async () => { + //@ts-expect-error We don't publicly export the internal signature + getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signature: 'internal' }); + const result = await isPluginFrontendSandboxEligible({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); }); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts index 42848ce7863..9d94a3556e0 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts @@ -64,7 +64,7 @@ export async function isPluginFrontendSandboxEligible({ try { // don't run grafana-signed plugins in sandbox const pluginMeta = await getPluginSettings(pluginId, { showErrorAlert: false }); - if (pluginMeta.signatureType === PluginSignatureType.grafana) { + if (pluginMeta.signatureType === PluginSignatureType.grafana || pluginMeta.signature === 'internal') { return false; } } catch (e) { From 275ee55e8f4d4ccdddf8318bb79c468073c8d7cb Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 13 Nov 2024 10:36:46 +0100 Subject: [PATCH 28/55] Auth: Extend the SAMLStrategy with entity_id (#96234) Add the ability to specify EntityID --- pkg/services/ssosettings/strategies/saml_strategy.go | 1 + pkg/services/ssosettings/strategies/saml_strategy_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pkg/services/ssosettings/strategies/saml_strategy.go b/pkg/services/ssosettings/strategies/saml_strategy.go index 740d86ee2c0..026fd5cef7f 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy.go +++ b/pkg/services/ssosettings/strategies/saml_strategy.go @@ -33,6 +33,7 @@ func (s *SAMLStrategy) loadSAMLSettings() map[string]any { section := s.settingsProvider.Section("auth.saml") result := map[string]any{ "enabled": section.KeyValue("enabled").MustBool(false), + "entity_id": section.KeyValue("entity_id").MustString(""), "name": section.KeyValue("name").MustString("SAML"), "single_logout": section.KeyValue("single_logout").MustBool(false), "allow_sign_up": section.KeyValue("allow_sign_up").MustBool(false), diff --git a/pkg/services/ssosettings/strategies/saml_strategy_test.go b/pkg/services/ssosettings/strategies/saml_strategy_test.go index d076f708400..c04ef280318 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy_test.go +++ b/pkg/services/ssosettings/strategies/saml_strategy_test.go @@ -15,6 +15,7 @@ var ( iniSAMLContent = ` [auth.saml] enabled = true + entity_id = custom-entity-id single_logout = true name = "SAML Test" allow_sign_up = true @@ -54,6 +55,7 @@ var ( expectedSAMLInfo = map[string]any{ "enabled": true, + "entity_id": "custom-entity-id", "single_logout": true, "allow_sign_up": true, "auto_login": true, From ba836dec63380e41a40e0662c85f21503056147e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:39:54 +0000 Subject: [PATCH 29/55] Update dependency @formatjs/intl-durationformat to ^0.6.0 (#96340) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 64 ++++++++++++++++++++++++++-------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 0e4184c06c7..b98fa5da925 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "@emotion/react": "11.13.3", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.27", - "@formatjs/intl-durationformat": "^0.5.0", + "@formatjs/intl-durationformat": "^0.6.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/aws-sdk": "0.5.0", "@grafana/azure-sdk": "0.0.5", diff --git a/yarn.lock b/yarn.lock index 9ee71611dfd..52ee42ba858 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2948,14 +2948,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.2.0": - version: 2.2.0 - resolution: "@formatjs/ecma402-abstract@npm:2.2.0" +"@formatjs/ecma402-abstract@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/ecma402-abstract@npm:2.2.3" dependencies: - "@formatjs/fast-memoize": "npm:2.2.1" - "@formatjs/intl-localematcher": "npm:0.5.5" - tslib: "npm:^2.7.0" - checksum: 10/f836dfa8787c8d0d2022f7c77fd15025a402ee519f9cb6e68dfce2b937956059e4f96b6059c40506c7260d93bdfd0517001b568cdbea8db08b439ce5d21f437e + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" + tslib: "npm:2" + checksum: 10/d39e9f0d36c296a635f52aa35e07a67b6aa90383a30a046a0508e5d730676399fd0e67188eff463fe2a4d5febc9f567af45788fdf881e070910be7eb9294dd8c languageName: node linkType: hard @@ -2968,12 +2968,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/fast-memoize@npm:2.2.1": - version: 2.2.1 - resolution: "@formatjs/fast-memoize@npm:2.2.1" +"@formatjs/fast-memoize@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/fast-memoize@npm:2.2.3" dependencies: - tslib: "npm:^2.7.0" - checksum: 10/7bb12904d4c93cfae17006388b26d97cc0e32fef5af510f80974437382cd13a88b439b4407d96b6646af1806977cf34113f3dc8f1c96b28f73856700ee7857fd + tslib: "npm:2" + checksum: 10/a9634acb5e03d051e09881eea5484ab02271f7d6b5f96ae9485674ab3c359aa881bc45fc07a1181ae4b2d6e288dadc169f578d142d698913ebbefa373014cac2 languageName: node linkType: hard @@ -2998,14 +2998,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:^0.5.0": - version: 0.5.0 - resolution: "@formatjs/intl-durationformat@npm:0.5.0" +"@formatjs/intl-durationformat@npm:^0.6.0": + version: 0.6.3 + resolution: "@formatjs/intl-durationformat@npm:0.6.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.0" - "@formatjs/intl-localematcher": "npm:0.5.5" - tslib: "npm:^2.7.0" - checksum: 10/3fe55f729cf76df13e5b5bf085cf06f7f46bb69b1eeddfbfa8a5223d87c65438a5e91199e39ff8a24fa7c79d5e83754ee406b3012adb9ae3ff370239cb067377 + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" + tslib: "npm:2" + checksum: 10/1adc77b06e2f5791aef526cc3041de339cf99bc7e68cce81490066e0b1b0124d5cbe03d9f28b6a46bdd9ba22a0363315ccb20b9d1859ee0bcea7f79e9365844a languageName: node linkType: hard @@ -3018,12 +3018,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.5.5": - version: 0.5.5 - resolution: "@formatjs/intl-localematcher@npm:0.5.5" +"@formatjs/intl-localematcher@npm:0.5.7": + version: 0.5.7 + resolution: "@formatjs/intl-localematcher@npm:0.5.7" dependencies: - tslib: "npm:^2.7.0" - checksum: 10/179069eb3a23510e17f118efa3b6a9873f18683977cb813e57ef08cba09bbac73fce13f42c557deb62d3da4f32a8c7ad7d522aae4f1b41625835ce564e59e750 + tslib: "npm:2" + checksum: 10/52201f12212e7e9cba1a4f99020da587b13e44e06e03c4ccd4e5ac0829b411e73dfe0904a9039ef81eeabeea04ed8cfae9e727e6791acd0230745b7bd3ad059e languageName: node linkType: hard @@ -18701,7 +18701,7 @@ __metadata: "@eslint/compat": "npm:^1.2.0" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.27" - "@formatjs/intl-durationformat": "npm:^0.5.0" + "@formatjs/intl-durationformat": "npm:^0.6.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/aws-sdk": "npm:0.5.0" "@grafana/azure-sdk": "npm:0.0.5" @@ -31431,6 +31431,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2, tslib@npm:^2.8.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 + languageName: node + linkType: hard + "tslib@npm:2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -31459,13 +31466,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2, tslib@npm:^2.7.0, tslib@npm:^2.8.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" From aacce9e5f7242b1280986fa6da7ddd9226aa93f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:40:14 +0000 Subject: [PATCH 30/55] Update dependency @grafana/faro-web-tracing to v1.11.0 (#96341) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 52ee42ba858..abc014d251c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3681,7 +3681,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.11.0, @grafana/faro-web-sdk@npm:^1.10.2, @grafana/faro-web-sdk@npm:^1.3.6": +"@grafana/faro-web-sdk@npm:1.11.0, @grafana/faro-web-sdk@npm:^1.11.0, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.11.0 resolution: "@grafana/faro-web-sdk@npm:1.11.0" dependencies: @@ -3693,10 +3693,10 @@ __metadata: linkType: hard "@grafana/faro-web-tracing@npm:^1.8.2": - version: 1.10.2 - resolution: "@grafana/faro-web-tracing@npm:1.10.2" + version: 1.11.0 + resolution: "@grafana/faro-web-tracing@npm:1.11.0" dependencies: - "@grafana/faro-web-sdk": "npm:^1.10.2" + "@grafana/faro-web-sdk": "npm:^1.11.0" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/context-zone": "npm:1.26.0" "@opentelemetry/core": "npm:^1.26.0" @@ -3708,7 +3708,7 @@ __metadata: "@opentelemetry/resources": "npm:^1.26.0" "@opentelemetry/sdk-trace-web": "npm:^1.26.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" - checksum: 10/c477d2373fc51d432f1492d012e828310c5044766a7a4d9317546d2e9ba074d5532a5d4642e9fc4bfa9a70bf12969a593bc3151df186a0865f408976ccd73a26 + checksum: 10/d9e46b42d574f8e012bb113a5f3bd03699433cab01f57c548a3461102dd1a5882b7e53c340b4a8374a340cfff69d30af49049b7c6f580c9d78ed7377819e1ecd languageName: node linkType: hard From 8eff84661ab9e73092f8538481075ecd989dbbf7 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 13 Nov 2024 11:08:21 +0100 Subject: [PATCH 31/55] Plugin history: Hide downgrade or upgrade buttons for preinstalled plugins (#96332) --- .../components/VersionInstallButton.test.tsx | 74 +++++++++++++++++++ .../admin/components/VersionInstallButton.tsx | 15 +++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index e052b0aa128..38511a2dfea 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; +import { config } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { Version } from '../types'; @@ -8,6 +9,13 @@ import { Version } from '../types'; import { VersionInstallButton } from './VersionInstallButton'; describe('VersionInstallButton', () => { + const originalConfig = { ...config }; + afterEach(() => { + config.featureToggles = { + ...originalConfig.featureToggles, + }; + config.pluginCatalogPreinstalledPlugins = originalConfig.pluginCatalogPreinstalledPlugins; + }); it('should show install when no version is installed', () => { const version: Version = { version: '', @@ -103,6 +111,72 @@ describe('VersionInstallButton', () => { const el = screen.getByText('Installed'); expect(el).toBeVisible(); }); + + it('should hide the upgrade button if preinstalled and pinned', () => { + const version: Version = { + version: '1.0.1', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.0'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.0' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Upgrade')).not.toBeVisible(); + }); + + it('should hide the downgrade button if preinstalled and pinned', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.1' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Downgrade')).not.toBeVisible(); + }); + + it('should hide the downgrade button if preinstalled', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Downgrade')).not.toBeVisible(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 46117ca6d96..a26973b704c 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -3,10 +3,11 @@ import { useEffect, useState } from 'react'; import { gt } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Badge, Button, ConfirmModal, Icon, Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import { isPreinstalledPlugin } from '../helpers'; import { useInstall } from '../state/hooks'; import { Version } from '../types'; @@ -89,11 +90,22 @@ export const VersionInstallButton = ({ }; let label = 'Downgrade'; + let hidden = false; + const isPreinstalled = isPreinstalledPlugin(pluginId); if (!installedVersion) { label = 'Install'; } else if (gt(version.version, installedVersion)) { label = 'Upgrade'; + if (isPreinstalled.withVersion) { + // Hide button if the plugin is preinstalled with a specific version + hidden = true; + } + } else { + if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { + // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated + hidden = true; + } } return ( @@ -106,6 +118,7 @@ export const VersionInstallButton = ({ variant={latestCompatibleVersion === version.version ? 'primary' : 'secondary'} onClick={onInstallClick} className={styles.button} + hidden={hidden} > {label} {isInstalling ? : getIcon(label)} From c28c8f920ffd3b41eb985bfdd85bd91e9ad28af4 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 13 Nov 2024 10:13:33 +0000 Subject: [PATCH 32/55] Alerting: Increase timeout and add retry to flaky EditContactPoint test (#96372) --- .../components/contact-points/EditContactPoint.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index 477955062bc..97c5b4627f3 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -36,6 +36,7 @@ const getTemplatePreviewContent = async () => const templatesSelectorTestId = 'existing-templates-selector'; describe('Edit contact point', () => { + jest.retryTimes(2); it('can edit a contact point with existing template field values', async () => { const { user } = renderEditContactPoint('lotsa-emails'); @@ -72,5 +73,5 @@ describe('Edit contact point', () => { await user.click(screen.getByRole('button', { name: /save contact point/i })); expect(await screen.findByText(/redirected/i)).toBeInTheDocument(); - }); + }, 600000); }); From 483a6c7fe24bffeb0734e06758a171502c3dc927 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:17:51 +0000 Subject: [PATCH 33/55] Update dependency @grafana/plugin-e2e to v1.12.0 (#96368) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index abc014d251c..4859eea3752 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3855,16 +3855,16 @@ __metadata: linkType: soft "@grafana/plugin-e2e@npm:^1.11.0": - version: 1.11.3 - resolution: "@grafana/plugin-e2e@npm:1.11.3" + version: 1.12.0 + resolution: "@grafana/plugin-e2e@npm:1.12.0" dependencies: "@grafana/e2e-selectors": "npm:^11.4.0-205692" semver: "npm:^7.5.4" - uuid: "npm:^11.0.0" + uuid: "npm:^11.0.2" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/a0e127bae12507c8c0a4e93d5f08d7c08ca369c0b8da3737d580c0f0ce843d7a50c1f1e6e7fa6b36546ca6f5acecf2472598f2aa44666fbab40d65712472b4f1 + checksum: 10/7e378bed8ad7a6f07a834fc9a02ca9fbcc63e5d842545e9fa443b0f24fcb62001f61a3592a45b0893af87a31f022b2646d388d152325f92df94213d94ecfcaad languageName: node linkType: hard @@ -32168,7 +32168,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.0": +"uuid@npm:^11.0.2": version: 11.0.3 resolution: "uuid@npm:11.0.3" bin: From f5bd4d078bfc003378aa496396b83b65176f11c2 Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 13 Nov 2024 11:18:27 +0100 Subject: [PATCH 34/55] Docs: Extend SAML docs (improvements, assertion mapping) (#96367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move the configuration options to the bottom and extend it * Improve assertion mapping docs for Okta * fix typo * Update index.md --------- Co-authored-by: Irene Rodríguez --- .../configure-authentication/saml/index.md | 112 +++++++++--------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index dca76450dc2..08e01f5dcc2 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -284,68 +284,31 @@ Grafana supports user authentication through Okta, which is useful when you want 1. Log in to the [Okta portal](https://login.okta.com/). 1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. 1. In the Admin Console, navigate to **Applications** > **Applications**. -1. Click **Add Application**. -1. Click **Create New App** to start the Application Integration Wizard. -1. Choose **Web** as a platform. -1. Select **SAML 2.0** in the Sign on method section. +1. Click **Create App Integration** to start the Application Integration Wizard. +1. Choose **SAML 2.0** as the **Sign-in method**. 1. Click **Create**. 1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. 1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. + - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, by default it is the `/saml/metadata` endpoint of your Grafana instance (for example `https://example.grafana.com/saml/metadata`). This could be configured differently, but the value here must match the `entity_id` setting of the SAML settings of Grafana. - Leave the default values for **Name ID format** and **Application username**. + {{% admonition type="note" %}} + If you plan to enable SAML Single Logout, consider setting the **Name ID format** to `EmailAddress` or `Persistent`. This must match the `name_id_format` setting of the Grafana instance. + {{% /admonition %}} - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana. The attribute names in Okta need to match exactly what is defined within Grafana, for example: - | Attribute name (in Grafana) | Name and value (in Okta profile) | - | --------------------------- | -------------------------------------------------- | - | Login | Login `user.login` | - | Email | Email `user.email` | - | DisplayName | DisplayName `user.firstName + " " + user.lastName` | + | Attribute name (in Grafana) | Name and value (in Okta profile) | Grafana configuration (under `auth.saml`) | + | --------------------------- | ---------------------------------------------------- | ----------------------------------------- | + | Login | Login - `user.login` | `assertion_attribute_login = Login` | + | Email | Email - `user.email` | `assertion_attribute_email = Email` | + | DisplayName | DisplayName - `user.firstName + " " + user.lastName` | `assertion_attribute_name = DisplayName` | - - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. + - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`, ensure it matches the `asssertion_attribute_groups` setting in Grafana) and set filter to `Matches regex .*` to return all user groups. 1. Click **Next**. 1. On the final Feedback tab, fill out the form and then click **Finish**. -## Configure SAML authentication in Grafana - -The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). - -| Setting | Required | Description | Default | -| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | -| `enabled` | No | Whether SAML authentication is allowed | `false` | -| `name` | No | Name used to refer to the SAML authentication in the Grafana user interface. | `SAML` | -| `single_logout` | No | Whether SAML Single Logout enabled | `false` | -| `allow_sign_up` | No | Whether to allow new Grafana user creation through SAML login. If set to `false`, then only existing Grafana users can log in with SAML. | `true` | -| `auto_login` | No | Whether SAML auto login is enabled | `false` | -| `allow_idp_initiated` | No | Whether SAML IdP-initiated login is allowed | `false` | -| `certificate` or `certificate_path` | Yes | Base64-encoded string or Path for the SP X.509 certificate | | -| `private_key` or `private_key_path` | Yes | Base64-encoded string or Path for the SP private key | | -| `signature_algorithm` | No | Signature algorithm used for signing requests to the IdP. Supported values are rsa-sha1, rsa-sha256, rsa-sha512. | | -| `idp_metadata`, `idp_metadata_path`, or `idp_metadata_url` | Yes | Base64-encoded string, Path or URL for the IdP SAML metadata XML | | -| `max_issue_delay` | No | Duration, since the IdP issued a response and the SP is allowed to process it | `90s` | -| `metadata_valid_duration` | No | Duration, for how long the SP metadata is valid | `48h` | -| `relay_state` | No | Relay state for IdP-initiated login. Should match relay state configured in IdP | | -| `assertion_attribute_name` | No | Friendly name or name of the attribute within the SAML assertion to use as the user name. Alternatively, this can be a template with variables that match the names of attributes within the SAML assertion. | `displayName` | -| `assertion_attribute_login` | No | Friendly name or name of the attribute within the SAML assertion to use as the user login handle | `mail` | -| `assertion_attribute_email` | No | Friendly name or name of the attribute within the SAML assertion to use as the user email | `mail` | -| `assertion_attribute_groups` | No | Friendly name or name of the attribute within the SAML assertion to use as the user groups | | -| `assertion_attribute_role` | No | Friendly name or name of the attribute within the SAML assertion to use as the user roles | | -| `assertion_attribute_org` | No | Friendly name or name of the attribute within the SAML assertion to use as the user organization | | -| `allowed_organizations` | No | List of comma- or space-separated organizations. User should be a member of at least one organization to log in. | | -| `org_mapping` | No | List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. | | -| `role_values_none` | No | List of comma- or space-separated roles which will be mapped into the None role | | -| `role_values_viewer` | No | List of comma- or space-separated roles which will be mapped into the Viewer role | | -| `role_values_editor` | No | List of comma- or space-separated roles which will be mapped into the Editor role | | -| `role_values_admin` | No | List of comma- or space-separated roles which will be mapped into the Admin role | | -| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role | | -| `name_id_format` | No | The Name ID Format to request within the SAML assertion | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | -| `client_id` | No | Client ID of the IdP service application used to retrieve more information about the user from the IdP. | | -| `client_secret` | No | Client secret of the IdP service application used to retrieve more information about the user from the IdP. | | -| `token_url` | No | URL to retrieve the access token from the IdP. | | -| `force_use_graph_api` | No | Whether to use the IdP service application retrieve more information about the user from the IdP. | `false` | - ### Signature algorithm The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests. If the `signature_algorithm` option is configured, Grafana will put a digital signature into SAML requests. Supported signature types are `rsa-sha1`, `rsa-sha256`, `rsa-sha512`. This option should match your IdP configuration, otherwise, signature validation will fail. Grafana uses key and certificate configured with `private_key` and `certificate` options for signing SAML requests. @@ -358,10 +321,13 @@ By default, this is set to `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` The following list includes valid configuration field values: -- `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` -- `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` -- `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` -- `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` +| `name_id_format` value in the configuration file or Terraform | `Name identifier format` on the UI | +| ------------------------------------------------------------- | ---------------------------------- | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | Default | +| `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` | Unspecified | +| `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` | Email address | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` | Persistent | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | Transient | ### IdP metadata @@ -721,3 +687,43 @@ cookie_secure = true ``` Ensure cookie_secure is set to true to ensure that cookies are only sent over HTTPS. + +## Configure SAML authentication in Grafana + +The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). + +| Setting | Required | Description | Default | +| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | +| `enabled` | No | Whether SAML authentication is allowed. | `false` | +| `name` | No | Name used to refer to the SAML authentication in the Grafana user interface. | `SAML` | +| `entity_id` | No | The entity ID of the service provider. This is the unique identifier of the service provider. | `https://{Grafana URL}/saml/metadata` | +| `single_logout` | No | Whether SAML Single Logout is enabled. | `false` | +| `allow_sign_up` | No | Whether to allow new Grafana user creation through SAML login. If set to `false`, then only existing Grafana users can log in with SAML. | `true` | +| `auto_login` | No | Whether SAML auto login is enabled. | `false` | +| `allow_idp_initiated` | No | Whether SAML IdP-initiated login is allowed. | `false` | +| `certificate` or `certificate_path` | Yes | Base64-encoded string or Path for the SP X.509 certificate. | | +| `private_key` or `private_key_path` | Yes | Base64-encoded string or Path for the SP private key. | | +| `signature_algorithm` | No | Signature algorithm used for signing requests to the IdP. Supported values are rsa-sha1, rsa-sha256, rsa-sha512. | | +| `idp_metadata`, `idp_metadata_path`, or `idp_metadata_url` | Yes | Base64-encoded string, Path or URL for the IdP SAML metadata XML. | | +| `max_issue_delay` | No | Maximum time allowed between the issuance of an AuthnRequest by the SP and the processing of the Response. | `90s` | +| `metadata_valid_duration` | No | Duration for which the SP metadata remains valid. | `48h` | +| `relay_state` | No | Relay state for IdP-initiated login. This should match the relay state configured in the IdP. | | +| `assertion_attribute_name` | No | Friendly name or name of the attribute within the SAML assertion to use as the user name. Alternatively, this can be a template with variables that match the names of attributes within the SAML assertion. | `displayName` | +| `assertion_attribute_login` | No | Friendly name or name of the attribute within the SAML assertion to use as the user login handle. | `mail` | +| `assertion_attribute_email` | No | Friendly name or name of the attribute within the SAML assertion to use as the user email. | `mail` | +| `assertion_attribute_groups` | No | Friendly name or name of the attribute within the SAML assertion to use as the user groups. | | +| `assertion_attribute_role` | No | Friendly name or name of the attribute within the SAML assertion to use as the user roles. | | +| `assertion_attribute_org` | No | Friendly name or name of the attribute within the SAML assertion to use as the user organization | | +| `allowed_organizations` | No | List of comma- or space-separated organizations. User should be a member of at least one organization to log in. | | +| `org_mapping` | No | List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. | | +| `role_values_none` | No | List of comma- or space-separated roles which will be mapped into the None role. | | +| `role_values_viewer` | No | List of comma- or space-separated roles which will be mapped into the Viewer role. | | +| `role_values_editor` | No | List of comma- or space-separated roles which will be mapped into the Editor role. | | +| `role_values_admin` | No | List of comma- or space-separated roles which will be mapped into the Admin role. | | +| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role. | | +| `skip_org_role_sync` | No | Whether to skip organization role synchronization. | `false` | +| `name_id_format` | No | Specifies the format of the requested NameID element in the SAML AuthnRequest. | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | +| `client_id` | No | Client ID of the IdP service application used to retrieve more information about the user from the IdP. (Microsoft Entra ID only) | | +| `client_secret` | No | Client secret of the IdP service application used to retrieve more information about the user from the IdP. (Microsoft Entra ID only) | | +| `token_url` | No | URL to retrieve the access token from the IdP. (Microsoft Entra ID only) | | +| `force_use_graph_api` | No | Whether to use the IdP service application retrieve more information about the user from the IdP. (Microsoft Entra ID only) | `false` | From 33b4c71cb2f438bb89fe2afb5643a86b7098d74c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:18:43 +0000 Subject: [PATCH 35/55] Update dependency @reduxjs/toolkit to v2.3.0 (#96370) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b98fa5da925..91c5f95c4e0 100644 --- a/package.json +++ b/package.json @@ -297,7 +297,7 @@ "@react-aria/overlays": "3.23.4", "@react-aria/utils": "3.25.3", "@react-awesome-query-builder/ui": "6.6.3", - "@reduxjs/toolkit": "2.2.8", + "@reduxjs/toolkit": "2.3.0", "@testing-library/react-hooks": "^8.0.1", "@visx/event": "3.3.0", "@visx/gradient": "3.3.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 12b94fd5674..04db63e5501 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -50,7 +50,7 @@ "@lezer/highlight": "1.2.1", "@lezer/lr": "1.4.2", "@prometheus-io/lezer-promql": "0.55.1", - "@reduxjs/toolkit": "2.2.8", + "@reduxjs/toolkit": "2.3.0", "d3": "7.9.0", "date-fns": "3.6.0", "debounce-promise": "3.1.2", diff --git a/yarn.lock b/yarn.lock index 4859eea3752..704b8b97a97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3889,7 +3889,7 @@ __metadata: "@lezer/highlight": "npm:1.2.1" "@lezer/lr": "npm:1.4.2" "@prometheus-io/lezer-promql": "npm:0.55.1" - "@reduxjs/toolkit": "npm:2.2.8" + "@reduxjs/toolkit": "npm:2.3.0" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:15.3.0" "@swc/core": "npm:1.4.2" @@ -6995,9 +6995,9 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:2.2.8": - version: 2.2.8 - resolution: "@reduxjs/toolkit@npm:2.2.8" +"@reduxjs/toolkit@npm:2.3.0": + version: 2.3.0 + resolution: "@reduxjs/toolkit@npm:2.3.0" dependencies: immer: "npm:^10.0.3" redux: "npm:^5.0.1" @@ -7011,7 +7011,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10/d89bf5fd3e763669d34169fdc68da2a673cebc07a20bfc835277112dcdc2e7276258646fce9aa9792e3680caef719813bdc2dd654117632018ec027266026873 + checksum: 10/3aa0bf29ce4dfe4ae75416cb5338a91ccc89f06c5a2fcdae5dc137dca02b3990c46306062696e158869ac399b50cae6476e5536902df9bd1ad1a998195221af6 languageName: node linkType: hard @@ -18755,7 +18755,7 @@ __metadata: "@react-types/menu": "npm:3.9.12" "@react-types/overlays": "npm:3.8.10" "@react-types/shared": "npm:3.25.0" - "@reduxjs/toolkit": "npm:2.2.8" + "@reduxjs/toolkit": "npm:2.3.0" "@rtk-query/codegen-openapi": "npm:^1.2.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@stylistic/eslint-plugin-ts": "npm:^2.9.0" From f0c57622f86c88bc9c8fe556fd150cde45c4cd77 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 13 Nov 2024 11:46:10 +0100 Subject: [PATCH 36/55] Alerting: useProduceNewAlertmanagerConfiguration for notification templates (#95290) --- .../useNotificationTemplates.ts | 117 +++++------------ .../components/receivers/TemplateForm.tsx | 16 +-- .../components/receivers/TemplatesTable.tsx | 4 +- .../hooks/useProduceNewAlertmanagerConfig.ts | 8 +- .../notificationTemplates.test.ts.snap | 55 ++++++++ .../notificationTemplates.test.ts | 119 ++++++++++++++++++ .../alertmanager/notificationTemplates.ts | 83 ++++++++++++ 7 files changed, 304 insertions(+), 98 deletions(-) create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/notificationTemplates.test.ts.snap create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.test.ts create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.ts diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts index a640e936dcf..160a28001a6 100644 --- a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -1,17 +1,20 @@ -import { produce } from 'immer'; import { useEffect } from 'react'; import { Validate } from 'react-hook-form'; -import { useDispatch } from 'app/types'; - import { AlertManagerCortexConfig } from '../../../../../plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { templatesApi } from '../../api/templateApi'; +import { useAsync } from '../../hooks/useAsync'; +import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup, ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroupList, } from '../../openapi/templatesApi.gen'; -import { updateAlertManagerConfigAction } from '../../state/actions'; +import { + addNotificationTemplateAction, + deleteNotificationTemplateAction, + updateNotificationTemplateAction, +} from '../../reducers/alertmanager/notificationTemplates'; import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants'; import { getAnnotation, getK8sNamespace, shouldUseK8sApi } from '../../utils/k8s/utils'; import { ensureDefine } from '../../utils/templates'; @@ -30,6 +33,7 @@ export interface NotificationTemplate { } const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; + const { useListNamespacedTemplateGroupQuery, useLazyReadNamespacedTemplateGroupQuery, @@ -146,39 +150,17 @@ interface CreateTemplateParams { } export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { - const dispatch = useDispatch(); - - const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery(); const [createNamespacedTemplateGroup] = useCreateNamespacedTemplateGroupMutation(); + const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); const k8sApiSupported = shouldUseK8sApi(alertmanager); - async function createUsingConfigFileApi({ templateValues }: CreateTemplateParams) { - const amConfig = await fetchAmConfig(alertmanager).unwrap(); - // wrap content in "define" if it's not already wrapped, in case user did not do it/ - // it's not obvious that this is needed for template to work - const content = ensureDefine(templateValues.title, templateValues.content); + const createUsingConfigFileApi = useAsync(({ templateValues }: CreateTemplateParams) => { + const action = addNotificationTemplateAction({ template: templateValues }); + return updateAlertmanagerConfiguration(action); + }); - const targetTemplateExists = amConfig.template_files?.[templateValues.title] !== undefined; - if (targetTemplateExists) { - throw new Error('target template already exists'); - } - - const updatedConfig = produce(amConfig, (draft) => { - draft.template_files[templateValues.title] = content; - draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), templateValues.title]; - }); - - return dispatch( - updateAlertManagerConfigAction({ - alertManagerSourceName: alertmanager, - newConfig: updatedConfig, - oldConfig: amConfig, - }) - ).unwrap(); - } - - async function createUsingK8sApi({ templateValues }: CreateTemplateParams) { + const createUsingK8sApi = useAsync(({ templateValues }: CreateTemplateParams) => { const content = ensureDefine(templateValues.title, templateValues.content); return createNamespacedTemplateGroup({ @@ -188,7 +170,7 @@ export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanager metadata: {}, }, }).unwrap(); - } + }); return k8sApiSupported ? createUsingK8sApi : createUsingConfigFileApi; } @@ -199,43 +181,17 @@ interface UpdateTemplateParams { } export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { - const dispatch = useDispatch(); - - const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery(); const [replaceNamespacedTemplateGroup] = useReplaceNamespacedTemplateGroupMutation(); + const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); const k8sApiSupported = shouldUseK8sApi(alertmanager); - async function updateUsingConfigFileApi({ template, patch }: UpdateTemplateParams) { - const amConfig = await fetchAmConfig(alertmanager).unwrap(); - // wrap content in "define" if it's not already wrapped, in case user did not do it/ - // it's not obvious that this is needed for template to work - const content = ensureDefine(patch.title, patch.content); + const updateUsingConfigFileApi = useAsync(({ template, patch }: UpdateTemplateParams) => { + const action = updateNotificationTemplateAction({ name: template.title, template: patch }); + return updateAlertmanagerConfiguration(action); + }); - const originalName = template.title; // For ConfigFile API name is the same as uid - const nameChanged = originalName !== patch.title; - - // TODO Maybe we could simplify or extract this logic - const updatedConfig = produce(amConfig, (draft) => { - if (nameChanged) { - delete draft.template_files[originalName]; - draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== originalName); - } - - draft.template_files[patch.title] = content; - draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), patch.title]; - }); - - return dispatch( - updateAlertManagerConfigAction({ - alertManagerSourceName: alertmanager, - newConfig: updatedConfig, - oldConfig: amConfig, - }) - ).unwrap(); - } - - async function updateUsingK8sApi({ template, patch }: UpdateTemplateParams) { + const updateUsingK8sApi = useAsync(({ template, patch }: UpdateTemplateParams) => { const content = ensureDefine(patch.title, patch.content); return replaceNamespacedTemplateGroup({ @@ -246,43 +202,30 @@ export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanager metadata: { name: template.uid }, }, }).unwrap(); - } + }); return k8sApiSupported ? updateUsingK8sApi : updateUsingConfigFileApi; } export function useDeleteNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { - const dispatch = useDispatch(); - const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery(); const [deleteNamespacedTemplateGroup] = useDeleteNamespacedTemplateGroupMutation(); + const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); - async function deleteUsingConfigFileApi({ uid }: { uid: string }) { - const amConfig = await fetchAmConfig(alertmanager).unwrap(); + const deleteUsingConfigAPI = useAsync(async ({ uid }: { uid: string }) => { + const action = deleteNotificationTemplateAction({ name: uid }); + return updateAlertmanagerConfiguration(action); + }); - const updatedConfig = produce(amConfig, (draft) => { - delete draft.template_files[uid]; - draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== uid); - }); - - return dispatch( - updateAlertManagerConfigAction({ - alertManagerSourceName: alertmanager, - newConfig: updatedConfig, - oldConfig: amConfig, - }) - ).unwrap(); - } - - async function deleteUsingK8sApi({ uid }: { uid: string }) { + const deleteUsingK8sApi = useAsync(({ uid }: { uid: string }) => { return deleteNamespacedTemplateGroup({ namespace: getK8sNamespace(), name: uid, ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, }).unwrap(); - } + }); const k8sApiSupported = shouldUseK8sApi(alertmanager); - return k8sApiSupported ? deleteUsingK8sApi : deleteUsingConfigFileApi; + return k8sApiSupported ? deleteUsingK8sApi : deleteUsingConfigAPI; } interface ValidateNotificationTemplateParams { diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 205c49fcc5f..ad2c8bd2f71 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -10,18 +10,18 @@ import { GrafanaTheme2 } from '@grafana/data'; import { isFetchError, locationService } from '@grafana/runtime'; import { Alert, + Box, Button, + Drawer, Dropdown, FieldSet, + InlineField, Input, LinkButton, Menu, - useStyles2, Stack, useSplitter, - Drawer, - InlineField, - Box, + useStyles2, } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; @@ -93,8 +93,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const appNotification = useAppNotification(); - const createNewTemplate = useCreateNotificationTemplate({ alertmanager }); - const updateTemplate = useUpdateNotificationTemplate({ alertmanager }); + const [createNewTemplate] = useCreateNotificationTemplate({ alertmanager }); + const [updateTemplate] = useUpdateNotificationTemplate({ alertmanager }); const { titleIsUnique } = useValidateNotificationTemplate({ alertmanager, originalTemplate }); useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState)); @@ -149,9 +149,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) try { if (!originalTemplate) { - await createNewTemplate({ templateValues: values }); + await createNewTemplate.execute({ templateValues: values }); } else { - await updateTemplate({ template: originalTemplate, patch: values }); + await updateTemplate.execute({ template: originalTemplate, patch: values }); } appNotification.success('Template saved', `Template ${values.title} has been saved`); locationService.push(returnLink); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 7debc6c7f16..62087390139 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -30,7 +30,7 @@ interface Props { export const TemplatesTable = ({ alertManagerName, templates }: Props) => { const appNotification = useAppNotification(); - const deleteTemplate = useDeleteNotificationTemplate({ alertmanager: alertManagerName }); + const [deleteTemplate] = useDeleteNotificationTemplate({ alertmanager: alertManagerName }); const tableStyles = useStyles2(getAlertTableStyles); @@ -39,7 +39,7 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => { const onDeleteTemplate = async () => { if (templateToDelete) { try { - await deleteTemplate({ uid: templateToDelete.uid }); + await deleteTemplate.execute({ uid: templateToDelete.uid }); appNotification.success('Template deleted', `Template ${templateToDelete.title} has been deleted`); } catch (error) { appNotification.error('Error deleting template', `Error deleting template ${templateToDelete.title}`); diff --git a/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts b/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts index d5a7ceb9562..b91e809b512 100644 --- a/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts +++ b/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts @@ -5,6 +5,7 @@ import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/ty import { alertmanagerApi } from '../api/alertmanagerApi'; import { muteTimingsReducer } from '../reducers/alertmanager/muteTimings'; +import { notificationTemplatesReducer } from '../reducers/alertmanager/notificationTemplates'; import { receiversReducer } from '../reducers/alertmanager/receivers'; import { useAlertmanager } from '../state/AlertmanagerContext'; @@ -26,7 +27,12 @@ export const initialAlertmanagerConfiguration: AlertManagerCortexConfig = { template_files: {}, }; -const configurationReducer = reduceReducers(initialAlertmanagerConfiguration, muteTimingsReducer, receiversReducer); +const configurationReducer = reduceReducers( + initialAlertmanagerConfiguration, + muteTimingsReducer, + receiversReducer, + notificationTemplatesReducer +); /** * This hook will make sure we are always applying actions that mutate the Alertmanager configuration diff --git a/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/notificationTemplates.test.ts.snap b/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/notificationTemplates.test.ts.snap new file mode 100644 index 00000000000..370761c3fe6 --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/notificationTemplates.test.ts.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`notification templates should add a new notification template 1`] = ` +{ + "alertmanager_config": { + "templates": [ + "foo", + ], + }, + "template_files": { + "foo": "{{ define "foo" }} + foo +{{ end }}", + }, +} +`; + +exports[`notification templates should allow renaming a notification template 1`] = ` +{ + "alertmanager_config": { + "templates": [ + "rename-me-copy", + ], + }, + "template_files": { + "rename-me-copy": "{{ define "rename-me-copy" }} + rename me, please +{{ end }}", + }, +} +`; + +exports[`notification templates should remove a notification template 1`] = ` +{ + "alertmanager_config": { + "templates": [], + }, + "template_files": {}, +} +`; + +exports[`notification templates should update a notification template without renaming 1`] = ` +{ + "alertmanager_config": { + "templates": [ + "update-me", + ], + }, + "template_files": { + "update-me": "{{ define "update-me" }} + update me, please +{{ end }}", + }, +} +`; diff --git a/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.test.ts b/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.test.ts new file mode 100644 index 00000000000..c7cacdf2e5f --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.test.ts @@ -0,0 +1,119 @@ +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; + +import { TemplateFormValues } from '../../components/receivers/TemplateForm'; + +import { + addNotificationTemplateAction, + deleteNotificationTemplateAction, + notificationTemplatesReducer, + updateNotificationTemplateAction, +} from './notificationTemplates'; + +describe('notification templates', () => { + it('should add a new notification template', () => { + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: {}, + template_files: {}, + }; + const newNotificationTemplate: TemplateFormValues = { + title: 'foo', + content: 'foo', + }; + + const action = addNotificationTemplateAction({ template: newNotificationTemplate }); + expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot(); + }); + + it('should not add a new notification template if the name already exists', () => { + const name = 'existing'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { templates: [name] }, + template_files: { + [name]: 'foo', + }, + }; + const newNotificationTemplate: TemplateFormValues = { + title: name, + content: 'foo', + }; + + const action = addNotificationTemplateAction({ template: newNotificationTemplate }); + expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/already exists/); + }); + + it('should update a notification template without renaming', () => { + const name = 'update-me'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { + templates: [name], + }, + template_files: { + [name]: 'update me', + }, + }; + + const action = updateNotificationTemplateAction({ name, template: { title: name, content: 'update me, please' } }); + expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot(); + }); + + it('should not update if target does not exist', () => { + const name = 'update-me'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: {}, + template_files: {}, + }; + + const action = updateNotificationTemplateAction({ name, template: { title: name, content: 'update me, please' } }); + expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/did not find it/); + }); + + it('should not update if renaming and new template name exist', () => { + const name = 'rename-me'; + const name2 = 'rename-me-2'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { + templates: [name, name2], + }, + template_files: { + [name]: 'foo', + [name2]: 'bar', + }, + }; + + const action = updateNotificationTemplateAction({ name, template: { title: name2, content: 'foo' } }); + expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/duplicate/i); + }); + + it('should allow renaming a notification template', () => { + const name = 'rename-me'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { + templates: [name], + }, + template_files: { + [name]: 'rename me', + }, + }; + + const action = updateNotificationTemplateAction({ + name, + template: { title: 'rename-me-copy', content: 'rename me, please' }, + }); + expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot(); + }); + + it('should remove a notification template', () => { + const name = 'delete-me'; + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { + templates: [name], + }, + template_files: { + [name]: 'delete me please', + }, + }; + + const action = deleteNotificationTemplateAction({ name }); + expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.ts b/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.ts new file mode 100644 index 00000000000..47bc215e719 --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/notificationTemplates.ts @@ -0,0 +1,83 @@ +import { createAction, createReducer } from '@reduxjs/toolkit'; +import { remove, toArray, unset } from 'lodash'; + +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; + +import { TemplateFormValues } from '../../components/receivers/TemplateForm'; +import { ensureDefine } from '../../utils/templates'; + +export const addNotificationTemplateAction = createAction<{ template: TemplateFormValues }>('notificationTemplate/add'); +export const updateNotificationTemplateAction = createAction<{ + name: string; + template: TemplateFormValues; +}>('notificationTemplate/update'); +export const deleteNotificationTemplateAction = createAction<{ name: string }>('notificationTemplate/delete'); + +const initialState: AlertManagerCortexConfig = { + alertmanager_config: {}, + template_files: {}, +}; + +/** + * This reducer will manage action related to notification templates and make sure all operations on the alertmanager + * configuration happen immutably and only mutate what they need. + */ +export const notificationTemplatesReducer = createReducer(initialState, (builder) => { + builder + .addCase(addNotificationTemplateAction, (draft, { payload }) => { + const { alertmanager_config = {}, template_files = {} } = draft; + const { template } = payload; + + const targetTemplateExists = template_files[template.title] !== undefined; + if (targetTemplateExists) { + throw new Error('target template already exists'); + } + + // wrap content in "define" if it's not already wrapped, in case user did not do it/ + // it's not obvious that this is needed for template to work + const content = ensureDefine(template.title, template.content); + + // add the template to the list of template files + template_files[template.title] = content; + + // add the template to the alertmanager_config + alertmanager_config.templates = toArray(alertmanager_config.templates).concat(template.title); + }) + .addCase(updateNotificationTemplateAction, (draft, { payload }) => { + const { alertmanager_config = {}, template_files = {} } = draft; + const { name, template } = payload; + const renaming = name !== template.title; + + const targetExists = template_files[name] !== undefined; + if (!targetExists) { + throw new Error(`Expected notification template ${name} to exist, but did not find it in the config`); + } + + // wrap content in "define" if it's not already wrapped, in case user did not do it/ + // it's not obvious that this is needed for template to work + const content = ensureDefine(template.title, template.content); + + if (renaming) { + const oldName = name; + const newName = template.title; + + const targetExists = template_files[newName] !== undefined; + if (targetExists) { + throw new Error(`Duplicate template name ${newName}`); + } + + unset(template_files, oldName); + remove(alertmanager_config.templates ?? [], (templateName) => templateName === oldName); + alertmanager_config.templates = toArray(alertmanager_config.templates).concat(template.title); + } + + template_files[template.title] = content; + }) + .addCase(deleteNotificationTemplateAction, (draft, { payload }) => { + const { name } = payload; + const { alertmanager_config = {}, template_files = {} } = draft; + + unset(template_files, name); + remove(alertmanager_config.templates ?? [], (templateName) => templateName === name); + }); +}); From 94762d1e10da26610b00a99d814eaa8c75dd5595 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:04:15 +0000 Subject: [PATCH 37/55] Update dependency @stylistic/eslint-plugin-ts to v2.10.1 (#96373) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 104 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/yarn.lock b/yarn.lock index 704b8b97a97..b71b972dc0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8309,15 +8309,15 @@ __metadata: linkType: hard "@stylistic/eslint-plugin-ts@npm:^2.9.0": - version: 2.9.0 - resolution: "@stylistic/eslint-plugin-ts@npm:2.9.0" + version: 2.10.1 + resolution: "@stylistic/eslint-plugin-ts@npm:2.10.1" dependencies: - "@typescript-eslint/utils": "npm:^8.8.0" - eslint-visitor-keys: "npm:^4.1.0" - espree: "npm:^10.2.0" + "@typescript-eslint/utils": "npm:^8.12.2" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10/f689bdb8047abbe80fad506dae884cf840254a60f943c60fbfa4d338fbcc2146458d5acd97f427293649a0b14dd1ccb30510cce30fa503f050e18001571352aa + checksum: 10/e3a966e30e4986f795592a4a574bea4133d42d3ae0489799bfd8498d6c7d1c5b4b0c0016e825fbb521156602041bccbed60f4909e0056a3c225d63cb8bffe324 languageName: node linkType: hard @@ -11078,6 +11078,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/scope-manager@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" + checksum: 10/48ff44a790254b5a98c17bf15176fbdc1408b58eb3ccd8eda9c5707811786de25e1bccc5c490dcc05cbd34b685e162ee4e92b28f57b071c522274fa97f23c98c + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/scope-manager@npm:8.9.0" @@ -11117,7 +11127,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.9.0, @typescript-eslint/types@npm:^8.9.0": +"@typescript-eslint/types@npm:8.14.0, @typescript-eslint/types@npm:^8.9.0": + version: 8.14.0 + resolution: "@typescript-eslint/types@npm:8.14.0" + checksum: 10/1924aef8efdf5399d6cc9ef3a5307fda39b1a2be129ab8cb24a46dc0a37156230e77f2809ab709d5d0a43891b6ffd67ce45292724e8f8164ac19e1786c5f4644 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/types@npm:8.9.0" checksum: 10/4d087153605ec23c980f9bc807b122edefff828e0c3b52ef531f4b8e1d30078c39f95e84019370a395bf97eed0d7886cc50b8cd545c287f8a2a21b301272377a @@ -11161,6 +11178,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b0b9f228071b6338dbf5e2ac52848fa6af630e8d84d4102e1cccaae67114f2bff82bd027af2818e3ad778668e3c3d4a2fb31b7f4c8a9796295e5aa87903fb313 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/typescript-estree@npm:8.9.0" @@ -11180,7 +11216,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.9.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.8.0, @typescript-eslint/utils@npm:^8.9.0": +"@typescript-eslint/utils@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/utils@npm:8.9.0" dependencies: @@ -11212,6 +11248,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.12.2, @typescript-eslint/utils@npm:^8.9.0": + version: 8.14.0 + resolution: "@typescript-eslint/utils@npm:8.14.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.14.0" + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/typescript-estree": "npm:8.14.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10/6d3b2583c473b452dd8f978524802aabd275055f98d461cc71ee6a9424291f4481d2a3416a3f77b2458939dd38a39c0fd8e0c9b47915141c8409e63528a1216b + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:^6.4.1": version: 6.21.0 resolution: "@typescript-eslint/utils@npm:6.21.0" @@ -11249,6 +11299,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10/735cc9c2ce3235e543d03afe0de740022888e69ed9f5027564e1c959a3a087106bcf21b5b8d3ac872171c0a585744f0442b76fe6ba68341a735a4b4a15f52a46 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/visitor-keys@npm:8.9.0" @@ -11782,12 +11842,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.12.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": - version: 8.12.1 - resolution: "acorn@npm:8.12.1" +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" bin: acorn: bin/acorn - checksum: 10/d08c2d122bba32d0861e0aa840b2ee25946c286d5dc5990abca991baf8cdbfbe199b05aacb221b979411a2fea36f83e26b5ac4f6b4e0ce49038c62316c1848f0 + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 languageName: node linkType: hard @@ -17037,10 +17097,10 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.1.0": - version: 4.1.0 - resolution: "eslint-visitor-keys@npm:4.1.0" - checksum: 10/3fb5bd1b2f36db89d0ac57ddd66d36ccd3b1e3cddb2a55a0f9f6f1c85268cfcc1cc32e7eda4990e3423107a120dd254fb6cb52d6154cf81d344d8c3fa671f7c2 +"eslint-visitor-keys@npm:^4.1.0, eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10/9651b3356b01760e586b4c631c5268c0e1a85236e3292bf754f0472f465bf9a856c0ddc261fceace155334118c0151778effafbab981413dbf9288349343fa25 languageName: node linkType: hard @@ -17110,14 +17170,14 @@ __metadata: languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.2.0": - version: 10.2.0 - resolution: "espree@npm:10.2.0" +"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.2.0, espree@npm:^10.3.0": + version: 10.3.0 + resolution: "espree@npm:10.3.0" dependencies: - acorn: "npm:^8.12.0" + acorn: "npm:^8.14.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.1.0" - checksum: 10/365076a963ca84244c1e2d36e4f812362d21cfa7e7df10d67f7b82b759467796df81184721d153c4e235d9ef5eb5b4d044167dd66be3be00f53a21a515b1bfb1 + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/3412d44d4204c9e29d6b5dd0277400cfa0cd68495dc09eae1b9ce79d0c8985c1c5cc09cb9ba32a1cd963f48a49b0c46bdb7736afe395a300aa6bb1c0d86837e8 languageName: node linkType: hard From 3e9033c3bc9225b64ee703f033e800da530b4762 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:04:40 +0000 Subject: [PATCH 38/55] Update dependency @testing-library/jest-dom to v6.6.3 (#96374) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 37 ++++++------------- 8 files changed, 18 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 91c5f95c4e0..9ef22f0f1aa 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "@swc/core": "1.4.2", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 04db63e5501..6d4a75821e1 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -83,7 +83,7 @@ "@swc/core": "1.4.2", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d02364677ae..78e681dac35 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -135,7 +135,7 @@ "@storybook/react-webpack5": "^8.1.6", "@storybook/theming": "^8.1.6", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/chance": "1.1.6", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 7acdcad9f4c..fe4d3308359 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@grafana/plugin-configs": "11.4.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 30c32a5e00e..16d3fefee70 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 7bac25d8881..691fd413983 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@grafana/plugin-configs": "11.4.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 86fb5a2eb05..07b1b616b38 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", diff --git a/yarn.lock b/yarn.lock index b71b972dc0b..1d0accd3f8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3136,7 +3136,7 @@ __metadata: "@grafana/schema": "npm:11.4.0-pre" "@grafana/ui": "npm:11.4.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3218,7 +3218,7 @@ __metadata: "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3410,7 +3410,7 @@ __metadata: "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.27.0" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3461,7 +3461,7 @@ __metadata: "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" @@ -3895,7 +3895,7 @@ __metadata: "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" @@ -4209,7 +4209,7 @@ __metadata: "@storybook/theming": "npm:^8.1.6" "@tanstack/react-virtual": "npm:^3.5.1" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/chance": "npm:1.1.6" @@ -9341,9 +9341,9 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.5.0": - version: 6.5.0 - resolution: "@testing-library/jest-dom@npm:6.5.0" +"@testing-library/jest-dom@npm:6.6.3, @testing-library/jest-dom@npm:^6.1.2": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" @@ -9352,22 +9352,7 @@ __metadata: dom-accessibility-api: "npm:^0.6.3" lodash: "npm:^4.17.21" redent: "npm:^3.0.0" - checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 - languageName: node - linkType: hard - -"@testing-library/jest-dom@npm:^6.1.2": - version: 6.6.1 - resolution: "@testing-library/jest-dom@npm:6.6.1" - dependencies: - "@adobe/css-tools": "npm:^4.4.0" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" - redent: "npm:^3.0.0" - checksum: 10/006357d7547cc50624564a1b0e9986d0f0252365a6e5ac1c7c989032159c47226adfbd9a9ac17eef236738e9597541d344176176b5c3ed0e06e8f4b33387e135 + checksum: 10/1f3427e45870eab9dcc59d6504b780d4a595062fe1687762ae6e67d06a70bf439b40ab64cf58cbace6293a99e3764d4647fdc8300a633b721764f5ce39dade18 languageName: node linkType: hard @@ -18822,7 +18807,7 @@ __metadata: "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" From 924b0d086324f72181dad51c1e6dfc83c2ea89bc Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Wed, 13 Nov 2024 06:07:00 -0500 Subject: [PATCH 39/55] CloudMigration: Add user uid to on prem event collection (#94804) * add useruid to report events * test --- pkg/services/cloudmigration/api/api.go | 6 ++--- pkg/services/cloudmigration/cloudmigration.go | 6 ++--- .../cloudmigrationimpl/cloudmigration.go | 22 ++++++++++--------- .../cloudmigrationimpl/cloudmigration_noop.go | 6 ++--- .../cloudmigrationimpl/cloudmigration_test.go | 11 +++++----- .../fake/cloudmigration_fake.go | 6 ++--- pkg/services/cloudmigration/gmsclient/dtos.go | 1 + 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 0fb47d737fa..61457166380 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -245,7 +245,7 @@ func (cma *CloudMigrationAPI) CreateSession(c *contextmodel.ReqContext) response return response.ErrOrFallback(http.StatusBadRequest, "bad request data", err) } - s, err := cma.cloudMigrationService.CreateSession(ctx, cloudmigration.CloudMigrationSessionRequest{ + s, err := cma.cloudMigrationService.CreateSession(ctx, c.SignedInUser, cloudmigration.CloudMigrationSessionRequest{ AuthToken: cmd.AuthToken, OrgID: c.SignedInUser.OrgID, }) @@ -286,7 +286,7 @@ func (cma *CloudMigrationAPI) DeleteSession(c *contextmodel.ReqContext) response return response.ErrOrFallback(http.StatusBadRequest, "invalid session uid", err) } - _, err := cma.cloudMigrationService.DeleteSession(ctx, c.OrgID, uid) + _, err := cma.cloudMigrationService.DeleteSession(ctx, c.OrgID, c.SignedInUser, uid) if err != nil { span.SetStatus(codes.Error, "session delete error") span.RecordError(err) @@ -511,7 +511,7 @@ func (cma *CloudMigrationAPI) UploadSnapshot(c *contextmodel.ReqContext) respons return response.ErrOrFallback(http.StatusBadRequest, "invalid snapshot uid", err) } - if err := cma.cloudMigrationService.UploadSnapshot(ctx, c.OrgID, sessUid, snapshotUid); err != nil { + if err := cma.cloudMigrationService.UploadSnapshot(ctx, c.OrgID, c.SignedInUser, sessUid, snapshotUid); err != nil { span.SetStatus(codes.Error, "error uploading snapshot") span.RecordError(err) diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index 4ee8c433979..faebba11d50 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -16,14 +16,14 @@ type Service interface { ValidateToken(ctx context.Context, mig CloudMigrationSession) error DeleteToken(ctx context.Context, uid string) error - CreateSession(ctx context.Context, req CloudMigrationSessionRequest) (*CloudMigrationSessionResponse, error) + CreateSession(ctx context.Context, signedInUser *user.SignedInUser, req CloudMigrationSessionRequest) (*CloudMigrationSessionResponse, error) GetSession(ctx context.Context, orgID int64, migUID string) (*CloudMigrationSession, error) - DeleteSession(ctx context.Context, orgID int64, migUID string) (*CloudMigrationSession, error) + DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, migUID string) (*CloudMigrationSession, error) GetSessionList(ctx context.Context, orgID int64) (*CloudMigrationSessionListResponse, error) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*CloudMigrationSnapshot, error) GetSnapshot(ctx context.Context, query GetSnapshotsQuery) (*CloudMigrationSnapshot, error) GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error) - UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error + UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index d749c1d6428..2540df5442e 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -394,7 +394,7 @@ func (s *Service) GetSessionList(ctx context.Context, orgID int64) (*cloudmigrat return &cloudmigration.CloudMigrationSessionListResponse{Sessions: migrations}, nil } -func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (s *Service) CreateSession(ctx context.Context, signedInUser *user.SignedInUser, cmd cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateSession") defer span.End() @@ -419,7 +419,7 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig return nil, cloudmigration.ErrSessionCreationFailure.Errorf("error creating migration") } - s.report(ctx, &migration, gmsclient.EventConnect, 0, nil) + s.report(ctx, &migration, gmsclient.EventConnect, 0, nil, signedInUser.UserUID) return &cloudmigration.CloudMigrationSessionResponse{ UID: cm.UID, @@ -429,18 +429,18 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig }, nil } -func (s *Service) DeleteSession(ctx context.Context, orgID int64, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { +func (s *Service) DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.DeleteSession") defer span.End() session, snapshots, err := s.store.DeleteMigrationSessionByUID(ctx, orgID, sessionUID) if err != nil { - s.report(ctx, session, gmsclient.EventDisconnect, 0, err) + s.report(ctx, session, gmsclient.EventDisconnect, 0, err, signedInUser.UserUID) return nil, fmt.Errorf("deleting migration from db for session %v: %w", sessionUID, err) } err = s.deleteLocalFiles(snapshots) - s.report(ctx, session, gmsclient.EventDisconnect, 0, err) + s.report(ctx, session, gmsclient.EventDisconnect, 0, err, signedInUser.UserUID) return session, nil } @@ -506,7 +506,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI asyncCtx, cancelFunc := context.WithCancel(asyncCtx) s.cancelFunc = cancelFunc - s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil) + s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil, signedInUser.UserUID) start := time.Now() err := s.buildSnapshot(asyncCtx, signedInUser, initResp.MaxItemsPerPartition, initResp.Metadata, snapshot) @@ -527,7 +527,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI } span.SetStatus(codes.Ok, "snapshot built") - s.report(asyncCtx, session, gmsclient.EventDoneBuildingSnapshot, time.Since(start), err) + s.report(asyncCtx, session, gmsclient.EventDoneBuildingSnapshot, time.Since(start), err, signedInUser.UserUID) }() return &snapshot, nil @@ -685,7 +685,7 @@ func (s *Service) GetSnapshotList(ctx context.Context, query cloudmigration.List return snapshotList, nil } -func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error { +func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.UploadSnapshot", trace.WithAttributes( attribute.String("sessionUid", sessionUid), @@ -740,7 +740,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, sessionUid st asyncCtx, s.cancelFunc = context.WithCancel(asyncCtx) - s.report(asyncCtx, session, gmsclient.EventStartUploadingSnapshot, 0, nil) + s.report(asyncCtx, session, gmsclient.EventStartUploadingSnapshot, 0, nil, signedInUser.UserUID) start := time.Now() err := s.uploadSnapshot(asyncCtx, session, snapshot, uploadUrl) @@ -760,7 +760,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, sessionUid st } } - s.report(asyncCtx, session, gmsclient.EventDoneUploadingSnapshot, time.Since(start), err) + s.report(asyncCtx, session, gmsclient.EventDoneUploadingSnapshot, time.Since(start), err, signedInUser.UserUID) }() return nil @@ -808,6 +808,7 @@ func (s *Service) report( t gmsclient.LocalEventType, d time.Duration, evtErr error, + userUID string, ) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.report") defer span.End() @@ -832,6 +833,7 @@ func (s *Service) report( e := gmsclient.EventRequestDTO{ Event: t, LocalID: id, + UserUID: userUID, } if d != 0 { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 50da4fa57c0..28bd9ef5cc0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -37,11 +37,11 @@ func (s *NoopServiceImpl) GetSessionList(ctx context.Context, orgID int64) (*clo return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) CreateSession(ctx context.Context, cm cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (s *NoopServiceImpl) CreateSession(ctx context.Context, signedInUser *user.SignedInUser, cm cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { return nil, cloudmigration.ErrMigrationDisabled } -func (s *NoopServiceImpl) DeleteSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *NoopServiceImpl) DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, uid string) (*cloudmigration.CloudMigrationSession, error) { return nil, cloudmigration.ErrFeatureDisabledError } @@ -57,7 +57,7 @@ func (s *NoopServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrat return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, orgID int64, sessionUid string, snapshotUid string) error { +func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error { return cloudmigration.ErrFeatureDisabledError } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 50c0bfb6095..957c60fc911 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -547,12 +547,13 @@ func Test_NonCoreDataSourcesHaveWarning(t *testing.T) { func TestDeleteSession(t *testing.T) { s := setUpServiceTest(t, false).(*Service) + user := &user.SignedInUser{UserUID: "user123"} t.Run("when deleting a session that does not exist in the database, it returns an error", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - session, err := s.DeleteSession(ctx, 2, "invalid-session-uid") + session, err := s.DeleteSession(ctx, 2, user, "invalid-session-uid") require.Nil(t, session) require.Error(t, err) }) @@ -570,12 +571,12 @@ func TestDeleteSession(t *testing.T) { OrgID: 3, } - createResp, err := s.CreateSession(ctx, cmd) + createResp, err := s.CreateSession(ctx, user, cmd) require.NoError(t, err) require.NotEmpty(t, createResp.UID) require.NotEmpty(t, createResp.Slug) - deletedSession, err := s.DeleteSession(ctx, cmd.OrgID, createResp.UID) + deletedSession, err := s.DeleteSession(ctx, cmd.OrgID, user, createResp.UID) require.NoError(t, err) require.NotNil(t, deletedSession) require.Equal(t, deletedSession.UID, createResp.UID) @@ -597,7 +598,7 @@ func TestReportEvent(t *testing.T) { s.gmsClient = gmsMock require.NotPanics(t, func() { - s.report(ctx, nil, gmsclient.EventConnect, time.Minute, nil) + s.report(ctx, nil, gmsclient.EventConnect, time.Minute, nil, "user123") }) require.Zero(t, gmsMock.reportEventCalled) @@ -613,7 +614,7 @@ func TestReportEvent(t *testing.T) { s.gmsClient = gmsMock require.NotPanics(t, func() { - s.report(ctx, &cloudmigration.CloudMigrationSession{}, gmsclient.EventConnect, time.Minute, nil) + s.report(ctx, &cloudmigration.CloudMigrationSession{}, gmsclient.EventConnect, time.Minute, nil, "user123") }) require.Equal(t, 1, gmsMock.reportEventCalled) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index 37d2d8498eb..020e5e96122 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -45,7 +45,7 @@ func (m FakeServiceImpl) DeleteToken(_ context.Context, _ string) error { return nil } -func (m FakeServiceImpl) CreateSession(_ context.Context, _ cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (m FakeServiceImpl) CreateSession(_ context.Context, _ *user.SignedInUser, _ cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { if m.ReturnError { return nil, cloudmigration.ErrSessionCreationFailure } @@ -64,7 +64,7 @@ func (m FakeServiceImpl) GetSession(_ context.Context, _ int64, _ string) (*clou return &cloudmigration.CloudMigrationSession{UID: "fake"}, nil } -func (m FakeServiceImpl) DeleteSession(_ context.Context, _ int64, _ string) (*cloudmigration.CloudMigrationSession, error) { +func (m FakeServiceImpl) DeleteSession(_ context.Context, _ int64, _ *user.SignedInUser, _ string) (*cloudmigration.CloudMigrationSession, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } @@ -154,7 +154,7 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati return cloudSnapshots, nil } -func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, _ int64, sessionUid string, snapshotUid string) error { +func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, _ int64, _ *user.SignedInUser, sessionUid string, snapshotUid string) error { if m.ReturnError { return fmt.Errorf("mock error") } diff --git a/pkg/services/cloudmigration/gmsclient/dtos.go b/pkg/services/cloudmigration/gmsclient/dtos.go index 75ff01e16a0..299e6a29edf 100644 --- a/pkg/services/cloudmigration/gmsclient/dtos.go +++ b/pkg/services/cloudmigration/gmsclient/dtos.go @@ -50,6 +50,7 @@ type EventRequestDTO struct { Event LocalEventType `json:"event"` Error string `json:"error"` DurationIfFinished time.Duration `json:"duration"` + UserUID string `json:"userUid"` } type LocalEventType string From 29ee90bc265e28370abb5e5df0cabee93e158b50 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Wed, 13 Nov 2024 12:20:56 +0100 Subject: [PATCH 40/55] CloudMigrations: add parent folder name to alert rule resource (#96366) --- .../cloudmigrationimpl/cloudmigration_test.go | 8 ++++++- .../cloudmigrationimpl/snapshot_mgmt.go | 21 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 957c60fc911..70929b6e570 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -697,6 +697,7 @@ func TestGetParentNames(t *testing.T) { folders []folder.CreateFolderCommand dashboards []dashboards.Dashboard libraryElements []libraryElement + alertRules []alertRule expectedParentNames map[cloudmigration.MigrateDataType][]string }{ { @@ -717,10 +718,15 @@ func TestGetParentNames(t *testing.T) { {UID: "libraryElementUID-0", FolderUID: &libraryElementFolderUID}, {UID: "libraryElementUID-1"}, }, + alertRules: []alertRule{ + {UID: "alertRuleUID-0", FolderUID: ""}, + {UID: "alertRuleUID-1", FolderUID: "folderUID-B"}, + }, expectedParentNames: map[cloudmigration.MigrateDataType][]string{ cloudmigration.DashboardDataType: {"", "Folder A", "Folder B"}, cloudmigration.FolderDataType: {"Folder A"}, cloudmigration.LibraryElementDataType: {"Folder A"}, + cloudmigration.AlertRuleType: {"Folder B"}, }, }, } @@ -728,7 +734,7 @@ func TestGetParentNames(t *testing.T) { for _, tc := range testcases { s.folderService = &foldertest.FakeService{ExpectedFolders: tc.fakeFolders} - dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.dashboards, tc.folders, tc.libraryElements) + dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.dashboards, tc.folders, tc.libraryElements, tc.alertRules) require.NoError(t, err) for dataType, expectedParentNames := range tc.expectedParentNames { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 1722eb213ce..2daa17bc0b4 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -195,7 +195,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S } // Obtain the names of parent elements for Dashboard and Folders data types - parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) + parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements, alertRules) if err != nil { s.log.Error("Failed to get parent folder names", "err", err) } @@ -662,7 +662,14 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser // getParentNames finds the parent names for resources and returns a map of data type: {data UID : parentName} // for dashboards, folders and library elements - the parent is the parent folder -func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedInUser, dashboards []dashboards.Dashboard, folders []folder.CreateFolderCommand, libraryElements []libraryElement) (map[cloudmigration.MigrateDataType]map[string](string), error) { +func (s *Service) getParentNames( + ctx context.Context, + signedInUser *user.SignedInUser, + dashboards []dashboards.Dashboard, + folders []folder.CreateFolderCommand, + libraryElements []libraryElement, + alertRules []alertRule, +) (map[cloudmigration.MigrateDataType]map[string](string), error) { parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string]string) for _, dataType := range currentMigrationTypes { parentNamesByType[dataType] = make(map[string]string) @@ -684,6 +691,11 @@ func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedI parentFolderUIDsSet[*libraryElement.FolderUID] = struct{}{} } } + for _, alertRule := range alertRules { + if alertRule.FolderUID != "" { + parentFolderUIDsSet[alertRule.FolderUID] = struct{}{} + } + } parentFolderUIDsSlice := make([]string, 0, len(parentFolderUIDsSet)) for parentFolderUID := range parentFolderUIDsSet { parentFolderUIDsSlice = append(parentFolderUIDsSlice, parentFolderUID) @@ -708,6 +720,11 @@ func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedI parentNamesByType[cloudmigration.LibraryElementDataType][libraryElement.UID] = foldersUIDsToFolderName[*libraryElement.FolderUID] } } + for _, alertRule := range alertRules { + if alertRule.FolderUID != "" { + parentNamesByType[cloudmigration.AlertRuleType][alertRule.UID] = foldersUIDsToFolderName[alertRule.FolderUID] + } + } return parentNamesByType, err } From 092b2ef5d26cfce94aa0f3d7825ef8172bb578a6 Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 13 Nov 2024 12:38:43 +0100 Subject: [PATCH 41/55] Docs: Improve Google OAuth docs (#96377) * Add Configuration options to the bottom, align the setup with Cloud docs * Fixes, format changes * Update docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md Co-authored-by: xavi <114113189+volcanonoodle@users.noreply.github.com> * fix --------- Co-authored-by: xavi <114113189+volcanonoodle@users.noreply.github.com> --- .../configure-authentication/azuread/index.md | 4 +- .../generic-oauth/index.md | 4 +- .../configure-authentication/google/index.md | 81 +++++++++++++++---- .../configure-authentication/okta/index.md | 2 +- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index b9ecc19f0e1..532d992b665 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -461,7 +461,7 @@ The following table outlines the various Azure AD/Entra ID configuration options | `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | | `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | | `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | -| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with generic OAuth if set to `false`. | `true` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with Azure AD/Entra ID if set to `false`. | `true` | | `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | | `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Map roles]({{< relref "#map-roles" >}}). | `false` | | `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | @@ -470,7 +470,7 @@ The following table outlines the various Azure AD/Entra ID configuration options | `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | | `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal]({{< relref "#configure-group-membership-claims-on-the-azure-portal" >}}). | | | `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | -| `allowed_domains` | No | Yes | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | | `tls_client_cert` | No | No | The path to the certificate. | | | `tls_client_key` | No | No | The path to the key. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 5b038280047..177e24dbebd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -382,7 +382,7 @@ The following table outlines the various generic OAuth2 configuration options. Y | `auth_style` | No | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | | `scopes` | No | List of comma- or space-separated OAuth2 scopes. | `user:email` | | `empty_scopes` | No | Set to `true` to use an empty scope during authentication. | `false` | -| `allow_sign_up` | No | Controls Grafana user creation through the generic OAuth2 login. Only existing Grafana users can log in with generic OAuth if set to `false`. | `true` | +| `allow_sign_up` | No | Controls Grafana user creation through the generic OAuth2 login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` | | `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | | `id_token_attribute_name` | No | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` | | `login_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login]({{< relref "#configure-login" >}}). | | @@ -398,7 +398,7 @@ The following table outlines the various generic OAuth2 configuration options. Y | `groups_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | | | `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | | `allowed_organizations` | No | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | | -| `allowed_domains` | No | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `team_ids` | No | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | | | `team_ids_attribute_path` | No | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | | | `teams_url` | No | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 338b95a7644..f80bd7a9703 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -7,14 +7,14 @@ labels: - cloud - enterprise - oss -menuTitle: Google OAuth2 -title: Configure Google OAuth2 authentication +menuTitle: Google OAuth +title: Configure Google OAuth authentication weight: 1100 --- -# Configure Google OAuth2 authentication +# Configure Google OAuth authentication -To enable Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use. +To enable Google OAuth you must register your application with Google. Google will generate a client ID and secret key for you to use. {{% admonition type="note" %}} If Users use the same email address in Google that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. @@ -25,13 +25,21 @@ If Users use the same email address in Google that they use with other authentic First, you need to create a Google OAuth Client: 1. Go to https://console.developers.google.com/apis/credentials. +1. Create a new project if you don't have one already. + 1. Enter a project name. The **Organization** and **Location** fields should both be set to your organization's information. + 1. In **OAuth consent screen** select the **External** User Type. Click **CREATE**. + 1. Fill out the requested information using the URL of your Grafana Cloud instance. + 1. Accept the defaults, or customize the consent screen options. 1. Click **Create Credentials**, then click **OAuth Client ID** in the drop-down menu 1. Enter the following: - - Application Type: Web Application - - Name: Grafana - - Authorized JavaScript Origins: https://grafana.mycompany.com - - Authorized Redirect URLs: https://grafana.mycompany.com/login/google - - Replace https://grafana.mycompany.com with the URL of your Grafana instance. + - **Application Type**: Web application + - **Name**: Grafana + - **Authorized JavaScript origins**: `https://` + - **Authorized redirect URIs**: `https:///login/google` + - Replace `` with the URL of your Grafana instance. + {{< admonition type="note" >}} + The URL you enter is the one for your Grafana instance home page, not your Grafana Cloud portal URL. + {{< /admonition >}} 1. Click Create 1. Copy the Client ID and Client Secret from the 'OAuth Client' modal @@ -41,7 +49,7 @@ First, you need to create a Google OAuth Client: Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. {{% /admonition %}} -As a Grafana Admin, you can configure Google OAuth2 client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. +As a Grafana Admin, you can configure Google OAuth client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -130,7 +138,9 @@ introduces "proof key for code exchange" (PKCE) which provides additional protection against some forms of authorization code interception attacks. PKCE will be required in [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-03). -> You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.google]` section. +{{% admonition type="note" %}} +You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.google]` section. +{{% /admonition %}} #### Configure refresh token @@ -183,7 +193,9 @@ to a comma or space separated list of groups. Google groups are referenced by the group email key. For example, `developers@google.com`. -> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups +{{% admonition type="note" %}} +Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups. +{{% /admonition %}} #### Configure role mapping @@ -196,8 +208,9 @@ If no valid role is found, the user is assigned the role specified by [the `auto You can disable this default role assignment by setting `role_attribute_strict = true`. This setting denies user access if no role or an invalid role is returned after evaluating the `role_attribute_path` and the `org_mapping` expressions. To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads. - -> By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions. +{{% admonition type="note" %}} +By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions. +{{% /admonition %}} ##### Role mapping examples @@ -237,7 +250,9 @@ role_attribute_path = contains(groups[*], 'example-group@google.com') && 'Editor skip_org_role_sync = false ``` -> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups +{{% admonition type="note" %}} +Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups. +{{% /admonition %}} ###### Map server administrator role @@ -258,3 +273,39 @@ In this example, all users will be assigned `Viewer` role regardless of the user role_attribute_path = "'Viewer'" skip_org_role_sync = false ``` + +## Configuration options + +The following table outlines the various Google OAuth configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). + +| Setting | Required | Supported on Cloud | Description | Default | +| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| `enabled` | No | Yes | Enables Google authentication. | `false` | +| `name` | No | Yes | Name that refers to the Google authentication from the Grafana user interface. | `Google` | +| `icon` | No | Yes | Icon used for the Google authentication in the Grafana user interface. | `google` | +| `client_id` | Yes | Yes | Client ID of the App. | | +| `client_secret` | Yes | Yes | Client secret of the App. | | +| `auth_url` | Yes | Yes | Authorization endpoint of the Google OAuth provider. | `https://accounts.google.com/o/oauth2/v2/auth` | +| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | `https://oauth2.googleapis.com/token` | +| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://openidconnect.googleapis.com/v1/userinfo` | +| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | +| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Google login. Only existing Grafana users can log in with Google if set to `false`. | `true` | +| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `hosted_domain` | No | Yes | Specifies the domain to restrict access to users from that domain. This value is appended to the authorization request using the `hd` parameter. | | +| `validate_hd` | No | Yes | Set to `false` to disable the validation of the `hd` parameter from the Google ID token. For more informatiion, refer to [Enable Google OAuth in Grafana]({{< relref "#enable-google-oauth-in-grafana" >}}). | `true` | +| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Google user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles]({{< relref "#map-roles" >}}). | `false` | +| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Google to include the `groups` claim following [Configure allowed groups]({{< relref "#configure-allowed-groups" >}}). | | +| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | No | The path to the certificate. | | +| `tls_client_key` | No | No | The path to the key. | | +| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `promp=consent` and `access_type=offline` parameters to the authorization request. | `true` | +| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 09ad9c98fb6..dba1580f535 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -281,6 +281,6 @@ The following table outlines the various Okta OIDC configuration options. You ca | `org_mapping` | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | | `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | | `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | | -| `allowed_domains` | No | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | | `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. | `false` | From 6db8aa2d40531ec0d717bbcdc5a4a7785bc9dc90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:55:19 +0000 Subject: [PATCH 42/55] Update dependency core-js to v3.39.0 (#96379) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 9ef22f0f1aa..3c3cc763310 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "chrome-remote-interface": "0.33.2", "codeowners": "^5.1.1", "copy-webpack-plugin": "12.0.2", - "core-js": "3.38.1", + "core-js": "3.39.0", "crashme": "0.0.15", "css-loader": "7.1.2", "css-minimizer-webpack-plugin": "6.0.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 78e681dac35..67dff576b28 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -161,7 +161,7 @@ "@types/uuid": "9.0.8", "chance": "1.1.12", "common-tags": "1.8.2", - "core-js": "3.38.1", + "core-js": "3.39.0", "css-loader": "7.1.2", "csstype": "3.1.3", "esbuild": "0.24.0", diff --git a/yarn.lock b/yarn.lock index 1d0accd3f8c..d17714c226d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4240,7 +4240,7 @@ __metadata: chance: "npm:1.1.12" classnames: "npm:2.5.1" common-tags: "npm:1.8.2" - core-js: "npm:3.38.1" + core-js: "npm:3.39.0" css-loader: "npm:7.1.2" csstype: "npm:3.1.3" d3: "npm:7.9.0" @@ -14253,10 +14253,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.38.1, core-js@npm:^3.6.0, core-js@npm:^3.8.3": - version: 3.38.1 - resolution: "core-js@npm:3.38.1" - checksum: 10/3c25fdf0b2595ed37ceb305213a61e2cf26185f628455e99d1c736dda5f69e2de4de7126e6a1da136f54260c4fcc982c4215e37b5a618790a597930f854c0a37 +"core-js@npm:3.39.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": + version: 3.39.0 + resolution: "core-js@npm:3.39.0" + checksum: 10/a3d34e669783dfc878e545f1983f60d9ff48a3867cd1d7ff8839b849e053002a208c7c14a5ca354b8e0b54982901e2f83dc87c3d9b95de0a94b4071d1c74e5f6 languageName: node linkType: hard @@ -18895,7 +18895,7 @@ __metadata: comlink: "npm:4.4.2" common-tags: "npm:1.8.2" copy-webpack-plugin: "npm:12.0.2" - core-js: "npm:3.38.1" + core-js: "npm:3.39.0" crashme: "npm:0.0.15" css-loader: "npm:7.1.2" css-minimizer-webpack-plugin: "npm:6.0.0" From 1129647039703fd1780393309d7d6f9822c702a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:56:07 +0000 Subject: [PATCH 43/55] Update dependency dompurify to v3.2.0 (#96380) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d17714c226d..3c28e19121c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15864,9 +15864,9 @@ __metadata: linkType: hard "dompurify@npm:^3.0.0": - version: 3.1.7 - resolution: "dompurify@npm:3.1.7" - checksum: 10/dc637a064306f83cf911caa267ffe1f973552047602020e3b6723c90f67962813edf8a65a0b62e8c9bc13fcd173a2691212a3719bc116226967f46bcd6181277 + version: 3.2.0 + resolution: "dompurify@npm:3.2.0" + checksum: 10/d9139fdc69e95efe43d154362216a9a6768c3a238b02b988a7ac0ddd044a992422d6c5fae5b319ab47aef8465824a815d104b843800b294352657f70d5168bfa languageName: node linkType: hard From d9b87ef9875d5129fe56472529a8c376fa53058a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 13 Nov 2024 15:17:15 +0300 Subject: [PATCH 44/55] Storage: Add authz access client stubs (#95548) authz stubs authz stubs lint fix Add folder move check.Name on creat fix watch --- pkg/storage/unified/resource/access.go | 26 +++++ pkg/storage/unified/resource/hooks.go | 14 --- pkg/storage/unified/resource/server.go | 154 ++++++++++++++++++++++--- pkg/storage/unified/sql/server.go | 31 +++-- 4 files changed, 185 insertions(+), 40 deletions(-) create mode 100644 pkg/storage/unified/resource/access.go diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go new file mode 100644 index 00000000000..af6f2582d33 --- /dev/null +++ b/pkg/storage/unified/resource/access.go @@ -0,0 +1,26 @@ +package resource + +import ( + "context" + + "github.com/grafana/authlib/authz" + "github.com/grafana/authlib/claims" +) + +type staticAuthzClient struct { + allowed bool +} + +// Check implements authz.AccessClient. +func (c *staticAuthzClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) { + return authz.CheckResponse{Allowed: c.allowed}, nil +} + +// Compile implements authz.AccessClient. +func (c *staticAuthzClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) { + return func(namespace string, name, folder string) bool { + return c.allowed + }, nil +} + +var _ authz.AccessClient = &staticAuthzClient{} diff --git a/pkg/storage/unified/resource/hooks.go b/pkg/storage/unified/resource/hooks.go index 605700791f9..b7160c0fc35 100644 --- a/pkg/storage/unified/resource/hooks.go +++ b/pkg/storage/unified/resource/hooks.go @@ -8,10 +8,6 @@ import ( ) type WriteAccessHooks struct { - // Check if a user has access to write folders - // When this is nil, no resources can have folders configured - Folder func(ctx context.Context, user claims.AuthInfo, uid string) bool - // When configured, this will make sure a user is allowed to save to a given origin Origin func(ctx context.Context, user claims.AuthInfo, origin string) bool } @@ -24,16 +20,6 @@ type LifecycleHooks interface { Stop(context.Context) error } -func (a *WriteAccessHooks) CanWriteFolder(ctx context.Context, user claims.AuthInfo, uid string) error { - if a.Folder == nil { - return fmt.Errorf("writing folders is not supported") - } - if !a.Folder(ctx, user, uid) { - return fmt.Errorf("not allowed to write resource to folder") - } - return nil -} - func (a *WriteAccessHooks) CanWriteOrigin(ctx context.Context, user claims.AuthInfo, uid string) error { if a.Origin == nil || uid == "UI" { return nil // default to OK diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index ff6f3b5e838..97549b19dba 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -17,6 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/authlib/authz" "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -140,7 +141,10 @@ type ResourceServerOptions struct { // Check if a user has access to write folders // When this is nil, no resources can have folders configured - WriteAccess WriteAccessHooks + WriteHooks WriteAccessHooks + + // Link RBAC + AccessClient authz.AccessClient // Callbacks for startup and shutdown Lifecycle LifecycleHooks @@ -161,6 +165,10 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { return nil, fmt.Errorf("missing Backend implementation") } + if opts.AccessClient == nil { + opts.AccessClient = &staticAuthzClient{allowed: true} // everything OK + } + if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } @@ -209,7 +217,8 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { index: opts.Index, blob: blobstore, diagnostics: opts.Diagnostics, - access: opts.WriteAccess, + access: opts.AccessClient, + writeHooks: opts.WriteHooks, lifecycle: opts.Lifecycle, now: opts.Now, ctx: ctx, @@ -228,7 +237,8 @@ type server struct { blob BlobSupport index ResourceIndexServer diagnostics DiagnosticsServer - access WriteAccessHooks + access authz.AccessClient + writeHooks WriteAccessHooks lifecycle LifecycleHooks now func() int64 mostRecentRV atomic.Int64 // The most recent resource version seen by the server @@ -312,6 +322,13 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour s.log.Error("object must not include a resource version", "key", key) } + check := authz.CheckRequest{ + Verb: "create", + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + } + event := &WriteEvent{ Value: value, Key: key, @@ -321,6 +338,7 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour event.Type = WatchEvent_ADDED } else { event.Type = WatchEvent_MODIFIED + check.Verb = "update" temp := &unstructured.Unstructured{} err = temp.UnmarshalJSON(oldValue) @@ -364,19 +382,24 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour return nil, err } - folder := obj.GetFolder() - if folder != "" { - err = s.access.CanWriteFolder(ctx, user, folder) - if err != nil { - return nil, AsErrorResult(err) + check.Folder = obj.GetFolder() + check.Name = key.Name + a, err := s.access.Check(ctx, user, check) + if err != nil { + return nil, AsErrorResult(err) + } + if !a.Allowed { + return nil, &ErrorResult{ + Code: http.StatusForbidden, } } + origin, err := obj.GetOriginInfo() if err != nil { return nil, NewBadRequestError("invalid origin info") } if origin != nil { - err = s.access.CanWriteOrigin(ctx, user, origin.Name) + err = s.writeHooks.CanWriteOrigin(ctx, user, origin.Name) if err != nil { return nil, AsErrorResult(err) } @@ -406,7 +429,7 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons if found != nil && len(found.Value) > 0 { rsp.Error = &ErrorResult{ Code: http.StatusConflict, - Message: "key already exists", + Message: "key already exists", // TODO?? soft delete replace? } return rsp, nil } @@ -416,6 +439,7 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons rsp.Error = e return rsp, nil } + var err error rsp.ResourceVersion, err = s.backend.WriteEvent(ctx, *event) if err != nil { @@ -491,6 +515,14 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons if req.ResourceVersion < 0 { return nil, apierrors.NewBadRequest("update must include the previous version") } + user, ok := claims.From(ctx) + if !ok || user == nil { + rsp.Error = &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + } + return rsp, nil + } latest := s.backend.ReadResource(ctx, &ReadRequest{ Key: req.Key, @@ -504,6 +536,25 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons return rsp, nil } + access, err := s.access.Check(ctx, user, authz.CheckRequest{ + Verb: "delete", + Group: req.Key.Group, + Resource: req.Key.Resource, + Namespace: req.Key.Namespace, + Name: req.Key.Name, + Folder: latest.Folder, + }) + if err != nil { + rsp.Error = AsErrorResult(err) + return rsp, nil + } + if !access.Allowed { + rsp.Error = &ErrorResult{ + Code: http.StatusForbidden, + } + return rsp, nil + } + now := metav1.NewTime(time.UnixMilli(s.now())) event := WriteEvent{ Key: req.Key, @@ -515,7 +566,7 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons return nil, apierrors.NewBadRequest("unable to get user") } marker := &DeletedMarker{} - err := json.Unmarshal(latest.Value, marker) + err = json.Unmarshal(latest.Value, marker) if err != nil { return nil, apierrors.NewBadRequest( fmt.Sprintf("unable to read previous object, %v", err)) @@ -551,6 +602,14 @@ func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err if err := s.Init(ctx); err != nil { return nil, err } + user, ok := claims.From(ctx) + if !ok || user == nil { + return &ReadResponse{ + Error: &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + }}, nil + } // if req.Key.Group == "" { // status, _ := AsErrorResult(apierrors.NewBadRequest("missing group")) @@ -561,7 +620,24 @@ func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err } rsp := s.backend.ReadResource(ctx, req) - // TODO, check folder permissions etc + + a, err := s.access.Check(ctx, user, authz.CheckRequest{ + Verb: "get", + Group: req.Key.Group, + Resource: req.Key.Resource, + Namespace: req.Key.Namespace, + Name: req.Key.Name, + Folder: rsp.Folder, + }) + if err != nil { + return &ReadResponse{Error: AsErrorResult(err)}, nil + } + if !a.Allowed { + return &ReadResponse{ + Error: &ErrorResult{ + Code: http.StatusForbidden, + }}, nil + } return &ReadResponse{ ResourceVersion: rsp.ResourceVersion, Value: rsp.Value, @@ -573,6 +649,15 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err ctx, span := s.tracer.Start(ctx, "storage_server.List") defer span.End() + user, ok := claims.From(ctx) + if !ok || user == nil { + return &ListResponse{ + Error: &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + }}, nil + } + if err := s.Init(ctx); err != nil { return nil, err } @@ -582,19 +667,37 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err maxPageBytes := 1024 * 1024 * 2 // 2mb/page pageBytes := 0 rsp := &ListResponse{} + + key := req.Options.Key + checker, err := s.access.Compile(ctx, user, authz.ListRequest{ + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + }) + if err != nil { + return &ListResponse{Error: AsErrorResult(err)}, nil + } + if checker == nil { + return &ListResponse{Error: &ErrorResult{ + Code: http.StatusForbidden, + }}, nil + } + rv, err := s.backend.ListIterator(ctx, req, func(iter ListIterator) error { for iter.Next() { if err := iter.Error(); err != nil { return err } - // TODO: add authz filters - item := &ResourceWrapper{ ResourceVersion: iter.ResourceVersion(), Value: iter.Value(), } + if !checker(iter.Namespace(), iter.Name(), iter.Folder()) { + continue + } + pageBytes += len(item.Value) rsp.Items = append(rsp.Items, item) if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { @@ -644,6 +747,7 @@ func (s *server) initWatcher() error { return err } +//nolint:gocyclo func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { ctx := srv.Context() @@ -651,6 +755,24 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { return err } + user, ok := claims.From(ctx) + if !ok || user == nil { + return apierrors.NewUnauthorized("no user found in context") + } + + key := req.Options.Key + checker, err := s.access.Compile(ctx, user, authz.ListRequest{ + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + }) + if err != nil { + return err + } + if checker == nil { + return apierrors.NewUnauthorized("not allowed to list anything") // ?? or a single error? + } + // Start listening -- this will buffer any changes that happen while we backfill. // If events are generated faster than we can process them, then some events will be dropped. // TODO: Think of a way to allow the client to catch up. @@ -724,6 +846,10 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } s.log.Debug("Server Broadcasting", "type", event.Type, "rv", event.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "resource", event.Key.Resource, "name", event.Key.Name) if event.ResourceVersion > since && matchesQueryKey(req.Options.Key, event.Key) { + if !checker(event.Key.Namespace, event.Key.Name, event.Folder) { + continue + } + value := event.Value // remove the delete marker stored in the value for deleted objects if event.Type == WatchEvent_DELETED { diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 7e89816cda7..e1b730d4744 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -5,14 +5,16 @@ import ( "os" "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" - "github.com/prometheus/client_golang/prometheus" ) // Creates a new ResourceServer @@ -48,17 +50,6 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, fea opts.Diagnostics = store opts.Lifecycle = store - if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) { - opts.WriteAccess = resource.WriteAccessHooks{ - Folder: func(ctx context.Context, user claims.AuthInfo, uid string) bool { - // #TODO build on the logic here - // #TODO only enable write access when the resource being written in the folder - // is another folder - return true - }, - } - } - if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { opts.Index = resource.NewResourceIndexServer(cfg, tracer) } @@ -70,6 +61,22 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, fea // Initialize the indexer if one is configured if opts.Index != nil { + // TODO: Create a proper identity for the indexer + orgId := int64(1) + ctx = identity.WithRequester(ctx, &identity.StaticRequester{ + Type: claims.TypeServiceAccount, // system:apiserver + UserID: 1, + OrgID: int64(1), + Name: "admin", + Login: "admin", + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{ + orgId: { + "*": {"*"}, // all resources, all scopes + }, + }, + }) _, err = rs.(resource.ResourceIndexer).Index(ctx) if err != nil { return nil, err From 0459a1eb80b86529b992f0c1cb26fffe6bb9038d Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 13 Nov 2024 06:43:16 -0600 Subject: [PATCH 45/55] BarChart: Fix decimals=0 config (#96354) --- public/app/plugins/panel/barchart/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index df3769fd029..d6cf592062e 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -419,6 +419,7 @@ export const prepConfig = ({ series, totalSeries, color, orientation, options, t direction: vizOrientation.yDir, distribution: customConfig.scaleDistribution?.type, log: customConfig.scaleDistribution?.log, + decimals: field.config.decimals, }); if (customConfig.axisPlacement !== AxisPlacement.Hidden) { @@ -445,6 +446,7 @@ export const prepConfig = ({ series, totalSeries, color, orientation, options, t tickLabelRotation: vizOrientation.xOri === 1 ? xTickLabelRotation * -1 : 0, theme, grid: { show: customConfig.axisGridShow }, + decimals: field.config.decimals, }; if (customConfig.axisBorderShow) { From ed707098e10f8406758b8c0d93da02f928921576 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:01:43 +0000 Subject: [PATCH 46/55] Update dependency eslint to v9.14.0 (#96382) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 95 +++++++++++--------- 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 3c3cc763310..7ff8f53ca6d 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,7 @@ "esbuild": "0.24.0", "esbuild-loader": "4.2.2", "esbuild-plugin-browserslist": "^0.15.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.8.3", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 6950ce91f24..06e02805a6f 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@typescript-eslint/types": "^8.9.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "tslib": "2.7.0" }, "private": true diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index f13dff00b8b..ee0fa912cc0 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -11,7 +11,7 @@ "@swc/core": "1.4.2", "@types/eslint": "9.6.1", "copy-webpack-plugin": "12.0.2", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", "glob": "11.0.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 6d4a75821e1..2547623a4db 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -106,7 +106,7 @@ "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild": "0.24.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.8.3", diff --git a/yarn.lock b/yarn.lock index 3c28e19121c..9fc26ed1ffb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2765,10 +2765,10 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0": - version: 4.11.1 - resolution: "@eslint-community/regexpp@npm:4.11.1" - checksum: 10/934b6d3588c7f16b18d41efec4fdb89616c440b7e3256b8cb92cfd31ae12908600f2b986d6c1e61a84cbc10256b1dd3448cd1eec79904bd67ac365d0f1aba2e2 +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc languageName: node linkType: hard @@ -2795,10 +2795,10 @@ __metadata: languageName: node linkType: hard -"@eslint/core@npm:^0.6.0": - version: 0.6.0 - resolution: "@eslint/core@npm:0.6.0" - checksum: 10/ec5cce168c8773fbd60c5a505563c6cf24398b3e1fa352929878d63129e0dd5b134d3232be2f2c49e8124a965d03359b38962aa0dcf7dfaf50746059d2a2f798 +"@eslint/core@npm:^0.7.0": + version: 0.7.0 + resolution: "@eslint/core@npm:0.7.0" + checksum: 10/69227f33fddd9b402b7b0830732a6e84cae77d202cb5b56f0dbcc462882e07d00e80216b796cf2f243f5b775af3ef27545a0c439d78e66122eab71da4773b81c languageName: node linkType: hard @@ -2819,10 +2819,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.12.0": - version: 9.12.0 - resolution: "@eslint/js@npm:9.12.0" - checksum: 10/c4ec9f7ff664f778324002bccdfd63e4a563018e4d7efc838d8149898f9df8649fbc51a379c3d7deea40da4fba9e8e62f39f2df3ff2b9616e2241bbfc10456b0 +"@eslint/js@npm:9.14.0": + version: 9.14.0 + resolution: "@eslint/js@npm:9.14.0" + checksum: 10/897e26bd68f898e56e96f85c92a1d823ef3d9f34e17d88d0ff40e88882ddae28d2f35915150c21cf640e0c64cb23703d0fbe6f7c9b9d6328aabe58ca30d9b4fe languageName: node linkType: hard @@ -3614,7 +3614,7 @@ __metadata: dependencies: "@typescript-eslint/types": "npm:^8.9.0" "@typescript-eslint/utils": "npm:^8.9.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" tslib: "npm:2.7.0" languageName: unknown linkType: soft @@ -3842,7 +3842,7 @@ __metadata: "@swc/core": "npm:1.4.2" "@types/eslint": "npm:9.6.1" copy-webpack-plugin: "npm:12.0.2" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" glob: "npm:11.0.0" @@ -3921,7 +3921,7 @@ __metadata: date-fns: "npm:3.6.0" debounce-promise: "npm:3.1.2" esbuild: "npm:0.24.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.8.3" @@ -4358,20 +4358,20 @@ __metadata: languageName: node linkType: hard -"@humanfs/core@npm:^0.19.0": - version: 0.19.0 - resolution: "@humanfs/core@npm:0.19.0" - checksum: 10/9c4f96b9e934b7d2f69c5ee8b9414dcaf5c5a03225eb08f8ace3b80429c0fc796e11c4e2ef182172790e7b4560b1137ef984da4dc9662cdd5e3e92baceb02821 +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10/270d936be483ab5921702623bc74ce394bf12abbf57d9145a69e8a0d1c87eb1c768bd2d93af16c5705041e257e6d9cc7529311f63a1349f3678abc776fc28523 languageName: node linkType: hard -"@humanfs/node@npm:^0.16.5": - version: 0.16.5 - resolution: "@humanfs/node@npm:0.16.5" +"@humanfs/node@npm:^0.16.6": + version: 0.16.6 + resolution: "@humanfs/node@npm:0.16.6" dependencies: - "@humanfs/core": "npm:^0.19.0" + "@humanfs/core": "npm:^0.19.1" "@humanwhocodes/retry": "npm:^0.3.0" - checksum: 10/16e49b5f9d4a3cf8205af18f0909b8c6e00faa70a0e01bc606b413423ee20123e53028b6ca22c57725595341d62e148cd1908c297a761ee495087cc674f7b0a6 + checksum: 10/6d43c6727463772d05610aa05c83dab2bfbe78291022ee7a92cb50999910b8c720c76cc312822e2dea2b497aa1b3fef5fe9f68803fc45c9d4ed105874a65e339 languageName: node linkType: hard @@ -4382,13 +4382,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/retry@npm:^0.3.0, @humanwhocodes/retry@npm:^0.3.1": +"@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" checksum: 10/eb457f699529de7f07649679ec9e0353055eebe443c2efe71c6dd950258892475a038e13c6a8c5e13ed1fb538cdd0a8794faa96b24b6ffc4c87fb1fc9f70ad7f languageName: node linkType: hard +"@humanwhocodes/retry@npm:^0.4.0": + version: 0.4.1 + resolution: "@humanwhocodes/retry@npm:0.4.1" + checksum: 10/39fafc7319e88f61befebd5e1b4f0136534ea6a9bd10d74366698187bd63544210ec5d79a87ed4d91297f1cc64c4c53d45fb0077a2abfdce212cf0d3862d5f04 + languageName: node + linkType: hard + "@hutson/parse-repository-url@npm:^3.0.0": version: 3.0.2 resolution: "@hutson/parse-repository-url@npm:3.0.2" @@ -17065,13 +17072,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.1.0": - version: 8.1.0 - resolution: "eslint-scope@npm:8.1.0" +"eslint-scope@npm:^8.1.0, eslint-scope@npm:^8.2.0": + version: 8.2.0 + resolution: "eslint-scope@npm:8.2.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/4c34a12fbeb0677822a9e93e81f2027e39e6f27557c17bc1e5ff76debbd41e748c3673517561792bda9e276245f89fbfd9b0b24fcec3b33a04ee2196729b3489 + checksum: 10/cd9ab60d5a68f3a0fcac04d1cff5a7383d0f331964d5f1c446259123caec5b3ccc542284d07846e4f4d1389da77750821cc9a6e1ce18558c674977351666f9a6 languageName: node linkType: hard @@ -17082,7 +17089,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.1.0, eslint-visitor-keys@npm:^4.2.0": +"eslint-visitor-keys@npm:^4.2.0": version: 4.2.0 resolution: "eslint-visitor-keys@npm:4.2.0" checksum: 10/9651b3356b01760e586b4c631c5268c0e1a85236e3292bf754f0472f465bf9a856c0ddc261fceace155334118c0151778effafbab981413dbf9288349343fa25 @@ -17105,20 +17112,20 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.12.0": - version: 9.12.0 - resolution: "eslint@npm:9.12.0" +"eslint@npm:9.14.0": + version: 9.14.0 + resolution: "eslint@npm:9.14.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.11.0" + "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.18.0" - "@eslint/core": "npm:^0.6.0" + "@eslint/core": "npm:^0.7.0" "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.12.0" + "@eslint/js": "npm:9.14.0" "@eslint/plugin-kit": "npm:^0.2.0" - "@humanfs/node": "npm:^0.16.5" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@humanwhocodes/retry": "npm:^0.3.1" + "@humanwhocodes/retry": "npm:^0.4.0" "@types/estree": "npm:^1.0.6" "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" @@ -17126,9 +17133,9 @@ __metadata: cross-spawn: "npm:^7.0.2" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.1.0" - eslint-visitor-keys: "npm:^4.1.0" - espree: "npm:^10.2.0" + eslint-scope: "npm:^8.2.0" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -17151,11 +17158,11 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/c3f10d1ca3798bf1d0f71e43846e254d4bf0ea9ffbb0e61f9686a98e412aa762a454c5e5ef4e74fd71956b1500c04817c9f08dbf7a0cec47317160e28f585e4f + checksum: 10/6ce59dda56ecff9483c6e8cd28e91138d2c43cbf08c923f731f507fd9b4aba9d72761c99882dc313a72ea915a5e380ab0b4f01e208a7a37d71490ddfd29ee063 languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.2.0, espree@npm:^10.3.0": +"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0": version: 10.3.0 resolution: "espree@npm:10.3.0" dependencies: @@ -18912,7 +18919,7 @@ __metadata: esbuild: "npm:0.24.0" esbuild-loader: "npm:4.2.2" esbuild-plugin-browserslist: "npm:^0.15.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.8.3" From 85caabf8e48409f31c4c81fc9ca251d630ea5762 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:02:12 +0000 Subject: [PATCH 47/55] Update dependency eslint-plugin-jest to v28.9.0 (#96383) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7ff8f53ca6d..fb57ff4a374 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "28.8.3", + "eslint-plugin-jest": "28.9.0", "eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-jsdoc": "50.4.3", "eslint-plugin-jsx-a11y": "6.10.2", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 2547623a4db..f8fda0d91a7 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -109,7 +109,7 @@ "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "28.8.3", + "eslint-plugin-jest": "28.9.0", "eslint-plugin-jsdoc": "50.4.3", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", diff --git a/yarn.lock b/yarn.lock index 9fc26ed1ffb..e00f62bbe18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3924,7 +3924,7 @@ __metadata: eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:28.8.3" + eslint-plugin-jest: "npm:28.9.0" eslint-plugin-jsdoc: "npm:50.4.3" eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" @@ -16904,9 +16904,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:28.8.3": - version: 28.8.3 - resolution: "eslint-plugin-jest@npm:28.8.3" +"eslint-plugin-jest@npm:28.9.0": + version: 28.9.0 + resolution: "eslint-plugin-jest@npm:28.9.0" dependencies: "@typescript-eslint/utils": "npm:^6.0.0 || ^7.0.0 || ^8.0.0" peerDependencies: @@ -16918,7 +16918,7 @@ __metadata: optional: true jest: optional: true - checksum: 10/3f1798c61e143981eefcfb2fbc4b2e5b329378ebaafdec6485f443c79ee0d3304e9409e8ea8ce089ac15abb4e700d8d838e0a1da29feb528d77a2b3cce6989ec + checksum: 10/399dd882205da996c2d46b768b744093346ffe4f730972825c72a3f85cc2e26bc08ccd73a7b487f8da4c39fa0178c58b571d5d611a22ced5518b5dfccb48d906 languageName: node linkType: hard @@ -18922,7 +18922,7 @@ __metadata: eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:28.8.3" + eslint-plugin-jest: "npm:28.9.0" eslint-plugin-jest-dom: "npm:^5.4.0" eslint-plugin-jsdoc: "npm:50.4.3" eslint-plugin-jsx-a11y: "npm:6.10.2" From 82fdd4f03fc7e964d840838bd024e965fc04ac75 Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Wed, 13 Nov 2024 14:25:37 +0100 Subject: [PATCH 48/55] Docs: Clean up basic auth, anonymous access and main pages for auth configuration (#96259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: Clean up basic auth, anonymous access and main pages for auth configuration * Fix the typo * Prettier should fix this * fix minor typo * Update docs/sources/setup-grafana/configure-security/configure-authentication/_index.md Co-authored-by: Misi --------- Co-authored-by: Irene Rodríguez Co-authored-by: Misi --- .../configure-authentication/_index.md | 83 ++------- .../anonymous-auth/index.md | 61 +++++++ .../configure-authentication/grafana/index.md | 166 ++---------------- 3 files changed, 87 insertions(+), 223 deletions(-) create mode 100644 docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md index 56c8eac59f9..fe98f938a20 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md @@ -17,12 +17,14 @@ weight: 200 Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user permissions and org memberships. -The following table shows all supported authentication providers and the features available for them. [Team sync]({{< relref "../configure-team-sync" >}}) and [active sync]({{< relref "./enhanced-ldap#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. +The following table shows all supported authentication methods and the features available for them. [Team sync]({{< relref "../configure-team-sync" >}}) and [active sync]({{< relref "./enhanced-ldap#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. -| Provider | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | +| Authentication method | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | | :---------------------------------------------------- | :---------------- | :----------- | :----------- | :-------------------- | :-------- | :------------- | :---------- | :------------------- | :--------- | :------------ | +| [Anonymous access]({{< relref "./anonymous-auth" >}}) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | | [Auth Proxy]({{< relref "./auth-proxy" >}}) | no | yes | yes | no | yes | no | N/A | no | N/A | N/A | | [Azure AD OAuth]({{< relref "./azuread" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Basic auth]({{< relref "./grafana" >}}) | yes | N/A | yes | yes | N/A | N/A | N/A | N/A | N/A | N/A | | [Generic OAuth]({{< relref "./generic-oauth" >}}) | yes | yes | yes | yes | yes | no | N/A | yes | yes | yes | | [GitHub OAuth]({{< relref "./github" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [GitLab OAuth]({{< relref "./gitlab" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | @@ -33,17 +35,9 @@ The following table shows all supported authentication providers and the feature | [LDAP]({{< relref "./ldap" >}}) | yes | yes | yes | yes | yes | yes | yes | no | N/A | N/A | | [JWT Proxy]({{< relref "./jwt" >}}) | no | yes | yes | yes | no | no | N/A | no | N/A | N/A | -N/A = Not applicable +Fields explanation: -## Auth Proxy - -| Feature | Supported? | -| :---------------- | :--------- | -| Multi Org Mapping | no | -| Enforce Sync | N/A | -| Role Mapping | yes | - -**Multi Org Mapping:** Able to add a user and role map him to multiple orgs +**Multi Org Mapping:** Able to add a user and map roles to multiple organizations **Enforce Sync:** If the information provided by the identity provider is empty, does the integration skip setting that user’s field or does it enforce a default. @@ -82,15 +76,9 @@ If users want to use the same email address with multiple identity providers (fo To enable this option, refer to the [Enable email lookup](#enable-email-lookup) section. -## Grafana Auth +## Login and short-lived tokens -Grafana of course has a built in user authentication system with password authentication enabled by default. You can -disable authentication by enabling anonymous access. You can also hide the login form and only allow login through an auth -provider (listed above). There are also options for allowing self sign up. - -### Login and short-lived tokens - -> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration. +> The following applies when using Grafana's basic authentication, LDAP (without Auth proxy) or OAuth integration. Grafana uses short-lived tokens as a mechanism for verifying authenticated users. These short-lived tokens are rotated on an interval specified by `token_rotation_interval_minutes` for active authenticated users. @@ -99,11 +87,6 @@ Inactive authenticated users will remain logged in for a duration specified by ` This means that a user can close a Grafana window and return before `now + login_maximum_inactive_lifetime_duration` to continue their session. This is true as long as the time since last user login is less than `login_maximum_lifetime_duration`. -#### Remote logout - -You can logout from other devices by removing login sessions from the bottom of your profile page. If you are -a Grafana admin user, you can also do the same for any user from the Server Admin / Edit User view. - ## Settings Example: @@ -130,50 +113,7 @@ api_key_max_seconds_to_live = -1 oauth_allow_insecure_email_lookup = false ``` -### Anonymous authentication - -You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. - -Example: - -```bash -[auth.anonymous] -enabled = true - -{{< admonition type="note" >}} -Enabling anonymous access is a disallowed configuration setting on Hosted Grafana and not recommended due [security implications](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/#implications-of-enabling-anonymous-access-to-dashboards). -For sharing dashboards with a wider audience, consider using the [public dashboard feature](https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/) instead. -{{< /admonition >}} - -# Organization name that should be used for unauthenticated users -org_name = Main Org. - -# Role for unauthenticated users, other valid values are `Editor` and `Admin` -org_role = Viewer -``` - -If you change your organization name in the Grafana UI, this setting needs to be updated to match the new name. - -### Basic authentication - -Basic auth is enabled by default and works with the built-in Grafana user-password authentication system and LDAP -authentication integration. - -To disable basic auth: - -```bash -[auth.basic] -enabled = false -``` - -### Disable login form - -Hide the Grafana login form using the below configuration settings. - -```bash -[auth] -disable_login_form = true -``` +## Extended authentication settings ### Enable email lookup @@ -244,6 +184,11 @@ Example for Generic OAuth: signout_redirect_url = ``` +### Remote logout + +You can log out from other devices by removing login sessions from the bottom of your profile page. If you are +a Grafana admin user, you can also do the same for any user from the Server Admin / Edit User view. + ### Protected roles {{% admonition type="note" %}} diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md new file mode 100644 index 00000000000..2d33683a7de --- /dev/null +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md @@ -0,0 +1,61 @@ +--- +aliases: + - ../../../auth/anonymous-auth/ +description: Learn how to configure anonymous access in Grafana +labels: + products: + - enterprise + - oss +menuTitle: Anonymous access +title: Configure anonymous access +weight: 250 +--- + +# Anonymous authentication + +You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. + +{{< admonition type="note" >}} +Anonymous users are charged as active users in Grafana Enterprise +{{< /admonition >}} + +## Anonymous devices + +The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. + +Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. + +- Go to **Administration -> Users** to access the anonymous devices tab. +- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. + +The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. + +To display anonymous users and devices for versions 10.2, 10.3, 10.4, you need to enable the feature toggle `displayAnonymousStats` + +```bash +[feature_toggles] +enable = displayAnonymousStats +``` + +## Configuration + +Example: + +```bash +[auth.anonymous] +enabled = true + +# Organization name that should be used for unauthenticated users +org_name = Main Org. + +# Role for unauthenticated users, other valid values are `Editor` and `Admin` +org_role = Viewer + +# Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) +hide_version = true + +# Setting this limits the number of anonymous devices in your instance. Any new anonymous devices added after the limit has been reached will be denied access. +device_limit = +``` + +If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md index 3d74d1bd6fe..c0d97fcbb16 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md @@ -1,127 +1,32 @@ --- aliases: - ../../../auth/grafana/ -description: Grafana OAuthentication Guide +description: Learn how to configure basic authentication in Grafana labels: products: - enterprise - oss menuTitle: Basic auth -title: Configure Grafana authentication +title: Configure basic authentication weight: 200 --- -## Configure Grafana authentication +# Configure basic authentication -Grafana of course has a built in user authentication system with password authentication enabled by default. You can -disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth -provider (listed above). There is also options for allowing self sign up. +Grafana provides a basic authentication system with password authentication enabled by default. This document details configuration options to manage and enhance basic authentication. -### Login and short-lived tokens +## Disable basic authentication -> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration. - -Grafana uses short-lived tokens as a mechanism for verifying authenticated users. -These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user. - -An active authenticated user that gets it token rotated will extend the `login_maximum_inactive_lifetime_duration` time from "now" that Grafana will remember the user. -This means that a user can close its browser and come back before `now + login_maximum_inactive_lifetime_duration` and still being authenticated. -This is true as long as the time since user login is less than `login_maximum_lifetime_duration`. - -#### Remote logout - -You can logout from other devices by removing login sessions from the bottom of your profile page. If you are -a Grafana admin user you can also do the same for any user from the Server Admin / Edit User view. - -## Settings - -Example: - -```bash -[auth] - -# Login cookie name -login_cookie_name = grafana_session - -# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. -login_maximum_inactive_lifetime_duration = 7d - -# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. -login_maximum_lifetime_duration = 30d - -# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. -token_rotation_interval_minutes = 10 - -# The maximum lifetime (seconds) an api key can be used. If it is set all the api keys should have limited lifetime that is lower than this value. -api_key_max_seconds_to_live = -1 -``` - -### Anonymous authentication - -You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. For more information, refer to [Anonymous authentication]({{< relref "../../configure-authentication#anonymous-authentication" >}}). - -#### Anonymous devices - -The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. - -Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. - -- Go to **Administration -> Users** to access the anonymous devices tab. -- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. - -The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. - -To display anonymous users and devices for versions 10.2, 10.3, 10.4, you need to enable the feature toggle `displayAnonymousStats` - -```bash -[feature_toggles] -enable = displayAnonymousStats -``` - -#### Anonymous users - -{{< admonition type="note" >}} -Anonymous users are charged as active users in Grafana Enterprise -{{< /admonition >}} - -#### Configuration - -Example: - -```bash -[auth.anonymous] -enabled = true - -# Organization name that should be used for unauthenticated users -org_name = Main Org. - -# Role for unauthenticated users, other valid values are `Editor` and `Admin` -org_role = Viewer - -# Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) -hide_version = true - -# Setting this limits the number of anonymous devices in your instance. Any new anonymous devices added after the limit has been reached will be denied access. -device_limit = -``` - -If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. - -### Basic authentication - -Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP -authentication integration. - -To disable basic auth: +To disable basic authentication, use the following configuration: ```bash [auth.basic] enabled = false ``` -### Strong password policy +## Password policy -By default, the password policy for all basic auth users is set to a minimum of four characters. You can enable a stronger password policy with the `password_policy` configuration option. +By default, Grafana’s password policy requires a minimum of four characters for basic auth users. For a stronger password policy, enable the `password_policy` configuration option. With the `password_policy` option enabled, new and updated passwords must meet the following criteria: @@ -137,63 +42,16 @@ password_policy = true ``` {{% admonition type="note" %}} -Existing passwords that don't comply with the new password policy will not be impacted until the user updates their password. +Existing passwords that do not comply with the new password policy will not be affected until the user updates their password. {{% /admonition %}} -### Disable login form +## Disable login form -You can hide the Grafana login form using the below configuration settings. +To hide the Grafana login form, use the following configuration setting: ```bash [auth] disable_login_form = true ``` -### Automatic OAuth login - -Set to true to attempt login with specific OAuth provider automatically, skipping the login screen. -This setting is ignored if multiple auth providers are configured to use auto login. -Defaults to `false`. - -```bash -[auth.generic_oauth] -auto_login = true -``` - -### Hide sign-out menu - -Set the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy or JWT authentication. - -```bash -[auth] -disable_signout_menu = true -``` - -### URL redirect after signing out - -The URL to redirect the user to after signing out from Grafana can be configured under `[auth]` or under a specific OAuth provider section (for example, `[auth.generic_oauth]`). The URL configured under a specific OAuth provider section takes precedence over the URL configured in `[auth]` section. This can, for example, enable signout from the OAuth provider. - -```bash -[auth.generic_oauth] -signout_redirect_url = - -[auth] -signout_redirect_url = -``` - -### Protected roles - -{{% admonition type="note" %}} -Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud]({{< relref "../../../../introduction/grafana-cloud" >}}). -{{% /admonition %}} - -By default, after you configure an authorization provider, Grafana will adopt existing users into the new authentication scheme. For example, if you have created a user with basic authentication having the login `jsmith@example.com`, then set up SAML authentication where `jsmith@example.com` is an account, the user's authentication type will be changed to SAML if they perform a SAML sign-in. - -You can disable this user adoption for certain roles using the `protected_roles` property: - -```bash -[auth.security] -protected_roles = server_admins org_admins -``` - -The value of `protected_roles` should be a list of roles to protect, separated by spaces. Valid roles are `viewers`, `editors`, `org_admins`, `server_admins`, and `all` (a superset of the other roles). +This can be helpful in setups where authentication is handled entirely through external mechanisms or single sign-on (SSO). From 20fa35f9317daca90629ee0d13daafa796753b49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:26:28 +0200 Subject: [PATCH 49/55] Update dependency eslint-plugin-jsdoc to v50.5.0 (#96385) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index fb57ff4a374..5a422a6f0be 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.9.0", "eslint-plugin-jest-dom": "^5.4.0", - "eslint-plugin-jsdoc": "50.4.3", + "eslint-plugin-jsdoc": "50.5.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-no-barrel-files": "^1.1.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index f8fda0d91a7..b305c672b5d 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -110,7 +110,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.9.0", - "eslint-plugin-jsdoc": "50.4.3", + "eslint-plugin-jsdoc": "50.5.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-react": "7.37.2", diff --git a/yarn.lock b/yarn.lock index e00f62bbe18..396a753ed24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3925,7 +3925,7 @@ __metadata: eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.9.0" - eslint-plugin-jsdoc: "npm:50.4.3" + eslint-plugin-jsdoc: "npm:50.5.0" eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-react: "npm:7.37.2" @@ -16922,9 +16922,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:50.4.3": - version: 50.4.3 - resolution: "eslint-plugin-jsdoc@npm:50.4.3" +"eslint-plugin-jsdoc@npm:50.5.0": + version: 50.5.0 + resolution: "eslint-plugin-jsdoc@npm:50.5.0" dependencies: "@es-joy/jsdoccomment": "npm:~0.49.0" are-docs-informative: "npm:^0.0.2" @@ -16939,7 +16939,7 @@ __metadata: synckit: "npm:^0.9.1" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/1950e907ba1237e0261da79f41c98ac51a4ea45e5163bb928671828868b93c2fc688448ff67d123b56f9248238ffb0d09d371a0b988320d743366826135861d4 + checksum: 10/4abe3ac34ad0d3bf34238968b6961c4ea9ec8eb058046f554cf60a702a91ac294eda8e475cadeaa1951e1a5317dd055543407787b04256fbd0728454af7e2ad0 languageName: node linkType: hard @@ -18924,7 +18924,7 @@ __metadata: eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.9.0" eslint-plugin-jest-dom: "npm:^5.4.0" - eslint-plugin-jsdoc: "npm:50.4.3" + eslint-plugin-jsdoc: "npm:50.5.0" eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-no-barrel-files: "npm:^1.1.1" From 7ecdad5a2d6d439f8934ff2209b68f785cf133c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:28:18 +0000 Subject: [PATCH 50/55] Update dependency eslint-plugin-no-barrel-files to v1.2.0 (#96387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 81 +++++++------------------------------------------------ 1 file changed, 9 insertions(+), 72 deletions(-) diff --git a/yarn.lock b/yarn.lock index 396a753ed24..7f27e129d50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10287,7 +10287,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -10753,7 +10753,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": +"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 @@ -11060,16 +11060,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/scope-manager@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - checksum: 10/fe91ac52ca8e09356a71dc1a2f2c326480f3cccfec6b2b6d9154c1a90651ab8ea270b07c67df5678956c3bbf0bbe7113ab68f68f21b20912ea528b1214197395 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:8.14.0": version: 8.14.0 resolution: "@typescript-eslint/scope-manager@npm:8.14.0" @@ -11112,13 +11102,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/types@npm:6.21.0" - checksum: 10/e26da86d6f36ca5b6ef6322619f8ec55aabcd7d43c840c977ae13ae2c964c3091fc92eb33730d8be08927c9de38466c5323e78bfb270a9ff1d3611fe821046c5 - languageName: node - linkType: hard - "@typescript-eslint/types@npm:8.14.0, @typescript-eslint/types@npm:^8.9.0": version: 8.14.0 resolution: "@typescript-eslint/types@npm:8.14.0" @@ -11151,25 +11134,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - minimatch: "npm:9.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/b32fa35fca2a229e0f5f06793e5359ff9269f63e9705e858df95d55ca2cd7fdb5b3e75b284095a992c48c5fc46a1431a1a4b6747ede2dd08929dc1cbacc589b8 - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:8.14.0": version: 8.14.0 resolution: "@typescript-eslint/typescript-estree@npm:8.14.0" @@ -11240,7 +11204,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.12.2, @typescript-eslint/utils@npm:^8.9.0": +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.12.2, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.9.0": version: 8.14.0 resolution: "@typescript-eslint/utils@npm:8.14.0" dependencies: @@ -11254,23 +11218,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.4.1": - version: 6.21.0 - resolution: "@typescript-eslint/utils@npm:6.21.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:6.21.0" - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/typescript-estree": "npm:6.21.0" - semver: "npm:^7.5.4" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - checksum: 10/b404a2c55a425a79d054346ae123087d30c7ecf7ed7abcf680c47bf70c1de4fabadc63434f3f460b2fa63df76bc9e4a0b9fa2383bb8a9fcd62733fb5c4e4f3e3 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -11281,16 +11228,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" - dependencies: - "@typescript-eslint/types": "npm:6.21.0" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/30422cdc1e2ffad203df40351a031254b272f9c6f2b7e02e9bfa39e3fc2c7b1c6130333b0057412968deda17a3a68a578a78929a8139c6acef44d9d841dc72e1 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:8.14.0": version: 8.14.0 resolution: "@typescript-eslint/visitor-keys@npm:8.14.0" @@ -16980,11 +16917,11 @@ __metadata: linkType: hard "eslint-plugin-no-barrel-files@npm:^1.1.1": - version: 1.1.1 - resolution: "eslint-plugin-no-barrel-files@npm:1.1.1" + version: 1.2.0 + resolution: "eslint-plugin-no-barrel-files@npm:1.2.0" dependencies: - "@typescript-eslint/utils": "npm:^6.4.1" - checksum: 10/26d7cee608455bf31ccf691b7fff68adf2dbdba90a3e6fd06af84e022ad214a6993af0ad74e8854dfe3c8aaa5a6994e7c3e98af0c63397cc1f972d7592fb61f9 + "@typescript-eslint/utils": "npm:^8.13.0" + checksum: 10/da54a4930ddfd7d14bd82766e857af9eea5315125a2599d5ba03326530214e0d416f78915335d6c239b19df21b989196ffc4146919eff05477cef7e34261b946 languageName: node linkType: hard @@ -17082,7 +17019,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b @@ -31339,7 +31276,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.0.1, ts-api-utils@npm:^1.3.0": +"ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" peerDependencies: From 19c04168c34d9ace5fcba9a1ea43cac6de13c173 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:30:29 +0200 Subject: [PATCH 51/55] Fix dashboards listing table appearing distorted (#96371) * fix CSS * approx height calc to account for header div --- .../features/plugins/admin/components/PluginDetailsBody.tsx | 6 +++++- .../features/plugins/admin/components/PluginDetailsPage.tsx | 1 + .../app/features/plugins/admin/components/PluginUsage.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 6bdcb492fc5..e0fc9a77e3c 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -118,7 +118,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E if (pageId === PluginTabIds.USAGE && pluginConfig) { return ( -
+
); @@ -140,6 +140,10 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E } export const getStyles = (theme: GrafanaTheme2) => ({ + wrap: css({ + width: '100%', + height: '50vh', + }), readme: css({ '& img': { maxWidth: '100%', diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index 8aca36f54fa..e11f83c2c89 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -115,6 +115,7 @@ export const getStyles = (theme: GrafanaTheme2) => { // Needed due to block formatting context tabContent: css({ paddingLeft: '5px', + width: '100%', }), }; }; diff --git a/public/app/features/plugins/admin/components/PluginUsage.tsx b/public/app/features/plugins/admin/components/PluginUsage.tsx index 5bee5f99d45..162986160fe 100644 --- a/public/app/features/plugins/admin/components/PluginUsage.tsx +++ b/public/app/features/plugins/admin/components/PluginUsage.tsx @@ -88,7 +88,7 @@ export const getStyles = (theme: GrafanaTheme2) => { return { wrap: css({ width: '100%', - height: '100%', + height: '90%', }), info: css({ paddingBottom: '30px', From 16330e41134b8351e009b99c4e8dc1a97ec968ad Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 13 Nov 2024 14:36:12 +0100 Subject: [PATCH 52/55] Loki: Adds a `suggestions` resource call that inject scopes for finding label/labelvalues (#96025) Signed-off-by: bergquist Co-authored-by: Sven Grossmann --- pkg/tsdb/loki/loki.go | 30 +++++++++++++---- pkg/tsdb/loki/scopes.go | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index a290c73ce1b..72ca44fe673 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "regexp" + "strings" "sync" "time" @@ -131,13 +132,30 @@ func callResource(ctx context.Context, req *backend.CallResourceRequest, sender api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, plog, tracer, false) - rawLokiResponse, err := api.RawQuery(ctx, lokiURL) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - plog.Error("Failed resource call from loki", "err", err, "url", lokiURL) - return err + var rawLokiResponse RawLokiResponse + var err error + + // suggestions is a resource endpoint that will return label and label value suggestions based + // on queries and the existing scope. By moving this to the backend we can use the logql parser to + // rewrite queries safely. + if req.Method == http.MethodPost && strings.EqualFold(req.Path, "suggestions") { + rawLokiResponse, err = GetSuggestions(ctx, api, req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + plog.FromContext(ctx).Error("Failed to get suggestions from loki", "err", err) + return err + } + } else { + rawLokiResponse, err = api.RawQuery(ctx, lokiURL) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + plog.Error("Failed resource call from loki", "err", err, "url", lokiURL) + return err + } } + respHeaders := map[string][]string{ "content-type": {"application/json"}, } diff --git a/pkg/tsdb/loki/scopes.go b/pkg/tsdb/loki/scopes.go index c946cbb14fc..c9f502a780b 100644 --- a/pkg/tsdb/loki/scopes.go +++ b/pkg/tsdb/loki/scopes.go @@ -1,12 +1,83 @@ package loki import ( + "context" + "encoding/json" "fmt" + "net/url" + "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery" "github.com/grafana/loki/v3/pkg/logql/syntax" + "github.com/prometheus/prometheus/promql/parser" ) +// SuggestionRequest is the request body for the GetSuggestions resource. +type SuggestionRequest struct { + // LabelName, if provided, will result in label values being returned for the given label name. + LabelName string `json:"labelName"` + + Query string `json:"query"` + + Scopes []models.ScopeFilter `json:"scopes"` + AdhocFilters []models.ScopeFilter `json:"adhocFilters"` + + // Start and End are proxied directly to the prometheus endpoint (which is rfc3339 | unix_timestamp) + Start string `json:"start"` + End string `json:"end"` +} + +// GetSuggestions returns label names or label values for the given queries and scopes. +func GetSuggestions(ctx context.Context, lokiAPI *LokiAPI, req *backend.CallResourceRequest) (RawLokiResponse, error) { + sugReq := SuggestionRequest{} + err := json.Unmarshal(req.Body, &sugReq) + if err != nil { + return RawLokiResponse{}, fmt.Errorf("error unmarshalling suggestion request: %v", err) + } + + values := url.Values{} + if sugReq.Query != "" { + // the query is used to find label/labelvalues the duration and interval does not matter. + // If the user want to filter values based on time it should used the `start` and `end` fields + interpolatedQuery := interpolateVariables(sugReq.Query, time.Minute, time.Minute, dataquery.LokiQueryTypeRange, time.Minute) + + if len(sugReq.Scopes) > 0 { + rewrittenQuery, err := ApplyScopes(interpolatedQuery, sugReq.Scopes) + if err == nil { + values.Add("query", rewrittenQuery) + } else { + values.Add("query", interpolatedQuery) + } + } + } else if len(sugReq.Scopes) > 0 { + matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters) + if err != nil { + return RawLokiResponse{}, fmt.Errorf("error converting filters to matchers: %v", err) + } + vs := parser.VectorSelector{LabelMatchers: matchers} + values.Add("query", vs.String()) + } + + if sugReq.Start != "" { + values.Add("start", sugReq.Start) + } + + if sugReq.End != "" { + values.Add("end", sugReq.End) + } + + var path string + if sugReq.LabelName != "" { + path = "/loki/api/v1/label/" + url.QueryEscape(sugReq.LabelName) + "/values?" + values.Encode() + } else { + path = "/loki/api/v1/labels?" + values.Encode() + } + + return lokiAPI.RawQuery(ctx, path) +} + // ApplyScopes applies the given scope filters to the given raw expression. func ApplyScopes(rawExpr string, scopeFilters []models.ScopeFilter) (string, error) { if len(scopeFilters) == 0 { From 7bfaf2a7bf65b2409744b0e5825b49bd9d36ad5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Wed, 13 Nov 2024 15:06:32 +0100 Subject: [PATCH 53/55] [Cloud Migrations] Fix panic in unit tests (#96389) * Fix panic in unit test * Reset mock to new one between tests --- .../cloudmigrationimpl/cloudmigration_test.go | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 70929b6e570..67554200b90 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -2,6 +2,7 @@ package cloudmigrationimpl import ( "context" + "errors" "maps" "os" "path/filepath" @@ -82,8 +83,8 @@ func Test_CreateGetAndDeleteToken(t *testing.T) { func Test_GetSnapshotStatusFromGMS(t *testing.T) { s := setUpServiceTest(t, false).(*Service) - gmsClientMock := &gmsClientMock{} - s.gmsClient = gmsClientMock + gmsClientFake := &gmsClientMock{} + s.gmsClient = gmsClientFake // Insert a session and snapshot into the database before we start createTokenResp, err := s.CreateToken(context.Background()) @@ -111,7 +112,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) assert.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) - assert.Never(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() > 0 }, time.Second, 10*time.Millisecond) + assert.Never(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() > 0 }, time.Second, 10*time.Millisecond) // Make the status pending processing and ensure GMS gets called err = s.store.UpdateSnapshot(context.Background(), cloudmigration.UpdateSnapshotCmd{ @@ -122,12 +123,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { assert.NoError(t, err) cleanupFunc := func() { - gmsClientMock.getStatusCalled = 0 - gmsClientMock.getSnapshotResponse = nil if s.cancelFunc != nil { s.cancelFunc() } + gmsClientFake = &gmsClientMock{} + s.gmsClient = gmsClientFake + err = s.store.UpdateSnapshot(context.Background(), cloudmigration.UpdateSnapshotCmd{ UID: uid, SessionID: sess.UID, @@ -151,7 +153,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { } t.Run("test case: gms snapshot initialized", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateInitialized, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -160,12 +162,12 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusPendingProcessing), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot processing", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateProcessing, } _, err := s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -174,12 +176,12 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusProcessing), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot finished", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateFinished, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -188,13 +190,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusFinished), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot canceled", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateCanceled, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -203,13 +205,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusCanceled), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot error", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateError, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -218,13 +220,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusError), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot unknown", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateUnknown, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -234,7 +236,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { assert.NoError(t, err) // snapshot status should remain unchanged - require.Eventually(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) + require.Eventually(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ SnapshotUID: uid, SessionUID: sess.UID, @@ -248,7 +250,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { t.Run("GMS results applied to local snapshot", func(t *testing.T) { t.Skip("This test is flaky and needs to be fixed") - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateFinished, Results: []cloudmigration.CloudMigrationResource{ { @@ -267,7 +269,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { SessionUID: sess.UID, }) assert.NoError(t, err) - require.Eventually(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) + require.Eventually(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ SnapshotUID: uid, @@ -941,6 +943,9 @@ func (m *gmsClientMock) GetSnapshotStatus(_ context.Context, _ cloudmigration.Cl m.mu.Lock() m.getStatusCalled++ m.mu.Unlock() + if m.getSnapshotResponse == nil { + return nil, errors.New("no response set") + } return m.getSnapshotResponse, nil } From 30b9f3e22a5926dbdbb8582f1a8b7d4194d5f4c5 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:14:27 -0500 Subject: [PATCH 54/55] Docs: Add auth entries to what's new 11.2 (#96353) --- docs/sources/whatsnew/whats-new-in-v11-2.md | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-in-v11-2.md b/docs/sources/whatsnew/whats-new-in-v11-2.md index 1fe71c25854..921739746b8 100644 --- a/docs/sources/whatsnew/whats-new-in-v11-2.md +++ b/docs/sources/whatsnew/whats-new-in-v11-2.md @@ -277,3 +277,33 @@ _Available in public preview in all editions of Grafana_ [The SSO settings API](https://grafana.com/docs/grafana//developers/http_api/sso-settings/) has been updated to include support for LDAP settings. This feature is experimental behind the feature flag `ssoSettingsLDAP`. You will soon be able to configure LDAP from the UI and Terraform. + +### Reduce number of required fields from the SAML form + + + +_Generally available in Grafana Enterprise and Grafana Cloud Pro and Advanced_ + +The private key and certificate fields are no longer mandatory in the SAML form. To configure SAML without providing a private key and a certificate you have to opt out from using signed requests. + +{{< figure src="/media/docs/grafana/screenshot-grafana-11-2-saml-sign-requests.png" alt="Sign requests in SAML config form" >}} + +### Generate SAML certificate and private key + + + +_Generally available in Grafana Enterprise and Grafana Cloud Pro_ + +You can generate a new certificate and private key for SAML directly from the UI form. Click on the **Generate key and certificate** button from the **Sign requests** tab in the SAML form and then fill in the information you want to be embedded in your generated certificate. + +{{< video-embed src="/media/docs/grafana/screen-recording-11-2-generate-saml-certificate.mp4" >}} + +### OpenID Connect Discovery URL for Generic OAuth + + + +_Generally available in all editions of Grafana_ + +The OpenID Connect Discovery URL is available in the Generic OAuth form. The info extracted from this URL will be used to populate the Auth URL, Token URL and API URL fields. + +{{< video-embed src="/media/docs/grafana/screen-recording-11-2-openid-discovery-url.mp4" >}} From 9e408f842cf4d55f2db77a97dc287e628ae0fea1 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 13 Nov 2024 11:21:44 -0300 Subject: [PATCH 55/55] Alerting: Skip sanitizing labels when sending alerts to the remote Alertmanager (#96251) * Alerting: Skip sanitizing labels when sending alerts to the remote Alertmanager * fix drone image name --- .drone.yml | 21 +++++++++------- .../blocks/mimir_backend/docker-compose.yaml | 3 ++- pkg/services/ngalert/remote/alertmanager.go | 7 +++++- .../ngalert/remote/alertmanager_test.go | 24 ++++++++++--------- pkg/services/ngalert/sender/sender.go | 24 +++++++++++++++---- scripts/drone/services/services.star | 2 +- scripts/drone/utils/images.star | 2 +- 7 files changed, 56 insertions(+), 27 deletions(-) diff --git a/.drone.yml b/.drone.yml index d3fb536d1e0..d010b7598b2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -942,8 +942,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -1383,8 +1384,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -2455,8 +2457,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -3039,8 +3042,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4978,8 +4982,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -5451,7 +5456,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r304-3872ccb + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r316-55f47f8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine @@ -5489,7 +5494,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack - trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8 - trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r304-3872ccb + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r316-55f47f8 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32 - trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine @@ -5735,6 +5740,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 250d3c2ae9ddf7cbfd2a9fed01fe81312a882ba09b8f8725f4704629da00a774 +hmac: bb28bcd274c9d2ce724db12659ed6e3dcc461f8a07ae0eb9385a64ca5daad4de ... diff --git a/devenv/docker/blocks/mimir_backend/docker-compose.yaml b/devenv/docker/blocks/mimir_backend/docker-compose.yaml index a09635e80e3..544b836e1dd 100644 --- a/devenv/docker/blocks/mimir_backend/docker-compose.yaml +++ b/devenv/docker/blocks/mimir_backend/docker-compose.yaml @@ -1,9 +1,10 @@ mimir_backend: - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 container_name: mimir_backend command: - -target=backend - -alertmanager.grafana-alertmanager-compatibility-enabled + - -alertmanager.utf8-strict-mode-enabled nginx: environment: - NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 36c6e631b67..3d209aa85b9 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -150,7 +150,12 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn Decrypt return c.Do(req.WithContext(ctx)) } senderLogger := log.New("ngalert.sender.external-alertmanager") - s, err := sender.NewExternalAlertmanagerSender(senderLogger, prometheus.NewRegistry(), sender.WithDoFunc(doFunc)) + s, err := sender.NewExternalAlertmanagerSender( + senderLogger, + prometheus.NewRegistry(), + sender.WithDoFunc(doFunc), + sender.WithUTF8Labels(), + ) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index b8cb77ef422..7ad086dc7f2 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -785,16 +785,18 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { require.Equal(t, 0, len(alertGroups)) // Let's create two active alerts and one expired one. - alert1 := genAlert(true, map[string]string{"test_1": "test_1", "empty": "", alertingModels.NamespaceUIDLabel: "test_1"}) - alert2 := genAlert(true, map[string]string{"test_2": "test_2", "empty": "", alertingModels.NamespaceUIDLabel: "test_2"}) - alert3 := genAlert(false, map[string]string{"test_3": "test_3", "empty": "", alertingModels.NamespaceUIDLabel: "test_3"}) + // UTF-8 label names should be preserved. + utf8LabelName := "test utf-8 label 😳" + alert1 := genAlert(true, map[string]string{utf8LabelName: "test_1", "empty": "", alertingModels.NamespaceUIDLabel: "test_1"}) + alert2 := genAlert(true, map[string]string{utf8LabelName: "test_2", "empty": "", alertingModels.NamespaceUIDLabel: "test_2"}) + alert3 := genAlert(false, map[string]string{utf8LabelName: "test_3", "empty": "", alertingModels.NamespaceUIDLabel: "test_3"}) postableAlerts := apimodels.PostableAlerts{ PostableAlerts: []amv2.PostableAlert{alert1, alert2, alert3}, } err = am.PutAlerts(context.Background(), postableAlerts) require.NoError(t, err) - // We should have two alerts and one group now. + // We should eventually have two active alerts. require.Eventually(t, func() bool { alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{}, "") require.NoError(t, err) @@ -806,15 +808,15 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { require.Equal(t, 1, len(alertGroups)) // Labels with empty values and the namespace UID label should be removed. + // UTF-8 label names should remain unchanged. for _, a := range alertGroups { - require.NotContains(t, a.Labels, "empty") - require.NotContains(t, a.Labels, alertingModels.NamespaceUIDLabel) + require.Len(t, a.Alerts, 2) + for _, a := range a.Alerts { + require.NotContains(t, a.Labels, "empty") + require.NotContains(t, a.Labels, alertingModels.NamespaceUIDLabel) + require.Contains(t, a.Labels, utf8LabelName) + } } - - // Filtering by `test_1=test_1` should return one alert. - alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{"test_1=test_1"}, "") - require.NoError(t, err) - require.Equal(t, 1, len(alerts)) } func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 2553e124780..8f4cd77df47 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -37,8 +37,9 @@ type ExternalAlertmanager struct { manager *Manager - sdCancel context.CancelFunc - sdManager *discovery.Manager + sanitizeLabelSetFn func(lbls models.LabelSet) labels.Labels + sdCancel context.CancelFunc + sdManager *discovery.Manager } type ExternalAMcfg struct { @@ -57,6 +58,20 @@ func WithDoFunc(doFunc doFunc) Option { } } +// WithUTF8Labels skips sanitizing labels and annotations before sending alerts to the external Alertmanager(s). +// It assumes UTF-8 label names are supported by the Alertmanager(s). +func WithUTF8Labels() Option { + return func(s *ExternalAlertmanager) { + s.sanitizeLabelSetFn = func(lbls models.LabelSet) labels.Labels { + ls := make(labels.Labels, 0, len(lbls)) + for k, v := range lbls { + ls = append(ls, labels.Label{Name: k, Value: v}) + } + return ls + } + } +} + func (cfg *ExternalAMcfg) SHA256() string { return asSHA256([]string{cfg.headerString(), cfg.URL}) } @@ -87,6 +102,7 @@ func NewExternalAlertmanagerSender(l log.Logger, reg prometheus.Registerer, opts sdCancel: sdCancel, } + s.sanitizeLabelSetFn = s.sanitizeLabelSet s.manager = NewManager( // Injecting a new registry here means these metrics are not exported. // Once we fix the individual Alertmanager metrics we should fix this scenario too. @@ -240,8 +256,8 @@ func buildNotifierConfig(alertmanagers []ExternalAMcfg) (*config.Config, map[str func (s *ExternalAlertmanager) alertToNotifierAlert(alert models.PostableAlert) *Alert { // Prometheus alertmanager has stricter rules for annotations/labels than grafana's internal alertmanager, so we sanitize invalid keys. return &Alert{ - Labels: s.sanitizeLabelSet(alert.Alert.Labels), - Annotations: s.sanitizeLabelSet(alert.Annotations), + Labels: s.sanitizeLabelSetFn(alert.Alert.Labels), + Annotations: s.sanitizeLabelSetFn(alert.Annotations), StartsAt: time.Time(alert.StartsAt), EndsAt: time.Time(alert.EndsAt), GeneratorURL: alert.Alert.GeneratorURL.String(), diff --git a/scripts/drone/services/services.star b/scripts/drone/services/services.star index 6d3044e932a..629f6016157 100644 --- a/scripts/drone/services/services.star +++ b/scripts/drone/services/services.star @@ -57,7 +57,7 @@ def integration_test_services(): "name": "mimir_backend", "image": images["mimir"], "environment": {}, - "commands": ["/bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled"], + "commands": ["/bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled -alertmanager.utf8-strict-mode-enabled"], }, { "name": "redis", diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 74d4a51ec94..1c87a607abb 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -22,7 +22,7 @@ images = { "plugins_slack": "plugins/slack", "python": "python:3.8", "postgres_alpine": "postgres:12.3-alpine", - "mimir": "grafana/mimir-alpine:r304-3872ccb", + "mimir": "grafana/mimir-alpine:r316-55f47f8", "mysql5": "mysql:5.7.39", "mysql8": "mysql:8.0.32", "redis_alpine": "redis:6.2.11-alpine",