From 7698970f22190653a0ea10d6d806c8df7a39c0a5 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 6 Jan 2026 11:30:04 -0300 Subject: [PATCH] Secrets: changes to allow a 3rd party keeper / secret references (#115156) * Secrets: changes to allow a 3rd party keeper / secret references * fix test * make gofmt * lint * fix tests * assign aws secrets manager to @grafana/grafana-operator-experience-squad * rename Keeper.Reference to Keeper.RetrieveReference * rename ModelSecretsManager to ModelAWSSecretsManager * validator: ensure that only one of keeper.Spec.Aws.AccessKey or keeper.Spec.Aws.AssumeRole are set * move secrets manager dep / go mod tidy * move secrets manager dep * keeper validator: move 3rd party secret stores validation to their own functions * add github.com/aws/aws-sdk-go-v2/service/secretsmanager pkg/extensions/enterprise_imports * make update-workspace * undo go.mod changes in /apps * make update-workspace * fix test * add github.com/aws/aws-sdk-go-v2/service/secretsmanager to enterprise_imports * make update-workspace * gcworker: handle refs * make update-workspace * create toggle: FeatureStageExperimental * allow features.IsEnabled for now * format --- apps/advisor/go.mod | 8 +- apps/advisor/go.sum | 16 +- apps/iam/go.mod | 8 +- apps/iam/go.sum | 20 +- apps/plugins/go.mod | 8 +- apps/plugins/go.sum | 16 +- apps/secret/kinds/v1beta1/keeper.cue | 15 +- .../apis/secret/v1beta1/keeper_spec_gen.go | 31 +- .../pkg/apis/secret/v1beta1/keeper_type.go | 25 +- .../pkg/apis/secret/v1beta1/zz_openapi_gen.go | 67 ++- go.mod | 9 +- go.sum | 18 +- go.work.sum | 4 + .../src/types/featureToggles.gen.ts | 4 + pkg/extensions/enterprise_imports.go | 1 + pkg/registry/apis/secret/contracts/keeper.go | 7 +- .../apis/secret/contracts/secure_value.go | 6 +- .../secret/garbagecollectionworker/worker.go | 9 +- .../garbagecollectionworker/worker_test.go | 162 +++---- .../secret/secretkeeper/sqlkeeper/keeper.go | 28 +- .../secretkeeper/sqlkeeper/keeper_test.go | 27 +- .../apis/secret/service/secure_value.go | 51 +- .../apis/secret/service/secure_value_test.go | 146 ++++++ .../apis/secret/testutils/generators.go | 96 ++++ .../apis/secret/testutils/model_gsm.go | 321 ++++++++++++ .../apis/secret/testutils/testutils.go | 165 ++++++- pkg/registry/apis/secret/validator/keeper.go | 134 +++-- .../apis/secret/validator/keeper_test.go | 66 ++- pkg/services/featuremgmt/registry.go | 8 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 + pkg/setting/setting_secrets_manager.go | 7 + pkg/storage/secret/metadata/decrypt_store.go | 8 + .../secret/metadata/decrypt_store_test.go | 62 +++ pkg/storage/secret/metadata/keeper_model.go | 50 +- pkg/storage/secret/metadata/keeper_store.go | 13 +- .../secret/metadata/keeper_store_test.go | 46 +- .../metadata/secure_value_store_test.go | 27 +- .../secret/metadata/secure_value_test.go | 457 +++++------------- 40 files changed, 1485 insertions(+), 682 deletions(-) create mode 100644 pkg/registry/apis/secret/testutils/generators.go create mode 100644 pkg/registry/apis/secret/testutils/model_gsm.go diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 314726c5ecb..8200cad9e13 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -68,14 +68,14 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 112228d6ed8..3f15ad1534c 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -173,8 +173,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= -github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= @@ -185,10 +185,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= @@ -209,8 +209,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index aed406c5434..d9e0db56519 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -106,14 +106,14 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect @@ -124,7 +124,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/benbjohnson/clock v1.3.5 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 35997e0d1ec..8ddbe4d3b9f 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -238,8 +238,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= -github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= @@ -250,10 +250,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= @@ -280,14 +280,16 @@ github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:Pwbxovp github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -2321,6 +2323,8 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 0b9ba53e76a..d2e00f4e823 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -30,14 +30,14 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 3a7e9849fad..34b430536ea 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -28,22 +28,22 @@ github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= -github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= diff --git a/apps/secret/kinds/v1beta1/keeper.cue b/apps/secret/kinds/v1beta1/keeper.cue index 36e20198ab7..4ade465d1b5 100644 --- a/apps/secret/kinds/v1beta1/keeper.cue +++ b/apps/secret/kinds/v1beta1/keeper.cue @@ -30,11 +30,22 @@ KeeperSpec: { } #AWSConfig: { - accessKeyID: #CredentialValue - secretAccessKey: #CredentialValue + region: string + accessKey?: #AWSAccessKey + assumeRole?: #AWSAssumeRole kmsKeyID?: string } +#AWSAccessKey: { + accessKeyID: #CredentialValue + secretAccessKey: #CredentialValue +} + +#AWSAssumeRole: { + assumeRoleArn: string + externalID: string +} + #AzureConfig: { keyVaultName: string tenantID: string diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go index 76d232465c8..806a3df8294 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go @@ -4,14 +4,26 @@ package v1beta1 // +k8s:openapi-gen=true type KeeperAWSConfig struct { - AccessKeyID KeeperCredentialValue `json:"accessKeyID"` - SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"` - KmsKeyID *string `json:"kmsKeyID,omitempty"` + Region string `json:"region"` + AccessKey *KeeperAWSAccessKey `json:"accessKey,omitempty"` + AssumeRole *KeeperAWSAssumeRole `json:"assumeRole,omitempty"` + KmsKeyID *string `json:"kmsKeyID,omitempty"` } // NewKeeperAWSConfig creates a new KeeperAWSConfig object. func NewKeeperAWSConfig() *KeeperAWSConfig { - return &KeeperAWSConfig{ + return &KeeperAWSConfig{} +} + +// +k8s:openapi-gen=true +type KeeperAWSAccessKey struct { + AccessKeyID KeeperCredentialValue `json:"accessKeyID"` + SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"` +} + +// NewKeeperAWSAccessKey creates a new KeeperAWSAccessKey object. +func NewKeeperAWSAccessKey() *KeeperAWSAccessKey { + return &KeeperAWSAccessKey{ AccessKeyID: *NewKeeperCredentialValue(), SecretAccessKey: *NewKeeperCredentialValue(), } @@ -36,6 +48,17 @@ func NewKeeperCredentialValue() *KeeperCredentialValue { return &KeeperCredentialValue{} } +// +k8s:openapi-gen=true +type KeeperAWSAssumeRole struct { + AssumeRoleArn string `json:"assumeRoleArn"` + ExternalID string `json:"externalID"` +} + +// NewKeeperAWSAssumeRole creates a new KeeperAWSAssumeRole object. +func NewKeeperAWSAssumeRole() *KeeperAWSAssumeRole { + return &KeeperAWSAssumeRole{} +} + // +k8s:openapi-gen=true type KeeperAzureConfig struct { KeyVaultName string `json:"keyVaultName"` diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go index 0feeac75dd1..6f61acf1a77 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go @@ -12,6 +12,7 @@ const ( AzureKeeperType KeeperType = "azure" GCPKeeperType KeeperType = "gcp" HashiCorpKeeperType KeeperType = "hashicorp" + SystemKeeperType KeeperType = "system" ) func (kt KeeperType) String() string { @@ -20,9 +21,31 @@ func (kt KeeperType) String() string { // KeeperConfig is an interface that all keeper config types must implement. type KeeperConfig interface { + // Returns the name of the keeper + GetName() string Type() KeeperType } +type NamedKeeperConfig[T interface { + Type() KeeperType +}] struct { + Name string + Cfg T +} + +func NewNamedKeeperConfig[T interface { + Type() KeeperType +}](keeperName string, cfg T) *NamedKeeperConfig[T] { + return &NamedKeeperConfig[T]{Name: keeperName, Cfg: cfg} +} + +func (c *NamedKeeperConfig[T]) GetName() string { + return c.Name +} +func (c *NamedKeeperConfig[T]) Type() KeeperType { + return c.Cfg.Type() +} + func (s *KeeperSpec) GetType() KeeperType { if s.Aws != nil { return AWSKeeperType @@ -43,7 +66,7 @@ func (s *KeeperSpec) GetType() KeeperType { type SystemKeeperConfig struct{} func (*SystemKeeperConfig) Type() KeeperType { - return "system" + return SystemKeeperType } func (s *KeeperAWSConfig) Type() KeeperType { diff --git a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go index 101e4bb55b3..4d295874224 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go @@ -14,6 +14,8 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper": schema_pkg_apis_secret_v1beta1_Keeper(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey": schema_pkg_apis_secret_v1beta1_KeeperAWSAccessKey(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole": schema_pkg_apis_secret_v1beta1_KeeperAWSAssumeRole(ref), "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig": schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref), "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig": schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref), "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue": schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref), @@ -79,7 +81,7 @@ func schema_pkg_apis_secret_v1beta1_Keeper(ref common.ReferenceCallback) common. } } -func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperAWSAccessKey(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -97,6 +99,65 @@ func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), }, }, + }, + Required: []string{"accessKeyID", "secretAccessKey"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperAWSAssumeRole(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "assumeRoleArn": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "externalID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"assumeRoleArn", "externalID"}, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "region": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "accessKey": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey"), + }, + }, + "assumeRole": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole"), + }, + }, "kmsKeyID": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -104,11 +165,11 @@ func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback }, }, }, - Required: []string{"accessKeyID", "secretAccessKey"}, + Required: []string{"region"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAccessKey", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSAssumeRole"}, } } diff --git a/go.mod b/go.mod index fa38e9ec99d..bba74f9b656 100644 --- a/go.mod +++ b/go.mod @@ -32,13 +32,14 @@ require ( github.com/apache/arrow-go/v18 v18.4.1 // @grafana/plugins-platform-backend github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources - github.com/aws/aws-sdk-go-v2 v1.39.1 // @grafana/aws-datasources + github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources - github.com/aws/smithy-go v1.23.1 // @grafana/aws-datasources + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad + github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad @@ -344,8 +345,8 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect diff --git a/go.sum b/go.sum index 7d2582cf711..32ef27a6559 100644 --- a/go.sum +++ b/go.sum @@ -850,8 +850,8 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= -github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= @@ -862,10 +862,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQU github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= @@ -892,14 +892,16 @@ github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:Pwbxovp github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= diff --git a/go.work.sum b/go.work.sum index f676971746a..f7c59731300 100644 --- a/go.work.sum +++ b/go.work.sum @@ -423,6 +423,7 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1 h1:nMp7diZObd4XEVUR0pEvn7/E13JI github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= @@ -436,8 +437,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQG github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= @@ -491,6 +494,7 @@ github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 06aa45d2275..ec009948a9b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1259,4 +1259,8 @@ export interface FeatureToggles { * Enables the ASAP smoothing transformation for time series data */ smoothingTransformation?: boolean; + /** + * Enables the creation of keepers that manage secrets stored on AWS secrets manager + */ + secretsManagementAppPlatformAwsKeeper?: boolean; } diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 472652cc103..feaf1755c94 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -46,6 +46,7 @@ import ( _ "sigs.k8s.io/randfill" _ "xorm.io/builder" + _ "github.com/aws/aws-sdk-go-v2/service/secretsmanager" _ "github.com/grafana/authlib/authn" _ "github.com/grafana/authlib/authz" _ "github.com/grafana/authlib/cache" diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index 1e8f5e2acfb..584deea009e 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -9,6 +9,11 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) +const ( + // This constant can be used as a key in resource tags + GrafanaSecretsManagerName = "grafana-secrets-manager" +) + var ( // The name used to refer to the system keeper SystemKeeperName = "system" @@ -102,8 +107,8 @@ func (s ExternalID) String() string { // Keeper is the interface for secret keepers. type Keeper interface { Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) - Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) + RetrieveReference(ctx context.Context, cfg secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error } diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index 09702fa7f5f..8c3d8988433 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -21,8 +21,10 @@ type DecryptSecureValue struct { } var ( - ErrSecureValueNotFound = errors.New("secure value not found") - ErrSecureValueAlreadyExists = errors.New("secure value already exists") + ErrSecureValueNotFound = errors.New("secure value not found") + ErrSecureValueAlreadyExists = errors.New("secure value already exists") + ErrReferenceWithSystemKeeper = errors.New("tried to create secure value using reference with system keeper, references can only be used with 3rd party keepers") + ErrSecureValueMissingSecretAndRef = errors.New("secure value spec doesn't have neither a secret or reference") ) type ReadOpts struct { diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker.go b/pkg/registry/apis/secret/garbagecollectionworker/worker.go index b967b1f21a1..2fab7562163 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker.go @@ -103,9 +103,12 @@ func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) err return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Status.Keeper, err) } - // Keeper deletion is idempotent - if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil { - return fmt.Errorf("deleting secure value from keeper: %w", err) + // If the secure value doesn't use a reference, delete the secret + if sv.Spec.Ref == nil { + // Keeper deletion is idempotent + if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil { + return fmt.Errorf("deleting secure value from keeper: %w", err) + } } // Metadata deletion is not idempotent but not found errors are ignored diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 9b27aceefc1..d8ef76b50a0 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -1,7 +1,6 @@ package garbagecollectionworker_test import ( - "slices" "testing" "time" @@ -97,27 +96,33 @@ func TestBasic(t *testing.T) { require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv)) require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv)) }) -} -var ( - decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) - nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) - namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) - anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { - return &secretv1beta1.SecureValue{ + t.Run("cleaning up secure values that use references", func(t *testing.T) { + sut := testutils.Setup(t) + + keeper, err := sut.CreateAWSKeeper(t.Context()) + require.NoError(t, err) + + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name)) + + sv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{ ObjectMeta: metav1.ObjectMeta{ - Name: nameGen.Draw(t, "name"), - Namespace: namespaceGen.Draw(t, "ns"), + Namespace: keeper.Namespace, + Name: "sv1", }, Spec: secretv1beta1.SecureValueSpec{ - Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), - Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))), - Decrypters: rapid.SliceOfDistinct(decryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), + Description: "desc1", + Ref: ptr.To("ref1"), + Decrypters: []string{"decrypter1"}, }, - Status: secretv1beta1.SecureValueStatus{}, - } + })) + require.NoError(t, err) + + _, err = sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) + require.NoError(t, err) + require.NoError(t, sut.GarbageCollectionWorker.Cleanup(t.Context(), sv)) }) -) +} func TestProperty(t *testing.T) { t.Parallel() @@ -126,26 +131,59 @@ func TestProperty(t *testing.T) { rapid.Check(t, func(t *rapid.T) { sut := testutils.Setup(tt) - model := newModel() + model := testutils.NewModelGsm(nil) t.Repeat(map[string]func(*rapid.T){ "create": func(t *rapid.T) { - sv := anySecureValueGen.Draw(t, "sv") + var sv *secretv1beta1.SecureValue + if rapid.Bool().Draw(t, "withRef") { + sv = testutils.AnySecureValueWithRefGen.Draw(t, "sv") + } else { + sv = testutils.AnySecureValueGen.Draw(t, "sv") + } + svCopy := sv.DeepCopy() createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv)) - svCopy.UID = createdSv.UID - modelErr := model.create(sut.Clock.Now(), svCopy) + if err == nil { + svCopy.UID = createdSv.UID + } + _, modelErr := model.Create(sut.Clock.Now(), svCopy) require.ErrorIs(t, err, modelErr) }, + "createKeeper": func(t *rapid.T) { + input := testutils.AnyKeeperGen.Draw(t, "keeper") + modelKeeper, modelErr := model.CreateKeeper(input) + keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), input, "actor-uid") + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelKeeper.Name, keeper.Name) + }, + "setKeeperAsActive": func(t *rapid.T) { + namespace := testutils.NamespaceGen.Draw(t, "namespace") + var keeper string + if rapid.Bool().Draw(t, "systemKeeper") { + keeper = contracts.SystemKeeperName + } else { + keeper = testutils.KeeperNameGen.Draw(t, "keeper") + } + modelErr := model.SetKeeperAsActive(namespace, keeper) + err := sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), keeper) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + }, "delete": func(t *rapid.T) { - if len(model.items) == 0 { + if len(model.SecureValues) == 0 { return } - i := rapid.IntRange(0, len(model.items)-1).Draw(t, "index") - sv := model.items[i] - modelErr := model.delete(sv.Namespace, sv.Name) + i := rapid.IntRange(0, len(model.SecureValues)-1).Draw(t, "index") + sv := model.SecureValues[i] + _, modelErr := model.Delete(sv.Namespace, sv.Name) _, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) require.ErrorIs(t, err, modelErr) }, @@ -153,7 +191,7 @@ func TestProperty(t *testing.T) { // Taken from secureValueMetadataStorage.acquireLeases minAge := 300 * time.Second maxBatchSize := sut.GarbageCollectionWorker.Cfg.SecretsManagement.GCWorkerMaxBatchSize - modelDeleted, modelErr := model.cleanupInactiveSecureValues(sut.Clock.Now(), minAge, maxBatchSize) + modelDeleted, modelErr := model.CleanupInactiveSecureValues(sut.Clock.Now(), minAge, maxBatchSize) deleted, err := sut.GarbageCollectionWorker.CleanupInactiveSecureValues(t.Context()) require.ErrorIs(t, err, modelErr) @@ -174,77 +212,3 @@ func TestProperty(t *testing.T) { }) }) } - -type model struct { - items []*modelSecureValue -} - -type modelSecureValue struct { - *secretv1beta1.SecureValue - active bool - created time.Time -} - -func newModel() *model { - return &model{ - items: make([]*modelSecureValue, 0), - } -} - -func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) error { - created := now - for _, item := range m.items { - if item.active && item.Namespace == sv.Namespace && item.Name == sv.Name { - item.active = false - created = item.created - break - } - } - m.items = append(m.items, &modelSecureValue{SecureValue: sv, active: true, created: created}) - return nil -} - -func (m *model) delete(ns string, name string) error { - for _, sv := range m.items { - if sv.active && sv.Namespace == ns && sv.Name == name { - sv.active = false - return nil - } - } - - return contracts.ErrSecureValueNotFound -} - -func (m *model) cleanupInactiveSecureValues(now time.Time, minAge time.Duration, maxBatchSize uint16) ([]*modelSecureValue, error) { - // Using a slice to allow duplicates - toDelete := make([]*modelSecureValue, 0) - - // The implementation query sorts by created time ascending - slices.SortFunc(m.items, func(a, b *modelSecureValue) int { - if a.created.Before(b.created) { - return -1 - } else if a.created.After(b.created) { - return 1 - } - return 0 - }) - - for _, sv := range m.items { - if len(toDelete) >= int(maxBatchSize) { - break - } - - if !sv.active && now.Sub(sv.created) > minAge { - toDelete = append(toDelete, sv) - } - } - - // PERF: The slices are always small - m.items = slices.DeleteFunc(m.items, func(v1 *modelSecureValue) bool { - return slices.ContainsFunc(toDelete, func(v2 *modelSecureValue) bool { - return v2.UID == v1.UID - }) - }) - - return toDelete, nil -} diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go index 294eff1a7af..952c9cd9da6 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -107,6 +107,10 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return exposedValue, nil } +func (s *SQLKeeper) RetrieveReference(ctx context.Context, cfg secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) { + return "", fmt.Errorf("reference is not implemented by the SQLKeeper") +} + func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes( attribute.String("namespace", namespace.String()), @@ -125,27 +129,3 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, return nil } - -func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error { - ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes( - attribute.String("namespace", namespace.String()), - attribute.String("name", name), - attribute.Int64("version", version), - )) - defer span.End() - - start := time.Now() - encryptedData, err := s.encryptionManager.Encrypt(ctx, namespace, []byte(exposedValueOrRef)) - if err != nil { - return fmt.Errorf("unable to encrypt value: %w", err) - } - - err = s.store.Update(ctx, namespace, name, version, encryptedData) - if err != nil { - return fmt.Errorf("failed to update encrypted value: %w", err) - } - - s.metrics.UpdateDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds()) - - return nil -} diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index 34249227790..251f2b1a9f8 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -26,7 +26,7 @@ func Test_SQLKeeperSetup(t *testing.T) { plaintext1 := "very secret string in namespace 1" plaintext2 := "very secret string in namespace 2" - keeperCfg := &secretv1beta1.SystemKeeperConfig{} + keeperCfg := secretv1beta1.NewNamedKeeperConfig("k1", &secretv1beta1.SystemKeeperConfig{}) t.Run("storing an encrypted value returns no error", func(t *testing.T) { sut := testutils.Setup(t) @@ -123,31 +123,6 @@ func Test_SQLKeeperSetup(t *testing.T) { require.NoError(t, err) }) - t.Run("updating an existent encrypted value returns no error", func(t *testing.T) { - sut := testutils.Setup(t) - - _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) - require.NoError(t, err) - - err = sut.SQLKeeper.Update(t.Context(), keeperCfg, namespace1, name1, version1, plaintext2) - require.NoError(t, err) - - exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1) - require.NoError(t, err) - assert.NotNil(t, exposedVal) - assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue()) - }) - - t.Run("updating a non existent encrypted value returns error", func(t *testing.T) { - sut := testutils.Setup(t) - - _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) - require.NoError(t, err) - - err = sut.SQLKeeper.Update(t.Context(), nil, namespace1, "non_existing_name", version1, plaintext2) - require.Error(t, err) - }) - t.Run("data key migration only runs if both secrets db migrations are enabled", func(t *testing.T) { t.Parallel() diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 30b7f4a625d..c3a01293fae 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -141,7 +141,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeper: %q %w", newSecureValue.Namespace, currentVersion.Status.Keeper, err) } - if newSecureValue.Spec.Value == nil { + if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil { keeper, err := s.keeperService.KeeperForConfig(keeperCfg) if err != nil { return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Status.Keeper, err) @@ -150,7 +150,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version) if err != nil { - return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) + return nil, false, fmt.Errorf("reading secret value from keeper: %w %w", contracts.ErrSecureValueMissingSecretAndRef, err) } newSecureValue.Spec.Value = &secret @@ -174,6 +174,10 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName st return nil, contracts.NewErrValidateSecureValue(errorList) } + if sv.Spec.Ref != nil && keeperCfg.Type() == secretv1beta1.SystemKeeperType { + return nil, contracts.ErrReferenceWithSystemKeeper + } + createdSv, err := s.secureValueMetadataStorage.Create(ctx, keeperName, sv, actorUID) if err != nil { return nil, fmt.Errorf("creating secure value: %w", err) @@ -189,18 +193,28 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName st return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, keeperName, err) } logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "type", keeperCfg.Type()) - // TODO: can we stop using external id? // TODO: store uses only the namespace and returns and id. It could be a kv instead. // TODO: check that the encrypted store works with multiple versions - externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) - if err != nil { - return nil, fmt.Errorf("storing secure value in keeper: %w", err) - } - createdSv.Status.ExternalID = string(externalID) + switch { + case sv.Spec.Value != nil: + externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + if err != nil { + return nil, fmt.Errorf("storing secure value in keeper: %w", err) + } + createdSv.Status.ExternalID = string(externalID) - if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil { - return nil, fmt.Errorf("setting secure value external id: %w", err) + if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil { + return nil, fmt.Errorf("setting secure value external id: %w", err) + } + + case sv.Spec.Ref != nil: + // No-op, there's nothing to store in the keeper since the + // secret is already stored in the 3rd party secret store + // and it's being referenced. + + default: + return nil, fmt.Errorf("secure value doesn't specify either a secret value or a reference") } if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil { @@ -366,3 +380,20 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa return sv, nil } + +func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error { + // The system keeper is not in the database, so skip checking it exists. + // TODO: should the system keeper be in the database? + if name != contracts.SystemKeeperName { + // Check keeper exists. No need to worry about time of check to time of use + // since trying to activate a just deleted keeper will result in all + // keepers being inactive and defaulting to the system keeper. + if _, err := s.keeperMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{}); err != nil { + return fmt.Errorf("reading keeper before setting as active: %w", err) + } + } + if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil { + return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/secret/service/secure_value_test.go b/pkg/registry/apis/secret/service/secure_value_test.go index 1304e5b0ce1..40458ac5e22 100644 --- a/pkg/registry/apis/secret/service/secure_value_test.go +++ b/pkg/registry/apis/secret/service/secure_value_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) @@ -93,4 +94,149 @@ func TestCrud(t *testing.T) { _, err = sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name, contracts.ReadOpts{}) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) }) + + t.Run("secret can be referenced only when the active keeper is a 3rd party keeper", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + ref := "path-to-secret" + sv := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "desc1", + Ref: &ref, + Decrypters: []string{"decrypter1"}, + }, + Status: secretv1beta1.SecureValueStatus{}, + } + + // Creating a secure value using ref with the system keeper + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv)) + require.NotNil(t, err) + require.Nil(t, createdSv) + require.Contains(t, err.Error(), "tried to create secure value using reference with system keeper, references can only be used with 3rd party keepers") + + // Create a 3rd party keeper + keeper := &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "k1", + Namespace: "ns1", + }, + Spec: secretv1beta1.KeeperSpec{ + Description: "desc", + Aws: &secretv1beta1.KeeperAWSConfig{ + Region: "us-east-1", + AssumeRole: &secretv1beta1.KeeperAWSAssumeRole{ + AssumeRoleArn: "arn", + ExternalID: "id", + }, + }, + }, + } + + // Create a 3rd party keeper + _, err = sut.KeeperMetadataStorage.Create(t.Context(), keeper, "actor-uid") + require.NoError(t, err) + + // Set the new keeper as active + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name)) + + // Create a secure value using a ref + createdSv, err = sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + require.Equal(t, keeper.Name, createdSv.Status.Keeper) + }) + + t.Run("creating secure value with reference", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + // Create a keeper because references cannot be used with the system keeper + keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "k1", + }, + Spec: secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name)) + + sv, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + require.NotNil(t, sv) + }) +} + +func Test_SetAsActive(t *testing.T) { + t.Parallel() + + t.Run("setting the system keeper as the active keeper", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + namespace := "ns" + + // Create a new keeper + keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "k1", + }, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + + // Set the new keeper as active + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name)) + keeperName, _, err := sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), namespace) + require.NoError(t, err) + require.Equal(t, keeper.Name, keeperName) + + // Set the system keeper as active + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), contracts.SystemKeeperName)) + keeperName, _, err = sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), namespace) + require.NoError(t, err) + require.Equal(t, contracts.SystemKeeperName, keeperName) + }) + + t.Run("each namespace can have one active keeper", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + k1, err := sut.CreateKeeper(t.Context(), func(ckc *testutils.CreateKeeperConfig) { + ckc.Keeper.Namespace = "ns1" + ckc.Keeper.Name = "k1" + }) + require.NoError(t, err) + k2, err := sut.CreateKeeper(t.Context(), func(ckc *testutils.CreateKeeperConfig) { + ckc.Keeper.Namespace = "ns2" + ckc.Keeper.Name = "k2" + }) + require.NoError(t, err) + + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) + + keeperName, _, err := sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k1.Namespace) + require.NoError(t, err) + require.Equal(t, k1.Name, keeperName) + + keeperName, _, err = sut.KeeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k2.Namespace) + require.NoError(t, err) + require.Equal(t, k2.Name, keeperName) + }) } diff --git a/pkg/registry/apis/secret/testutils/generators.go b/pkg/registry/apis/secret/testutils/generators.go new file mode 100644 index 00000000000..34686ccbf6e --- /dev/null +++ b/pkg/registry/apis/secret/testutils/generators.go @@ -0,0 +1,96 @@ +package testutils + +import ( + "fmt" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "pgregory.net/rapid" +) + +var ( + DecryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) + SecureValueNameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) + KeeperNameGen = rapid.SampledFrom([]string{"k1", "k2", "k3", "k4", "k5"}) + NamespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) + SecretsToRefGen = rapid.SampledFrom([]string{"ref1", "ref2", "ref3", "ref4", "ref5"}) + // Generator for secure values that specify a secret value + AnySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + return &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecureValueNameGen.Draw(t, "name"), + Namespace: NamespaceGen.Draw(t, "ns"), + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), + Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))), + Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), + }, + Status: secretv1beta1.SecureValueStatus{}, + } + }) + // Generator for secure values that reference values from 3rd party stores + AnySecureValueWithRefGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + return &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecureValueNameGen.Draw(t, "name"), + Namespace: NamespaceGen.Draw(t, "ns"), + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), + Ref: ptr.To(SecretsToRefGen.Draw(t, "ref")), + Decrypters: rapid.SliceOfDistinct(DecryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), + }, + Status: secretv1beta1.SecureValueStatus{}, + } + }) + UpdateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + sv := AnySecureValueGen.Draw(t, "sv") + // Maybe update the secret value, maybe not + if !rapid.Bool().Draw(t, "should_update_value") { + sv.Spec.Value = nil + } + return sv + }) + DecryptGen = rapid.Custom(func(t *rapid.T) DecryptInput { + return DecryptInput{ + Namespace: NamespaceGen.Draw(t, "ns"), + Name: SecureValueNameGen.Draw(t, "name"), + Decrypter: DecryptersGen.Draw(t, "decrypter"), + } + }) + AnyKeeperGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.Keeper { + spec := secretv1beta1.KeeperSpec{ + Description: rapid.String().Draw(t, "description"), + } + + keeperType := rapid.SampledFrom([]string{"isAwsKeeper", "isAzureKeeper", "isGcpKeeper", "isVaultKeeper"}).Draw(t, "keeperType") + switch keeperType { + case "isAwsKeeper": + spec.Aws = &secretv1beta1.KeeperAWSConfig{} + case "isAzureKeeper": + spec.Azure = &secretv1beta1.KeeperAzureConfig{} + case "isGcpKeeper": + spec.Gcp = &secretv1beta1.KeeperGCPConfig{} + case "isVaultKeeper": + spec.HashiCorpVault = &secretv1beta1.KeeperHashiCorpConfig{} + default: + panic(fmt.Sprintf("unhandled keeper type '%+v', did you forget a switch case?", keeperType)) + } + + return &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: KeeperNameGen.Draw(t, "name"), + Namespace: NamespaceGen.Draw(t, "ns"), + }, + Spec: spec, + } + }) +) + +type DecryptInput struct { + Namespace string + Name string + Decrypter string +} diff --git a/pkg/registry/apis/secret/testutils/model_gsm.go b/pkg/registry/apis/secret/testutils/model_gsm.go new file mode 100644 index 00000000000..d2cefde6ee0 --- /dev/null +++ b/pkg/registry/apis/secret/testutils/model_gsm.go @@ -0,0 +1,321 @@ +package testutils + +import ( + "context" + "fmt" + "slices" + "time" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/apps/secret/pkg/decrypt" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type ModelSecureValue struct { + *secretv1beta1.SecureValue + active bool + created time.Time + leaseCreated time.Time +} + +type ModelKeeper struct { + namespace string + name string + active bool + keeperType secretv1beta1.KeeperType +} + +// A simplified in memoruy model of the grafana secrets manager +type ModelGsm struct { + SecureValues []*ModelSecureValue + Keepers []*ModelKeeper + modelSecretsManager *ModelAWSSecretsManager +} + +func NewModelGsm(modelSecretsManager *ModelAWSSecretsManager) *ModelGsm { + return &ModelGsm{modelSecretsManager: modelSecretsManager} +} + +func (m *ModelGsm) getNewVersionNumber(namespace, name string) int64 { + latestVersion := int64(0) + for _, sv := range m.SecureValues { + if sv.Namespace == namespace && sv.Name == name { + latestVersion = max(latestVersion, sv.Status.Version) + } + } + return latestVersion + 1 +} + +func (m *ModelGsm) SetVersionToActive(namespace, name string, version int64) { + for _, sv := range m.SecureValues { + if sv.Namespace == namespace && sv.Name == name { + sv.active = sv.Status.Version == version + } + } +} + +func (m *ModelGsm) SetVersionToInactive(namespace, name string, version int64) { + for _, sv := range m.SecureValues { + if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version { + sv.active = false + return + } + } +} + +func (m *ModelGsm) ReadActiveVersion(namespace, name string) *ModelSecureValue { + for _, sv := range m.SecureValues { + if sv.Namespace == namespace && sv.Name == name && sv.active { + return sv + } + } + + return nil +} + +func (m *ModelGsm) Create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) { + keeper := m.getActiveKeeper(sv.Namespace) + + if sv.Spec.Ref != nil && keeper.keeperType == secretv1beta1.SystemKeeperType { + return nil, contracts.ErrReferenceWithSystemKeeper + } + + sv = sv.DeepCopy() + + // Preserve the original creation time if this secure value already exists + created := now + if sv := m.ReadActiveVersion(sv.Namespace, sv.Name); sv != nil { + created = sv.created + } + + modelSv := &ModelSecureValue{SecureValue: sv, active: false, created: created} + modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) + modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) + modelSv.Status.Keeper = keeper.name + m.SecureValues = append(m.SecureValues, modelSv) + m.SetVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *ModelGsm) getActiveKeeper(namespace string) *ModelKeeper { + for _, k := range m.Keepers { + if k.namespace == namespace && k.active { + return k + } + } + + // Default to the system keeper when there are no active keepers in the namespace + return &ModelKeeper{ + namespace: namespace, + name: contracts.SystemKeeperName, + active: true, + keeperType: secretv1beta1.SystemKeeperType, + } +} + +func (m *ModelGsm) keeperExists(namespace, name string) bool { + return m.findKeeper(namespace, name) != nil +} + +func (m *ModelGsm) findKeeper(namespace, name string) *ModelKeeper { + // The system keeper is not in the list of keepers + if name == contracts.SystemKeeperName { + return &ModelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true, keeperType: secretv1beta1.SystemKeeperType} + } + for _, k := range m.Keepers { + if k.namespace == namespace && k.name == name { + return k + } + } + return nil +} + +func (m *ModelGsm) CreateKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) { + if m.keeperExists(keeper.Namespace, keeper.Name) { + return nil, contracts.ErrKeeperAlreadyExists + } + + var keeperType secretv1beta1.KeeperType + switch { + case keeper.Spec.Aws != nil: + keeperType = secretv1beta1.AWSKeeperType + case keeper.Spec.Gcp != nil: + keeperType = secretv1beta1.GCPKeeperType + case keeper.Spec.Azure != nil: + keeperType = secretv1beta1.AzureKeeperType + case keeper.Spec.HashiCorpVault != nil: + keeperType = secretv1beta1.HashiCorpKeeperType + default: + keeperType = secretv1beta1.SystemKeeperType + } + + m.Keepers = append(m.Keepers, &ModelKeeper{namespace: keeper.Namespace, name: keeper.Name, keeperType: keeperType}) + + return keeper.DeepCopy(), nil +} + +func (m *ModelGsm) SetKeeperAsActive(namespace, keeperName string) error { + // Set every other keeper in the namespace as inactive + for _, k := range m.Keepers { + if k.namespace == namespace { + k.active = k.name == keeperName + } + } + + return nil +} + +func (m *ModelGsm) Update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) { + sv := m.ReadActiveVersion(newSecureValue.Namespace, newSecureValue.Name) + if sv == nil { + return nil, false, contracts.ErrSecureValueNotFound + } + + // If the keeper doesn't exist, return an error + if !m.keeperExists(sv.Namespace, sv.Status.Keeper) { + return nil, false, contracts.ErrKeeperNotFound + } + + // If the payload doesn't contain a value and it's not using a reference, get the value from current version + if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil { + // Tried to update a secure value without providing a new value or a ref + if sv.Spec.Value == nil { + return nil, false, contracts.ErrSecureValueMissingSecretAndRef + } + newSecureValue.Spec.Value = sv.Spec.Value + } + + createdSv, err := m.Create(now, newSecureValue) + + return createdSv, true, err +} + +func (m *ModelGsm) Delete(namespace, name string) (*secretv1beta1.SecureValue, error) { + modelSv := m.ReadActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + m.SetVersionToInactive(namespace, name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *ModelGsm) List(namespace string) (*secretv1beta1.SecureValueList, error) { + out := make([]secretv1beta1.SecureValue, 0) + + for _, v := range m.SecureValues { + if v.Namespace == namespace && v.active { + out = append(out, *v.SecureValue) + } + } + + return &secretv1beta1.SecureValueList{Items: out}, nil +} + +func (m *ModelGsm) Decrypt(ctx context.Context, decrypter, namespace, name string) (map[string]decrypt.DecryptResult, error) { + for _, v := range m.SecureValues { + if v.Namespace == namespace && + v.Name == name && + v.active { + if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { + switch { + // It's a secure value that specifies the secret + case v.Spec.Value != nil: + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value), + }, nil + + // It's a secure value that references a secret on a 3rd party store + case v.Spec.Ref != nil: + keeper := m.findKeeper(v.Namespace, v.Status.Keeper) + switch keeper.keeperType { + case secretv1beta1.AWSKeeperType: + exposedValue, err := m.modelSecretsManager.RetrieveReference(ctx, nil, *v.Spec.Ref) + if err != nil { + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultErr(fmt.Errorf("%w: %w", contracts.ErrDecryptFailed, err)), + }, nil + } + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultValue(&exposedValue), + }, nil + + // Other keepers are not implemented so we default to the system keeper + default: + // The system keeper doesn't implement Reference so decryption always fails + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultErr(contracts.ErrDecryptFailed), + }, nil + } + + default: + panic("bug: secure value where Spec.Value and Spec.Ref are nil") + } + } + + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), + }, nil + } + } + return map[string]decrypt.DecryptResult{ + name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotFound), + }, nil +} + +func (m *ModelGsm) Read(namespace, name string) (*secretv1beta1.SecureValue, error) { + modelSv := m.ReadActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + return modelSv.SecureValue, nil +} + +func (m *ModelGsm) LeaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) { + out := make([]*ModelSecureValue, 0) + + for _, sv := range m.SecureValues { + if len(out) >= int(maxBatchSize) { + break + } + if !sv.active && now.Sub(sv.created) > minAge && now.Sub(sv.leaseCreated) > leaseTTL { + sv.leaseCreated = now + out = append(out, sv) + } + } + + return out, nil +} + +func (m *ModelGsm) CleanupInactiveSecureValues(now time.Time, minAge time.Duration, maxBatchSize uint16) ([]*ModelSecureValue, error) { + // Using a slice to allow duplicates + toDelete := make([]*ModelSecureValue, 0) + + // The implementation query sorts by created time ascending + slices.SortFunc(m.SecureValues, func(a, b *ModelSecureValue) int { + if a.created.Before(b.created) { + return -1 + } else if a.created.After(b.created) { + return 1 + } + return 0 + }) + + for _, sv := range m.SecureValues { + if len(toDelete) >= int(maxBatchSize) { + break + } + + if !sv.active && now.Sub(sv.created) > minAge { + toDelete = append(toDelete, sv) + } + } + + // PERF: The slices are always small + m.SecureValues = slices.DeleteFunc(m.SecureValues, func(v1 *ModelSecureValue) bool { + return slices.ContainsFunc(toDelete, func(v2 *ModelSecureValue) bool { + return v2.UID == v1.UID + }) + }) + + return toDelete, nil +} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 37394905b79..a3b05c3505f 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -2,6 +2,7 @@ package testutils import ( "context" + "fmt" "testing" "time" @@ -143,7 +144,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage) require.NoError(t, err) - var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) + mockAwsKeeper := NewModelSecretsManager() + var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper, mockAwsKeeper) if setupCfg.KeeperService != nil { keeperService = setupCfg.KeeperService @@ -190,6 +192,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { Clock: clock, KeeperService: keeperService, KeeperMetadataStorage: keeperMetadataStorage, + ModelSecretsManager: mockAwsKeeper, } } @@ -212,6 +215,8 @@ type Sut struct { Clock *FakeClock KeeperService contracts.KeeperService KeeperMetadataStorage contracts.KeeperMetadataStorage + // A mock of AWS secrets manager that implements contracts.Keeper + ModelSecretsManager *ModelAWSSecretsManager } type CreateSvConfig struct { @@ -260,16 +265,54 @@ func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv1be return sv, err } -type keeperServiceWrapper struct { - keeper contracts.Keeper +type CreateKeeperConfig struct { + // The default keeper payload. Mutate it to change which keeper ends up being created + Keeper *secretv1beta1.Keeper } -func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper { - return &keeperServiceWrapper{keeper: keeper} +func (s *Sut) CreateAWSKeeper(ctx context.Context) (*secretv1beta1.Keeper, error) { + return s.CreateKeeper(ctx, func(cfg *CreateKeeperConfig) { + cfg.Keeper.Spec = secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{}, + } + }) +} + +func (s *Sut) CreateKeeper(ctx context.Context, opts ...func(*CreateKeeperConfig)) (*secretv1beta1.Keeper, error) { + cfg := CreateKeeperConfig{ + Keeper: &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, + } + for _, opt := range opts { + opt(&cfg) + } + + return s.KeeperMetadataStorage.Create(ctx, cfg.Keeper, "actor-uid") +} + +type keeperServiceWrapper struct { + sqlKeeper *sqlkeeper.SQLKeeper + awsKeeper *ModelAWSSecretsManager +} + +func newKeeperServiceWrapper(sqlKeeper *sqlkeeper.SQLKeeper, awsKeeper *ModelAWSSecretsManager) *keeperServiceWrapper { + return &keeperServiceWrapper{sqlKeeper: sqlKeeper, awsKeeper: awsKeeper} } func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv1beta1.KeeperConfig) (contracts.Keeper, error) { - return wrapper.keeper, nil + switch cfg.(type) { + case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAWSConfig]: + return wrapper.awsKeeper, nil + default: + return wrapper.sqlKeeper, nil + } } func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context { @@ -390,3 +433,113 @@ type NoopMigrationExecutor struct { func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) { return 0, nil } + +// A mock of AWS secrets manager, used for testing. +type ModelAWSSecretsManager struct { + secrets map[string]entry + alreadyDeleted map[string]bool +} + +type entry struct { + exposedValueOrRef string + externalID string +} + +func NewModelSecretsManager() *ModelAWSSecretsManager { + return &ModelAWSSecretsManager{ + secrets: make(map[string]entry), + alreadyDeleted: make(map[string]bool), + } +} + +func (m *ModelAWSSecretsManager) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (externalID contracts.ExternalID, err error) { + if exposedValueOrRef == "" { + return "", fmt.Errorf("failed to satisfy constraint: Member must have length greater than or equal to 1") + } + + versionID := buildVersionID(namespace, name, version) + if e, ok := m.secrets[versionID]; ok { + // Ignore duplicated requests + if e.exposedValueOrRef == exposedValueOrRef { + return contracts.ExternalID(e.externalID), nil + } + + // Tried to create a secret that already exists + return "", fmt.Errorf("ResourceExistsException: The operation failed because the secret %+v already exists", versionID) + } + + // First time creating the secret + entry := entry{ + exposedValueOrRef: exposedValueOrRef, + externalID: "external-id", + } + m.secrets[versionID] = entry + + return contracts.ExternalID(entry.externalID), nil +} + +// Used to simulate the creation of secrets in the 3rd party secret store +func (m *ModelAWSSecretsManager) Create(name, value string) { + m.secrets[name] = entry{ + exposedValueOrRef: value, + externalID: fmt.Sprintf("external_id_%+v", value), + } +} + +func (m *ModelAWSSecretsManager) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (exposedValue secretv1beta1.ExposedSecureValue, err error) { + versionID := buildVersionID(namespace, name, version) + + if m.deleted(versionID) { + return "", fmt.Errorf("InvalidRequestException: You can't perform this operation on the secret because it was marked for deletion") + } + + entry, ok := m.secrets[versionID] + if !ok { + return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret") + } + + return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil +} + +// TODO: this could be namespaced to make it more realistic +func (m *ModelAWSSecretsManager) RetrieveReference(ctx context.Context, _ secretv1beta1.KeeperConfig, ref string) (secretv1beta1.ExposedSecureValue, error) { + entry, ok := m.secrets[ref] + if !ok { + return "", fmt.Errorf("ResourceNotFoundException: Secrets Manager can't find the specified secret") + } + return secretv1beta1.ExposedSecureValue(entry.exposedValueOrRef), nil +} + +func (m *ModelAWSSecretsManager) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (err error) { + versionID := buildVersionID(namespace, name, version) + + // Deleting a secret that existed at some point is idempotent + if m.deleted(versionID) { + return nil + } + + // If the secret is being deleted for the first time + if m.exists(versionID) { + m.delete(versionID) + } + + return nil +} + +func (m *ModelAWSSecretsManager) deleted(versionID string) bool { + return m.alreadyDeleted[versionID] +} + +func (m *ModelAWSSecretsManager) exists(versionID string) bool { + _, ok := m.secrets[versionID] + return ok +} + +func (m *ModelAWSSecretsManager) delete(versionID string) { + m.alreadyDeleted[versionID] = true + delete(m.secrets, versionID) +} + +func buildVersionID(namespace xkube.Namespace, name string, version int64) string { + return fmt.Sprintf("%s/%s/%d", namespace, name, version) +} diff --git a/pkg/registry/apis/secret/validator/keeper.go b/pkg/registry/apis/secret/validator/keeper.go index b4220b45a65..0a1700a79c2 100644 --- a/pkg/registry/apis/secret/validator/keeper.go +++ b/pkg/registry/apis/secret/validator/keeper.go @@ -1,6 +1,8 @@ package validator import ( + "context" + "fmt" "strings" "k8s.io/apimachinery/pkg/util/validation" @@ -9,14 +11,17 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) -type keeperValidator struct{} +type keeperValidator struct { + features featuremgmt.FeatureToggles +} var _ contracts.KeeperValidator = &keeperValidator{} -func ProvideKeeperValidator() contracts.KeeperValidator { - return &keeperValidator{} +func ProvideKeeperValidator(features featuremgmt.FeatureToggles) contracts.KeeperValidator { + return &keeperValidator{features: features} } func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList { @@ -57,51 +62,110 @@ func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secr } if keeper.Spec.Aws != nil { - if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyID"), keeper.Spec.Aws.AccessKeyID); err != nil { - errs = append(errs, err) - } - - if err := validateCredentialValue(field.NewPath("spec", "aws", "secretAccessKey"), keeper.Spec.Aws.SecretAccessKey); err != nil { - errs = append(errs, err) + //nolint + if !v.features.IsEnabled(context.Background(), featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper) { + errs = append(errs, + field.Forbidden(field.NewPath("spec", "aws"), + fmt.Sprintf("enable aws keeper feature toggle to create aws keepers: %s", featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper))) + } else { + errs = append(errs, validateAws(keeper.Spec.Aws)...) } } if keeper.Spec.Azure != nil { - if keeper.Spec.Azure.KeyVaultName == "" { - errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required")) - } - - if keeper.Spec.Azure.TenantID == "" { - errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required")) - } - - if keeper.Spec.Azure.ClientID == "" { - errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required")) - } - - if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), keeper.Spec.Azure.ClientSecret); err != nil { - errs = append(errs, err) - } + errs = append(errs, validateAzure(keeper.Spec.Azure)...) } if keeper.Spec.Gcp != nil { - if keeper.Spec.Gcp.ProjectID == "" { - errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required")) - } - - if keeper.Spec.Gcp.CredentialsFile == "" { - errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required")) - } + errs = append(errs, validateGcp(keeper.Spec.Gcp)...) } if keeper.Spec.HashiCorpVault != nil { - if keeper.Spec.HashiCorpVault.Address == "" { - errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required")) - } + errs = append(errs, validateHashiCorpVault(keeper.Spec.HashiCorpVault)...) + } - if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), keeper.Spec.HashiCorpVault.Token); err != nil { + return errs +} + +func validateAws(cfg *secretv1beta1.KeeperAWSConfig) field.ErrorList { + errs := make(field.ErrorList, 0) + + if cfg.Region == "" { + errs = append(errs, field.Required(field.NewPath("spec", "aws", "region"), "region must be present")) + } + + switch { + case cfg.AccessKey == nil && cfg.AssumeRole == nil: + errs = append(errs, field.Required(field.NewPath("spec", "aws"), "one of `accessKey` or `assumeRole` must be present")) + + case cfg.AccessKey != nil && cfg.AssumeRole != nil: + errs = append(errs, field.Required(field.NewPath("spec", "aws"), "only one of `accessKey` or `assumeRole` can be present")) + + case cfg.AccessKey != nil: + if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKey", "accessKeyID"), cfg.AccessKey.AccessKeyID); err != nil { errs = append(errs, err) } + if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKey", "secretAccessKey"), cfg.AccessKey.SecretAccessKey); err != nil { + errs = append(errs, err) + } + + case cfg.AssumeRole != nil: + if cfg.AssumeRole.AssumeRoleArn == "" { + errs = append(errs, field.Required(field.NewPath("spec", "aws", "assumeRole", "assumeRoleArn"), "arn of the role to assume must be present")) + } + if cfg.AssumeRole.ExternalID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "aws", "assumeRole", "externalId"), "externalId must be present")) + } + } + + return errs +} + +func validateAzure(cfg *secretv1beta1.KeeperAzureConfig) field.ErrorList { + errs := make(field.ErrorList, 0) + + if cfg.KeyVaultName == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required")) + } + + if cfg.TenantID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required")) + } + + if cfg.ClientID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), cfg.ClientSecret); err != nil { + errs = append(errs, err) + } + + return errs +} + +func validateGcp(cfg *secretv1beta1.KeeperGCPConfig) field.ErrorList { + errs := make(field.ErrorList, 0) + + if cfg.ProjectID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required")) + } + + if cfg.CredentialsFile == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required")) + } + + return errs +} + +func validateHashiCorpVault(cfg *secretv1beta1.KeeperHashiCorpConfig) field.ErrorList { + errs := make(field.ErrorList, 0) + + if cfg.Address == "" { + errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), cfg.Token); err != nil { + errs = append(errs, err) } return errs diff --git a/pkg/registry/apis/secret/validator/keeper_test.go b/pkg/registry/apis/secret/validator/keeper_test.go index 99b108e480e..c26f1924733 100644 --- a/pkg/registry/apis/secret/validator/keeper_test.go +++ b/pkg/registry/apis/secret/validator/keeper_test.go @@ -10,11 +10,12 @@ import ( "k8s.io/utils/ptr" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestValidateKeeper(t *testing.T) { objectMeta := metav1.ObjectMeta{Name: "test", Namespace: "test"} - validator := ProvideKeeperValidator() + validator := ProvideKeeperValidator(featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatformAwsKeeper)) t.Run("when creating a new keeper", func(t *testing.T) { t.Run("the `description` must be present", func(t *testing.T) { @@ -22,9 +23,12 @@ func TestValidateKeeper(t *testing.T) { ObjectMeta: objectMeta, Spec: secretv1beta1.KeeperSpec{ Aws: &secretv1beta1.KeeperAWSConfig{ - AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, - SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, - KmsKeyID: ptr.To("kms-key-id"), + Region: "us-east-1", + AccessKey: &secretv1beta1.KeeperAWSAccessKey{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, + }, + KmsKeyID: ptr.To("kms-key-id"), }, }, } @@ -41,30 +45,42 @@ func TestValidateKeeper(t *testing.T) { Spec: secretv1beta1.KeeperSpec{ Description: "description", Aws: &secretv1beta1.KeeperAWSConfig{ - AccessKeyID: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "some-value", - }, - SecretAccessKey: secretv1beta1.KeeperCredentialValue{ - SecureValueName: "some-value", + Region: "us-east-1", + AccessKey: &secretv1beta1.KeeperAWSAccessKey{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "some-value", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + SecureValueName: "some-value", + }, }, KmsKeyID: ptr.To("optional"), }, }, } + t.Run("aws keeper feature flag must be enabled", func(t *testing.T) { + // Validator with feature disabled + validator := ProvideKeeperValidator(featuremgmt.WithFeatures()) + errs := validator.Validate(validKeeperAWS.DeepCopy(), nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws", errs[0].Field) + require.Contains(t, errs[0].Detail, "secretsManagementAppPlatformAwsKeeper") + }) + t.Run("`accessKeyID` must be present", func(t *testing.T) { t.Run("at least one of the credential value must be present", func(t *testing.T) { keeper := validKeeperAWS.DeepCopy() - keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{} + keeper.Spec.Aws.AccessKey.AccessKeyID = secretv1beta1.KeeperCredentialValue{} errs := validator.Validate(keeper, nil, admission.Create) require.Len(t, errs, 1) - require.Equal(t, "spec.aws.accessKeyID", errs[0].Field) + require.Equal(t, "spec.aws.accessKey.accessKeyID", errs[0].Field) }) t.Run("at most one of the credential value must be present", func(t *testing.T) { keeper := validKeeperAWS.DeepCopy() - keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{ + keeper.Spec.Aws.AccessKey.AccessKeyID = secretv1beta1.KeeperCredentialValue{ SecureValueName: "a", ValueFromEnv: "b", ValueFromConfig: "c", @@ -72,23 +88,23 @@ func TestValidateKeeper(t *testing.T) { errs := validator.Validate(keeper, nil, admission.Create) require.Len(t, errs, 1) - require.Equal(t, "spec.aws.accessKeyID", errs[0].Field) + require.Equal(t, "spec.aws.accessKey.accessKeyID", errs[0].Field) }) }) t.Run("`secretAccessKey` must be present", func(t *testing.T) { t.Run("at least one of the credential value must be present", func(t *testing.T) { keeper := validKeeperAWS.DeepCopy() - keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{} + keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{} errs := validator.Validate(keeper, nil, admission.Create) require.Len(t, errs, 1) - require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + require.Equal(t, "spec.aws.accessKey.secretAccessKey", errs[0].Field) }) t.Run("at most one of the credential value must be present", func(t *testing.T) { keeper := validKeeperAWS.DeepCopy() - keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{ + keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{ SecureValueName: "a", ValueFromEnv: "b", ValueFromConfig: "c", @@ -96,7 +112,23 @@ func TestValidateKeeper(t *testing.T) { errs := validator.Validate(keeper, nil, admission.Create) require.Len(t, errs, 1) - require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + require.Equal(t, "spec.aws.accessKey.secretAccessKey", errs[0].Field) + }) + + t.Run("only one of accessKey or assumeRole can be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.Aws.AccessKey.SecretAccessKey = secretv1beta1.KeeperCredentialValue{ + SecureValueName: "a", + } + keeper.Spec.Aws.AssumeRole = &secretv1beta1.KeeperAWSAssumeRole{ + AssumeRoleArn: "arn", + ExternalID: "id", + } + + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws", errs[0].Field) + require.Equal(t, "only one of `accessKey` or `assumeRole` can be present", errs[0].Detail) }) }) }) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2933551d1ad..d325597a04b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2082,6 +2082,14 @@ var ( FrontendOnly: true, Owner: grafanaDataProSquad, }, + { + Name: "secretsManagementAppPlatformAwsKeeper", + Description: "Enables the creation of keepers that manage secrets stored on AWS secrets manager", + Stage: FeatureStageExperimental, + HideFromDocs: true, + FrontendOnly: false, + Owner: grafanaOperatorExperienceSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index e2d15a8466b..a5041e1a42d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -282,3 +282,4 @@ kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,fals useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true smoothingTransformation,experimental,@grafana/datapro,false,false,true +secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5de71954e2f..5ef42eb6543 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -781,4 +781,8 @@ const ( // FlagKubernetesAlertingHistorian // Adds support for Kubernetes alerting historian APIs FlagKubernetesAlertingHistorian = "kubernetesAlertingHistorian" + + // FlagSecretsManagementAppPlatformAwsKeeper + // Enables the creation of keepers that manage secrets stored on AWS secrets manager + FlagSecretsManagementAppPlatformAwsKeeper = "secretsManagementAppPlatformAwsKeeper" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 66910dc9d1c..6831c7045b3 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3254,6 +3254,22 @@ "codeowner": "@grafana/grafana-operator-experience-squad" } }, + { + "metadata": { + "name": "secretsManagementAppPlatformAwsKeeper", + "resourceVersion": "1767706420889", + "creationTimestamp": "2026-01-06T12:55:50Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-06 13:33:40.889447 +0000 UTC" + } + }, + "spec": { + "description": "Enables the creation of keepers that manage secrets stored on AWS secrets manager", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad", + "hideFromDocs": true + } + }, { "metadata": { "name": "secretsManagementAppPlatformUI", diff --git a/pkg/setting/setting_secrets_manager.go b/pkg/setting/setting_secrets_manager.go index 260b98264d8..5730d27a74f 100644 --- a/pkg/setting/setting_secrets_manager.go +++ b/pkg/setting/setting_secrets_manager.go @@ -40,6 +40,10 @@ type SecretsManagerSettings struct { RunSecretsDBMigrations bool // Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true. RunDataKeyMigration bool + + // AWS Keeper + AWSKeeperAccessKeyID string + AWSKeeperSecretAccessKey string } func (cfg *Cfg) readSecretsManagerSettings() { @@ -63,6 +67,9 @@ func (cfg *Cfg) readSecretsManagerSettings() { cfg.SecretsManagement.RunSecretsDBMigrations = secretsMgmt.Key("run_secrets_db_migrations").MustBool(true) cfg.SecretsManagement.RunDataKeyMigration = secretsMgmt.Key("run_data_key_migration").MustBool(true) + cfg.SecretsManagement.AWSKeeperAccessKeyID = secretsMgmt.Key("aws_access_key_id").MustString("") + cfg.SecretsManagement.AWSKeeperSecretAccessKey = secretsMgmt.Key("aws_secret_access_key").MustString("") + // Extract available KMS providers from configuration sections providers := make(map[string]map[string]string) for _, section := range cfg.Raw.Sections() { diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index 4c0ebabb5e8..0896ab89df4 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -145,6 +145,14 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed) } + if sv.Spec.Ref != nil { + exposedValue, err := keeper.RetrieveReference(ctx, keeperConfig, *sv.Spec.Ref) + if err != nil { + return "", fmt.Errorf("failed to expose secret using reference: %v (%w)", err, contracts.ErrDecryptFailed) + } + return exposedValue, nil + } + exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version) if err != nil { return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed) diff --git a/pkg/storage/secret/metadata/decrypt_store_test.go b/pkg/storage/secret/metadata/decrypt_store_test.go index c07b65dbf04..36e49df0146 100644 --- a/pkg/storage/secret/metadata/decrypt_store_test.go +++ b/pkg/storage/secret/metadata/decrypt_store_test.go @@ -9,12 +9,14 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/stretchr/testify/require" grpcmetadata "google.golang.org/grpc/metadata" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -324,6 +326,66 @@ func TestIntegrationDecrypt(t *testing.T) { } } }) + + t.Run("happy path, referencing a secret in a 3rd party store", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + tokenSvcIdentity := "svc" + stSvcIdentity := "st-svc" + + // Create auth context with proper permissions that match the decrypters + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, tokenSvcIdentity, types.TypeUser) + + // Needs to be incoming because we are pretending we received the metadata from a gRPC request + ctx = grpcmetadata.NewIncomingContext(authCtx, grpcmetadata.New(map[string]string{ + contracts.HeaderGrafanaServiceIdentityName: stSvcIdentity, + })) + + // Setup service + sut := testutils.Setup(t) + + // Create a secret on the 3rd party secret store + sut.ModelSecretsManager.Create("ref1", "value") + + // Create a 3rd party keeper + keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Name: "k1", + }, + Spec: secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(keeper.Namespace), keeper.Name)) + + // Create a secure value + sv := &secretv1beta1.SecureValue{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Name: "sv-test", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{tokenSvcIdentity}, + Ref: ptr.To("ref1"), + }} + + _, err = sut.CreateSv(ctx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + fakeLogger := &mockLogger{} + + loggerCtx := logging.Context(ctx, fakeLogger) + + exposed, err := sut.DecryptStorage.Decrypt(loggerCtx, "default", "sv-test") + require.NoError(t, err) + require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) + }) } func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context { diff --git a/pkg/storage/secret/metadata/keeper_model.go b/pkg/storage/secret/metadata/keeper_model.go index 4831ee25a2e..c97f6adf679 100644 --- a/pkg/storage/secret/metadata/keeper_model.go +++ b/pkg/storage/secret/metadata/keeper_model.go @@ -59,16 +59,16 @@ func (kp *keeperDB) toKubernetes() (*secretv1beta1.Keeper, error) { } // Obtain provider configs - provider := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload) + provider := parseKeeperConfigJson(kp.Name, secretv1beta1.KeeperType(kp.Type), kp.Payload) switch v := provider.(type) { - case *secretv1beta1.KeeperAWSConfig: - resource.Spec.Aws = v - case *secretv1beta1.KeeperAzureConfig: - resource.Spec.Azure = v - case *secretv1beta1.KeeperGCPConfig: - resource.Spec.Gcp = v - case *secretv1beta1.KeeperHashiCorpConfig: - resource.Spec.HashiCorpVault = v + case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAWSConfig]: + resource.Spec.Aws = v.Cfg + case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperAzureConfig]: + resource.Spec.Azure = v.Cfg + case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperGCPConfig]: + resource.Spec.Gcp = v.Cfg + case *secretv1beta1.NamedKeeperConfig[*secretv1beta1.KeeperHashiCorpConfig]: + resource.Spec.HashiCorpVault = v.Cfg } // Set all meta fields here for consistency. @@ -214,34 +214,34 @@ func toTypeAndPayload(kp *secretv1beta1.Keeper) (secretv1beta1.KeeperType, strin return "", "", fmt.Errorf("no keeper type found") } -// toProvider maps a KeeperType and payload into a provider config struct. +// parseKeeperConfigJson maps a KeeperType and payload into a provider config struct. // TODO: Move as method of KeeperType -func toProvider(keeperType secretv1beta1.KeeperType, payload string) secretv1beta1.KeeperConfig { +func parseKeeperConfigJson(keeperName string, keeperType secretv1beta1.KeeperType, payload string) secretv1beta1.KeeperConfig { switch keeperType { case secretv1beta1.AWSKeeperType: aws := &secretv1beta1.KeeperAWSConfig{} if err := json.Unmarshal([]byte(payload), aws); err != nil { return nil } - return aws + return secretv1beta1.NewNamedKeeperConfig(keeperName, aws) case secretv1beta1.AzureKeeperType: azure := &secretv1beta1.KeeperAzureConfig{} if err := json.Unmarshal([]byte(payload), azure); err != nil { return nil } - return azure + return secretv1beta1.NewNamedKeeperConfig(keeperName, azure) case secretv1beta1.GCPKeeperType: gcp := &secretv1beta1.KeeperGCPConfig{} if err := json.Unmarshal([]byte(payload), gcp); err != nil { return nil } - return gcp + return secretv1beta1.NewNamedKeeperConfig(keeperName, gcp) case secretv1beta1.HashiCorpKeeperType: hashicorp := &secretv1beta1.KeeperHashiCorpConfig{} if err := json.Unmarshal([]byte(payload), hashicorp); err != nil { return nil } - return hashicorp + return secretv1beta1.NewNamedKeeperConfig(keeperName, hashicorp) default: return nil } @@ -253,12 +253,16 @@ func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} { case kp.Spec.Aws != nil: secureValues := make(map[string]struct{}, 0) - if kp.Spec.Aws.AccessKeyID.SecureValueName != "" { - secureValues[kp.Spec.Aws.AccessKeyID.SecureValueName] = struct{}{} + if kp.Spec.Aws.AccessKey == nil { + return secureValues } - if kp.Spec.Aws.SecretAccessKey.SecureValueName != "" { - secureValues[kp.Spec.Aws.SecretAccessKey.SecureValueName] = struct{}{} + if kp.Spec.Aws.AccessKey.AccessKeyID.SecureValueName != "" { + secureValues[kp.Spec.Aws.AccessKey.AccessKeyID.SecureValueName] = struct{}{} + } + + if kp.Spec.Aws.AccessKey.SecretAccessKey.SecureValueName != "" { + secureValues[kp.Spec.Aws.AccessKey.SecretAccessKey.SecureValueName] = struct{}{} } return secureValues @@ -284,13 +288,13 @@ func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} { func getKeeperConfig(keeper *secretv1beta1.Keeper) secretv1beta1.KeeperConfig { switch keeper.Spec.GetType() { case secretv1beta1.AWSKeeperType: - return keeper.Spec.Aws + return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Aws) case secretv1beta1.AzureKeeperType: - return keeper.Spec.Azure + return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Azure) case secretv1beta1.GCPKeeperType: - return keeper.Spec.Gcp + return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.Gcp) case secretv1beta1.HashiCorpKeeperType: - return keeper.Spec.HashiCorpVault + return secretv1beta1.NewNamedKeeperConfig(keeper.Name, keeper.Spec.HashiCorpVault) default: return nil } diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go index d4516158c81..fe57816444d 100644 --- a/pkg/storage/secret/metadata/keeper_store.go +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -609,7 +609,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s // Check if keeper is the systemwide one. if name == contracts.SystemKeeperName { - return &secretv1beta1.SystemKeeperConfig{}, nil + return secretv1beta1.NewNamedKeeperConfig(contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}), nil } // Load keeper config from metadata store, or TODO: keeper cache. @@ -618,7 +618,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s return nil, err } - keeperConfig := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload) + keeperConfig := parseKeeperConfigJson(kp.Name, secretv1beta1.KeeperType(kp.Type), kp.Payload) // TODO: this would be a good place to check if credentials are secure values and load them. return keeperConfig, nil @@ -636,13 +636,6 @@ func (s *keeperMetadataStorage) SetAsActive(ctx context.Context, namespace xkube return fmt.Errorf("template %q: %w", sqlKeeperSetAsActive.Name(), err) } - // Check keeper exists. No need to worry about time of check to time of use - // since trying to activate a just deleted keeper will result in all - // keepers being inactive and defaulting to the system keeper. - if _, err := s.read(ctx, namespace.String(), name, contracts.ReadOpts{}); err != nil { - return fmt.Errorf("reading keeper before setting as active: %w", err) - } - _, err = s.db.ExecContext(ctx, query, req.GetArgs()...) if err != nil { return fmt.Errorf("setting keeper as active %q: %w", query, err) @@ -726,7 +719,7 @@ func (s *keeperMetadataStorage) GetActiveKeeperConfig(ctx context.Context, names if err != nil { // When there are not active keepers, default to the system keeper if errors.Is(err, contracts.ErrKeeperNotFound) { - return contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}, nil + return contracts.SystemKeeperName, secretv1beta1.NewNamedKeeperConfig(contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}), nil } return "", nil, fmt.Errorf("fetching active keeper from db: %w", err) } diff --git a/pkg/storage/secret/metadata/keeper_store_test.go b/pkg/storage/secret/metadata/keeper_store_test.go index a38e0ec85b3..fe7b3cb9eaf 100644 --- a/pkg/storage/secret/metadata/keeper_store_test.go +++ b/pkg/storage/secret/metadata/keeper_store_test.go @@ -43,7 +43,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // get system keeper config keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, contracts.SystemKeeperName, contracts.ReadOpts{}) require.NoError(t, err) - require.IsType(t, &secretv1beta1.SystemKeeperConfig{}, keeperConfig) + require.IsType(t, &secretv1beta1.NamedKeeperConfig[*secretv1beta1.SystemKeeperConfig]{}, keeperConfig) }) t.Run("get test keeper config", func(t *testing.T) { @@ -188,11 +188,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { Spec: secretv1beta1.KeeperSpec{ Description: "initial description", Aws: &secretv1beta1.KeeperAWSConfig{ - AccessKeyID: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID_1", - }, - SecretAccessKey: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1", + AccessKey: &secretv1beta1.KeeperAWSAccessKey{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_1", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1", + }, }, KmsKeyID: ptr.To("kms-key-id-1"), }, @@ -208,8 +210,8 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // Verify initial AWS config keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) require.NoError(t, err) - require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.Aws.AccessKeyID.ValueFromEnv) - require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.Aws.SecretAccessKey.ValueFromEnv) + require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.Aws.AccessKey.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.Aws.AccessKey.SecretAccessKey.ValueFromEnv) require.Equal(t, "kms-key-id-1", *keeper.Spec.Aws.KmsKeyID) // Update with new AWS config @@ -217,11 +219,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { Spec: secretv1beta1.KeeperSpec{ Description: "updated description", Aws: &secretv1beta1.KeeperAWSConfig{ - AccessKeyID: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID_2", - }, - SecretAccessKey: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2", + AccessKey: &secretv1beta1.KeeperAWSAccessKey{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_2", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2", + }, }, KmsKeyID: ptr.To("kms-key-id-2"), }, @@ -237,8 +241,8 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // Verify updated AWS config updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) require.NoError(t, err) - require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.Aws.AccessKeyID.ValueFromEnv) - require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.Aws.SecretAccessKey.ValueFromEnv) + require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.Aws.AccessKey.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.Aws.AccessKey.SecretAccessKey.ValueFromEnv) require.Equal(t, "kms-key-id-2", *updatedKeeper.Spec.Aws.KmsKeyID) }) @@ -278,11 +282,13 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { Spec: secretv1beta1.KeeperSpec{ Description: "initial description", Aws: &secretv1beta1.KeeperAWSConfig{ - AccessKeyID: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID", - }, - SecretAccessKey: secretv1beta1.KeeperCredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY", + AccessKey: &secretv1beta1.KeeperAWSAccessKey{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY", + }, }, }, }, diff --git a/pkg/storage/secret/metadata/secure_value_store_test.go b/pkg/storage/secret/metadata/secure_value_store_test.go index f7551e9e484..9f3a99a2372 100644 --- a/pkg/storage/secret/metadata/secure_value_store_test.go +++ b/pkg/storage/secret/metadata/secure_value_store_test.go @@ -194,12 +194,12 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) { rapid.Check(t, func(t *rapid.T) { sut := testutils.Setup(tt) - model := newModel() + model := testutils.NewModelGsm(nil) t.Repeat(map[string]func(*rapid.T){ "create": func(t *rapid.T) { - sv := anySecureValueGen.Draw(t, "sv") - modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy()) + sv := testutils.AnySecureValueGen.Draw(t, "sv") + modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy()) createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -209,10 +209,23 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) { require.Equal(t, modelCreatedSv.Name, createdSv.Name) require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) }, + "read": func(t *rapid.T) { + ns := testutils.NamespaceGen.Draw(t, "ns") + name := testutils.SecureValueNameGen.Draw(t, "name") + modelSv, modelErr := model.Read(ns, name) + sv, err := sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(ns), name, contracts.ReadOpts{}) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, sv.Namespace) + require.Equal(t, modelSv.Name, sv.Name) + require.Equal(t, modelSv.Status.Version, sv.Status.Version) + }, "delete": func(t *rapid.T) { - ns := namespaceGen.Draw(t, "ns") - name := secureValueNameGen.Draw(t, "name") - modelSv, modelErr := model.delete(ns, name) + ns := testutils.NamespaceGen.Draw(t, "ns") + name := testutils.SecureValueNameGen.Draw(t, "name") + modelSv, modelErr := model.Delete(ns, name) sv, err := sut.DeleteSv(t.Context(), ns, name) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -227,7 +240,7 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) { minAge := 300 * time.Second leaseTTL := 30 * time.Second maxBatchSize := rapid.Uint16Range(1, 10).Draw(t, "maxBatchSize") - modelSvs, modelErr := model.leaseInactiveSecureValues(sut.Clock.Now(), minAge, leaseTTL, maxBatchSize) + modelSvs, modelErr := model.LeaseInactiveSecureValues(sut.Clock.Now(), minAge, leaseTTL, maxBatchSize) svs, err := sut.SecureValueMetadataStorage.LeaseInactiveSecureValues(t.Context(), maxBatchSize) require.ErrorIs(t, err, modelErr) require.Equal(t, len(modelSvs), len(svs)) diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index dafd96fc3ad..e12fa9578ab 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -1,7 +1,6 @@ package metadata_test import ( - "fmt" "slices" "testing" "time" @@ -12,305 +11,11 @@ import ( "pgregory.net/rapid" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" - "github.com/grafana/grafana/apps/secret/pkg/decrypt" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) -type modelSecureValue struct { - *secretv1beta1.SecureValue - active bool - created time.Time - leaseCreated time.Time -} - -type modelKeeper struct { - namespace string - name string - active bool -} - -// A simplified model of the grafana secrets manager -type model struct { - secureValues []*modelSecureValue - keepers []*modelKeeper -} - -func newModel() *model { - return &model{} -} - -func (m *model) getNewVersionNumber(namespace, name string) int64 { - latestVersion := int64(0) - for _, sv := range m.secureValues { - if sv.Namespace == namespace && sv.Name == name { - latestVersion = max(latestVersion, sv.Status.Version) - } - } - return latestVersion + 1 -} - -func (m *model) setVersionToActive(namespace, name string, version int64) { - for _, sv := range m.secureValues { - if sv.Namespace == namespace && sv.Name == name { - sv.active = sv.Status.Version == version - } - } -} - -func (m *model) setVersionToInactive(namespace, name string, version int64) { - for _, sv := range m.secureValues { - if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version { - sv.active = false - return - } - } -} - -func (m *model) readActiveVersion(namespace, name string) *modelSecureValue { - for _, sv := range m.secureValues { - if sv.Namespace == namespace && sv.Name == name && sv.active { - return sv - } - } - - return nil -} - -func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) { - keeper := m.getActiveKeeper(sv.Namespace) - sv = sv.DeepCopy() - - // Preserve the original creation time if this secure value already exists - created := now - if sv := m.readActiveVersion(sv.Namespace, sv.Name); sv != nil { - created = sv.created - } - - modelSv := &modelSecureValue{SecureValue: sv, active: false, created: created} - modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) - modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) - modelSv.Status.Keeper = keeper.name - m.secureValues = append(m.secureValues, modelSv) - m.setVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) - return modelSv.SecureValue, nil -} - -func (m *model) getActiveKeeper(namespace string) *modelKeeper { - for _, k := range m.keepers { - if k.namespace == namespace && k.active { - return k - } - } - - // Default to the system keeper when there are no active keepers in the namespace - return &modelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true} -} - -func (m *model) keeperExists(namespace, name string) bool { - return m.findKeeper(namespace, name) != nil -} - -func (m *model) findKeeper(namespace, name string) *modelKeeper { - // The system keeper is not in the list of keepers - if name == contracts.SystemKeeperName { - return &modelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true} - } - for _, k := range m.keepers { - if k.namespace == namespace && k.name == name { - return k - } - } - return nil -} - -func (m *model) createKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) { - if m.keeperExists(keeper.Namespace, keeper.Name) { - return nil, contracts.ErrKeeperAlreadyExists - } - - m.keepers = append(m.keepers, &modelKeeper{namespace: keeper.Namespace, name: keeper.Name}) - - return keeper.DeepCopy(), nil -} - -func (m *model) setKeeperAsActive(namespace, keeperName string) error { - keeper := m.findKeeper(namespace, keeperName) - if keeper == nil { - return contracts.ErrKeeperNotFound - } - // Set the keeper as active - keeper.active = true - - // Set every other keeper in the namespace as inactive - for _, k := range m.keepers { - if k.namespace == namespace && k.name != keeperName { - k.active = false - } - } - - return nil -} - -func (m *model) update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) { - sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) - if sv == nil { - return nil, false, contracts.ErrSecureValueNotFound - } - - // If the keeper doesn't exist, return an error - if !m.keeperExists(sv.Namespace, sv.Status.Keeper) { - return nil, false, contracts.ErrKeeperNotFound - } - - // If the payload doesn't contain a value, get the value from current version - if newSecureValue.Spec.Value == nil { - newSecureValue.Spec.Value = sv.Spec.Value - } - - createdSv, err := m.create(now, newSecureValue) - - return createdSv, true, err -} - -func (m *model) delete(namespace, name string) (*secretv1beta1.SecureValue, error) { - modelSv := m.readActiveVersion(namespace, name) - if modelSv == nil { - return nil, contracts.ErrSecureValueNotFound - } - m.setVersionToInactive(namespace, name, modelSv.Status.Version) - return modelSv.SecureValue, nil -} - -func (m *model) list(namespace string) (*secretv1beta1.SecureValueList, error) { - out := make([]secretv1beta1.SecureValue, 0) - - for _, v := range m.secureValues { - if v.Namespace == namespace && v.active { - out = append(out, *v.SecureValue) - } - } - - return &secretv1beta1.SecureValueList{Items: out}, nil -} - -func (m *model) decrypt(decrypter, namespace, name string) (map[string]decrypt.DecryptResult, error) { - for _, v := range m.secureValues { - if v.Namespace == namespace && - v.Name == name && - v.active { - if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { - return map[string]decrypt.DecryptResult{ - name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value), - }, nil - } - - return map[string]decrypt.DecryptResult{ - name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), - }, nil - } - } - return map[string]decrypt.DecryptResult{ - name: decrypt.NewDecryptResultErr(contracts.ErrDecryptNotFound), - }, nil -} - -func (m *model) read(namespace, name string) (*secretv1beta1.SecureValue, error) { - modelSv := m.readActiveVersion(namespace, name) - if modelSv == nil { - return nil, contracts.ErrSecureValueNotFound - } - return modelSv.SecureValue, nil -} - -func (m *model) leaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.Duration, maxBatchSize uint16) ([]*modelSecureValue, error) { - out := make([]*modelSecureValue, 0) - - for _, sv := range m.secureValues { - if len(out) >= int(maxBatchSize) { - break - } - if !sv.active && now.Sub(sv.created) > minAge && now.Sub(sv.leaseCreated) > leaseTTL { - sv.leaseCreated = now - out = append(out, sv) - } - } - - return out, nil -} - -var ( - decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) - secureValueNameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) - keeperNameGen = rapid.SampledFrom([]string{"k1", "k2", "k3", "k4", "k5"}) - namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) - anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { - return &secretv1beta1.SecureValue{ - ObjectMeta: metav1.ObjectMeta{ - Name: secureValueNameGen.Draw(t, "name"), - Namespace: namespaceGen.Draw(t, "ns"), - }, - Spec: secretv1beta1.SecureValueSpec{ - Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), - Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))), - Decrypters: rapid.SliceOfDistinct(decryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), - }, - Status: secretv1beta1.SecureValueStatus{}, - } - }) - updateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { - sv := anySecureValueGen.Draw(t, "sv") - // Maybe update the secret value, maybe not - if !rapid.Bool().Draw(t, "should_update_value") { - sv.Spec.Value = nil - } - return sv - }) - // Any secure value will do - deleteSecureValueGen = anySecureValueGen - decryptGen = rapid.Custom(func(t *rapid.T) decryptInput { - return decryptInput{ - namespace: namespaceGen.Draw(t, "ns"), - name: secureValueNameGen.Draw(t, "name"), - decrypter: decryptersGen.Draw(t, "decrypter"), - } - }) - anyKeeperGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.Keeper { - spec := secretv1beta1.KeeperSpec{ - Description: rapid.String().Draw(t, "description"), - } - - keeperType := rapid.SampledFrom([]string{"isAwsKeeper", "isAzureKeeper", "isGcpKeeper", "isVaultKeeper"}).Draw(t, "keeperType") - switch keeperType { - case "isAwsKeeper": - spec.Aws = &secretv1beta1.KeeperAWSConfig{} - case "isAzureKeeper": - spec.Azure = &secretv1beta1.KeeperAzureConfig{} - case "isGcpKeeper": - spec.Gcp = &secretv1beta1.KeeperGCPConfig{} - case "isVaultKeeper": - spec.HashiCorpVault = &secretv1beta1.KeeperHashiCorpConfig{} - default: - panic(fmt.Sprintf("unhandled keeper type '%+v', did you forget a switch case?", keeperType)) - } - - return &secretv1beta1.Keeper{ - ObjectMeta: metav1.ObjectMeta{ - Name: keeperNameGen.Draw(t, "name"), - Namespace: namespaceGen.Draw(t, "ns"), - }, - Spec: spec, - } - }) -) - -type decryptInput struct { - namespace string - name string - decrypter string -} - func TestModel(t *testing.T) { t.Parallel() @@ -330,18 +35,18 @@ func TestModel(t *testing.T) { t.Run("creating secure values", func(t *testing.T) { t.Parallel() - m := newModel() + m := testutils.NewModelGsm(nil) now := time.Now() // Create a secure value - sv1, err := m.create(now, sv.DeepCopy()) + sv1, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv1.Namespace) require.Equal(t, sv.Name, sv1.Name) require.EqualValues(t, 1, sv1.Status.Version) // Create a new version of a secure value - sv2, err := m.create(now, sv.DeepCopy()) + sv2, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv2.Namespace) require.Equal(t, sv.Name, sv2.Name) @@ -351,15 +56,15 @@ func TestModel(t *testing.T) { t.Run("updating secure values", func(t *testing.T) { t.Parallel() - m := newModel() + m := testutils.NewModelGsm(nil) now := time.Now() - sv1, err := m.create(now, sv.DeepCopy()) + sv1, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) // Create a new version of a secure value by updating it - sv2, _, err := m.update(now, sv1.DeepCopy()) + sv2, _, err := m.Update(now, sv1.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv2.Namespace) require.Equal(t, sv.Name, sv2.Name) @@ -369,55 +74,55 @@ func TestModel(t *testing.T) { sv3 := sv2.DeepCopy() sv3.Name = "i_dont_exist" sv3.Spec.Value = nil - _, _, err = m.update(now, sv3) + _, _, err = m.Update(now, sv3) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) // Updating a value that doesn't exist creates a new version sv4 := sv3.DeepCopy() sv4.Name = "i_dont_exist" sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4")) - _, _, err = m.update(now, sv4) + _, _, err = m.Update(now, sv4) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) }) t.Run("deleting a secure value", func(t *testing.T) { t.Parallel() - m := newModel() + m := testutils.NewModelGsm(nil) now := time.Now() - sv1, err := m.create(now, sv.DeepCopy()) + sv1, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) // Deleting a secure value - deletedSv, err := m.delete(sv1.Namespace, sv1.Name) + deletedSv, err := m.Delete(sv1.Namespace, sv1.Name) require.NoError(t, err) require.Equal(t, sv1.Namespace, deletedSv.Namespace) require.Equal(t, sv1.Name, deletedSv.Name) require.EqualValues(t, sv1.Status.Version, deletedSv.Status.Version) // Deleting a secure value that doesn't exist results in an error - _, err = m.delete(sv1.Namespace, sv1.Name) + _, err = m.Delete(sv1.Namespace, sv1.Name) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) }) t.Run("listing secure values", func(t *testing.T) { t.Parallel() - m := newModel() + m := testutils.NewModelGsm(nil) now := time.Now() // No secure values exist yet - list, err := m.list(sv.Namespace) + list, err := m.List(sv.Namespace) require.NoError(t, err) require.Equal(t, 0, len(list.Items)) // Create a secure value - sv1, err := m.create(now, sv.DeepCopy()) + sv1, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) // 1 secure value exists and it should be returned - list, err = m.list(sv.Namespace) + list, err = m.List(sv.Namespace) require.NoError(t, err) require.Equal(t, 1, len(list.Items)) require.Equal(t, sv1.Namespace, list.Items[0].Namespace) @@ -428,11 +133,11 @@ func TestModel(t *testing.T) { t.Run("decrypting secure values", func(t *testing.T) { t.Parallel() - m := newModel() + m := testutils.NewModelGsm(nil) now := time.Now() // Decrypting a secure value that does not exist - result, err := m.decrypt("decrypter", "namespace", "name") + result, err := m.Decrypt(t.Context(), "decrypter", "namespace", "name") require.NoError(t, err) require.Equal(t, 1, len(result)) require.Nil(t, result["name"].Value()) @@ -440,16 +145,62 @@ func TestModel(t *testing.T) { // Create a secure value secret := "v1" - sv1, err := m.create(now, sv.DeepCopy()) + sv1, err := m.Create(now, sv.DeepCopy()) require.NoError(t, err) // Decrypt the just created secure value - result, err = m.decrypt(sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name) + result, err = m.Decrypt(t.Context(), sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name) require.NoError(t, err) require.Equal(t, 1, len(result)) require.Nil(t, result[sv1.Name].Error()) require.Equal(t, secret, result[sv1.Name].Value().DangerouslyExposeAndConsumeValue()) }) + + t.Run("decrypting with reference", func(t *testing.T) { + t.Parallel() + + secretsManager := testutils.NewModelSecretsManager() + m := testutils.NewModelGsm(secretsManager) + now := time.Now() + + keeper, err := m.CreateKeeper(&secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "k1", + }, + Spec: secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }) + require.NoError(t, err) + require.NoError(t, m.SetKeeperAsActive(keeper.Namespace, keeper.Name)) + + // Store the secret on the 3rd party secrets store + secret := "v1" + secretsManager.Create("ref1", secret) + + // Create a secure value that references the secret on the 3rd party secret store + sv, err := m.Create(now, &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "desc1", + Ref: ptr.To("ref1"), + Decrypters: []string{"decrypter1"}, + }, + Status: secretv1beta1.SecureValueStatus{}, + }) + require.NoError(t, err) + + // Decrypt the just created secure value + result, err := m.Decrypt(t.Context(), sv.Spec.Decrypters[0], sv.Namespace, sv.Name) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result[sv.Name].Error()) + require.Equal(t, secret, result[sv.Name].Value().DangerouslyExposeAndConsumeValue()) + }) } func TestStateMachine(t *testing.T) { @@ -459,14 +210,13 @@ func TestStateMachine(t *testing.T) { rapid.Check(t, func(t *rapid.T) { sut := testutils.Setup(tt) - model := newModel() + model := testutils.NewModelGsm(sut.ModelSecretsManager) t.Repeat(map[string]func(*rapid.T){ - "create": func(t *rapid.T) { - sv := anySecureValueGen.Draw(t, "sv") - - modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy()) + "createSecureValueWithSecretValue": func(t *rapid.T) { + sv := testutils.AnySecureValueGen.Draw(t, "sv") + modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy()) createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -476,9 +226,27 @@ func TestStateMachine(t *testing.T) { require.Equal(t, modelCreatedSv.Name, createdSv.Name) require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) }, + "createSecureValueWithRef": func(t *rapid.T) { + sv := testutils.AnySecureValueWithRefGen.Draw(t, "sv") + + modelCreatedSv, modelErr := model.Create(sut.Clock.Now(), sv.DeepCopy()) + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace) + require.Equal(t, modelCreatedSv.Name, createdSv.Name) + require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) + }, + "createSecretOn3rdPartyKeeper": func(t *rapid.T) { + name := testutils.SecretsToRefGen.Draw(t, "name") + value := rapid.String().Draw(t, "value") + sut.ModelSecretsManager.Create(name, value) + }, "update": func(t *rapid.T) { - sv := updateSecureValueGen.Draw(t, "sv") - modelCreatedSv, _, modelErr := model.update(sut.Clock.Now(), sv.DeepCopy()) + sv := testutils.UpdateSecureValueGen.Draw(t, "sv") + modelCreatedSv, _, modelErr := model.Update(sut.Clock.Now(), sv.DeepCopy()) createdSv, err := sut.UpdateSv(t.Context(), sv.DeepCopy()) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -489,9 +257,10 @@ func TestStateMachine(t *testing.T) { require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) }, "delete": func(t *rapid.T) { - sv := deleteSecureValueGen.Draw(t, "sv") - modelSv, modelErr := model.delete(sv.Namespace, sv.Name) - deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) + ns := testutils.NamespaceGen.Draw(t, "ns") + name := testutils.SecureValueNameGen.Draw(t, "name") + modelSv, modelErr := model.Delete(ns, name) + deletedSv, err := sut.DeleteSv(t.Context(), ns, name) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -501,12 +270,12 @@ func TestStateMachine(t *testing.T) { require.Equal(t, modelSv.Status.Version, deletedSv.Status.Version) }, "list": func(t *rapid.T) { - sv := anySecureValueGen.Draw(t, "sv") - authCtx := testutils.CreateUserAuthContext(t.Context(), sv.Namespace, map[string][]string{ + ns := testutils.NamespaceGen.Draw(t, "ns") + authCtx := testutils.CreateUserAuthContext(t.Context(), ns, map[string][]string{ "securevalues:read": {"securevalues:uid:*"}, }) - modelList, modelErr := model.list(sv.Namespace) - list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(sv.Namespace)) + modelList, modelErr := model.List(ns) + list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(ns)) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -525,9 +294,10 @@ func TestStateMachine(t *testing.T) { } }, "get": func(t *rapid.T) { - sv := anySecureValueGen.Draw(t, "sv") - modelSv, modelErr := model.read(sv.Namespace, sv.Name) - readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name) + ns := testutils.NamespaceGen.Draw(t, "ns") + name := testutils.SecureValueNameGen.Draw(t, "name") + modelSv, modelErr := model.Read(ns, name) + readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(ns), name) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -537,9 +307,9 @@ func TestStateMachine(t *testing.T) { require.Equal(t, modelSv.Status.Version, readSv.Status.Version) }, "decrypt": func(t *rapid.T) { - input := decryptGen.Draw(t, "decryptInput") - modelResult, modelErr := model.decrypt(input.decrypter, input.namespace, input.name) - result, err := sut.DecryptService.Decrypt(t.Context(), input.decrypter, input.namespace, input.name) + input := testutils.DecryptGen.Draw(t, "decryptInput") + modelResult, modelErr := model.Decrypt(t.Context(), input.Decrypter, input.Namespace, input.Name) + result, err := sut.DecryptService.Decrypt(t.Context(), input.Decrypter, input.Namespace, input.Name) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -547,13 +317,13 @@ func TestStateMachine(t *testing.T) { require.Equal(t, len(modelResult), len(result)) for name := range modelResult { - require.Equal(t, modelResult[name].Error(), result[name].Error()) + require.ErrorIs(t, modelResult[name].Error(), result[name].Error()) require.Equal(t, modelResult[name].Value(), result[name].Value()) } }, "createKeeper": func(t *rapid.T) { - input := anyKeeperGen.Draw(t, "keeper") - modelKeeper, modelErr := model.createKeeper(input) + input := testutils.AnyKeeperGen.Draw(t, "keeper") + modelKeeper, modelErr := model.CreateKeeper(input) keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), input, "actor-uid") if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -562,9 +332,14 @@ func TestStateMachine(t *testing.T) { require.Equal(t, modelKeeper.Name, keeper.Name) }, "setKeeperAsActive": func(t *rapid.T) { - namespace := namespaceGen.Draw(t, "namespace") - keeper := keeperNameGen.Draw(t, "keeper") - modelErr := model.setKeeperAsActive(namespace, keeper) + namespace := testutils.NamespaceGen.Draw(t, "namespace") + var keeper string + if rapid.Bool().Draw(t, "systemKeeper") { + keeper = contracts.SystemKeeperName + } else { + keeper = testutils.KeeperNameGen.Draw(t, "keeper") + } + modelErr := model.SetKeeperAsActive(namespace, keeper) err := sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), keeper) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr)