From b706601bf7da110b068c13f53d7a4ea0a2454359 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 24 Feb 2026 10:49:33 -0700 Subject: [PATCH 001/468] Backport go: upgrade go.opentelemetry.io/otel/sdk => 1.40.0 and filippo.io/edwards25519 => v1.1.1 into ce/main (#12496) Upgrade filippo.io/edwards25519 v1.1.0 => v1.1.1 to resolve GO-2026-4503 Upgrade go.opentelemetry.io/auto/sdk v1.1.0 => v1.2.1 to resolve GO-2026-4394 Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- api/auth/gcp/go.mod | 15 +++++++++------ api/auth/gcp/go.sum | 38 ++++++++++++++++++++------------------ changelog/_12482.txt | 6 ++++++ go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- sdk/go.mod | 19 ++++++++++--------- sdk/go.sum | 40 ++++++++++++++++++++-------------------- tools/pipeline/go.mod | 14 ++++++++------ tools/pipeline/go.sum | 36 ++++++++++++++++++------------------ 9 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 changelog/_12482.txt diff --git a/api/auth/gcp/go.mod b/api/auth/gcp/go.mod index c2719aee6c..dd18db88f1 100644 --- a/api/auth/gcp/go.mod +++ b/api/auth/gcp/go.mod @@ -13,9 +13,10 @@ require ( cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect @@ -32,17 +33,19 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/api v0.242.0 // indirect diff --git a/api/auth/gcp/go.sum b/api/auth/gcp/go.sum index 03972a911a..f901a1a62f 100644 --- a/api/auth/gcp/go.sum +++ b/api/auth/gcp/go.sum @@ -8,6 +8,8 @@ cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -17,8 +19,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= @@ -70,24 +72,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= @@ -96,8 +98,8 @@ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= diff --git a/changelog/_12482.txt b/changelog/_12482.txt new file mode 100644 index 0000000000..250fccfdec --- /dev/null +++ b/changelog/_12482.txt @@ -0,0 +1,6 @@ +```release-note:security +vault/sdk: Upgrade `go.opentelemetry.io/otel/sdk` to v1.40.0 to resolve GO-2026-4394 +``` +```release-note:security +Upgrade `filippo.io/edwards25519` to v1.1.1 to resolve GO-2026-4503 +``` diff --git a/go.mod b/go.mod index 7a16dfc35b..5f4bb33724 100644 --- a/go.mod +++ b/go.mod @@ -216,9 +216,9 @@ require ( go.etcd.io/etcd/client/v3 v3.6.0 go.mongodb.org/atlas v0.38.0 go.mongodb.org/mongo-driver v1.17.4 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 - go.opentelemetry.io/otel/trace v1.37.0 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.47.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 @@ -268,7 +268,7 @@ require ( cloud.google.com/go/kms v1.23.0 // indirect; indirect\ cloud.google.com/go/longrunning v0.6.7 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect @@ -552,12 +552,12 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.6.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.31.0 // indirect diff --git a/go.sum b/go.sum index 11755e497d..d1cb64dd4d 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= @@ -2264,30 +2264,30 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= diff --git a/sdk/go.mod b/sdk/go.mod index 1868748990..19091ef342 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -47,7 +47,7 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible github.com/robfig/cron/v3 v3.0.1 github.com/ryanuber/go-glob v1.0.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/tink-crypto/tink-go/v2 v2.2.0 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.45.0 @@ -58,9 +58,11 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/moby/go-archive v0.1.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect ) require ( @@ -71,7 +73,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/armon/go-metrics v0.4.1 // indirect + github.com/armon/go-metrics v0.4.1 github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -83,7 +85,7 @@ require ( github.com/frankban/quicktest v1.14.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -126,20 +128,19 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/objx v0.5.2 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/api v0.221.0 // indirect diff --git a/sdk/go.sum b/sdk/go.sum index 710bde08ea..7d0a8ff8ff 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -8,8 +8,8 @@ cloud.google.com/go/cloudsqlconn v1.4.3 h1:/WYFbB1NtMtoMxCbqpzzTFPDkxxlLTPme390K cloud.google.com/go/cloudsqlconn v1.4.3/go.mod h1:QL3tuStVOO70txb3rs4G8j5uMfo5ztZii8K3oGD3VYA= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -104,8 +104,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= @@ -479,8 +479,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tink-crypto/tink-go/v2 v2.2.0 h1:L2Da0F2Udh2agtKztdr69mV/KpnY3/lGTkMgLTVIXlA= github.com/tink-crypto/tink-go/v2 v2.2.0/go.mod h1:JJ6PomeNPF3cJpfWC0lgyTES6zpJILkAX0cJNwlS3xU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -490,24 +490,24 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -640,8 +640,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/tools/pipeline/go.mod b/tools/pipeline/go.mod index a885e94553..30bc9b0bbc 100644 --- a/tools/pipeline/go.mod +++ b/tools/pipeline/go.mod @@ -16,7 +16,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 github.com/spf13/cobra v1.9.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/veqryn/slog-context v0.8.0 github.com/zclconf/go-cty v1.17.0 golang.org/x/mod v0.29.0 @@ -28,6 +28,7 @@ require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fatih/color v1.18.0 // indirect @@ -68,13 +69,14 @@ require ( github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/spf13/pflag v1.0.7 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.8 // indirect diff --git a/tools/pipeline/go.sum b/tools/pipeline/go.sum index ff17171c00..a86daca15a 100644 --- a/tools/pipeline/go.sum +++ b/tools/pipeline/go.sum @@ -30,8 +30,8 @@ github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIc github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -171,8 +171,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= @@ -188,8 +188,8 @@ github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/veqryn/slog-context v0.8.0 h1:lDhwAgjwx52K5StqqQzi5d0Y/F4SNyGZbsXGd8MtucM= @@ -201,16 +201,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -267,8 +267,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From e1e533bfb59bc7f46277f944858344ff50ca19e0 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 24 Feb 2026 16:21:44 -0700 Subject: [PATCH 002/468] UI: Add playwright test for Userpass Auth Method (#12470) (#12517) * adding workflow test for userpass auth method * adding conditionals for intro pages, fix assertion Co-authored-by: Dan Rivera --- ui/e2e/init.setup.ts | 7 +++- ui/e2e/tests/superuser/tools.spec.ts | 1 - ui/e2e/tests/superuser/userpass.spec.ts | 47 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 ui/e2e/tests/superuser/userpass.spec.ts diff --git a/ui/e2e/init.setup.ts b/ui/e2e/init.setup.ts index c56e0d5a16..f80dd12f28 100644 --- a/ui/e2e/init.setup.ts +++ b/ui/e2e/init.setup.ts @@ -42,7 +42,12 @@ setup('initialize vault and setup user for testing', async ({ page, userType }) // create a policy for a specific user persona // defaults to superuser but should be passed in via the project config in playwright.config.ts await page.getByRole('link', { name: 'Access control', exact: true }).click(); - await page.getByRole('link', { name: 'Create ACL policy' }).click(); + // if the intro page is shown, click the create policy link there, otherwise click the create policy link in the toolbar on main page + if (await page.getByRole('link', { name: 'Create a policy' }).isVisible()) { + await page.getByRole('link', { name: 'Create a policy' }).click(); + } else { + await page.getByRole('link', { name: 'Create ACL policy' }).click(); + } await page.getByRole('textbox', { name: 'Policy name' }).fill(userType); await page.getByRole('radio', { name: 'Code editor' }).check(); await page.getByRole('textbox', { name: 'Policy editor' }).fill(USER_POLICY_MAP[userType]); diff --git a/ui/e2e/tests/superuser/tools.spec.ts b/ui/e2e/tests/superuser/tools.spec.ts index 3a5f1e0d9d..781a57b7c6 100644 --- a/ui/e2e/tests/superuser/tools.spec.ts +++ b/ui/e2e/tests/superuser/tools.spec.ts @@ -65,5 +65,4 @@ test('tools workflow', async ({ page }) => { await expect(page.locator('#operations-0-tokenLookUpAccessor')).toContainText( '/auth/token/lookup-accessor' ); - await expect(page.locator('#operations-0-tokenLookUpAccessor')).toContainText('tokenLookUpAccessor'); }); diff --git a/ui/e2e/tests/superuser/userpass.spec.ts b/ui/e2e/tests/superuser/userpass.spec.ts new file mode 100644 index 0000000000..a954a4d2ff --- /dev/null +++ b/ui/e2e/tests/superuser/userpass.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('userpass workflow', async ({ page }) => { + // nav to access control and enable userpass auth method + await page.goto('dashboard'); + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'Authentication methods' }).click(); + + // if intro page is visible, click enable method there otherwise click enable method in toolbar + if (await page.getByRole('link', { name: 'Enable a new method' }).isVisible()) { + await page.getByRole('link', { name: 'Enable a new method' }).click(); + } else { + await page.getByRole('link', { name: 'Enable new method' }).click(); + } + + // enable userpass auth method + await page.getByLabel('Userpass - enabled engine type').click(); + await page.getByRole('button', { name: 'Enable method' }).click(); + await page.getByRole('button', { name: 'Update options' }).click(); + await expect(page.getByRole('link', { name: 'Type of auth mount userpass/' })).toBeVisible(); + + // create a test user + await page.getByRole('link', { name: 'Type of auth mount userpass/' }).click(); + await page.getByLabel('toolbar actions').getByRole('link', { name: 'Create user' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('testUser'); + await page.getByRole('textbox', { name: 'password', exact: true }).fill('test'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByRole('link', { name: 'testuser', exact: true })).toBeVisible(); + + // log out and log in with the new user + await page.getByRole('button', { name: 'User menu' }).click(); + await page.getByRole('link', { name: 'Log out' }).click(); + await page.getByLabel('Method').selectOption('userpass'); + await page.getByRole('textbox', { name: 'Username' }).fill('testUser'); + await page.getByRole('textbox', { name: 'Password' }).fill('test'); + await page.getByRole('button', { name: 'Sign in' }).click(); + + // verify login was successful by verifying the user menu is visible and contains the username + await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible(); + await page.getByRole('button', { name: 'User menu' }).click(); + await expect(page.getByText('Testuser')).toBeVisible(); +}); From 4d2ccaa86e605e2d8d6c6310a2303d916abbd50a Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 24 Feb 2026 17:20:11 -0700 Subject: [PATCH 003/468] [COMPLIANCE] Update Copyright and License Headers (#11034) (#12518) Co-authored-by: oss-core-libraries-dashboard[bot] <206901675+oss-core-libraries-dashboard[bot]@users.noreply.github.com> Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> --- enos/modules/cloud_docker_vault_cluster/main.tf | 2 +- enos/modules/docker_namespace_token/main.tf | 2 +- enos/modules/docker_network/main.tf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/enos/modules/cloud_docker_vault_cluster/main.tf b/enos/modules/cloud_docker_vault_cluster/main.tf index 8248e2c773..801c193c29 100644 --- a/enos/modules/cloud_docker_vault_cluster/main.tf +++ b/enos/modules/cloud_docker_vault_cluster/main.tf @@ -1,4 +1,4 @@ -# Copyright (c) HashiCorp, Inc. +# Copyright IBM Corp. 2016, 2025 # SPDX-License-Identifier: BUSL-1.1 terraform { diff --git a/enos/modules/docker_namespace_token/main.tf b/enos/modules/docker_namespace_token/main.tf index 69ad34d8d9..5342428d1d 100644 --- a/enos/modules/docker_namespace_token/main.tf +++ b/enos/modules/docker_namespace_token/main.tf @@ -1,4 +1,4 @@ -# Copyright (c) HashiCorp, Inc. +# Copyright IBM Corp. 2016, 2025 # SPDX-License-Identifier: BUSL-1.1 variable "vault_root_token" { diff --git a/enos/modules/docker_network/main.tf b/enos/modules/docker_network/main.tf index bea258681d..81a3d1d1fe 100644 --- a/enos/modules/docker_network/main.tf +++ b/enos/modules/docker_network/main.tf @@ -1,4 +1,4 @@ -# Copyright (c) HashiCorp, Inc. +# Copyright IBM Corp. 2016, 2025 # SPDX-License-Identifier: BUSL-1.1 terraform { From ad9144da7ebdab5df3bc6984027fe85e2da42c5d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 24 Feb 2026 19:03:43 -0700 Subject: [PATCH 004/468] Fix issues from partitioning tests (#12523) (#12525) * lets add some waits * maybe its the wizard? * clear logout state Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- ui/tests/acceptance/managed-namespace-test.js | 1 + ui/tests/acceptance/policy/policies-test.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/tests/acceptance/managed-namespace-test.js b/ui/tests/acceptance/managed-namespace-test.js index cd21ea9af0..2b8deec55c 100644 --- a/ui/tests/acceptance/managed-namespace-test.js +++ b/ui/tests/acceptance/managed-namespace-test.js @@ -18,6 +18,7 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) { this.server.get('/sys/internal/ui/feature-flags', () => { return { feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'] }; }); + window.localStorage.clear(); }); test('it shows the managed namespace toolbar when feature flag exists', async function (assert) { diff --git a/ui/tests/acceptance/policy/policies-test.js b/ui/tests/acceptance/policy/policies-test.js index 68d16081bd..701342555f 100644 --- a/ui/tests/acceptance/policy/policies-test.js +++ b/ui/tests/acceptance/policy/policies-test.js @@ -3,11 +3,12 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, currentURL, currentRouteName, visit, fillIn } from '@ember/test-helpers'; +import { click, currentURL, currentRouteName, visit, fillIn, waitFor } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import localStorage from 'vault/lib/local-storage'; module('Acceptance | policies', function (hooks) { setupApplicationTest(hooks); @@ -40,8 +41,10 @@ module('Acceptance | policies', function (hooks) { test('it navigates to and from policy show page from sidebar', async function (assert) { await visit('/vault/dashboard'); + localStorage.setItem('dismissed-wizards', ['acl-policy']); await click(GENERAL.navLink('Access control')); assert.strictEqual(currentURL(), '/vault/policies/acl', 'currentURL is /vault/policies/acl'); + await waitFor('[data-test-component="navigate-input"]'); await fillIn('[data-test-component="navigate-input"]', 'default'); // filter for the policy in case there are many on this view and the default policy is on the second page await click('[data-test-policy-link="default"]'); assert.strictEqual(currentURL(), '/vault/policy/acl/default'); From b593ca128e43dd447d04cb6991eaa6d50069a4a8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 25 Feb 2026 11:37:43 -0700 Subject: [PATCH 005/468] Backport [VAULT-40813] ManageDropdown component into ce/main (#12508) * [VAULT-40813] ManageDropdown component (#12295) * [VAULT-40813] ManageDropdown component * address pr comments, add routing test coverage * fix test: generate policy is an enterprise-only feature --------- Co-authored-by: Shannon Roberts (Beagin) Co-authored-by: Shannon Roberts --- ui/app/components/manage-dropdown.ts | 6 + ui/app/components/mount-backend-form.ts | 14 +- ui/app/components/mount-backend/type-form.js | 10 +- ui/app/components/secret-engine/list.hbs | 35 +- ui/app/components/secret-engine/list.ts | 36 +- ui/app/components/secret-engines/catalog.ts | 20 +- .../vault/cluster/secrets/backend/list.js | 25 +- ui/app/forms/secrets/engine.ts | 2 +- ui/app/helpers/engines-display-data.ts | 58 +- ui/app/models/mfa-login-enforcement.js | 6 +- ui/app/models/secret-engine.js | 13 +- ui/app/resources/secrets/engine.ts | 4 +- .../vault/cluster/secrets/backend/list.js | 14 +- .../vault/cluster/secrets/backend/list.hbs | 38 +- ui/app/utils/all-engines-metadata.ts | 353 +----------- .../core/addon/components/manage-dropdown.hbs | 46 ++ .../core/addon/components/manage-dropdown.ts | 117 ++++ .../addon/helpers/engines-display-data.ts | 62 +++ .../core/addon/utils/all-engines-metadata.ts | 353 ++++++++++++ ui/lib/core/app/components/manage-dropdown.js | 6 + .../core/app/helpers/engines-display-data.js | 6 + ui/lib/kmip/addon/components/page/scopes.hbs | 26 +- ui/lib/kmip/addon/components/page/scopes.ts | 31 +- .../addon/components/kubernetes-header.hbs | 16 +- .../addon/components/kubernetes-header.ts | 57 -- ui/lib/kv/addon/components/page/list.hbs | 27 +- ui/lib/kv/addon/components/page/list.js | 26 +- ui/lib/ldap/addon/components/ldap-header.hbs | 26 +- ui/lib/ldap/addon/components/ldap-header.ts | 57 -- .../pki/addon/components/pki-page-header.hbs | 26 +- .../pki/addon/components/pki-page-header.ts | 32 +- .../secrets/manage-dropdown-routing-test.js | 518 ++++++++++++++++++ ui/tests/acceptance/secrets/mounts-test.js | 32 +- .../secrets/secrets-nav-test-helper.js | 4 +- ui/tests/acceptance/settings-test.js | 14 +- .../components/kmip/page/scopes-test.js | 14 +- .../components/manage-dropdown-test.js | 161 ++++++ .../unit/components/manage-dropdown-test.js | 88 +++ .../unit/helpers/engines-display-data-test.js | 2 +- 39 files changed, 1490 insertions(+), 891 deletions(-) create mode 100644 ui/app/components/manage-dropdown.ts create mode 100644 ui/lib/core/addon/components/manage-dropdown.hbs create mode 100644 ui/lib/core/addon/components/manage-dropdown.ts create mode 100644 ui/lib/core/addon/helpers/engines-display-data.ts create mode 100644 ui/lib/core/addon/utils/all-engines-metadata.ts create mode 100644 ui/lib/core/app/components/manage-dropdown.js create mode 100644 ui/lib/core/app/helpers/engines-display-data.js delete mode 100644 ui/lib/kubernetes/addon/components/kubernetes-header.ts delete mode 100644 ui/lib/ldap/addon/components/ldap-header.ts create mode 100644 ui/tests/acceptance/secrets/manage-dropdown-routing-test.js create mode 100644 ui/tests/integration/components/manage-dropdown-test.js create mode 100644 ui/tests/unit/components/manage-dropdown-test.js diff --git a/ui/app/components/manage-dropdown.ts b/ui/app/components/manage-dropdown.ts new file mode 100644 index 0000000000..e524fcb744 --- /dev/null +++ b/ui/app/components/manage-dropdown.ts @@ -0,0 +1,6 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/components/manage-dropdown'; diff --git a/ui/app/components/mount-backend-form.ts b/ui/app/components/mount-backend-form.ts index c75e918baf..91f72c9f28 100644 --- a/ui/app/components/mount-backend-form.ts +++ b/ui/app/components/mount-backend-form.ts @@ -3,21 +3,21 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { action, set } from '@ember/object'; +import { service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { service } from '@ember/service'; -import { action, set } from '@ember/object'; +import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata'; import { task } from 'ember-concurrency'; -import { waitFor } from '@ember/test-waiters'; -import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; import { MOUNT_CATEGORIES } from 'vault/utils/plugin-catalog-helpers'; -import type FlashMessageService from 'vault/services/flash-messages'; +import type { ApiError } from '@ember-data/adapter/error'; import type Store from '@ember-data/store'; import type AuthMethodForm from 'vault/forms/auth/method'; -import type CapabilitiesService from 'vault/services/capabilities'; import type ApiService from 'vault/services/api'; -import type { ApiError } from '@ember-data/adapter/error'; +import type CapabilitiesService from 'vault/services/capabilities'; +import type FlashMessageService from 'vault/services/flash-messages'; import type { ValidationMap } from 'vault/vault/app-types'; /** diff --git a/ui/app/components/mount-backend/type-form.js b/ui/app/components/mount-backend/type-form.js index d1859a3e5a..fcc4a3bb32 100644 --- a/ui/app/components/mount-backend/type-form.js +++ b/ui/app/components/mount-backend/type-form.js @@ -3,18 +3,18 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Component from '@glimmer/component'; -import { service } from '@ember/service'; import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata'; import keys from 'core/utils/keys'; -import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; import { - enhanceEnginesWithCatalogData, categorizeEnginesByStatus, + enhanceEnginesWithCatalogData, MOUNT_CATEGORIES, - PLUGIN_TYPES, PLUGIN_CATEGORIES, + PLUGIN_TYPES, } from 'vault/utils/plugin-catalog-helpers'; /** diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 8232cd1166..663b66c494 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -140,7 +140,7 @@ @text="Disable engines" @color="critical" @icon="trash" - {{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}} + {{on "click" (fn this.setEnginesToDisable this.selectedItems)}} /> {{/if}} @@ -171,28 +171,7 @@ <:popupMenu as |rowData|> {{#let (this.getEngineResourceData rowData.path) as |backendData|}} - - - View configuration - {{#if (not-eq backendData.type "cubbyhole")}} - Delete - {{/if}} - + {{/let}} @@ -201,16 +180,6 @@ {{/if}} {{! End Table Section }} - {{#if this.engineToDisable}} - - {{/if}} - {{#if this.enginesToDisable}} { @service declare readonly wizard: WizardService; @tracked secretEngineOptions: Array | [] = []; - @tracked engineToDisable: SecretsEngineResource | undefined = undefined; @tracked enginesToDisable: Array | null = null; @tracked engineTypeFilters: Array = []; @@ -289,6 +288,16 @@ export default class SecretEngineList extends Component { this.selectedItems = tableData.selectedRowsKeys; } + @action + setEnginesToDisable(engines: Array) { + this.enginesToDisable = engines; + } + + @action + clearEnginesToDisable() { + this.enginesToDisable = null; + } + async disableSingleEngine(engine: SecretsEngineResource) { const { engineType, id, path } = engine; try { @@ -302,14 +311,13 @@ export default class SecretEngineList extends Component { } } - @dropTask - *disableMultipleEngines(enginePathsToDisable: Array) { + disableMultipleEngines = dropTask(async (enginePathsToDisable: Array) => { const enginesToDisable = this.displayableBackends.filter((engine: SecretsEngineResource) => enginePathsToDisable.includes(engine.path) ); try { for (const engine of enginesToDisable) { - yield this.disableSingleEngine(engine); + await this.disableSingleEngine(engine); } // Navigate once all operations are complete @@ -317,15 +325,5 @@ export default class SecretEngineList extends Component { } finally { this.enginesToDisable = null; } - } - - @dropTask - *disableEngine(engine: SecretsEngineResource) { - try { - yield this.disableSingleEngine(engine); - this.router.transitionTo('vault.cluster.secrets.backends'); - } finally { - this.engineToDisable = undefined; - } - } + }); } diff --git a/ui/app/components/secret-engines/catalog.ts b/ui/app/components/secret-engines/catalog.ts index 550cf626f8..e6c06c5201 100644 --- a/ui/app/components/secret-engines/catalog.ts +++ b/ui/app/components/secret-engines/catalog.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Component from '@glimmer/component'; -import { service } from '@ember/service'; import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; -import { - enhanceEnginesWithCatalogData, - categorizeEnginesByStatus, - MOUNT_CATEGORIES, - PLUGIN_TYPES, - PLUGIN_CATEGORIES, -} from 'vault/utils/plugin-catalog-helpers'; +import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata'; import type { PluginCatalogData } from 'vault/services/plugin-catalog'; +import { + categorizeEnginesByStatus, + enhanceEnginesWithCatalogData, + MOUNT_CATEGORIES, + PLUGIN_CATEGORIES, + PLUGIN_TYPES, +} from 'vault/utils/plugin-catalog-helpers'; import type VersionService from 'vault/services/version'; diff --git a/ui/app/controllers/vault/cluster/secrets/backend/list.js b/ui/app/controllers/vault/cluster/secrets/backend/list.js index ab95c0c4aa..964ebfc285 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/list.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/list.js @@ -3,14 +3,13 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { or } from '@ember/object/computed'; -import { computed } from '@ember/object'; -import { service } from '@ember/service'; import Controller from '@ember/controller'; -import BackendCrumbMixin from 'vault/mixins/backend-crumb'; +import { computed } from '@ember/object'; +import { or } from '@ember/object/computed'; +import { service } from '@ember/service'; import ListController from 'core/mixins/list-controller'; import { keyIsFolder } from 'core/utils/key-utils'; -import { task } from 'ember-concurrency'; +import BackendCrumbMixin from 'vault/mixins/backend-crumb'; export default Controller.extend(ListController, BackendCrumbMixin, { flashMessages: service(), @@ -68,20 +67,4 @@ export default Controller.extend(ListController, BackendCrumbMixin, { }); }, }, - - disableEngine: task(function* (engine) { - const { engineType, id, path } = engine; - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engines at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = null; - } - }).drop(), }); diff --git a/ui/app/forms/secrets/engine.ts b/ui/app/forms/secrets/engine.ts index 7e39262492..a1dcb4f48d 100644 --- a/ui/app/forms/secrets/engine.ts +++ b/ui/app/forms/secrets/engine.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { ALL_ENGINES } from 'core/utils/all-engines-metadata'; import MountForm from 'vault/forms/mount'; -import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; import { isKnownExternalPlugin } from 'vault/utils/external-plugin-helpers'; import FormField from 'vault/utils/forms/field'; import FormFieldGroup from 'vault/utils/forms/field-group'; diff --git a/ui/app/helpers/engines-display-data.ts b/ui/app/helpers/engines-display-data.ts index 4924e9a7e7..a71fe61517 100644 --- a/ui/app/helpers/engines-display-data.ts +++ b/ui/app/helpers/engines-display-data.ts @@ -3,60 +3,4 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { ALL_ENGINES, type EngineDisplayData } from 'vault/utils/all-engines-metadata'; -import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; - -/** - * Default metadata for unknown engine plugins - */ -export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({ - type: methodType || 'unknown', - displayName: methodType || 'Unknown plugin', - glyph: 'lock', - mountCategory: ['secret', 'auth'], -}); - -/** - * Helper function to retrieve engine metadata for a given `methodType`. - * It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object. - * The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise. - * These details (such as mount type and enterprise licensing) are included in the returned engine object. - * - * For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"), - * this function returns the metadata for the corresponding builtin engine, preserving the original - * external plugin name in the type field. - * - * Example usage: - * const engineMetadata = engineDisplayData('kmip'); - * if (engineMetadata?.requiresEnterprise) { - * console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`); - * } - * - * @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt"). - * @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth) - * and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object. - */ -export default function engineDisplayData(methodType: string): EngineDisplayData { - // First try to find an exact match - const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType); - if (builtinEngine) { - return builtinEngine; - } - - // If no direct match, check if this is a known external plugin and use its builtin mapping - const effectiveType = getEffectiveEngineType(methodType); - if (effectiveType !== methodType) { - // This is a known external plugin with a builtin mapping - const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType); - if (mappedEngine) { - // Return the mapped engine metadata but preserve the original external plugin type - return { - ...mappedEngine, - type: methodType, // Keep the original external plugin name for identification - }; - } - } - - // Return default unknown plugin metadata - return unknownEngineMetadata(methodType); -} +export { default } from 'core/helpers/engines-display-data'; diff --git a/ui/app/models/mfa-login-enforcement.js b/ui/app/models/mfa-login-enforcement.js index 9bc2d4bd7d..d7cd19b176 100644 --- a/ui/app/models/mfa-login-enforcement.js +++ b/ui/app/models/mfa-login-enforcement.js @@ -6,11 +6,11 @@ import Model, { attr, hasMany } from '@ember-data/model'; import ArrayProxy from '@ember/array/proxy'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -import { withModelValidations } from 'vault/decorators/model-validations'; -import { isPresent } from '@ember/utils'; import { service } from '@ember/service'; +import { isPresent } from '@ember/utils'; +import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata'; +import { withModelValidations } from 'vault/decorators/model-validations'; import { addManyToArray, addToArray } from 'vault/helpers/add-to-array'; -import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; const validations = { name: [{ type: 'presence', message: 'Name is required' }], diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index c6e798d248..5e2183c121 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -4,15 +4,14 @@ */ import Model, { attr, belongsTo } from '@ember-data/model'; -import { computed } from '@ember/object'; // eslint-disable-line -import { equal } from '@ember/object/computed'; // eslint-disable-line -import { withModelValidations } from 'vault/decorators/model-validations'; +import { ALL_ENGINES, isAddonEngine } from 'core/utils/all-engines-metadata'; import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; -import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; -import { ALL_ENGINES, INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata'; -import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; +import { withModelValidations } from 'vault/decorators/model-validations'; import engineDisplayData from 'vault/helpers/engines-display-data'; +import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { INTERNAL_ENGINE_TYPES } from 'vault/utils/all-engines-metadata'; +import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; +import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; const LINKED_BACKENDS = supportedSecretBackends(); diff --git a/ui/app/resources/secrets/engine.ts b/ui/app/resources/secrets/engine.ts index 0c522f0a1a..7f9dcd1181 100644 --- a/ui/app/resources/secrets/engine.ts +++ b/ui/app/resources/secrets/engine.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { baseResourceFactory } from 'vault/resources/base-factory'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import { supportedSecretBackends, SupportedSecretBackendsEnum, } from 'vault/helpers/supported-secret-backends'; +import { baseResourceFactory } from 'vault/resources/base-factory'; import { INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata'; import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; -import engineDisplayData from 'vault/helpers/engines-display-data'; import type { Mount } from 'vault/mount'; diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 4e82e3c8cd..c02b4dbdae 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -3,19 +3,19 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { assert } from '@ember/debug'; import { set } from '@ember/object'; -import { hash } from 'rsvp'; import Route from '@ember/routing/route'; +import { service } from '@ember/service'; +import { filterEnginesByMountCategory, isAddonEngine } from 'core/utils/all-engines-metadata'; +import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; +import { hash } from 'rsvp'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { isAddonEngine, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; +import { getEnginePathParam } from 'vault/utils/backend-route-helpers'; import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers'; -import { service } from '@ember/service'; import { normalizePath } from 'vault/utils/path-encoding-helpers'; -import { getEnginePathParam } from 'vault/utils/backend-route-helpers'; -import { assert } from '@ember/debug'; -import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; -import engineDisplayData from 'vault/helpers/engines-display-data'; const SUPPORTED_BACKENDS = supportedSecretBackends(); diff --git a/ui/app/templates/vault/cluster/secrets/backend/list.hbs b/ui/app/templates/vault/cluster/secrets/backend/list.hbs index 9b42cdda9a..4482cc5aa2 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/list.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/list.hbs @@ -9,24 +9,14 @@ (options-for-backend this.backendType this.tab) (engines-display-data this.backendType) as |options engineDisplayData| }} - - - Configure - Delete - + -{{/if}} \ No newline at end of file +{{/let}} \ No newline at end of file diff --git a/ui/app/utils/all-engines-metadata.ts b/ui/app/utils/all-engines-metadata.ts index 0adc37aa1e..d15b54ccff 100644 --- a/ui/app/utils/all-engines-metadata.ts +++ b/ui/app/utils/all-engines-metadata.ts @@ -3,355 +3,4 @@ * SPDX-License-Identifier: BUSL-1.1 */ -/** - * Metadata configuration for secret and auth engines, including enterprise. - * - * This file defines and exports engine metadata, including its - * displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a - * centralized source of truth for engine-related configurations. - * - * Key responsibilities: - * - Define metadata for all engines. - * - Provide utility functions or constants for accessing engine-specific data. - * - Facilitate dynamic engine rendering and behavior based on metadata. - * - * Example usage: - * If an enterprise license is present, return all secret engines; - * otherwise, return only the secret engines supported in OSS. - * return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise }); - */ - -export interface EngineDisplayData { - pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic' - displayName: string; - engineRoute?: string; // engines that have their own Ember engine will have this route defined. - glyph?: string; - isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation - mountCategory: string[]; - requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault - requiresEnterprise?: boolean; - isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions. - isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials). - type: string; - value?: string; - configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines) -} - -/** - * @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'. - * @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results. - * @returns Filtered array of engines that match the given mount category - */ -export function filterEnginesByMountCategory({ - mountCategory, - isEnterprise = false, -}: { - mountCategory: 'auth' | 'secret'; - isEnterprise: boolean; -}) { - return isEnterprise - ? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory)) - : ALL_ENGINES.filter( - (engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise - ); -} - -export function isAddonEngine(type: string, version: number) { - if (type === 'kv' && version === 1) { - return false; - } - const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute; - return !!engineRoute; -} - -// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing -// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines, -// and should be filtered in some scenarios, such as listing secrets engines. -export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry']; - -export const ALL_ENGINES: EngineDisplayData[] = [ - { - pluginCategory: 'cloud', - displayName: 'AliCloud', - glyph: 'alibaba-color', - mountCategory: ['auth', 'secret'], - type: 'alicloud', - }, - { - pluginCategory: 'generic', - displayName: 'AppRole', - glyph: 'cpu', - mountCategory: ['auth'], - type: 'approle', - value: 'approle', - }, - { - pluginCategory: 'cloud', - displayName: 'AWS', - glyph: 'aws-color', - isConfigurable: true, - isWIF: true, - mountCategory: ['auth', 'secret'], - type: 'aws', - }, - { - pluginCategory: 'cloud', - displayName: 'Azure', - glyph: 'azure-color', - isOnlyMountable: true, - isConfigurable: true, - isWIF: true, - mountCategory: ['auth', 'secret'], - type: 'azure', - }, - { - pluginCategory: 'infra', - displayName: 'Consul', - glyph: 'consul-color', - mountCategory: ['secret'], - type: 'consul', - }, - { - displayName: 'Cubbyhole', - type: 'cubbyhole', - mountCategory: ['secret'], - }, - { - pluginCategory: 'infra', - displayName: 'Databases', - glyph: 'database', - mountCategory: ['secret'], - type: 'database', - }, - { - pluginCategory: 'cloud', - displayName: 'GitHub', - glyph: 'github-color', - mountCategory: ['auth'], - type: 'github', - value: 'github', - }, - { - pluginCategory: 'cloud', - displayName: 'Google Cloud', - glyph: 'gcp-color', - isOnlyMountable: true, - isConfigurable: true, - isWIF: true, - mountCategory: ['auth', 'secret'], - type: 'gcp', - }, - { - pluginCategory: 'cloud', - displayName: 'Google Cloud KMS', - glyph: 'gcp-color', - mountCategory: ['secret'], - type: 'gcpkms', - }, - { - pluginCategory: 'generic', - displayName: 'JWT', - glyph: 'jwt', - mountCategory: ['auth'], - type: 'jwt', - value: 'jwt', - }, - { - pluginCategory: 'generic', - displayName: 'KV', - engineRoute: 'kv.list', - configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2 - glyph: 'key-values', - mountCategory: ['secret'], - type: 'kv', - }, - { - pluginCategory: 'generic', - displayName: 'KMIP', - engineRoute: 'kmip.scopes.index', - configRoute: 'kmip.configuration', - isConfigurable: true, - glyph: 'lock', - mountCategory: ['secret'], - requiredFeature: 'KMIP', - requiresEnterprise: true, - type: 'kmip', - }, - { - pluginCategory: 'generic', - displayName: 'Transform', - glyph: 'transform-data', - mountCategory: ['secret'], - requiredFeature: 'Transform Secrets Engine', - requiresEnterprise: true, - type: 'transform', - }, - { - pluginCategory: 'cloud', - displayName: 'Key Management', - glyph: 'key', - mountCategory: ['secret'], - requiredFeature: 'Key Management Secrets Engine', - requiresEnterprise: true, - type: 'keymgmt', - }, - { - pluginCategory: 'generic', - displayName: 'Kubernetes', - engineRoute: 'kubernetes.overview', - configRoute: 'kubernetes.configuration', - glyph: 'kubernetes-color', - isConfigurable: true, - mountCategory: ['auth', 'secret'], - type: 'kubernetes', - }, - { - pluginCategory: 'generic', - displayName: 'LDAP', - isConfigurable: true, - engineRoute: 'ldap.overview', - configRoute: 'ldap.configuration', - glyph: 'folder-users', - mountCategory: ['auth', 'secret'], - type: 'ldap', - }, - { - pluginCategory: 'infra', - displayName: 'Nomad', - glyph: 'nomad-color', - mountCategory: ['secret'], - type: 'nomad', - }, - { - pluginCategory: 'generic', - displayName: 'OIDC', - glyph: 'openid-color', - mountCategory: ['auth'], - type: 'oidc', - value: 'oidc', - }, - { - pluginCategory: 'infra', - displayName: 'Okta', - glyph: 'okta-color', - mountCategory: ['auth'], - type: 'okta', - value: 'okta', - }, - { - pluginCategory: 'generic', - displayName: 'PKI Certificates', - isConfigurable: true, - engineRoute: 'pki.overview', - configRoute: 'pki.configuration', - glyph: 'certificate', - mountCategory: ['secret'], - type: 'pki', - }, - { - pluginCategory: 'infra', - displayName: 'RADIUS', - glyph: 'mainframe', - mountCategory: ['auth'], - type: 'radius', - value: 'radius', - }, - { - pluginCategory: 'infra', - displayName: 'RabbitMQ', - glyph: 'rabbitmq-color', - mountCategory: ['secret'], - type: 'rabbitmq', - }, - { - pluginCategory: 'generic', - displayName: 'SAML', - glyph: 'saml-color', - mountCategory: ['auth'], - requiresEnterprise: true, - type: 'saml', - value: 'saml', - }, - { - pluginCategory: 'generic', - displayName: 'SSH', - glyph: 'terminal-screen', - isConfigurable: true, - mountCategory: ['secret'], - type: 'ssh', - }, - { - pluginCategory: 'generic', - displayName: 'TLS Certificates', - glyph: 'certificate', - mountCategory: ['auth'], - type: 'cert', - value: 'cert', - }, - { - pluginCategory: 'generic', - displayName: 'TOTP', - glyph: 'history', - mountCategory: ['secret'], - type: 'totp', - }, - { - pluginCategory: 'generic', - displayName: 'Transit', - glyph: 'swap-horizontal', - mountCategory: ['secret'], - type: 'transit', - }, - { - displayName: 'Token', - type: 'token', - mountCategory: ['auth'], - }, - { - pluginCategory: 'generic', - displayName: 'Userpass', - glyph: 'users', - mountCategory: ['auth'], - type: 'userpass', - value: 'userpass', - }, - - // TODO: enable builtin plugins after confirming with Product - // - // { - // pluginCategory: 'generic', - // displayName: 'Ad', - // glyph: 'folder', - // isOldEngine: true, - // isOnlyMountable: true, - // mountCategory: ['secret'], - // type: 'ad', - // }, - // { - // pluginCategory: 'cloud', - // displayName: 'MongoDB Atlas', - // glyph: 'mongodb-color', - // isOldEngine: true, - // isOnlyMountable: true, - // mountCategory: ['secret'], - // type: 'mongodbatlas', - // }, - // { - // pluginCategory: 'infra', - // displayName: 'OpenLDAP', - // glyph: 'folder-users', - // isOldEngine: true, - // isOnlyMountable: true, - // mountCategory: ['secret'], - // type: 'openldap', - // }, - // { - // pluginCategory: 'infra', - // displayName: 'Terraform', - // glyph: 'terraform-color', - // isOldEngine: true, - // isOnlyMountable: true, - // mountCategory: ['secret'], - // type: 'terraform', - // }, -]; +export * from 'core/utils/all-engines-metadata'; diff --git a/ui/lib/core/addon/components/manage-dropdown.hbs b/ui/lib/core/addon/components/manage-dropdown.hbs new file mode 100644 index 0000000000..9f4a154209 --- /dev/null +++ b/ui/lib/core/addon/components/manage-dropdown.hbs @@ -0,0 +1,46 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + + {{#if this.isIcon}} + + {{else}} + + {{/if}} + + {{! Yield point for engine-specific custom menu items (e.g., KV's Generate policy) }} + {{yield D}} + + Configure + + {{#if this.shouldShowDelete}} + Delete + {{/if}} + + +{{#if this.engineToDisable}} + +{{/if}} \ No newline at end of file diff --git a/ui/lib/core/addon/components/manage-dropdown.ts b/ui/lib/core/addon/components/manage-dropdown.ts new file mode 100644 index 0000000000..be019d9b7c --- /dev/null +++ b/ui/lib/core/addon/components/manage-dropdown.ts @@ -0,0 +1,117 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import type RouterService from '@ember/routing/router-service'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type ApiService from 'vault/services/api'; +import type FlashMessageService from 'vault/services/flash-messages'; + +/** + * @module ManageDropdown + * Reusable component for displaying the Manage dropdown used in secret engine headers & secret engine mount list. + * + * @example + * // In main app page headers and list components — uses the resource getter for the full absolute route + * + * + * // In Ember engine templates (pki, kubernetes, ldap, kmip, kv) — pass the short relative route, + * // since HDS @route resolves relative to the engine's router mount + * + * + * // With custom menu items (like KV's Generate policy) — icon variant in a Ember engine list + * + * Generate policy + * + * + * @param {SecretsEngineResource} model - The secrets engine resource containing the engine details + * @param {string} configRoute - Route for the Configure action. + * @param {string} variant - Set to "icon" for "..." icon button, otherwise shows "Manage" text button (default) + */ + +interface Args { + model: SecretsEngineResource; + configRoute: string; + variant?: 'icon'; +} + +export default class ManageDropdown extends Component { + @service declare readonly router: RouterService; + @service('app-router') declare readonly appRouter: RouterService; + @service declare readonly api: ApiService; + @service declare readonly flashMessages: FlashMessageService; + + @tracked engineToDisable: SecretsEngineResource | undefined = undefined; + + get isIcon() { + return this.args.variant === 'icon'; + } + + get configureRouteModel() { + return this.args.model.id; + } + + get shouldShowDelete() { + // Don't show delete for cubbyhole engine + return this.args.model.type !== 'cubbyhole'; + } + + transitionToBackends() { + // First try using the router service, which is available in most contexts + if (this.router) { + this.router.transitionTo('vault.cluster.secrets.backends'); + return; + } + + // Fallback for ember-engine components which use appRouter instead of router service + if (this.appRouter) { + this.appRouter.transitionTo('vault.cluster.secrets.backends'); + } + } + + @action + handleDeleteClick(engine: SecretsEngineResource) { + this.engineToDisable = engine; + } + + @action + handleModalClose() { + this.engineToDisable = undefined; + } + + @action + async handleModalConfirm() { + if (this.engineToDisable) { + const { engineType, id, path } = this.engineToDisable; + + try { + await this.api.sys.mountsDisableSecretsEngine(id); + this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); + this.transitionToBackends(); + } catch (error) { + const { message } = await this.api.parseError(error); + this.flashMessages.danger( + `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` + ); + } finally { + this.engineToDisable = undefined; + } + } + } +} diff --git a/ui/lib/core/addon/helpers/engines-display-data.ts b/ui/lib/core/addon/helpers/engines-display-data.ts new file mode 100644 index 0000000000..d54920d50f --- /dev/null +++ b/ui/lib/core/addon/helpers/engines-display-data.ts @@ -0,0 +1,62 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { ALL_ENGINES, type EngineDisplayData } from 'core/utils/all-engines-metadata'; +import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; + +/** + * Default metadata for unknown engine plugins + */ +export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({ + type: methodType || 'unknown', + displayName: methodType || 'Unknown plugin', + glyph: 'lock', + mountCategory: ['secret', 'auth'], +}); + +/** + * Helper function to retrieve engine metadata for a given `methodType`. + * It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object. + * The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise. + * These details (such as mount type and enterprise licensing) are included in the returned engine object. + * + * For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"), + * this function returns the metadata for the corresponding builtin engine, preserving the original + * external plugin name in the type field. + * + * Example usage: + * const engineMetadata = engineDisplayData('kmip'); + * if (engineMetadata?.requiresEnterprise) { + * console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`); + * } + * + * @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt"). + * @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth) + * and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object. + */ +export default function engineDisplayData(methodType: string): EngineDisplayData { + // First try to find an exact match + const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType); + if (builtinEngine) { + return builtinEngine; + } + + // If no direct match, check if this is a known external plugin and use its builtin mapping + const effectiveType = getEffectiveEngineType(methodType); + if (effectiveType !== methodType) { + // This is a known external plugin with a builtin mapping + const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType); + if (mappedEngine) { + // Return the mapped engine metadata but preserve the original external plugin type + return { + ...mappedEngine, + type: methodType, // Keep the original external plugin name for identification + }; + } + } + + // Return default unknown plugin metadata + return unknownEngineMetadata(methodType); +} diff --git a/ui/lib/core/addon/utils/all-engines-metadata.ts b/ui/lib/core/addon/utils/all-engines-metadata.ts new file mode 100644 index 0000000000..86cd6e8c12 --- /dev/null +++ b/ui/lib/core/addon/utils/all-engines-metadata.ts @@ -0,0 +1,353 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +/** + * Metadata configuration for secret and auth engines, including enterprise. + * + * This file defines and exports engine metadata, including its + * displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a + * centralized source of truth for engine-related configurations. + * + * Key responsibilities: + * - Define metadata for all engines. + * - Provide utility functions or constants for accessing engine-specific data. + * - Facilitate dynamic engine rendering and behavior based on metadata. + * + * Example usage: + * If an enterprise license is present, return all secret engines; + * otherwise, return only the secret engines supported in OSS. + * return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise }); + */ + +export interface EngineDisplayData { + pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic' + displayName: string; + engineRoute?: string; // engines that have their own Ember engine will have this route defined. + glyph?: string; + isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation + mountCategory: string[]; + requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault + requiresEnterprise?: boolean; + isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions. + isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials). + type: string; + value?: string; + configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines) +} + +/** + * @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'. + * @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results. + * @returns Filtered array of engines that match the given mount category + */ +export function filterEnginesByMountCategory({ + mountCategory, + isEnterprise = false, +}: { + mountCategory: 'auth' | 'secret'; + isEnterprise: boolean; +}) { + return isEnterprise + ? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory)) + : ALL_ENGINES.filter( + (engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise + ); +} + +export function isAddonEngine(type: string, version: number) { + if (type === 'kv' && version === 1) { + return false; + } + const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute; + return !!engineRoute; +} + +// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing +// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines, +// and should be filtered in some scenarios, such as listing secrets engines. +export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry']; + +export const ALL_ENGINES: EngineDisplayData[] = [ + { + pluginCategory: 'cloud', + displayName: 'AliCloud', + glyph: 'alibaba-color', + mountCategory: ['auth', 'secret'], + type: 'alicloud', + }, + { + pluginCategory: 'generic', + displayName: 'AppRole', + glyph: 'cpu', + mountCategory: ['auth'], + type: 'approle', + value: 'approle', + }, + { + pluginCategory: 'cloud', + displayName: 'AWS', + glyph: 'aws-color', + isConfigurable: true, + isWIF: true, + mountCategory: ['auth', 'secret'], + type: 'aws', + }, + { + pluginCategory: 'cloud', + displayName: 'Azure', + glyph: 'azure-color', + isOnlyMountable: true, + isConfigurable: true, + isWIF: true, + mountCategory: ['auth', 'secret'], + type: 'azure', + }, + { + pluginCategory: 'infra', + displayName: 'Consul', + glyph: 'consul-color', + mountCategory: ['secret'], + type: 'consul', + }, + { + displayName: 'Cubbyhole', + type: 'cubbyhole', + mountCategory: ['secret'], + }, + { + pluginCategory: 'infra', + displayName: 'Databases', + glyph: 'database', + mountCategory: ['secret'], + type: 'database', + }, + { + pluginCategory: 'cloud', + displayName: 'GitHub', + glyph: 'github-color', + mountCategory: ['auth'], + type: 'github', + value: 'github', + }, + { + pluginCategory: 'cloud', + displayName: 'Google Cloud', + glyph: 'gcp-color', + isOnlyMountable: true, + isConfigurable: true, + isWIF: true, + mountCategory: ['auth', 'secret'], + type: 'gcp', + }, + { + pluginCategory: 'cloud', + displayName: 'Google Cloud KMS', + glyph: 'gcp-color', + mountCategory: ['secret'], + type: 'gcpkms', + }, + { + pluginCategory: 'generic', + displayName: 'JWT', + glyph: 'jwt', + mountCategory: ['auth'], + type: 'jwt', + value: 'jwt', + }, + { + pluginCategory: 'generic', + displayName: 'KV', + engineRoute: 'kv.list', + configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2 + glyph: 'key-values', + mountCategory: ['secret'], + type: 'kv', + }, + { + pluginCategory: 'generic', + displayName: 'KMIP', + engineRoute: 'kmip.scopes.index', + configRoute: 'kmip.configuration', + isConfigurable: true, + glyph: 'lock', + mountCategory: ['secret'], + requiredFeature: 'KMIP', + requiresEnterprise: true, + type: 'kmip', + }, + { + pluginCategory: 'generic', + displayName: 'Transform', + glyph: 'transform-data', + mountCategory: ['secret'], + requiredFeature: 'Transform Secrets Engine', + requiresEnterprise: true, + type: 'transform', + }, + { + pluginCategory: 'cloud', + displayName: 'Key Management', + glyph: 'key', + mountCategory: ['secret'], + requiredFeature: 'Key Management Secrets Engine', + requiresEnterprise: true, + type: 'keymgmt', + }, + { + pluginCategory: 'generic', + displayName: 'Kubernetes', + engineRoute: 'kubernetes.overview', + configRoute: 'kubernetes.configuration', + glyph: 'kubernetes-color', + isConfigurable: true, + mountCategory: ['auth', 'secret'], + type: 'kubernetes', + }, + { + pluginCategory: 'generic', + displayName: 'LDAP', + isConfigurable: true, + engineRoute: 'ldap.overview', + configRoute: 'ldap.configuration', + glyph: 'folder-users', + mountCategory: ['auth', 'secret'], + type: 'ldap', + }, + { + pluginCategory: 'infra', + displayName: 'Nomad', + glyph: 'nomad-color', + mountCategory: ['secret'], + type: 'nomad', + }, + { + pluginCategory: 'generic', + displayName: 'OIDC', + glyph: 'openid-color', + mountCategory: ['auth'], + type: 'oidc', + value: 'oidc', + }, + { + pluginCategory: 'infra', + displayName: 'Okta', + glyph: 'okta-color', + mountCategory: ['auth'], + type: 'okta', + value: 'okta', + }, + { + pluginCategory: 'generic', + displayName: 'PKI Certificates', + isConfigurable: true, + engineRoute: 'pki.overview', + configRoute: 'pki.configuration', + glyph: 'certificate', + mountCategory: ['secret'], + type: 'pki', + }, + { + pluginCategory: 'infra', + displayName: 'RADIUS', + glyph: 'mainframe', + mountCategory: ['auth'], + type: 'radius', + value: 'radius', + }, + { + pluginCategory: 'infra', + displayName: 'RabbitMQ', + glyph: 'rabbitmq-color', + mountCategory: ['secret'], + type: 'rabbitmq', + }, + { + pluginCategory: 'generic', + displayName: 'SAML', + glyph: 'saml-color', + mountCategory: ['auth'], + requiresEnterprise: true, + type: 'saml', + value: 'saml', + }, + { + pluginCategory: 'generic', + displayName: 'SSH', + glyph: 'terminal-screen', + isConfigurable: true, + mountCategory: ['secret'], + type: 'ssh', + }, + { + pluginCategory: 'generic', + displayName: 'TLS Certificates', + glyph: 'certificate', + mountCategory: ['auth'], + type: 'cert', + value: 'cert', + }, + { + pluginCategory: 'generic', + displayName: 'TOTP', + glyph: 'history', + mountCategory: ['secret'], + type: 'totp', + }, + { + pluginCategory: 'generic', + displayName: 'Transit', + glyph: 'swap-horizontal', + mountCategory: ['secret'], + type: 'transit', + }, + { + displayName: 'Token', + type: 'token', + mountCategory: ['auth'], + }, + { + pluginCategory: 'generic', + displayName: 'Userpass', + glyph: 'users', + mountCategory: ['auth'], + type: 'userpass', + value: 'userpass', + }, + + // TODO: enable builtin plugins after confirming with Product + // + // { + // pluginCategory: 'generic', + // displayName: 'Ad', + // glyph: 'folder', + // isOnlyMountable: true, + // mountCategory: ['secret'], + // type: 'ad', + // }, + // { + // pluginCategory: 'cloud', + // displayName: 'MongoDB Atlas', + // glyph: 'mongodb-color', + // isOnlyMountable: true, + // mountCategory: ['secret'], + // type: 'mongodbatlas', + // }, + // { + // pluginCategory: 'infra', + // displayName: 'OpenLDAP', + // glyph: 'folder-users', + // isOnlyMountable: true, + // mountCategory: ['secret'], + // type: 'openldap', + // }, + // { + // pluginCategory: 'infra', + // displayName: 'Terraform', + // glyph: 'terraform-color', + // isOnlyMountable: true, + // mountCategory: ['secret'], + // type: 'terraform', + // }, +]; diff --git a/ui/lib/core/app/components/manage-dropdown.js b/ui/lib/core/app/components/manage-dropdown.js new file mode 100644 index 0000000000..e524fcb744 --- /dev/null +++ b/ui/lib/core/app/components/manage-dropdown.js @@ -0,0 +1,6 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/components/manage-dropdown'; diff --git a/ui/lib/core/app/helpers/engines-display-data.js b/ui/lib/core/app/helpers/engines-display-data.js new file mode 100644 index 0000000000..a71fe61517 --- /dev/null +++ b/ui/lib/core/app/helpers/engines-display-data.js @@ -0,0 +1,6 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/helpers/engines-display-data'; diff --git a/ui/lib/kmip/addon/components/page/scopes.hbs b/ui/lib/kmip/addon/components/page/scopes.hbs index b16ccc4234..ea9ec1de51 100644 --- a/ui/lib/kmip/addon/components/page/scopes.hbs +++ b/ui/lib/kmip/addon/components/page/scopes.hbs @@ -7,21 +7,7 @@ <:actions> - - - Configure - Delete - + @@ -117,14 +103,4 @@ {{/if}} -{{/if}} - -{{#if this.engineToDisable}} - {{/if}} \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/page/scopes.ts b/ui/lib/kmip/addon/components/page/scopes.ts index b4b9dac68c..89dec64c86 100644 --- a/ui/lib/kmip/addon/components/page/scopes.ts +++ b/ui/lib/kmip/addon/components/page/scopes.ts @@ -3,21 +3,21 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { service } from '@ember/service'; import { action } from '@ember/object'; import { getOwner } from '@ember/owner'; -import { task } from 'ember-concurrency'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import type RouterService from '@ember/routing/router-service'; -import type SecretMountPath from 'vault/services/secret-mount-path'; -import type ApiService from 'vault/services/api'; import type { CapabilitiesMap, EngineOwner } from 'vault/app-types'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type ApiService from 'vault/services/api'; import FlashMessageService from 'vault/services/flash-messages'; +import type SecretMountPath from 'vault/services/secret-mount-path'; interface Args { + secretsEngine: SecretsEngineResource; scopes: string[]; capabilities: CapabilitiesMap; } @@ -27,7 +27,6 @@ export default class KmipScopesPageComponent extends Component { @service declare readonly secretMountPath: SecretMountPath; @service declare readonly api: ApiService; @service declare readonly flashMessages: FlashMessageService; - @tracked engineToDisable: SecretsEngineResource | undefined = undefined; @tracked scopeToDelete: string | null = null; @@ -56,22 +55,4 @@ export default class KmipScopesPageComponent extends Component { this.flashMessages.danger(`Error deleting scope ${this.scopeToDelete}: ${message}`); } } - - @task - *disableEngine(engine: SecretsEngineResource) { - const { engineType, id, path } = engine; - - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = undefined; - } - } } diff --git a/ui/lib/kubernetes/addon/components/kubernetes-header.hbs b/ui/lib/kubernetes/addon/components/kubernetes-header.hbs index 8dca4cfee7..69f296d30f 100644 --- a/ui/lib/kubernetes/addon/components/kubernetes-header.hbs +++ b/ui/lib/kubernetes/addon/components/kubernetes-header.hbs @@ -15,21 +15,7 @@ {{#if @configRoute}} {{else}} - - - Configure - Delete - + {{/if}} diff --git a/ui/lib/kubernetes/addon/components/kubernetes-header.ts b/ui/lib/kubernetes/addon/components/kubernetes-header.ts deleted file mode 100644 index a6769c4fce..0000000000 --- a/ui/lib/kubernetes/addon/components/kubernetes-header.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { task } from 'ember-concurrency'; - -import type SecretsEngineResource from 'vault/resources/secrets/engine'; -import type RouterService from '@ember/routing/router-service'; -import type FlashMessageService from 'vault/services/flash-messages'; -import type ApiService from 'vault/services/api'; - -/** - * @module KubernetesHeader handles the ldap page header. - * - * @example - * - * - * @param {object} secretsEngine - A model contains a ldap secret engine resource. - * @param {object} config - A model contains the configuration of the ldap secret engine. - */ - -interface Args { - secretsEngine: SecretsEngineResource; - config: Record; -} - -export default class KubernetesHeader extends Component { - @service('app-router') declare readonly router: RouterService; - @service declare readonly api: ApiService; - @service declare readonly flashMessages: FlashMessageService; - - @tracked engineToDisable: SecretsEngineResource | undefined = undefined; - - @task - *disableEngine(engine: SecretsEngineResource) { - const { engineType, id, path } = engine; - - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = undefined; - } - } -} diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs index a3ba752d8e..d0ccd0be6e 100644 --- a/ui/lib/kv/addon/components/page/list.hbs +++ b/ui/lib/kv/addon/components/page/list.hbs @@ -11,8 +11,7 @@ <:actions> - - + <:customTrigger as |openFlyout|> @@ -20,19 +19,7 @@ - Configure - Delete - + {{/if}} {{/if}} -{{/if}} - -{{#if this.engineToDisable}} - {{/if}} \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/list.js b/ui/lib/kv/addon/components/page/list.js index 2f77aecdb3..f5c8fe9aa7 100644 --- a/ui/lib/kv/addon/components/page/list.js +++ b/ui/lib/kv/addon/components/page/list.js @@ -3,14 +3,13 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Component from '@glimmer/component'; -import { service } from '@ember/service'; import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; import { getOwner } from '@ember/owner'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import { ancestorKeysForKey } from 'core/utils/key-utils'; import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; -import { task } from 'ember-concurrency'; /** * @module List @@ -32,7 +31,6 @@ export default class KvListPageComponent extends Component { @tracked secretPath; @tracked metadataToDelete = null; // set to the metadata intended to delete - @tracked engineToDisable = undefined; // used for KV list and list-directory view // ex: beep/ @@ -59,24 +57,6 @@ export default class KvListPageComponent extends Component { }; } - @task - *disableEngine(engine) { - const { engineType, id, path } = engine; - - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = undefined; - } - } - @action async onDelete(secretPath) { try { diff --git a/ui/lib/ldap/addon/components/ldap-header.hbs b/ui/lib/ldap/addon/components/ldap-header.hbs index 422c21b0ba..f52fd7f9c4 100644 --- a/ui/lib/ldap/addon/components/ldap-header.hbs +++ b/ui/lib/ldap/addon/components/ldap-header.hbs @@ -15,21 +15,7 @@ {{#if @configRoute}} {{else}} - - - Configure - Delete - + {{/if}} @@ -51,16 +37,6 @@ {{/if}} -{{#if this.engineToDisable}} - -{{/if}} - {{yield to="toolbarFilters"}} diff --git a/ui/lib/ldap/addon/components/ldap-header.ts b/ui/lib/ldap/addon/components/ldap-header.ts deleted file mode 100644 index af9ce1d792..0000000000 --- a/ui/lib/ldap/addon/components/ldap-header.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { task } from 'ember-concurrency'; - -import type SecretsEngineResource from 'vault/resources/secrets/engine'; -import type RouterService from '@ember/routing/router-service'; -import type FlashMessageService from 'vault/services/flash-messages'; -import type ApiService from 'vault/services/api'; - -/** - * @module LdapHeader handles the ldap page header. - * - * @example - * - * - * @param {object} secretsEngine - A model contains a ldap secret engine resource. - * @param {object} config - A model contains the configuration of the ldap secret engine. - */ - -interface Args { - secretsEngine: SecretsEngineResource; - config: Record; -} - -export default class LdapHeader extends Component { - @service('app-router') declare readonly router: RouterService; - @service declare readonly api: ApiService; - @service declare readonly flashMessages: FlashMessageService; - - @tracked engineToDisable: SecretsEngineResource | undefined = undefined; - - @task - *disableEngine(engine: SecretsEngineResource) { - const { engineType, id, path } = engine; - - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = undefined; - } - } -} diff --git a/ui/lib/pki/addon/components/pki-page-header.hbs b/ui/lib/pki/addon/components/pki-page-header.hbs index c584b79447..4ea78c2fae 100644 --- a/ui/lib/pki/addon/components/pki-page-header.hbs +++ b/ui/lib/pki/addon/components/pki-page-header.hbs @@ -17,21 +17,7 @@ {{#if @configRoute}} {{else}} - - - Configure - Delete - + {{/if}} @@ -58,14 +44,4 @@
  • Tidy
  • -{{/if}} - -{{#if this.engineToDisable}} - {{/if}} \ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-page-header.ts b/ui/lib/pki/addon/components/pki-page-header.ts index 4e6436c7fa..e6f398cf47 100644 --- a/ui/lib/pki/addon/components/pki-page-header.ts +++ b/ui/lib/pki/addon/components/pki-page-header.ts @@ -4,16 +4,12 @@ */ import { service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; import Component from '@glimmer/component'; -import { task } from 'ember-concurrency'; -import type { PATH_MAP } from 'vault/utils/constants/capabilities'; -import type ApiService from 'vault/services/api'; -import type CapabilitiesService from 'vault/services/capabilities'; -import type FlashMessageService from 'vault/services/flash-messages'; import type RouterService from '@ember/routing/router-service'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type CapabilitiesService from 'vault/services/capabilities'; +import type { PATH_MAP } from 'vault/utils/constants/capabilities'; /** * @module PkiPageHeader @@ -25,7 +21,7 @@ import type SecretsEngineResource from 'vault/resources/secrets/engine'; */ interface Args { - backend: { id: string }; + backend: SecretsEngineResource; } const ROUTE_PATH_MAP = { @@ -36,12 +32,8 @@ const ROUTE_PATH_MAP = { export default class PkiPageHeader extends Component { @service('app-router') declare readonly router: RouterService; - @service declare readonly api: ApiService; - @service declare readonly flashMessages: FlashMessageService; @service declare readonly capabilities: CapabilitiesService; - @tracked engineToDisable = undefined; - get breadcrumbs() { return [ { label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true }, @@ -61,22 +53,4 @@ export default class PkiPageHeader extends Component { } return null; } - - @task - *disableEngine(engine: SecretsEngineResource) { - const { engineType, id, path } = engine; - - try { - yield this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.router.transitionTo('vault.cluster.secrets.backends'); - } catch (err) { - const { message } = yield this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } finally { - this.engineToDisable = undefined; - } - } } diff --git a/ui/tests/acceptance/secrets/manage-dropdown-routing-test.js b/ui/tests/acceptance/secrets/manage-dropdown-routing-test.js new file mode 100644 index 0000000000..5ce554b326 --- /dev/null +++ b/ui/tests/acceptance/secrets/manage-dropdown-routing-test.js @@ -0,0 +1,518 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { click, currentRouteName, fillIn, findAll, settled, visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import { v4 as uuidv4 } from 'uuid'; +import engineDisplayData from 'vault/helpers/engines-display-data'; +import { login } from 'vault/tests/helpers/auth/auth-helpers'; +import { runCmd } from 'vault/tests/helpers/commands'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; + +const SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES = [ + { + key: 'alicloud', + type: 'alicloud', + isEnginePathClickable: false, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'azure', + type: 'azure', + isEnginePathClickable: true, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'gcp', + type: 'gcp', + isEnginePathClickable: true, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'gcpkms', + type: 'gcpkms', + isEnginePathClickable: false, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'keymgmt', + type: 'keymgmt', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'kubernetes', + type: 'kubernetes', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + expectedActionConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.kubernetes.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.kubernetes.configure', + ], + expectedLandingConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.kubernetes.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.kubernetes.configure', + ], + }, + { + key: 'kvv1', + type: 'kv', + version: 1, + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'kvv2', + type: 'kv', + version: 2, + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: true, + showConfigure: true, + showDelete: true, + expectedLandingConfigureRoutesOverride: ['vault.cluster.secrets.backend.kv.configuration'], + }, + { + key: 'transform', + type: 'transform', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'transit', + type: 'transit', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'kmip', + type: 'kmip', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + expectedActionConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.kmip.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.kmip.configure', + ], + expectedLandingConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.kmip.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.kmip.configure', + ], + }, + { + key: 'ldap', + type: 'ldap', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + expectedActionConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.ldap.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.ldap.configure', + ], + expectedLandingConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.ldap.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.ldap.configure', + ], + }, + { + key: 'pki', + type: 'pki', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + expectedActionConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.pki.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.pki.configuration.create', + ], + expectedLandingConfigureRoutesOverride: [ + // if the engine is configured + 'vault.cluster.secrets.backend.pki.configuration', + // if the engine is not configured + 'vault.cluster.secrets.backend.pki.configuration.create', + ], + }, + { + key: 'ssh', + type: 'ssh', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'totp', + type: 'totp', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'aws', + type: 'aws', + isEnginePathClickable: true, + showManageDropdown: true, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'consul', + type: 'consul', + isEnginePathClickable: false, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'nomad', + type: 'nomad', + isEnginePathClickable: false, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'rabbitmq', + type: 'rabbitmq', + isEnginePathClickable: false, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + }, + { + key: 'database', + type: 'database', + isEnginePathClickable: true, + showManageDropdown: false, + showGeneratePolicy: false, + showConfigure: true, + showDelete: true, + expectedLandingRouteOverride: 'vault.cluster.secrets.backend.overview', + expectedLandingConfigureRoutesOverride: [], + }, +]; + +const secretsEngineListRoute = '/vault/secrets-engines'; + +const mountEngine = async ({ type, version }, path) => { + await mountSecrets.visit(); + await click(GENERAL.cardContainer(type)); + await fillIn(GENERAL.inputByAttr('path'), path); + if (type === 'kv' && version === 1) { + await click(GENERAL.button('Method Options')); + await mountSecrets.version(1); + } + await click(GENERAL.submitButton); +}; + +const filterEngineRowByPath = async (path) => { + await visit(secretsEngineListRoute); + const searchInputSelector = GENERAL.inputSearch('secret-engine-path'); + if (findAll(searchInputSelector).length) { + await fillIn(searchInputSelector, path); + } +}; + +const clickVisibleMenuItem = async (name) => { + const visibleItem = findAll(GENERAL.menuItem(name)).find((el) => el.offsetParent !== null); + if (!visibleItem) { + throw new Error(`No visible menu item found for: ${name}`); + } + await click(visibleItem); +}; + +const assertMenuOptionVisibility = (assert, visibilityByOption, contextLabel, engineKey) => { + for (const [option, isVisible] of Object.entries(visibilityByOption)) { + if (isVisible) { + assert.dom(GENERAL.menuItem(option)).exists(`${contextLabel} shows ${option} for ${engineKey}`); + } else { + assert + .dom(GENERAL.menuItem(option)) + .doesNotExist(`${contextLabel} does not show ${option} for ${engineKey}`); + } + } +}; + +const clickVisibleConfirmButton = async () => { + const visibleConfirmButton = findAll(GENERAL.confirmButton).find((el) => el.offsetParent !== null); + if (!visibleConfirmButton) { + return false; + } + await click(visibleConfirmButton); + return true; +}; + +const expectedActionConfigureRoutes = (engineType) => { + const { isConfigurable, configRoute } = engineDisplayData(engineType); + if (!isConfigurable) { + return ['vault.cluster.secrets.backend.configuration.general-settings']; + } + + if (configRoute) { + return [`vault.cluster.secrets.backend.${configRoute}`]; + } + + return [ + // if the engine is configured + 'vault.cluster.secrets.backend.configuration.plugin-settings', + // if the engine is not configured + 'vault.cluster.secrets.backend.configuration.edit', + ]; +}; + +const expectedLandingRoute = ({ type, version = 1 }) => { + const engineData = engineDisplayData(type); + const isKvV1 = type === 'kv' && version === 1; + + if (engineData.isOnlyMountable) { + return 'vault.cluster.secrets.backend.configuration.general-settings'; + } + if (engineData.engineRoute && !isKvV1) { + return `vault.cluster.secrets.backend.${engineData.engineRoute}`; + } + return 'vault.cluster.secrets.backend.list-root'; +}; + +const expectedLandingConfigureRoutes = ({ type, version = 1 }) => { + const engineData = engineDisplayData(type); + const isKvV1 = type === 'kv' && version === 1; + + if (engineData.engineRoute && !isKvV1) { + if (engineData.configRoute) { + return [`vault.cluster.secrets.backend.${engineData.configRoute}`]; + } + } + + if (engineData.isConfigurable) { + return [ + // if the engine is configured + 'vault.cluster.secrets.backend.configuration.plugin-settings', + // if the engine is not configured + 'vault.cluster.secrets.backend.configuration.edit', + ]; + } + + return ['vault.cluster.secrets.backend.configuration.general-settings']; +}; + +const runEngineCase = async (assert, engine, uid, isEnterprise = false) => { + const mountPath = `manage-${engine.key}-${uid}`; + const actionConfigureRoutes = + engine.expectedActionConfigureRoutesOverride || expectedActionConfigureRoutes(engine.type); + const expectedManage = { + showManageDropdown: engine.showManageDropdown ?? false, + showGeneratePolicy: (engine.showGeneratePolicy ?? false) && isEnterprise, + showConfigure: engine.showConfigure ?? true, + showDelete: engine.showDelete ?? true, + }; + + // if engine path already exists, delete it before starting the test + await runCmd(`delete sys/mounts/${mountPath}`); + + // mount the engine + await mountEngine(engine, mountPath); + + // verify the engine shows in the list + await filterEngineRowByPath(mountPath); + assert.dom(GENERAL.tableRow()).exists(`row renders for ${engine.key}`); + + assert.dom(GENERAL.menuTrigger).exists(`Action menu is shown for ${engine.key}`); + await click(GENERAL.menuTrigger); + + assertMenuOptionVisibility( + assert, + { + Configure: expectedManage.showConfigure, + Delete: expectedManage.showDelete, + }, + 'Action menu', + engine.key + ); + + if (expectedManage.showConfigure) { + // click configure and verify route + await clickVisibleMenuItem('Configure'); + await settled(); + assert.true( + actionConfigureRoutes.includes(currentRouteName()), + `Action: Configure routes correctly for ${engine.key}` + ); + await filterEngineRowByPath(mountPath); + } + + if (expectedManage.showDelete) { + // click delete and verify the engine is removed from the list + await filterEngineRowByPath(mountPath); + await click(GENERAL.menuTrigger); + await clickVisibleMenuItem('Delete'); + const didConfirmActionDelete = await clickVisibleConfirmButton(); + assert.true(didConfirmActionDelete, `Action: Delete shows confirm button for ${engine.key}`); + await settled(); + + await filterEngineRowByPath(mountPath); + assert.dom(GENERAL.tableRow()).doesNotExist(`Action: Delete removes ${engine.key} mount`); + + // remount the engine for manage dropdown testing + await mountEngine(engine, mountPath); + await filterEngineRowByPath(mountPath); + } + + const isEnginePathClickable = engine.isEnginePathClickable ?? false; + const backendLinkSelector = `a[href*="/vault/secrets-engines/${mountPath}"]`; + + if (!isEnginePathClickable) { + // if the engine path is not expected to be clickable, verify it's not a link and skip the rest of the test + assert.dom(backendLinkSelector).doesNotExist(`EnginePath is not a clickable link for ${engine.key}`); + await runCmd(`delete sys/mounts/${mountPath}`); + return; + } + + assert.dom(backendLinkSelector).exists(`EnginePath is a clickable link for ${engine.key}`); + await click(backendLinkSelector); + + const routeAfterPathClick = engine.expectedLandingRouteOverride || expectedLandingRoute(engine); + assert.strictEqual( + currentRouteName(), + routeAfterPathClick, + `Engine path click redirects to ${routeAfterPathClick} for ${engine.key}` + ); + + const shouldShowManageDropdown = expectedManage.showManageDropdown; + + if (!shouldShowManageDropdown) { + // if manage dropdown is not expected to show on the landing page, verify it's not shown and skip the rest of the test + assert + .dom(GENERAL.dropdownToggle('Manage')) + .doesNotExist(`Manage dropdown is not shown on landing page for ${engine.key}`); + await runCmd(`delete sys/mounts/${mountPath}`); + return; + } + + assert + .dom(GENERAL.dropdownToggle('Manage')) + .exists(`Manage dropdown shows on landing page for ${engine.key}`); + await click(GENERAL.dropdownToggle('Manage')); + assertMenuOptionVisibility( + assert, + { + 'Generate policy': expectedManage.showGeneratePolicy, + Configure: expectedManage.showConfigure, + Delete: expectedManage.showDelete, + }, + 'Manage dropdown', + engine.key + ); + + if (expectedManage.showConfigure) { + // click configure and verify route + await clickVisibleMenuItem('Configure'); + await settled(); + const allowedConfigureRoutes = + engine.expectedLandingConfigureRoutesOverride || expectedLandingConfigureRoutes(engine); + assert.true( + allowedConfigureRoutes.includes(currentRouteName()), + `Manage Configure routes correctly for ${engine.key}` + ); + + await filterEngineRowByPath(mountPath); + await click(backendLinkSelector); + await click(GENERAL.dropdownToggle('Manage')); + } + + if (expectedManage.showDelete) { + // click delete and verify the engine is removed from the list + await clickVisibleMenuItem('Delete'); + const didConfirmManageDelete = await clickVisibleConfirmButton(); + if (!didConfirmManageDelete) { + assert.true( + engine.type === 'kubernetes', + `Manage Delete missing confirm is only expected for kubernetes; got ${engine.key}` + ); + await runCmd(`delete sys/mounts/${mountPath}`); + await filterEngineRowByPath(mountPath); + assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`); + return; + } + await settled(); + + await filterEngineRowByPath(mountPath); + assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`); + return; // if the delete action is confirmed, the engine should be removed and we can end the test here without needing to clean up again + } +}; + +module('Acceptance | secrets-engines/manage-dropdown routing', function (hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(function () { + this.uid = uuidv4(); + return login(); + }); + + for (const engine of SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES) { + const isEnterpriseOnly = !!engineDisplayData(engine.type).requiresEnterprise; + const engineLabel = isEnterpriseOnly ? `${engine.key} (enterprise only)` : engine.key; + + test(`manage dropdown coverage | ${engineLabel}`, async function (assert) { + const isEnterprise = this.owner.lookup('service:version').isEnterprise; + await runEngineCase(assert, engine, this.uid, isEnterprise); + }); + } +}); diff --git a/ui/tests/acceptance/secrets/mounts-test.js b/ui/tests/acceptance/secrets/mounts-test.js index 46d25eb1ee..dbb87fbeb4 100644 --- a/ui/tests/acceptance/secrets/mounts-test.js +++ b/ui/tests/acceptance/secrets/mounts-test.js @@ -4,35 +4,34 @@ */ import { + click, currentRouteName, currentURL, - settled, - click, - findAll, fillIn, - visit, + findAll, + settled, typeIn, + visit, waitFor, } from '@ember/test-helpers'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { module, test, skip } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; +import { module, skip, test } from 'qunit'; import { v4 as uuidv4 } from 'uuid'; import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; import { create } from 'ember-cli-page-object'; -import page from 'vault/tests/pages/settings/mount-secret-backend'; -import { login } from 'vault/tests/helpers/auth/auth-helpers'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; -import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; -import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config'; -import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator'; -import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; import engineDisplayData from 'vault/helpers/engines-display-data'; +import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { login } from 'vault/tests/helpers/auth/auth-helpers'; +import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config'; +import { adminOidcCreate, adminOidcCreateRead } from 'vault/tests/helpers/secret-engine/policy-generator'; +import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; +import consoleClass from 'vault/tests/pages/components/console/ui-panel'; +import { default as mountSecrets, default as page } from 'vault/tests/pages/settings/mount-secret-backend'; +import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; const consoleComponent = create(consoleClass); @@ -152,6 +151,7 @@ module('Acceptance | secrets-engines/enable', function (hooks) { await page.secretList(); await settled(); + await fillIn(GENERAL.inputSearch('secret-engine-path'), path); assert .dom(GENERAL.tableData(`${path}/`, 'path')) .exists({ count: 1 }, 'renders only one instance of the engine'); diff --git a/ui/tests/acceptance/secrets/secrets-nav-test-helper.js b/ui/tests/acceptance/secrets/secrets-nav-test-helper.js index 8667ca4f2b..1186b987ef 100644 --- a/ui/tests/acceptance/secrets/secrets-nav-test-helper.js +++ b/ui/tests/acceptance/secrets/secrets-nav-test-helper.js @@ -32,7 +32,7 @@ export default (test, type) => { await fillIn(GENERAL.inputSearch('secret-engine-path'), backend); await click(GENERAL.menuTrigger); - await click(GENERAL.menuItem('View configuration')); + await click(GENERAL.menuItem('Configure')); assert.strictEqual( currentRouteName(), `${BASE_ROUTE}.${this.expectedConfigEditRoute}`, @@ -117,7 +117,7 @@ export default (test, type) => { await visit(`/vault/secrets-engines`); await fillIn(GENERAL.inputSearch('secret-engine-path'), backend); await click(GENERAL.menuTrigger); - await click(GENERAL.menuItem('View configuration')); + await click(GENERAL.menuItem('Configure')); // For configurable engines, clicking "View configuration" will direct to its plugin settings route await waitUntil(() => currentRouteName() === `${BASE_ROUTE}.${configRoute}`); diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js index e34a6a6ac0..e754176b4b 100644 --- a/ui/tests/acceptance/settings-test.js +++ b/ui/tests/acceptance/settings-test.js @@ -3,16 +3,16 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { currentURL, visit, click, fillIn, currentRouteName, waitUntil } from '@ember/test-helpers'; -import { module, test } from 'qunit'; +import { click, currentRouteName, currentURL, fillIn, visit, waitUntil } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; import { v4 as uuidv4 } from 'uuid'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; module('Acceptance | secret engine mount settings', function (hooks) { setupApplicationTest(hooks); @@ -63,7 +63,7 @@ module('Acceptance | secret engine mount settings', function (hooks) { await visit('/vault/secrets-engines'); await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(GENERAL.menuTrigger); - await click(GENERAL.menuItem('View configuration')); + await click(GENERAL.menuItem('Configure')); // since ldap hasn't been configured yet, it should redirect to configure page assert.strictEqual( currentURL(), @@ -93,7 +93,7 @@ module('Acceptance | secret engine mount settings', function (hooks) { await visit('/vault/secrets-engines'); await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(GENERAL.menuTrigger); - await click(GENERAL.menuItem('View configuration')); + await click(GENERAL.menuItem('Configure')); assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.general-settings'); assert.strictEqual( currentURL(), @@ -125,7 +125,7 @@ module('Acceptance | secret engine mount settings', function (hooks) { await visit('/vault/secrets-engines'); await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(GENERAL.menuTrigger); - await click(GENERAL.menuItem('View configuration')); + await click(GENERAL.menuItem('Configure')); assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.edit'); assert.strictEqual( currentURL(), diff --git a/ui/tests/integration/components/kmip/page/scopes-test.js b/ui/tests/integration/components/kmip/page/scopes-test.js index 1dc3fe05de..e16de02ea6 100644 --- a/ui/tests/integration/components/kmip/page/scopes-test.js +++ b/ui/tests/integration/components/kmip/page/scopes-test.js @@ -3,15 +3,16 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupEngine } from 'ember-engines/test-support'; -import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupEngine } from 'ember-engines/test-support'; +import { setupRenderingTest } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; +import { module, test } from 'qunit'; import sinon from 'sinon'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import SecretsEngineResource from 'vault/resources/secrets/engine'; import { getErrorResponse } from 'vault/tests/helpers/api/error-response'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Integration | Component | kmip | Page::Scopes', function (hooks) { setupRenderingTest(hooks); @@ -20,6 +21,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) { hooks.beforeEach(function () { this.backend = 'kmip-test'; + this.secretsEngine = new SecretsEngineResource({ path: this.backend, type: 'kmip' }); this.owner.lookup('service:secret-mount-path').update(this.backend); const { secrets } = this.owner.lookup('service:api'); @@ -51,7 +53,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) { this.renderComponent = () => render( - hbs``, + hbs``, { owner: this.engine } ); }); diff --git a/ui/tests/integration/components/manage-dropdown-test.js b/ui/tests/integration/components/manage-dropdown-test.js new file mode 100644 index 0000000000..48cfffe831 --- /dev/null +++ b/ui/tests/integration/components/manage-dropdown-test.js @@ -0,0 +1,161 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { click, render } from '@ember/test-helpers'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; +import SecretsEngineResource from 'vault/resources/secrets/engine'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; + +const DEFAULT_MOUNT_DATA = { + accessor: 'test_accessor', + config: {}, + description: '', + external_entropy_access: false, + local: false, + plugin_version: '', + running_plugin_version: '', + running_sha256: '', + seal_wrap: false, + uuid: 'test-uuid', +}; + +const TEST_CASES = [ + { + label: 'alicloud', + type: 'alicloud', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'azure', + type: 'azure', + expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings', + }, + { + label: 'gcp', + type: 'gcp', + expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings', + }, + { + label: 'gcpkms', + type: 'gcpkms', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'keymgmt', + type: 'keymgmt', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'kubernetes', + type: 'kubernetes', + expectedRoute: 'vault.cluster.secrets.backend.kubernetes.configuration', + }, + { + label: 'kvv1', + type: 'kv', + version: 1, + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'kvv2', + type: 'kv', + version: 2, + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'transform', + type: 'transform', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'transit', + type: 'transit', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { label: 'kmip', type: 'kmip', expectedRoute: 'vault.cluster.secrets.backend.kmip.configuration' }, + { label: 'ldap', type: 'ldap', expectedRoute: 'vault.cluster.secrets.backend.ldap.configuration' }, + { label: 'pki', type: 'pki', expectedRoute: 'vault.cluster.secrets.backend.pki.configuration' }, + { + label: 'ssh', + type: 'ssh', + expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings', + }, + { + label: 'totp', + type: 'totp', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'aws', + type: 'aws', + expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings', + }, + { + label: 'consul', + type: 'consul', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'nomad', + type: 'nomad', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'rabbitmq', + type: 'rabbitmq', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, + { + label: 'database', + type: 'database', + expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings', + }, +]; + +module('Integration | Component | manage-dropdown | Configure link', function (hooks) { + setupRenderingTest(hooks); + + const makeModel = ({ type, version, id }) => { + const options = version ? { version } : undefined; + return new SecretsEngineResource({ + ...DEFAULT_MOUNT_DATA, + path: `${id}/`, + type, + options, + }); + }; + + TEST_CASES.forEach(({ label, type, version, expectedRoute }) => { + test(`Configure link routes correctly for ${label}`, async function (assert) { + const routing = this.owner.lookup('service:-routing'); + const transitionSpy = sinon.stub(routing, 'transitionTo'); + const id = `${label}-integration-test`; + this.model = makeModel({ type, version, id }); + + await render( + hbs`` + ); + + await click(GENERAL.menuTrigger); + await click(GENERAL.menuItem('Configure')); + + assert.true(transitionSpy.called, `Configure action for ${label} triggers a route transition`); + assert.strictEqual( + transitionSpy.firstCall.args[0], + expectedRoute, + `Configure action for ${label} transitions to ${expectedRoute}` + ); + assert.true( + JSON.stringify(transitionSpy.firstCall.args).includes(id), + `Configure action for ${label} includes model id ${id}` + ); + + transitionSpy.restore(); + }); + }); +}); diff --git a/ui/tests/unit/components/manage-dropdown-test.js b/ui/tests/unit/components/manage-dropdown-test.js new file mode 100644 index 0000000000..50d6e0c64f --- /dev/null +++ b/ui/tests/unit/components/manage-dropdown-test.js @@ -0,0 +1,88 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import SecretsEngineResource from 'vault/resources/secrets/engine'; + +const makeResource = ({ type, version }) => { + const options = version ? { version } : undefined; + return new SecretsEngineResource({ + accessor: 'test_accessor', + config: {}, + description: '', + external_entropy_access: false, + local: false, + options, + path: `${type}-test/`, + plugin_version: '', + running_plugin_version: '', + running_sha256: '', + seal_wrap: false, + type, + uuid: 'test-uuid', + }); +}; + +module('Unit | Component | manage-dropdown', function (hooks) { + setupTest(hooks); + + test('backendConfigurationLink: addon engines with configRoute always use their config route', function (assert) { + const kubernetes = makeResource({ type: 'kubernetes' }); + assert.strictEqual( + kubernetes.backendConfigurationLink, + 'vault.cluster.secrets.backend.kubernetes.configuration', + 'kubernetes always routes to its configuration page' + ); + + const ldap = makeResource({ type: 'ldap' }); + assert.strictEqual( + ldap.backendConfigurationLink, + 'vault.cluster.secrets.backend.ldap.configuration', + 'ldap always routes to its configuration page' + ); + + const pki = makeResource({ type: 'pki' }); + assert.strictEqual( + pki.backendConfigurationLink, + 'vault.cluster.secrets.backend.pki.configuration', + 'pki always routes to its configuration page' + ); + }); + + test('backendConfigurationLink: configurable engines without configRoute route to plugin-settings', function (assert) { + const ssh = makeResource({ type: 'ssh' }); + assert.strictEqual( + ssh.backendConfigurationLink, + 'vault.cluster.secrets.backend.configuration.plugin-settings', + 'configurable engine routes to the plugin-settings view' + ); + }); + + test('backendConfigurationLink: non-configurable engines always route to general-settings', function (assert) { + const alicloud = makeResource({ type: 'alicloud' }); + assert.strictEqual( + alicloud.backendConfigurationLink, + 'vault.cluster.secrets.backend.configuration.general-settings' + ); + }); + + test('backendConfigurationLink: KV v1 routes to general-settings', function (assert) { + const kvV1 = makeResource({ type: 'kv', version: 1 }); + assert.strictEqual( + kvV1.backendConfigurationLink, + 'vault.cluster.secrets.backend.configuration.general-settings' + ); + }); + + test('backendConfigurationLink: KV v2 routes to general-settings (configRoute is display-only)', function (assert) { + const kvV2 = makeResource({ type: 'kv', version: 2 }); + assert.strictEqual( + kvV2.backendConfigurationLink, + 'vault.cluster.secrets.backend.configuration.general-settings', + "kv's configRoute is skipped because it's for display only" + ); + }); +}); diff --git a/ui/tests/unit/helpers/engines-display-data-test.js b/ui/tests/unit/helpers/engines-display-data-test.js index cefcd797c5..2fcd68681f 100644 --- a/ui/tests/unit/helpers/engines-display-data-test.js +++ b/ui/tests/unit/helpers/engines-display-data-test.js @@ -3,8 +3,8 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import engineDisplayData, { unknownEngineMetadata } from 'core/helpers/engines-display-data'; import { module, test } from 'qunit'; -import engineDisplayData, { unknownEngineMetadata } from 'vault/helpers/engines-display-data'; import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; module('Unit | Helper | engines-display-data', function () { From 24c34121359d26c4464d3a041374650a053aa1b8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 25 Feb 2026 12:09:42 -0700 Subject: [PATCH 006/468] Add V2 Form generator script (#12062) (#12089) Add automated form config generation from OpenAPI specs. Generator script can be utilized by passing API method name which will then parse the OAS and generate the necessary form configuration to be used with the upcoming V2 Form systems. Co-authored-by: Angelo Cordon --- ui/app/forms/v2/form-config.ts | 77 +++++ ui/app/forms/v2/generated/index.ts | 8 + .../mounts-enable-secrets-engine-config.ts | 118 +++++++ ui/app/forms/v2/overrides/index.ts | 8 + ui/app/utils/form-config-generator.js | 189 +++++++++++ ui/package.json | 1 + ui/scripts/generate-form-config.js | 133 ++++++++ ui/tests/helpers/stubs.js | 107 +++++++ .../unit/utils/form-config-generator-test.js | 295 ++++++++++++++++++ 9 files changed, 936 insertions(+) create mode 100644 ui/app/forms/v2/form-config.ts create mode 100644 ui/app/forms/v2/generated/index.ts create mode 100644 ui/app/forms/v2/generated/mounts-enable-secrets-engine-config.ts create mode 100644 ui/app/forms/v2/overrides/index.ts create mode 100644 ui/app/utils/form-config-generator.js create mode 100644 ui/scripts/generate-form-config.js create mode 100644 ui/tests/unit/utils/form-config-generator-test.js diff --git a/ui/app/forms/v2/form-config.ts b/ui/app/forms/v2/form-config.ts new file mode 100644 index 0000000000..991e6da911 --- /dev/null +++ b/ui/app/forms/v2/form-config.ts @@ -0,0 +1,77 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import CONFIG_REGISTRY from './generated/index'; +import type ApiService from 'vault/services/api'; + +export type FormConfigKey = keyof typeof CONFIG_REGISTRY; + +/** + * Form element types matching HDS (HashiCorp Design System) components. + * Currently only TextInput is supported. + * + * TODO: Add support for additional field types: + * - 'Toggle' | 'Checkbox' + * - 'Select' | 'SuperSelect' | 'Radio' | 'RadioCard' + * - 'TextArea' | 'MaskedInput' + * - 'FileInput' | 'KeyValueInput' + */ +export type FormElement = 'TextInput'; + +/** + * Union type for all possible field values. + */ +export type FieldValue = string | number | boolean | string[] | null | undefined; + +/** + * Form field definition with common properties. + */ +export type FormField = { + /** Field name supporting dotted-path notation for nested properties */ + name: string; + /** Form element type */ + type: FormElement; + /** Display label for the field */ + label: string; + /** Optional helper text shown below the field */ + helperText?: string; + /** Optional placeholder text */ + placeholder?: string; + /** Default value for the field */ + defaultValue?: FieldValue; +}; + +/** + * Form section grouping related fields together + */ +export interface FormSection { + /** Section identifier (used as key) */ + name: string; + /** Optional display title for the section */ + title?: string; + /** Optional description for the section */ + description?: string; + /** Fields belonging to this section */ + fields: FormField[]; +} + +export interface FormConfig { + /** Unique identifier for the form, typically matching the API method name */ + name: string; + /** Title or description for the form */ + title?: string; + description?: string; + /** + * Initial payload structure matching the API request shape. + */ + payload: Request; + /** + * Submit handler that receives the API service and typed payload, + * returning the typed API response + */ + submit: (api: ApiService, payload: Request) => Promise; + /** Organized groups of fields with type-safe field names */ + sections: FormSection[]; +} diff --git a/ui/app/forms/v2/generated/index.ts b/ui/app/forms/v2/generated/index.ts new file mode 100644 index 0000000000..87e396a52a --- /dev/null +++ b/ui/app/forms/v2/generated/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +const GENERATED_CONFIGS = {}; + +export default GENERATED_CONFIGS; diff --git a/ui/app/forms/v2/generated/mounts-enable-secrets-engine-config.ts b/ui/app/forms/v2/generated/mounts-enable-secrets-engine-config.ts new file mode 100644 index 0000000000..1fd0458a29 --- /dev/null +++ b/ui/app/forms/v2/generated/mounts-enable-secrets-engine-config.ts @@ -0,0 +1,118 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +// ⚠️ AUTO-GENERATED FILE - DO NOT EDIT +// This file is generated from openapi.json +// To customize this form, create an override in +// forms/v2/overrides/ + +import type ApiService from 'vault/services/api'; +import type { FormConfig } from '../form-config'; +import type { SystemApiMountsEnableSecretsEngineOperationRequest } from '@hashicorp/vault-client-typescript'; + +/** + * Form configuration for mountsEnableSecretsEngine + * Auto-generated from OpenAPI specification + */ +const mountsEnableSecretsEngineConfig: FormConfig< + SystemApiMountsEnableSecretsEngineOperationRequest, + unknown +> = { + name: 'mountsEnableSecretsEngine', + description: 'Mount a new backend at a new path.', + submit: async (api: ApiService, payload: SystemApiMountsEnableSecretsEngineOperationRequest) => { + return await api.sys.mountsEnableSecretsEngineRaw(payload); + }, + payload: { + path: '', + MountsEnableSecretsEngineRequest: { + config: {}, + description: '', + external_entropy_access: false, + local: false, + options: {}, + plugin_name: '', + plugin_version: '', + seal_wrap: false, + type: '', + }, + }, + sections: [ + { + name: 'params', + fields: [ + { + name: 'path', + type: 'TextInput', + label: 'Path', + helperText: 'The path to mount to. Example: "aws/east"', + }, + ], + }, + { + name: 'default', + fields: [ + { + name: 'MountsEnableSecretsEngineRequest.config', + type: 'TextInput', + label: 'Config', + helperText: 'Configuration for this mount, such as default_lease_ttl and max_lease_ttl.', + }, + { + name: 'MountsEnableSecretsEngineRequest.description', + type: 'TextInput', + label: 'Description', + helperText: 'User-friendly description for this mount.', + }, + { + name: 'MountsEnableSecretsEngineRequest.external_entropy_access', + type: 'TextInput', + label: 'External Entropy Access', + helperText: "Whether to give the mount access to Vault's external entropy.", + }, + { + name: 'MountsEnableSecretsEngineRequest.local', + type: 'TextInput', + label: 'Local', + helperText: + 'Mark the mount as a local mount, which is not replicated and is unaffected by replication.', + }, + { + name: 'MountsEnableSecretsEngineRequest.options', + type: 'TextInput', + label: 'Options', + helperText: + 'The options to pass into the backend. Should be a json object with string keys and values.', + }, + { + name: 'MountsEnableSecretsEngineRequest.plugin_name', + type: 'TextInput', + label: 'Plugin Name', + helperText: 'Name of the plugin to mount based from the name registered in the plugin catalog.', + }, + { + name: 'MountsEnableSecretsEngineRequest.plugin_version', + type: 'TextInput', + label: 'Plugin Version', + helperText: 'The semantic version of the plugin to use, or image tag if oci_image is provided.', + }, + { + name: 'MountsEnableSecretsEngineRequest.seal_wrap', + type: 'TextInput', + label: 'Seal Wrap', + helperText: 'Whether to turn on seal wrapping for the mount.', + }, + { + name: 'MountsEnableSecretsEngineRequest.type', + type: 'TextInput', + label: 'Type', + helperText: 'The type of the backend. Example: "passthrough"', + }, + ], + }, + ], +}; + +export default mountsEnableSecretsEngineConfig; diff --git a/ui/app/forms/v2/overrides/index.ts b/ui/app/forms/v2/overrides/index.ts new file mode 100644 index 0000000000..7bd69dc40a --- /dev/null +++ b/ui/app/forms/v2/overrides/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +const OVERRIDE_CONFIGS = {}; + +export default OVERRIDE_CONFIGS; diff --git a/ui/app/utils/form-config-generator.js b/ui/app/utils/form-config-generator.js new file mode 100644 index 0000000000..d5212fb3ef --- /dev/null +++ b/ui/app/utils/form-config-generator.js @@ -0,0 +1,189 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +/** + * Utility functions for generating form configurations from OpenAPI specs. + * These functions have no side effects and can be safely imported and used in unit tests. + */ + +import { dasherize, classify } from '@ember/string'; + +const TYPE_DEFAULTS = { + string: '', + number: 0, + integer: 0, + boolean: false, + object: {}, + array: [], +}; + +const API_CLASS_FROM_TAG = { + auth: 'auth', + identity: 'identity', + secrets: 'secrets', + system: 'sys', +}; + +const formatLabel = (str) => { + // HDS guidelines suggest using sentence case for labels + // "some_example_text" → "Some example text" + // "client-id" → "Client id" + const sentence = str.replace(/[_-]/g, ' ').toLowerCase(); + return sentence.charAt(0).toUpperCase() + sentence.slice(1); +}; + +/** + * Construct request type name to match the vault-client-typescript SDK conventions. + * Example: tag='system', methodName='mountsEnableSecretsEngine' + * → 'SystemApiMountsEnableSecretsEngineOperationRequest' + */ +const getRequestType = (tag, methodName) => { + const apiClassName = `${classify(tag)}Api`; + const methodPascal = classify(methodName); + return `${apiClassName}${methodPascal}OperationRequest`; +}; + +const getOperationDetails = (spec, operationId) => { + const { components } = spec; + + for (const pathUrl in spec.paths) { + const pathItem = spec.paths[pathUrl]; + const { post } = pathItem; + + if (post?.operationId !== operationId) continue; + + const ref = post.requestBody?.content?.['application/json']?.schema?.$ref; + const schemaName = ref?.split('/').pop(); + const requestBody = components.schemas[schemaName]; + + // There are typically two parts to the API operations - + // the parameters and the request body. These are in different places + // in the spec so we need to address them separately. + const params = []; + for (const param of pathItem.parameters) { + if (param.deprecated) continue; + params.push({ + name: param.name, + description: param.description, + required: param.required, + type: param.schema.type, + }); + } + + const properties = {}; + for (const [propName, prop] of Object.entries(requestBody.properties)) { + if (prop.deprecated) continue; + properties[propName] = prop; + } + + return { + operationId: post.operationId, + tag: post.tags?.[0], + description: pathItem.description || post.summary || '', + parameters: params, + requestBody: [schemaName, properties], + }; + } + + return null; +}; + +const buildPayloadFromOperation = (operation) => { + const [requestSchemaName, requestProperties] = operation.requestBody; + const payload = {}; + + for (const param of operation.parameters) { + payload[param.name] = TYPE_DEFAULTS[param.type]; + } + + const requestPayload = {}; + for (const [propName, prop] of Object.entries(requestProperties)) { + requestPayload[propName] = TYPE_DEFAULTS[prop.type]; + } + payload[requestSchemaName] = requestPayload; + + return payload; +}; + +const buildSectionsFromOperation = (operation) => { + const [requestSchemaName, requestProperties] = operation.requestBody; + // { default: [...], Advanced: [...] } + const groups = {}; + + // Group all of the parameter fields together + for (const param of operation.parameters) { + // `??=` is used to initialize the group if it doesn't exist + (groups.params ??= []).push({ + name: param.name, + type: 'TextInput', + label: formatLabel(param.name), + helperText: param.description, + }); + } + + // Add request body fields to their respective groups + for (const [propName, prop] of Object.entries(requestProperties)) { + const group = prop['x-vault-displayAttrs']?.group || 'default'; + (groups[group] ??= []).push({ + name: `${requestSchemaName}.${propName}`, + type: 'TextInput', + label: prop['x-vault-displayAttrs']?.name || formatLabel(propName), + helperText: prop.description, + }); + } + + // Returns a converted `group` object to the correct format for sections: + // [{ name: 'default', fields: [...] }, { name: 'Advanced', fields: [...] }, ...] + return Object.entries(groups).map(([name, fields]) => ({ name, fields })); +}; + +export const prepFormConfig = (spec, methodName) => { + const operation = getOperationDetails(spec, dasherize(methodName)); + + if (!operation) return null; + + return { + name: methodName, + description: operation.description, + payload: buildPayloadFromOperation(operation), + sections: buildSectionsFromOperation(operation), + apiClass: API_CLASS_FROM_TAG[operation.tag], + requestType: getRequestType(operation.tag, methodName), + }; +}; + +export const generateConfigContent = (config) => { + return ` + /** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + + // ⚠️ AUTO-GENERATED FILE - DO NOT EDIT + // This file is generated from openapi.json + // To customize this form, create an override in + // forms/v2/overrides/ + + import type ApiService from 'vault/services/api'; + import type { FormConfig } from '../form-config'; + import type { ${config.requestType} } from '@hashicorp/vault-client-typescript'; + + /** + * Form configuration for ${config.name} + * Auto-generated from OpenAPI specification + */ + const ${config.name}Config: FormConfig<${config.requestType},unknown> = { + name: '${config.name}', + description: '${config.description}', + submit: async (api: ApiService, payload: ${config.requestType}) => { + return await api.${config.apiClass}.${config.name}Raw(payload); + }, + payload: ${JSON.stringify(config.payload, null, 2)}, + sections: ${JSON.stringify(config.sections, null, 2)}, + }; + + export default ${config.name}Config; + `; +}; diff --git a/ui/package.json b/ui/package.json index 4225a942b4..1bd68e2bdd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,6 +29,7 @@ "fmt:js": "prettier --config .prettierrc.js --write '{app,tests,config,lib}/**/*.js'", "fmt:hbs": "prettier --config .prettierrc.js --write '**/*.hbs'", "fmt:styles": "prettier --write app/styles/**/*.*", + "generate:form-config": "node --no-warnings scripts/generate-form-config.js", "start": "VAULT_ADDR=http://127.0.0.1:8200; pnpm build:jsondiffpatch && ember server --proxy=$VAULT_ADDR", "start2": "pnpm build:jsondiffpatch && ember server --proxy=http://127.0.0.1:8202 --port=4202", "start:chroot": "ember server --proxy=http://127.0.0.1:8300 --port=4300", diff --git a/ui/scripts/generate-form-config.js b/ui/scripts/generate-form-config.js new file mode 100644 index 0000000000..d3e6341673 --- /dev/null +++ b/ui/scripts/generate-form-config.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node + +/* eslint-env node */ + +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +/** + * OpenAPI-based form config generator + * + * Usage: + * pnpm generate:form-config + * + * Example: + * pnpm generate:form-config mountsEnableSecretsEngine + * + * The API class is automatically determined from the OpenAPI spec's tags. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { prepFormConfig, generateConfigContent } from '../app/utils/form-config-generator.js'; +import { dasherize } from '@ember/string'; + +// ES module workaround to get absolute paths for __dirname, +// ensuring this script can work regardless of where it's run from +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OPENAPI_PATH = path.join(__dirname, '../node_modules/@hashicorp/vault-client-typescript/openapi.json'); +const OUTPUT_DIR = path.join(__dirname, '../app/forms/v2/generated'); + +const normalize = (msg) => { + return msg.trim().replace(/\n\s+/g, '\n'); +}; + +/** + * Compose functions left-to-right, passing output of each as input to the next + * Example: pipe(fn1, fn2, fn3)(x) === fn3(fn2(fn1(x))) + */ +const pipe = (...fns) => { + return (initial) => fns.reduce((value, fn) => fn(value), initial); +}; + +const parseArgs = () => { + const args = process.argv.slice(2); + const method = args[0]; + + if (!method || method.startsWith('--')) { + const err = normalize(` + ❌ Missing required argument: methodName + 💡 Usage: pnpm generate:form-config + 💡 Example: pnpm generate:form-config mountsEnableSecretsEngine + `); + + console.error(err); + process.exit(1); + } + + return { method }; +}; + +const loadOpenAPISpec = () => { + try { + const spec = JSON.parse(fs.readFileSync(OPENAPI_PATH, 'utf-8')); + console.log(`✅ Found openapi.json with ${Object.keys(spec.paths).length} paths...`); + return spec; + } catch (error) { + console.error(`❌ Error loading openapi.json: ${error.message}`); + process.exit(1); + } +}; + +const prepFormConfigWithLogging = (spec, methodName) => { + const operationId = dasherize(methodName); + console.log(`🔍 Searching for ${operationId} operation in openapi.json...`); + + const config = prepFormConfig(spec, methodName); + if (!config) { + const err = normalize(` + ❌ Operation "${operationId}" not found in openapi.json + 💡 If this is a plugin-based method, ensure the plugin is enabled and regenerate openapi.json + `); + console.error(err); + process.exit(1); + } + + if (!config.apiClass) { + const err = normalize(` + ❌ Could not determine API class for "${operationId}" + 💡 The operation may be missing tags in the OpenAPI spec + `); + console.error(err); + process.exit(1); + } + + console.log(`🔨 Building form config for ${config.name}... \n`); + return config; +}; + +const writeAndFormat = (content, methodName) => { + const filename = `${dasherize(methodName)}-config.ts`; + const filePath = path.join(OUTPUT_DIR, filename); + + fs.writeFileSync(filePath, content, 'utf-8'); + execSync(`pnpm prettier --write "${filePath}"`, { stdio: 'pipe' }); + + return filename; +}; + +const main = () => { + const startTime = Date.now(); + const { method } = parseArgs(); + console.log(`⚡️ Generating form config for ${method} method...\n`); + + const filename = pipe( + (spec) => prepFormConfigWithLogging(spec, method), + (config) => generateConfigContent(config), + (content) => writeAndFormat(content, method) + )(loadOpenAPISpec()); + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + const msg = normalize(` + ✨ Done! Completed in ${duration}s + Output: app/forms/v2/generated/${filename} + `); + + console.log(msg); +}; + +main(); diff --git a/ui/tests/helpers/stubs.js b/ui/tests/helpers/stubs.js index efee319f17..a1afb17376 100644 --- a/ui/tests/helpers/stubs.js +++ b/ui/tests/helpers/stubs.js @@ -78,3 +78,110 @@ export function overrideResponse(httpStatus = 200, payload = {}) { } export const formatError = (msg) => JSON.stringify({ errors: [msg] }); + +/** + * Minimal OpenAPI spec fixture for testing. + * Contains only the mounts-enable-secrets-engine operation. + */ +export const OAS_STUB = { + openapi: '3.0.0', + info: { + title: 'Vault API', + version: '1.0.0', + }, + paths: { + '/sys/mounts/{path}': { + description: 'Mount a new backend at a new path.', + parameters: [ + { + name: 'path', + description: 'The path to mount to. Example: "aws/east"', + in: 'path', + schema: { type: 'string' }, + required: true, + }, + ], + post: { + summary: 'Enable a new secrets engine at the given path.', + operationId: 'mounts-enable-secrets-engine', + tags: ['system'], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/MountsEnableSecretsEngineRequest', + }, + }, + }, + }, + responses: { + 204: { description: 'OK' }, + }, + }, + }, + }, + components: { + schemas: { + MountsEnableSecretsEngineRequest: { + type: 'object', + properties: { + config: { + type: 'object', + description: 'Configuration for this mount, such as default_lease_ttl and max_lease_ttl.', + }, + description: { + type: 'string', + description: 'User-friendly description for this mount.', + }, + external_entropy_access: { + type: 'boolean', + description: "Whether to give the mount access to Vault's external entropy.", + default: false, + deprecated: true, + }, + local: { + type: 'boolean', + description: + 'Mark the mount as a local mount, which is not replicated and is unaffected by replication.', + default: false, + }, + options: { + type: 'object', + description: + 'The options to pass into the backend. Should be a json object with string keys and values.', + }, + plugin_name: { + type: 'string', + description: 'Name of the plugin to mount based from the name registered in the plugin catalog.', + }, + plugin_version: { + type: 'string', + description: 'The semantic version of the plugin to use, or image tag if oci_image is provided.', + }, + seal_wrap: { + type: 'boolean', + description: 'Whether to turn on seal wrapping for the mount.', + default: false, + 'x-vault-displayAttrs': { + name: 'Seal Wrap', + group: 'Advanced', + }, + }, + type: { + type: 'string', + description: 'The type of the backend. Example: "passthrough"', + }, + allowed_managed_keys: { + type: 'array', + description: 'List of managed key names allowed for this mount.', + 'x-vault-displayAttrs': { + name: 'Allowed Managed Keys', + group: 'Advanced', + }, + }, + }, + }, + }, + }, +}; diff --git a/ui/tests/unit/utils/form-config-generator-test.js b/ui/tests/unit/utils/form-config-generator-test.js new file mode 100644 index 0000000000..caf6b7ecd9 --- /dev/null +++ b/ui/tests/unit/utils/form-config-generator-test.js @@ -0,0 +1,295 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { prepFormConfig, generateConfigContent } from 'vault/utils/form-config-generator'; +import { OAS_STUB as SPEC } from 'vault/tests/helpers/stubs'; + +module('Unit | Utility | form-config-generator', function ({ beforeEach }) { + beforeEach(function () { + // This is essentially what happens in the `main()` function + // within the CLI script generate-form-configs.ts + this.config = prepFormConfig(SPEC, 'mountsEnableSecretsEngine'); + this.result = generateConfigContent(this.config); + }); + + module('#prepFormConfig', function () { + test('returns null for unknown operation', function (assert) { + const result = prepFormConfig(SPEC, 'unknownOperation'); + assert.strictEqual(result, null, 'returns null for unknown operation'); + }); + + test('returns `name`, `apiClass`, and `requestType` properties', function (assert) { + assert.propContains( + this.config, + { + name: 'mountsEnableSecretsEngine', + apiClass: 'sys', + requestType: 'SystemApiMountsEnableSecretsEngineOperationRequest', + }, + 'config contains expected name, apiClass, and requestType' + ); + }); + + module('`payload` object', function () { + test('includes expected default values', function (assert) { + assert.deepEqual( + this.config.payload, + { + path: '', + MountsEnableSecretsEngineRequest: { + config: {}, + description: '', + local: false, + options: {}, + plugin_name: '', + plugin_version: '', + seal_wrap: false, + type: '', + allowed_managed_keys: [], + }, + }, + 'payload matches expected structure and defaults' + ); + }); + + test('does not include deprecated properties', function (assert) { + const { properties } = SPEC.components.schemas.MountsEnableSecretsEngineRequest; + const deprecatedProperties = Object.keys(properties).filter((p) => properties[p].deprecated); + const payloadKeys = Object.keys(this.config.payload.MountsEnableSecretsEngineRequest); + + // Double-check that the fixture actually includes deprecated properties + assert.true(deprecatedProperties.length > 0, 'fixture includes deprecated properties'); + + assert.notPropContains( + payloadKeys, + deprecatedProperties, + 'payload does not include deprecated properties' + ); + }); + }); + + module('`sections` array', function ({ beforeEach }) { + beforeEach(function () { + this.expectedSections = [ + { + name: 'params', + fields: [ + { + name: 'path', + type: 'TextInput', + label: 'Path', + helperText: 'The path to mount to. Example: "aws/east"', + }, + ], + }, + { + name: 'default', + fields: [ + { + name: 'MountsEnableSecretsEngineRequest.config', + type: 'TextInput', + label: 'Config', + helperText: 'Configuration for this mount, such as default_lease_ttl and max_lease_ttl.', + }, + { + name: 'MountsEnableSecretsEngineRequest.description', + type: 'TextInput', + label: 'Description', + helperText: 'User-friendly description for this mount.', + }, + { + name: 'MountsEnableSecretsEngineRequest.local', + type: 'TextInput', + label: 'Local', + helperText: + 'Mark the mount as a local mount, which is not replicated and is unaffected by replication.', + }, + { + name: 'MountsEnableSecretsEngineRequest.options', + type: 'TextInput', + label: 'Options', + helperText: + 'The options to pass into the backend. Should be a json object with string keys and values.', + }, + { + name: 'MountsEnableSecretsEngineRequest.plugin_name', + type: 'TextInput', + label: 'Plugin name', + helperText: + 'Name of the plugin to mount based from the name registered in the plugin catalog.', + }, + { + name: 'MountsEnableSecretsEngineRequest.plugin_version', + type: 'TextInput', + label: 'Plugin version', + helperText: + 'The semantic version of the plugin to use, or image tag if oci_image is provided.', + }, + { + name: 'MountsEnableSecretsEngineRequest.type', + type: 'TextInput', + label: 'Type', + helperText: 'The type of the backend. Example: "passthrough"', + }, + ], + }, + { + name: 'Advanced', + fields: [ + { + name: 'MountsEnableSecretsEngineRequest.seal_wrap', + type: 'TextInput', + label: 'Seal Wrap', + helperText: 'Whether to turn on seal wrapping for the mount.', + }, + { + name: 'MountsEnableSecretsEngineRequest.allowed_managed_keys', + type: 'TextInput', + label: 'Allowed Managed Keys', + helperText: 'List of managed key names allowed for this mount.', + }, + ], + }, + ]; + }); + + test('returns with expected fields', function (assert) { + assert.deepEqual( + this.config.sections, + this.expectedSections, + 'sections match expected structure and fields' + ); + }); + + test('excludes deprecated properties from sections', function (assert) { + const { properties } = SPEC.components.schemas.MountsEnableSecretsEngineRequest; + const deprecatedProperties = Object.keys(properties).filter((p) => properties[p].deprecated); + const allFieldNames = this.config.sections.flatMap((s) => s.fields.map((f) => f.name)); + + assert.notPropContains( + allFieldNames, + deprecatedProperties, + 'sections do not include deprecated properties' + ); + }); + + test('is grouped by x-vault-displayAttrs group', function (assert) { + const { properties } = SPEC.components.schemas.MountsEnableSecretsEngineRequest; + + // Create a hashmap of expected groups and their field names based on the spec + // eg: { default: ['config', 'description', ...], Advanced: ['seal_wrap', ...] } + const expectedGroups = Object.keys(properties).reduce((acc, prop) => { + if (properties[prop].deprecated) { + return acc; + } + const displayAttrs = properties[prop]['x-vault-displayAttrs']; + const group = displayAttrs?.group || 'default'; + if (!acc[group]) { + acc[group] = []; + } + acc[group].push(prop); + return acc; + }, {}); + + // Sections for path is less relevant... + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const [_pathSection, ...propSections] = this.config.sections; + + propSections.forEach((section) => { + const expectedFieldNames = expectedGroups[section.name]; + const currentFieldNames = section.fields.map((f) => + // Remove the prefix to match property names as field names + // as the prefix is utilized for mapping to the payload structure + f.name.replace('MountsEnableSecretsEngineRequest.', '') + ); + + assert.deepEqual( + currentFieldNames, + expectedFieldNames, + `${section.name} section contains correct fields from spec` + ); + }); + }); + + test('uses x-vault-displayAttrs.name as label when available', function (assert) { + const { properties } = SPEC.components.schemas.MountsEnableSecretsEngineRequest; + + // Capture a list of fields that have x-vault-displayAttrs.name defined + const fieldsWithDisplayNames = Object.keys(properties).reduce((acc, prop) => { + const displayAttrs = properties[prop]['x-vault-displayAttrs']; + if (displayAttrs && displayAttrs.name && !properties[prop].deprecated) { + acc.push({ key: prop, label: displayAttrs.name }); + } + return acc; + }, []); + + fieldsWithDisplayNames.forEach(({ key, label }) => { + const field = this.config.sections + .flatMap((s) => s.fields) + .find((field) => field.name.endsWith(key)); + + assert.strictEqual(field?.label, label, `${key} uses x-vault-displayAttrs.name as label`); + }); + }); + + test('falls back to sentence case for label when x-vault-displayAttrs.name is missing', function (assert) { + const defaultSection = this.config.sections.find((s) => s.name === 'default'); + const pluginNameField = defaultSection.fields.find((f) => f.name.includes('plugin_name')); + + assert.strictEqual(pluginNameField.label, 'Plugin name', 'falls back to sentence case for label'); + }); + }); + }); + + module('#generateConfigContent', function () { + test('produces content with correct imports', function (assert) { + assert.true( + this.result.includes("import type ApiService from 'vault/services/api'"), + 'includes ApiService import' + ); + assert.true( + this.result.includes(`import type { ${this.config.requestType} }`), + 'includes request type import' + ); + }); + + test('produces content with correct name property', function (assert) { + assert.true(this.result.includes(`name: '${this.config.name}'`), 'includes correct name property'); + }); + + test('produces content with correct submit implementation', function (assert) { + const expectedSubmit = `submit: async (api: ApiService, payload: ${this.config.requestType}) => { + return await api.${this.config.apiClass}.${this.config.name}Raw(payload); + },`; + + assert.true( + this.result.includes(expectedSubmit), + 'submit property has correct async function implementation' + ); + }); + + test('produces content with correct payload', function (assert) { + assert.true( + this.result.includes(JSON.stringify(this.config.payload, null, 2)), + 'payload matches config payload' + ); + }); + + test('produces content with correct sections', function (assert) { + assert.true( + this.result.includes(JSON.stringify(this.config.sections, null, 2)), + 'sections match config sections' + ); + }); + + test('produces content with correct export statement', function (assert) { + assert.true( + this.result.includes(`export default ${this.config.name}Config`), + 'exports config with correct name' + ); + }); + }); +}); From 75484c6082033f27820c45895ba20a217d5ce576 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 25 Feb 2026 14:17:34 -0700 Subject: [PATCH 007/468] Backport UI: Fix secrets engine list not refreshing after deleting within a namespace into ce/main (#12538) * no-op commit * UI: Fix secrets engine list not refreshing after deleting within a namespace (#12511) * rename arg to differentiate between selectionKey for HDS component * add more test coverage, fix bug for isObject rendering * fix namespace test coverage to actually test relevant stuff * more test updates to use latest selectors * add changelog * add more test coverage --------- Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- changelog/_12511.txt | 3 + ui/app/components/secret-engine/list.hbs | 47 +++--- ui/lib/core/addon/components/list-table.hbs | 30 ++-- ui/lib/core/addon/components/list-table.ts | 54 +++--- .../core/addon/components/manage-dropdown.ts | 33 ++-- ui/tests/acceptance/console-test.js | 4 +- .../secret-engine-list-view-test.js | 151 ++++++++--------- .../secrets/backend/database/secret-test.js | 4 +- .../secrets/backend/generic/secret-test.js | 2 +- .../kv/kv-v2-workflow-edge-cases-test.js | 4 +- .../kv/kv-v2-workflow-navigation-test.js | 4 +- ui/tests/acceptance/secrets/mounts-test.js | 43 +---- .../integration/components/list-table-test.js | 154 +++++++++++++----- ui/tests/integration/components/list-test.js | 20 +-- .../components/manage-dropdown-test.js | 56 +++++++ 15 files changed, 349 insertions(+), 260 deletions(-) create mode 100644 changelog/_12511.txt diff --git a/changelog/_12511.txt b/changelog/_12511.txt new file mode 100644 index 0000000000..a35a68e911 --- /dev/null +++ b/changelog/_12511.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix stale secrets engine list after deleting an engine within a namespace. +`` diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 663b66c494..302e6dad40 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -117,7 +117,7 @@ @@ -147,31 +147,38 @@ <:customTableItem as |itemData|> {{#let (this.getEngineResourceData itemData.path) as |backendData|}} - - - - {{#if backendData.isSupportedBackend}} - {{backendData.path}} - {{else}} - {{backendData.path}} + {{! Only render if we have backendData, so this doesn't try to render while data for the list refreshes }} + {{#if backendData}} + + + + {{#if backendData.isSupportedBackend}} + {{backendData.path}} + {{else}} + {{backendData.path}} + {{/if}} {{/if}} {{/let}} <:popupMenu as |rowData|> {{#let (this.getEngineResourceData rowData.path) as |backendData|}} - + {{! Only render if we have backendData, so this doesn't try to render while data for the list refreshes }} + {{#if backendData}} + + {{/if}} {{/let}} diff --git a/ui/lib/core/addon/components/list-table.hbs b/ui/lib/core/addon/components/list-table.hbs index 2b8ad804bb..8ca65fb394 100644 --- a/ui/lib/core/addon/components/list-table.hbs +++ b/ui/lib/core/addon/components/list-table.hbs @@ -8,33 +8,35 @@ <:body as |B|> - {{#each this.columnKeys as |key index|}} - {{#if (and (eq key "popupMenu") (has-block "popupMenu"))}} + {{#each @columns as |column|}} + {{#if (and (eq column.key "popupMenu") (has-block "popupMenu"))}} {{yield B.data to="popupMenu"}} {{else}} - {{#let (get B.data key) as |value|}} - - {{#if (get (get @columns index) "customTableItem")}} - {{yield B.data to="customTableItem"}} - {{else}} + + {{#if column.customTableItem}} + {{yield B.data to="customTableItem"}} + {{else}} + {{#let (get B.data column.key) as |value|}} {{! stringify value if it is an array or object, otherwise render directly }} {{if (this.isObject value) (stringify value) value}} - {{/if}} - - {{/let}} + {{/let}} + {{/if}} + {{/if}} {{/each}} diff --git a/ui/lib/core/addon/components/list-table.ts b/ui/lib/core/addon/components/list-table.ts index 32994e9869..208aef4459 100644 --- a/ui/lib/core/addon/components/list-table.ts +++ b/ui/lib/core/addon/components/list-table.ts @@ -11,27 +11,20 @@ import { paginate } from 'core/utils/paginate-list'; /** * @module ListTable - * `ListTable` component is used for rendering a list of items in a table. + * `ListTable` renders paginated table rows with optional row selection. * * @example * * - * @param {array} columns - An array of type TableColumn, for populating table headers and any optional functionality (ie. isSortable...) - * @param {array} data - An array of data to display corresponding to columns. (ie. the key from a column corresponds to the parameter value of the data object your passing in) - * @param {string} [selectionKey] - string of desired param value to be used as a unique identifier for a selected row - setting this arg automatically sets 'isSelectable' on the table to make rows selectable - * @param {function} [onSelectionChange] - Provided function for handling when rows are selected - * - * For custom column items that are not 1 to 1 with their dataset (ie. these items have conditional icons, colors, generated text etc) - * within the column type, the 'customTableItem' flag will allow the parent component to {{yield}} any custom implementation from the parent for those items - * but the yield block in the parent must be 'customTableItem'. - * - * similarly, if 'isSelectable' is true and 'onSelectionChange' is being handled - * For displaying new content based on what's selected, the parent component can also pass in a yield block 'selectedItems' + * @param {TableColumn[]} columns - Used to populate table headers and specify column display options or functionality (e.g. `isSortable`). See the `columns` in the component API for available HDS parameters @see https://helios.hashicorp.design/components/table/advanced-table?tab=code#advancedtable + * @param {object[]} data - An array of data to display corresponding to columns. (ie. the key from a column corresponds to the parameter value of the data object your passing in) + * @param {string} [selectionKeyField] - string of desired param to be use as a unique identifier for a selected row, if provided 'isSelectable' is set to "true" and table rows are selectable + * @param {OnSelectionChange} [onSelectionChange] - Provided function for handling when rows are selected * * If there's an 'Action' column (ie. possibly for manipulating data rows, or navigating to a page per that row data, etc) * The parent component must specify the key as 'popupMenu' for that column and pass in a yield block 'popupMenu' for it to render per each item under the 'action' column. @@ -41,20 +34,33 @@ import { paginate } from 'core/utils/paginate-list'; interface TableColumn { key: string; label: string; - selectionKey?: string; - customTableItem?: boolean; - onSelectionChange?: CallableFunction; + customTableItem?: boolean; // when true, the parent yields a custom display for that column } +interface SelectableRowState { + selectionKey: string; // value of selected item + isSelected: boolean; +} + +interface OnSelectionArgs { + selectionKey: string; + selectionCheckboxElement: HTMLInputElement; + selectedRowsKeys: string[]; + selectableRowsStates: SelectableRowState[]; +} + +type OnSelectionChange = (callbackArgs: OnSelectionArgs) => void; + interface Args { data: Array; columns: TableColumn[]; + selectionKeyField?: string; + onSelectionChange?: OnSelectionChange; } export default class ListTable extends Component { @tracked currentPage = 1; @tracked pageSize = 10; - // WORKAROUND to manually re-render Hds::Pagination::Numbered to force update @currentPage @tracked renderPagination = true; @@ -66,10 +72,6 @@ export default class ListTable extends Component { return paginated; } - get columnKeys() { - return this.args.columns.map((k: TableColumn) => k['key'] ?? k['label']); - } - @action handlePaginationChange(action: 'currentPage' | 'pageSize', value: number) { this[action] = value; @@ -84,4 +86,14 @@ export default class ListTable extends Component { this.renderPagination = true; }); } + + // TEMPLATE HELPERS + isObject = (value: any) => typeof value === 'object' && value !== null; + + identifier = (cellData: Record) => { + const firstColumn = this.args.columns[0]?.key; + // Use selectionKeyField if provided, otherwise default to value of the first column + const identifier = this.args.selectionKeyField || firstColumn; + return identifier ? cellData[identifier] : null; + }; } diff --git a/ui/lib/core/addon/components/manage-dropdown.ts b/ui/lib/core/addon/components/manage-dropdown.ts index be019d9b7c..d628998e9c 100644 --- a/ui/lib/core/addon/components/manage-dropdown.ts +++ b/ui/lib/core/addon/components/manage-dropdown.ts @@ -7,6 +7,7 @@ import { action } from '@ember/object'; import { service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import routerLookup from 'core/utils/router-lookup'; import type RouterService from '@ember/routing/router-service'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; @@ -18,20 +19,20 @@ import type FlashMessageService from 'vault/services/flash-messages'; * Reusable component for displaying the Manage dropdown used in secret engine headers & secret engine mount list. * * @example - * // In main app page headers and list components — uses the resource getter for the full absolute route + * In main app page headers and list components — uses the resource getter for the full absolute route * * - * // In Ember engine templates (pki, kubernetes, ldap, kmip, kv) — pass the short relative route, - * // since HDS @route resolves relative to the engine's router mount + * In Ember engine templates (pki, kubernetes, ldap, kmip, kv) — pass the short relative route, + * since HDS @route resolves relative to the engine's router mount * * - * // With custom menu items (like KV's Generate policy) — icon variant in a Ember engine list + * With custom menu items (like KV's Generate policy) — icon variant in a Ember engine list * { - @service declare readonly router: RouterService; - @service('app-router') declare readonly appRouter: RouterService; @service declare readonly api: ApiService; @service declare readonly flashMessages: FlashMessageService; @tracked engineToDisable: SecretsEngineResource | undefined = undefined; + get router(): RouterService { + return routerLookup(this); + } + get isIcon() { return this.args.variant === 'icon'; } @@ -72,17 +75,11 @@ export default class ManageDropdown extends Component { return this.args.model.type !== 'cubbyhole'; } - transitionToBackends() { - // First try using the router service, which is available in most contexts - if (this.router) { - this.router.transitionTo('vault.cluster.secrets.backends'); - return; - } - - // Fallback for ember-engine components which use appRouter instead of router service - if (this.appRouter) { - this.appRouter.transitionTo('vault.cluster.secrets.backends'); - } + transitionOrRefresh() { + const { currentRouteName } = this.router; + // Call refresh() when currently on the route so data properly refreshes even when in a namespace. + const method = currentRouteName === 'vault.cluster.secrets.backends' ? 'refresh' : 'transitionTo'; + this.router[method]('vault.cluster.secrets.backends'); } @action @@ -103,7 +100,7 @@ export default class ManageDropdown extends Component { try { await this.api.sys.mountsDisableSecretsEngine(id); this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - this.transitionToBackends(); + this.transitionOrRefresh(); } catch (error) { const { message } = await this.api.parseError(error); this.flashMessages.danger( diff --git a/ui/tests/acceptance/console-test.js b/ui/tests/acceptance/console-test.js index c56dbf44f8..4bb9e47c24 100644 --- a/ui/tests/acceptance/console-test.js +++ b/ui/tests/acceptance/console-test.js @@ -34,7 +34,7 @@ module('Acceptance | console', function (hooks) { await consoleComponent.runCommands('refresh'); await settled(); for (const id of ids) { - assert.dom(GENERAL.tableRow(`console-route-${id}/`)).exists('new engine is shown on the page'); + assert.dom(GENERAL.listItem(`console-route-${id}/`)).exists('new engine is shown on the page'); } // Clean up for (const id of ids) { @@ -45,7 +45,7 @@ module('Acceptance | console', function (hooks) { await consoleComponent.runCommands('refresh'); await settled(); for (const id of ids) { - assert.dom(GENERAL.tableRow(`console-route-${id}/`)).doesNotExist('engine was removed'); + assert.dom(GENERAL.listItem(`console-route-${id}/`)).doesNotExist('engine was removed'); } }); diff --git a/ui/tests/acceptance/secret-engine-list-view-test.js b/ui/tests/acceptance/secret-engine-list-view-test.js index 4266c2e8d8..6e14e8a295 100644 --- a/ui/tests/acceptance/secret-engine-list-view-test.js +++ b/ui/tests/acceptance/secret-engine-list-view-test.js @@ -10,13 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; -import { - createTokenCmd, - deleteEngineCmd, - mountEngineCmd, - runCmd, - tokenWithPolicyCmd, -} from 'vault/tests/helpers/commands'; +import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers'; import page from 'vault/tests/pages/settings/mount-secret-backend'; import localStorage from 'vault/lib/local-storage'; @@ -42,10 +36,11 @@ module('Acceptance | secret-engine list view', function (hooks) { // the new API service camelizes response keys, so this tests is to assert that does NOT happen when we re-implement it test('it does not camelize the secret mount path', async function (assert) { + const path = `aws_${this.uid}`; await visit('/vault/secrets-engines'); await page.enableEngine(); await click(GENERAL.cardContainer('aws')); - await fillIn(GENERAL.inputByAttr('path'), 'aws_engine'); + await fillIn(GENERAL.inputByAttr('path'), path); await click(GENERAL.submitButton); await click(GENERAL.breadcrumbLink('Secrets engines')); assert.strictEqual( @@ -53,9 +48,9 @@ module('Acceptance | secret-engine list view', function (hooks) { 'vault.cluster.secrets.backends', 'breadcrumb navigates to the list page' ); - assert.dom(GENERAL.tableData('aws_engine/', 'path')).hasTextContaining('aws_engine/'); - // cleanup - await runCmd(deleteEngineCmd('aws_engine')); + await fillIn(GENERAL.inputSearch('secret-engine-path'), path); + assert.dom(GENERAL.tableData(0, 'path')).hasText(`${path}/`); + await runCmd(deleteEngineCmd(path)); }); test('after enabling an unsupported engine it takes you to list page', async function (assert) { @@ -70,9 +65,11 @@ module('Acceptance | secret-engine list view', function (hooks) { }); test('after enabling a supported engine it takes you to mount page, can see configure and clicking breadcrumb takes you back to list page', async function (assert) { + const path = `aws-${this.uid}`; await visit('/vault/secrets-engines'); await page.enableEngine(); await click(GENERAL.cardContainer('aws')); + await fillIn(GENERAL.inputByAttr('path'), path); await click(GENERAL.submitButton); await click(GENERAL.dropdownToggle('Manage')); @@ -85,79 +82,7 @@ module('Acceptance | secret-engine list view', function (hooks) { 'breadcrumb navigates to the list page' ); // cleanup - await runCmd(deleteEngineCmd('aws')); - }); - - test('enterprise: cannot view list without permissions inside a namespace', async function (assert) { - this.namespace = `ns-${this.uid}`; - const enginePath1 = `kv-t1-${this.uid}`; - const userDefault = await runCmd(createTokenCmd()); // creates a default user token - - await runCmd([`write sys/namespaces/${this.namespace} -force`]); // creates a namespace - await loginNs(this.namespace); //logs into namespace with root token - await runCmd(mountEngineCmd('kv', enginePath1)); // mounts a kv engine in namespace - - await loginNs(this.namespace, userDefault); // logs into that same namespace with a default user token - - await visit(`/vault/secrets-engines?namespace=${this.namespace}`); // nav to specified namespace list - assert.strictEqual( - currentURL(), - `/vault/secrets-engines?namespace=${this.namespace}`, - 'Should be on main secret engines list page within namespace.' - ); - assert.dom(GENERAL.tableData(`${enginePath1}/`, 'path')).doesNotExist(); // without permissions, engine should not show for this user - - // cleanup namespace - await login(); - await runCmd(`delete sys/namespaces/${this.namespace}`); - }); - - test('enterprise: can view list with permissions inside a namespace', async function (assert) { - this.namespace = `ns-${this.uid}`; - const enginePath1 = `kv-t2-${this.uid}`; - const userToken = await runCmd( - tokenWithPolicyCmd( - 'policy', - `path "${this.namespace}/sys/*" { - capabilities = ["create", "read", "update", "delete", "list"] - }` - ) - ); - - await runCmd([`write sys/namespaces/${this.namespace} -force`]); - await loginNs(this.namespace, userToken); // logs into namespace with user token - await runCmd(mountEngineCmd('kv', enginePath1)); // mount kv engine as user - - await loginNs(this.namespace); // logs into namespace with root token - - await visit(`/vault/secrets-engines?namespace=${this.namespace}`); // nav to specified namespace list - assert.strictEqual( - currentURL(), - `/vault/secrets-engines?namespace=${this.namespace}`, - 'Should be on main secret engines list page within namespace.' - ); - - assert.dom(GENERAL.tableData(`${enginePath1}/`, 'path')).exists(); // with permissions, able to see the engine in list - - // cleanup namespace - await login(); - await runCmd(`delete sys/namespaces/${this.namespace}`); - }); - - test('enterprise: it should navigate to cubbyhole list view in child namespace', async function (assert) { - this.namespace = `ns-${this.uid}`; - - await runCmd([`write sys/namespaces/${this.namespace} -force`]); - await loginNs(this.namespace); - localStorage.setItem('dismissed-wizards', ['secret-engines']); - await visit(`/vault/secrets-engines?namespace=${this.namespace}`); - await click(`${GENERAL.tableData('cubbyhole/', 'path')} a`); - - assert.dom(GENERAL.emptyStateTitle).hasText('No secrets in this backend'); - - // cleanup namespace - await login(); - await runCmd(`delete sys/namespaces/${this.namespace}`); + await runCmd(deleteEngineCmd(path)); }); test('after disabling it stays on the list view', async function (assert) { @@ -167,7 +92,9 @@ module('Acceptance | secret-engine list view', function (hooks) { await visit('/vault/secrets-engines'); // to reduce flakiness, searching by engine name first in case there are pagination issues await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath); - assert.dom(GENERAL.tableData(`${enginePath}/`, 'path')).exists('the alicloud engine is mounted'); + assert + .dom(GENERAL.tableData(0, 'path')) + .hasTextContaining(`${enginePath}/`, 'the alicloud engine is mounted'); await click(GENERAL.menuTrigger); await click(GENERAL.menuItem('Delete')); @@ -188,7 +115,8 @@ module('Acceptance | secret-engine list view', function (hooks) { // check kv1 await visit('/vault/secrets-engines'); - await click(`${GENERAL.tableData(`${enginePath1}/`, 'path')} a`); + await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath1); + await click(GENERAL.linkTo(`${enginePath1}/`)); for (let i = 0; i <= 15; i++) { await createSecret(`secret-${i}`, 'foo', 'bar', enginePath1); } @@ -221,7 +149,8 @@ module('Acceptance | secret-engine list view', function (hooks) { // check kv1 await visit('/vault/secrets-engines'); - await click(`${GENERAL.tableData(`${enginePath1}/`, 'path')} a`); + await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath1); + await click(GENERAL.linkTo(`${enginePath1}/`)); for (let i = 0; i <= 15; i++) { await createSecret(`${parentPath}/secret-${i}`, 'foo', 'bar', enginePath1); } @@ -245,4 +174,52 @@ module('Acceptance | secret-engine list view', function (hooks) { // cleanup await runCmd(deleteEngineCmd(enginePath1)); }); + + module('enterprise | namespaces', function (hooks) { + hooks.beforeEach(async function () { + await login(); + this.namespace = `ns-${this.uid}`; + await runCmd([`write sys/namespaces/${this.namespace} -force`]); + await loginNs(this.namespace); // log into namespace with root token + // dismiss wizard + localStorage.setItem('dismissed-wizards', ['secret-engines']); + }); + + // Ember route models won't refresh within a namespace when this.router.transitionTo() is called + // because ?namespace is a query param that remains the same so the app doesn't detect any changes + // and therefore does not refire the model hook. + // this.router.refresh() must be called to refire model hooks and request fresh data. + test('list refreshes after deleting an engine in a namespace', async function (assert) { + const enginePath1 = `kv-t2-${this.uid}`; + await runCmd(mountEngineCmd('kv', enginePath1)); // mount kv engine in the namespace + await visit(`/vault/secrets-engines?namespace=${this.namespace}`); // nav to specified namespace list + + assert.dom(GENERAL.linkTo(`${enginePath1}/`)).exists(); + assert.dom(GENERAL.tableRow()).exists({ count: 2 }, 'only 2 secret engines are listed'); + // Delete the engine + await click(`${GENERAL.listItem(`${enginePath1}/`)} ${GENERAL.menuTrigger}`); + await click(GENERAL.menuItem('Delete')); + await click(GENERAL.confirmButton); + assert.strictEqual( + currentRouteName(), + 'vault.cluster.secrets.backends', + 'redirects to the backends list page' + ); + assert.dom(GENERAL.linkTo(enginePath1)).doesNotExist('deleted engine is no longer in list'); + assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'only 1 secret engine is listed'); + // cleanup namespace + await login(); + await runCmd(`delete sys/namespaces/${this.namespace}`); + }); + + test('it should navigate to cubbyhole list view in child namespace', async function (assert) { + await visit(`/vault/secrets-engines?namespace=${this.namespace}`); + await click(GENERAL.linkTo('cubbyhole/')); + assert.dom(GENERAL.emptyStateTitle).hasText('No secrets in this backend'); + + // cleanup namespace + await login(); + await runCmd(`delete sys/namespaces/${this.namespace}`); + }); + }); }); diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js index a329e19c31..6b1ffd805f 100644 --- a/ui/tests/acceptance/secrets/backend/database/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/database/secret-test.js @@ -41,7 +41,7 @@ const newConnection = async ( const navToConnection = async (backend, connection) => { await visit('/vault/secrets-engines'); - await click(`${GENERAL.tableData(`${backend}/`, 'path')} a`); + await click(GENERAL.linkTo(`${backend}/`)); await click(GENERAL.secretTab('Connections')); await click(SES.secretLink(connection)); return; @@ -536,7 +536,7 @@ module('Acceptance | secrets/database/*', function (hooks) { // Check with restricted permissions await login(token); await click(GENERAL.navLink('Secrets')); - assert.dom(GENERAL.tableData(`${backend}/`, 'path')).exists('Shows backend on secret list page'); + assert.dom(GENERAL.listItem(`${backend}/`)).exists('Shows backend on secret list page'); await navToConnection(backend, connection); assert.strictEqual( currentURL(), diff --git a/ui/tests/acceptance/secrets/backend/generic/secret-test.js b/ui/tests/acceptance/secrets/backend/generic/secret-test.js index 4f55636f39..99c14a736a 100644 --- a/ui/tests/acceptance/secrets/backend/generic/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/generic/secret-test.js @@ -67,7 +67,7 @@ module('Acceptance | secrets/generic/create', function (hooks) { ]); await visit('/vault/secrets-engines'); await fillIn(GENERAL.inputSearch('secret-engine-path'), path); - await click(`${GENERAL.tableData(`${path}/`, 'path')} a`); + await click(GENERAL.linkTo(`${path}/`)); assert.strictEqual( currentRouteName(), 'vault.cluster.secrets.backend.kv.list', diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js index 12bd215395..961b59a737 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js @@ -576,7 +576,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) const navToEngine = async (backend) => { await click(GENERAL.navLink('Secrets')); - return await click(`${GENERAL.tableData(`${backend}/`, 'path')} a`); + return await click(GENERAL.linkTo(`${backend}/`)); }; const assertDeleteActions = (assert, expected = ['delete', 'destroy']) => { @@ -734,6 +734,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) // undelete flow await click(PAGE.detail.undelete); + await waitFor(GENERAL.overviewCard.container('Current version')); assert .dom(GENERAL.overviewCard.container('Current version')) .hasTextContaining('Current version Create new The current version of this secret.'); @@ -748,6 +749,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) await click(PAGE.detail.deleteConfirm); await click(PAGE.secretTab('Secret')); assertDeleteActions(assert, []); + await waitFor(GENERAL.emptyStateTitle); assert .dom(GENERAL.emptyStateTitle) .hasText('Version 2 of this secret has been permanently destroyed', 'Shows destroyed message'); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js index 78e705c0ac..62bb5109a8 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js @@ -46,8 +46,8 @@ const navToBackend = async (backend) => { await visit(`/vault/secrets-engines`); // Use search to find the specific backend instead of relying on pagination await fillIn(GENERAL.inputSearch('secret-engine-path'), backend); - await waitUntil(() => find(`${GENERAL.tableData(`${backend}/`, 'path')} a`)); - return click(`${GENERAL.tableData(`${backend}/`, 'path')} a`); + await waitFor(GENERAL.linkTo(`${backend}/`)); + return click(GENERAL.linkTo(`${backend}/`)); }; const assertPolicyGenerator = async (assert, expectedPaths) => { assert.dom(GENERAL.cardContainer()).exists({ count: expectedPaths.length }); diff --git a/ui/tests/acceptance/secrets/mounts-test.js b/ui/tests/acceptance/secrets/mounts-test.js index dbb87fbeb4..4f0a1b4536 100644 --- a/ui/tests/acceptance/secrets/mounts-test.js +++ b/ui/tests/acceptance/secrets/mounts-test.js @@ -9,10 +9,9 @@ import { currentURL, fillIn, findAll, - settled, typeIn, visit, - waitFor, + waitUntil, } from '@ember/test-helpers'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { setupApplicationTest } from 'ember-qunit'; @@ -129,34 +128,6 @@ module('Acceptance | secrets-engines/enable', function (hooks) { assert.dom('[data-test-input="config.max_lease_ttl"] [data-test-select="ttl-unit"]').hasValue('s'); }); - test('it throws error if setting duplicate path name', async function (assert) { - const path = `kv-duplicate`; - - await consoleComponent.runCommands([ - // delete any kv-duplicate previously written here so that tests can be re-run - `delete sys/mounts/${path}`, - ]); - - await page.visit(); - - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.enable.index'); - await mountBackend('kv', path); - await page.secretList(); - await settled(); - await page.enableEngine(); - await mountBackend('kv', path); - await waitFor('[data-test-message-error-description]'); - assert.dom('[data-test-message-error-description]').containsText(`path is already in use at ${path}`); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.enable.create'); - - await page.secretList(); - await settled(); - await fillIn(GENERAL.inputSearch('secret-engine-path'), path); - assert - .dom(GENERAL.tableData(`${path}/`, 'path')) - .exists({ count: 1 }, 'renders only one instance of the engine'); - }); - test('it should transition to mountable addon engine after mount success', async function (assert) { // test supported backends that ARE ember engines (enterprise only engines are tested individually) const addons = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter( @@ -210,9 +181,11 @@ module('Acceptance | secrets-engines/enable', function (hooks) { const route = engineDisplayData(engine.type)?.isOnlyMountable ? 'configuration.general-settings' : 'list-root'; + const expectedRoute = `vault.cluster.secrets.backend.${route}`; + await waitUntil(() => currentRouteName() === expectedRoute); assert.strictEqual( currentRouteName(), - `vault.cluster.secrets.backend.${route}`, + expectedRoute, `${engine.type} navigates to the correct view (either list if not configuration only or configuration if it is).` ); @@ -292,8 +265,8 @@ module('Acceptance | secrets-engines/enable', function (hooks) { 'vault.cluster.secrets.backends', 'redirects to the backends page' ); - - assert.ok(GENERAL.tableData(`${enginePath}/`, 'path'), 'shows the alicloud engine'); + await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath); + assert.dom(GENERAL.listItem(`${enginePath}/`)).exists(); // cleanup await runCmd(`delete sys/mounts/${enginePath}`); @@ -309,8 +282,8 @@ module('Acceptance | secrets-engines/enable', function (hooks) { 'vault.cluster.secrets.backends', 'redirects to the backends page' ); - - assert.ok(GENERAL.tableData(`${enginePath}/`, 'path'), 'shows the gcpkms engine'); + await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath); + assert.dom(GENERAL.listItem(`${enginePath}/`)).exists(); // cleanup await runCmd(`delete sys/mounts/${enginePath}`); }); diff --git a/ui/tests/integration/components/list-table-test.js b/ui/tests/integration/components/list-table-test.js index d1fc3f4d0b..bc72e8e2d4 100644 --- a/ui/tests/integration/components/list-table-test.js +++ b/ui/tests/integration/components/list-table-test.js @@ -5,9 +5,10 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { click, fillIn, render, waitFor, find } from '@ember/test-helpers'; +import { click, fillIn, render, waitFor } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import sinon from 'sinon'; const MOCK_DATA = [ { island: 'Maldives', visit_length: 5, trip_date: '2025-06-22T00:00:00.000Z' }, @@ -21,33 +22,35 @@ module('Integration | Component | list-table', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(async function () { - this.data = undefined; + this.data = MOCK_DATA; + this.onSelectionChange = undefined; + this.selectionKeyField = undefined; this.columns = [ { key: 'island', label: 'Islands', isSortable: true }, { key: 'visit_length', label: 'Visit length', customTableItem: true }, { key: 'trip_date', label: 'Date trip starts' }, { key: 'popupMenu', label: 'Action' }, ]; - this; this.renderComponent = async () => { return render(hbs` <:customTableItem as |itemData|> - Custom Table Item rendered! + <:popupMenu as |rowData|> - - - - - Delete - + + + + + Delete + `); @@ -55,59 +58,123 @@ module('Integration | Component | list-table', function (hooks) { }); test('it renders and paginates data', async function (assert) { - this.data = MOCK_DATA; await this.renderComponent(); + assert.dom('input[type="checkbox"]').doesNotExist('table is not selectable by default'); assert.dom(GENERAL.paginationInfo).hasText(`1–6 of ${this.data.length}`); - - await fillIn(GENERAL.paginationSizeSelector, '5'); // Default is 10, so change to something else + // Default is 10, so change to something else to test pagination + await fillIn(GENERAL.paginationSizeSelector, '5'); + assert.dom(GENERAL.paginationInfo).hasText(`1–5 of ${this.data.length}`); + assert.dom(GENERAL.tableRow()).exists({ count: 5 }, 'only 5 rows render'); await click(GENERAL.nextPage); - assert.dom(GENERAL.tableRow('Seychelles', 'island')).exists('it paginates the data'); + assert.dom(GENERAL.paginationInfo).hasText(`6–6 of ${this.data.length}`); + assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'only 1 row renders on second page'); + assert.dom(GENERAL.tableData(0, 'island')).hasText('Seychelles', 'second page has expected row'); + }); + + test('it does not render popup menu if @columns does not include a popupMenu key', async function (assert) { + this.columns = [ + { key: 'island', label: 'Islands', isSortable: true }, + { key: 'visit_length', label: 'Visit length', customTableItem: true }, + { key: 'trip_date', label: 'Date trip starts' }, + ]; + await this.renderComponent(); + assert.dom(GENERAL.menuTrigger).doesNotExist(); + }); + + test('it stringifies object and array values for non-custom columns', async function (assert) { + this.columns = [ + { key: 'island', label: 'Islands' }, + { key: 'trip_details', label: 'Trip details' }, + { key: 'tags', label: 'Tags' }, + ]; + this.data = [ + { + island: 'Maldives', + trip_details: { hotel: 'Atoll Inn', nights: 5 }, + tags: ['beach', 'snorkel'], + }, + ]; + + await this.renderComponent(); + assert.dom(GENERAL.tableData(0, 'trip_details')).hasText('{ "hotel": "Atoll Inn", "nights": 5 }'); + assert.dom(GENERAL.tableData(0, 'tags')).hasText('[ "beach", "snorkel" ]'); + }); + + test('it does not render popup menu if parent does not yield one', async function (assert) { + await render(hbs` + `); + assert.dom(GENERAL.menuTrigger).doesNotExist(); }); test('it sorts table data by a sortable column', async function (assert) { - this.data = MOCK_DATA; - const assertSortOrder = (expectedValues, { column, page }) => { - expectedValues.forEach((value, idx) => { - assert - .dom(GENERAL.tableData(value, column)) - .hasText(value, `page ${page}, row ${idx} has ${column}: ${value}`); - }); - }; - await this.renderComponent(); - const column = find(GENERAL.icon('swap-vertical')); - await click(column); - assertSortOrder(['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini', 'Seychelles'], { - column: 'island', - page: 1, + await click(GENERAL.icon('swap-vertical')); + const expectedOrder = ['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini', 'Seychelles']; + expectedOrder.forEach((island, idx) => { + assert.dom(GENERAL.tableData(idx, 'island')).hasText(island); }); }); test('action column renders provided yield block with popup menu', async function (assert) { - this.data = MOCK_DATA; await this.renderComponent(); - - assert.dom(GENERAL.tableData('Maldives', 'popupMenu')).exists('action column renders'); - assert.dom(GENERAL.menuTrigger).exists('button trigger exists for popup menu'); + assert.dom(GENERAL.menuTrigger).exists({ count: this.data.length }, 'popup trigger exists for each item'); + await click(`${GENERAL.tableRow(2)} ${GENERAL.menuTrigger}`); + assert.dom('li').hasText(this.data[2].island, 'popup menu renders relevant row data'); }); - test('selectable column renders when isSelectable is true', async function (assert) { - this.data = MOCK_DATA; + test('selectable checkboxes render and are selectable when selectionKeyField is provided', async function (assert) { + this.selectionKeyField = 'island'; + const count = this.data.length + 1; + this.onSelectionChange = sinon.spy(); await this.renderComponent(); - assert - .dom(`${GENERAL.tableRow('Maldives')} > .hds-advanced-table__th`) - .hasClass('hds-advanced-table__th--is-selectable', 'selectable column renders for row'); + .dom('input[type="checkbox"]') + .exists({ count }, 'it renders a checkbox for each row plus the header to select all'); + assert + .dom(`${GENERAL.tableRow(0)} input[type="checkbox"]`) + .hasAttribute( + 'aria-label', + `Select row ${this.data[0][this.selectionKeyField]}`, + 'selection aria label suffix uses selectionKeyField in value' + ); + await click(`${GENERAL.tableRow(0)} input[type="checkbox"]`); + await click(`${GENERAL.tableRow(2)} input[type="checkbox"]`); + assert.true(this.onSelectionChange.calledTwice, 'onSelectionChange is called twice'); + const [callbackArgs] = this.onSelectionChange.lastCall.args; + const { selectionKey, selectedRowsKeys, selectableRowsStates } = callbackArgs; + const lastItemSelected = this.data[2]; + assert.strictEqual(selectionKey, lastItemSelected.island, 'selectionKey is last selected row'); + assert.propEqual(selectedRowsKeys, ['Maldives', 'Fiji'], 'callback passes selectedRowKeys'); + const expectedRowStates = [ + { selectionKey: 'Maldives', isSelected: true }, + { selectionKey: 'Bora Bora', isSelected: false }, + { selectionKey: 'Fiji', isSelected: true }, + { selectionKey: 'Santorini', isSelected: false }, + { selectionKey: 'Maui', isSelected: false }, + { selectionKey: 'Seychelles', isSelected: false }, + ]; + assert.propEqual(selectableRowsStates, expectedRowStates, 'callback contains selectableRowsStates'); + }); + + test('it is still selectable when selection callback is not provided', async function (assert) { + this.selectionKeyField = 'island'; + await this.renderComponent(); + assert.dom('input[type="checkbox"]').exists({ count: this.data.length + 1 }); + const firstRowCheckbox = '[data-test-table-row="0"] input[type="checkbox"]'; + await click(firstRowCheckbox); + assert.dom(firstRowCheckbox).isChecked('row checkbox can be toggled without @onSelectionChange'); }); - // check that a custom item block will render test('custom item renders provided yield block with customTableItem for a column has customTableItem set to true', async function (assert) { - this.data = MOCK_DATA; await this.renderComponent(); - assert - .dom(GENERAL.tableData('Maldives', 'visit_length')) - .hasText('Custom Table Item rendered!', 'custom item renders'); + .dom(`${GENERAL.tableData(0, 'visit_length')} .hds-badge-count`) + .hasText('5', 'custom table item renders yielded badge'); }); test('it resets pagination when data changes', async function (assert) { @@ -122,7 +189,6 @@ module('Integration | Component | list-table', function (hooks) { this.data = [...MOCK_DATA, ...moreData]; await this.renderComponent(); await click(GENERAL.nextPage); - ``; assert.dom(GENERAL.paginationInfo).hasText(`11–12 of ${this.data.length}`, 'it navigates to next page'); // Changing the @data arg should trigger an update and reset pagination this.set('data', [ diff --git a/ui/tests/integration/components/list-test.js b/ui/tests/integration/components/list-test.js index e16fb0a2ae..41bece1504 100644 --- a/ui/tests/integration/components/list-test.js +++ b/ui/tests/integration/components/list-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; -import { render, click, findAll, triggerEvent, fillIn, find } from '@ember/test-helpers'; +import { render, click, findAll, triggerEvent, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { v4 as uuidv4 } from 'uuid'; import sinon from 'sinon'; @@ -58,11 +58,8 @@ module('Integration | Component | secret-engine/list', function (hooks) { }); await render(hbs``); - assert - .dom(GENERAL.tableData(`${enginePath}/`, 'path')) - .exists('shows the link for the kvv2 secrets engine'); - const row = GENERAL.tableRow(`${enginePath}/`); - await click(`${row} ${GENERAL.menuTrigger}`); + assert.dom(GENERAL.linkTo(`${enginePath}/`)).exists('shows the link for the kvv2 secrets engine'); + await click(`${GENERAL.listItem(`${enginePath}/`)} ${GENERAL.menuTrigger}`); await click(GENERAL.menuItem('Delete')); await click(GENERAL.confirmButton); @@ -125,13 +122,10 @@ module('Integration | Component | secret-engine/list', function (hooks) { test('path name does not render as link for unsupported secret engines', async function (assert) { await render(hbs``); - const unsupportedPath = find(`${GENERAL.tableData('nomad-test/', 'path')} a`); assert - .dom(unsupportedPath) + .dom(GENERAL.linkTo('nomad-test/')) .doesNotExist(`path text doesn't render as a link for unsupported nomad engine.`); - - const supportedPath = find(`${GENERAL.tableData('aws-1/', 'path')} a`); - assert.dom(supportedPath).exists(`path text renders as a link for supported aws engines.`); + assert.dom(GENERAL.linkTo('aws-1/')).exists(`path text renders as a link for supported aws engines.`); }); test('it filters by engine path and engine type', async function (assert) { @@ -175,8 +169,8 @@ module('Integration | Component | secret-engine/list', function (hooks) { test('it applies overflow styling', async function (assert) { await render(hbs``); assert - .dom(GENERAL.tableData('aws-1/', 'path')) - .hasClass('text-overflow-ellipsis', 'secret engine name has text overflow class '); + .dom(GENERAL.tableData(0, 'path')) + .hasClass('text-overflow-ellipsis', 'secret engine path has text overflow class '); }); test('it shows the intro page when only default engines are enabled', async function (assert) { diff --git a/ui/tests/integration/components/manage-dropdown-test.js b/ui/tests/integration/components/manage-dropdown-test.js index 48cfffe831..66401971bf 100644 --- a/ui/tests/integration/components/manage-dropdown-test.js +++ b/ui/tests/integration/components/manage-dropdown-test.js @@ -130,6 +130,62 @@ module('Integration | Component | manage-dropdown | Configure link', function (h }); }; + hooks.beforeEach(function () { + const router = this.owner.lookup('service:router'); + this.transitionStub = sinon.stub(router, 'transitionTo'); + this.refreshStub = sinon.stub(router, 'refresh'); + this.currentRouteStub = sinon.stub(router, 'currentRouteName'); + const api = this.owner.lookup('service:api'); + this.mountDisableApiStub = sinon.stub(api.sys, 'mountsDisableSecretsEngine'); + }); + + hooks.afterEach(function () { + this.transitionStub.restore(); + this.refreshStub.restore(); + this.mountDisableApiStub.restore(); + }); + + test('it disables a mount', async function (assert) { + this.model = makeModel({ type: 'ldap', id: 'ldap' }); + await render( + hbs`` + ); + await click(GENERAL.menuTrigger); + await click(GENERAL.menuItem('Delete')); + await click(GENERAL.confirmButton); + const [id] = this.mountDisableApiStub.lastCall.args; + assert.strictEqual(id, 'ldap', 'it calls disable with the secret engine id'); + }); + + test('it calls refresh() when current route is secrets.backends', async function (assert) { + this.currentRouteStub.value('vault.cluster.secrets.backends'); + this.model = makeModel({ type: 'ldap', id: 'ldap' }); + await render( + hbs`` + ); + await click(GENERAL.menuTrigger); + await click(GENERAL.menuItem('Delete')); + await click(GENERAL.confirmButton); + assert.true( + this.refreshStub.calledOnce, + 'refresh is called because the current route is vault.cluster.secrets.backends' + ); + assert.true(this.transitionStub.notCalled, 'transitionTo is not called'); + }); + + test('it calls transitionTo() when current route is NOT secrets.backends', async function (assert) { + this.currentRouteStub.value('vault.cluster.secrets.backend.ldap.overview'); + this.model = makeModel({ type: 'ldap', id: 'ldap' }); + await render( + hbs`` + ); + await click(GENERAL.menuTrigger); + await click(GENERAL.menuItem('Delete')); + await click(GENERAL.confirmButton); + assert.true(this.transitionStub.calledOnce, 'transitionTo() is called'); + assert.true(this.refreshStub.notCalled, 'refresh() is not called'); + }); + TEST_CASES.forEach(({ label, type, version, expectedRoute }) => { test(`Configure link routes correctly for ${label}`, async function (assert) { const routing = this.owner.lookup('service:-routing'); From 73aa757660596dba1705e1333508e5ce92239f84 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 25 Feb 2026 14:57:02 -0700 Subject: [PATCH 008/468] UI: Remove bulk action to disable secrets engines (#12493) (#12546) * remove bulk delete operation * remove selectedItems yield * cleanup disable engine action * remove var left behind from rebase * add changelog Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- changelog/_12493.txt | 3 ++ ui/app/components/secret-engine/list.hbs | 43 +---------------- ui/app/components/secret-engine/list.ts | 52 +-------------------- ui/lib/core/addon/components/list-table.hbs | 4 +- 4 files changed, 6 insertions(+), 96 deletions(-) create mode 100644 changelog/_12493.txt diff --git a/changelog/_12493.txt b/changelog/_12493.txt new file mode 100644 index 0000000000..d42d514dd4 --- /dev/null +++ b/changelog/_12493.txt @@ -0,0 +1,3 @@ +```release-note:change +ui: Remove ability to bulk delete secrets engines from the list view. +``` \ No newline at end of file diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 302e6dad40..201f70e9cb 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -114,37 +114,7 @@ {{! Table Section }} {{#if this.sortedDisplayableBackends}} - - <:selectedItems> - {{#if this.selectedItems}} - - - {{this.selectedItems.length}} - selected out of - {{this.sortedDisplayableBackends.length}} - - - - {{/if}} - + <:customTableItem as |itemData|> {{#let (this.getEngineResourceData itemData.path) as |backendData|}} {{! Only render if we have backendData, so this doesn't try to render while data for the list refreshes }} @@ -185,15 +155,4 @@ {{else}} {{/if}} - {{! End Table Section }} - - {{#if this.enginesToDisable}} - - {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/app/components/secret-engine/list.ts b/ui/app/components/secret-engine/list.ts index dcd156bfd7..66cff512bc 100644 --- a/ui/app/components/secret-engine/list.ts +++ b/ui/app/components/secret-engine/list.ts @@ -7,10 +7,10 @@ import { action } from '@ember/object'; import { service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { dropTask } from 'ember-concurrency'; import engineDisplayData from 'vault/helpers/engines-display-data'; import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; +import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard'; import type RouterService from '@ember/routing/router-service'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; @@ -19,7 +19,6 @@ import type FlashMessageService from 'vault/services/flash-messages'; import type NamespaceService from 'vault/services/namespace'; import type VersionService from 'vault/services/version'; import type WizardService from 'vault/services/wizard'; -import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard'; /** * @module SecretEngineList handles the display of the list of secret engines, including the filtering. @@ -44,9 +43,6 @@ export default class SecretEngineList extends Component { @service declare readonly version: VersionService; @service declare readonly wizard: WizardService; - @tracked secretEngineOptions: Array | [] = []; - @tracked enginesToDisable: Array | null = null; - @tracked engineTypeFilters: Array = []; @tracked engineVersionFilters: Array = []; @tracked searchText = ''; @@ -55,8 +51,6 @@ export default class SecretEngineList extends Component { @tracked typeSearchText = ''; @tracked versionSearchText = ''; - @tracked selectedItems = Array(); - @tracked shouldRenderIntroModal = false; wizardId = WIZARD_ID; @@ -282,48 +276,4 @@ export default class SecretEngineList extends Component { this.engineTypeFilters = []; this.engineVersionFilters = []; } - - @action - updateSelectedItems(tableData: { selectedRowsKeys: string[] }) { - this.selectedItems = tableData.selectedRowsKeys; - } - - @action - setEnginesToDisable(engines: Array) { - this.enginesToDisable = engines; - } - - @action - clearEnginesToDisable() { - this.enginesToDisable = null; - } - - async disableSingleEngine(engine: SecretsEngineResource) { - const { engineType, id, path } = engine; - try { - await this.api.sys.mountsDisableSecretsEngine(id); - this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`); - } catch (err) { - const { message } = await this.api.parseError(err); - this.flashMessages.danger( - `There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.` - ); - } - } - - disableMultipleEngines = dropTask(async (enginePathsToDisable: Array) => { - const enginesToDisable = this.displayableBackends.filter((engine: SecretsEngineResource) => - enginePathsToDisable.includes(engine.path) - ); - try { - for (const engine of enginesToDisable) { - await this.disableSingleEngine(engine); - } - - // Navigate once all operations are complete - this.router.transitionTo('vault.cluster.secrets.backends'); - } finally { - this.enginesToDisable = null; - } - }); } diff --git a/ui/lib/core/addon/components/list-table.hbs b/ui/lib/core/addon/components/list-table.hbs index 8ca65fb394..3848b6b4a8 100644 --- a/ui/lib/core/addon/components/list-table.hbs +++ b/ui/lib/core/addon/components/list-table.hbs @@ -2,9 +2,7 @@ Copyright IBM Corp. 2016, 2025 SPDX-License-Identifier: BUSL-1.1 }} -{{#if (has-block "selectedItems")}} - {{yield to="selectedItems"}} -{{/if}} + Date: Thu, 26 Feb 2026 08:41:01 -0700 Subject: [PATCH 009/468] dismiss wizard for sidebar nav tests (#12550) (#12553) Co-authored-by: Matthew Irish <39469+meirish@users.noreply.github.com> --- ui/tests/acceptance/sidebar-nav-test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/tests/acceptance/sidebar-nav-test.js b/ui/tests/acceptance/sidebar-nav-test.js index cb325e5616..590a10532d 100644 --- a/ui/tests/acceptance/sidebar-nav-test.js +++ b/ui/tests/acceptance/sidebar-nav-test.js @@ -11,6 +11,7 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers'; import modifyPassthroughResponse from 'vault/mirage/helpers/modify-passthrough-response'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import localStorage from 'vault/lib/local-storage'; const link = (label) => `[data-test-sidebar-nav-link="${label}"]`; const panel = (label) => `[data-test-sidebar-nav-panel="${label}"]`; @@ -19,7 +20,7 @@ module('Acceptance | sidebar navigation', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - hooks.beforeEach(function () { + hooks.beforeEach(async function () { // set storage_type to raft to test link this.server.get('/sys/seal-status', (schema, req) => { return modifyPassthroughResponse(req, { storage_type: 'raft' }); @@ -31,7 +32,9 @@ module('Acceptance | sidebar navigation', function (hooks) { 'nested-interactive': { enabled: false }, }, }); - return login(); + await login(); + // dismiss wizard + localStorage.setItem('dismissed-wizards', ['auth-methods']); }); test('it should navigate back to the dashboard when logo is clicked in app header', async function (assert) { From d744b80e68125684939efd21b422830fa0144e15 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 10:20:30 -0700 Subject: [PATCH 010/468] [UI] API Client Update / Enum Updates (#12549) (#12565) * bumps api client version * updates imported enum names from api client * updates sync activation request method * updates pki list enums imports Co-authored-by: Jordan Reimer --- .../dashboard/quick-actions-card.ts | 20 ++++++++--------- ui/app/routes/vault/cluster/clients.ts | 6 +++-- .../vault/cluster/recovery/snapshots.ts | 4 ++-- .../vault/cluster/recovery/snapshots/load.ts | 4 ++-- .../addon/components/kv-suggestion-input.ts | 8 +++++-- ui/lib/kmip/addon/routes/configuration.ts | 6 +++-- ui/lib/kmip/addon/routes/credentials/index.ts | 4 ++-- ui/lib/kmip/addon/routes/scope/roles.ts | 4 ++-- ui/lib/kmip/addon/routes/scopes/index.ts | 7 ++++-- ui/lib/kubernetes/addon/routes/overview.ts | 4 ++-- ui/lib/kubernetes/addon/routes/roles/index.ts | 4 ++-- ui/lib/ldap/addon/components/page/overview.ts | 8 +++---- ui/lib/ldap/addon/routes/libraries.ts | 8 +++---- ui/lib/ldap/addon/routes/overview.ts | 8 +++---- ui/lib/ldap/addon/routes/roles.ts | 22 ++++++++++++------- .../components/page/pki-issuer-rotate-root.ts | 4 ++-- .../pki/addon/components/pki-generate-csr.ts | 8 +++---- .../pki/addon/components/pki-generate-root.ts | 12 +++++----- .../addon/components/pki-issuer-cross-sign.js | 4 ++-- ui/lib/pki/addon/decorators/check-issuers.js | 4 ++-- ui/lib/pki/addon/routes/certificates/index.js | 4 ++-- ui/lib/pki/addon/routes/issuers/index.js | 4 ++-- ui/lib/pki/addon/routes/keys/index.js | 4 ++-- ui/lib/pki/addon/routes/overview.js | 12 +++++----- ui/lib/pki/addon/routes/roles/create.js | 4 ++-- ui/lib/pki/addon/routes/roles/index.js | 4 ++-- ui/lib/pki/addon/routes/roles/role/edit.js | 4 ++-- .../secrets/sync-activation-modal.ts | 2 +- .../routes/secrets/destinations/index.ts | 6 +++-- ui/lib/sync/addon/routes/secrets/overview.ts | 12 ++++++---- ui/package.json | 2 +- ui/pnpm-lock.yaml | 18 +++++++-------- 32 files changed, 124 insertions(+), 101 deletions(-) diff --git a/ui/app/components/dashboard/quick-actions-card.ts b/ui/app/components/dashboard/quick-actions-card.ts index c682b39e7a..051f8b6ff3 100644 --- a/ui/app/components/dashboard/quick-actions-card.ts +++ b/ui/app/components/dashboard/quick-actions-card.ts @@ -11,11 +11,11 @@ import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; import { task } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; import { - DatabaseListStaticRolesListEnum, - DatabaseListRolesListEnum, - PkiListRolesListEnum, - PkiListCertsListEnum, - PkiListIssuersListEnum, + SecretsApiDatabaseListStaticRolesListEnum, + SecretsApiDatabaseListRolesListEnum, + SecretsApiPkiListRolesListEnum, + SecretsApiPkiListCertsListEnum, + SecretsApiPkiListIssuersListEnum, } from '@hashicorp/vault-client-typescript'; import type RouterService from '@ember/routing/router-service'; @@ -131,15 +131,15 @@ export default class DashboardQuickActionsCard extends Component { if (action === 'Generate credentials for database') { return [ - secrets.databaseListStaticRoles(id, DatabaseListStaticRolesListEnum.TRUE), - secrets.databaseListRoles(id, DatabaseListRolesListEnum.TRUE), + secrets.databaseListStaticRoles(id, SecretsApiDatabaseListStaticRolesListEnum.TRUE), + secrets.databaseListRoles(id, SecretsApiDatabaseListRolesListEnum.TRUE), ]; } else if (action === 'Issue certificate') { - return [secrets.pkiListRoles(id, PkiListRolesListEnum.TRUE)]; + return [secrets.pkiListRoles(id, SecretsApiPkiListRolesListEnum.TRUE)]; } else if (action === 'View certificate') { - return [secrets.pkiListCerts(id, PkiListCertsListEnum.TRUE)]; + return [secrets.pkiListCerts(id, SecretsApiPkiListCertsListEnum.TRUE)]; } else if (action === 'View issuer') { - return [secrets.pkiListIssuers(id, PkiListIssuersListEnum.TRUE)]; + return [secrets.pkiListIssuers(id, SecretsApiPkiListIssuersListEnum.TRUE)]; } return []; } diff --git a/ui/app/routes/vault/cluster/clients.ts b/ui/app/routes/vault/cluster/clients.ts index 8a2747e7e2..22686fd7bc 100644 --- a/ui/app/routes/vault/cluster/clients.ts +++ b/ui/app/routes/vault/cluster/clients.ts @@ -9,7 +9,7 @@ import { ModelFrom } from 'vault/route'; import type ApiService from 'vault/services/api'; import type CapabilitiesService from 'vault/services/capabilities'; -import { VersionHistoryListEnum } from '@hashicorp/vault-client-typescript'; +import { SystemApiVersionHistoryListEnum } from '@hashicorp/vault-client-typescript'; export type ClientsRouteModel = ModelFrom; @@ -20,7 +20,9 @@ export default class ClientsRoute extends Route { async model() { const { canRead: canReadConfig, canUpdate: canUpdateConfig } = await this.capabilities.for('clientsConfig'); - const response = await this.api.sys.versionHistory(VersionHistoryListEnum.TRUE).catch(() => undefined); + const response = await this.api.sys + .versionHistory(SystemApiVersionHistoryListEnum.TRUE) + .catch(() => undefined); const versionHistory = response ? this.api.keyInfoToArray(response, 'version') : []; const config = await this.api.sys.internalClientActivityReadConfiguration().catch(() => ({})); return { canReadConfig, canUpdateConfig, versionHistory, config }; diff --git a/ui/app/routes/vault/cluster/recovery/snapshots.ts b/ui/app/routes/vault/cluster/recovery/snapshots.ts index 65731d18f6..e80b354778 100644 --- a/ui/app/routes/vault/cluster/recovery/snapshots.ts +++ b/ui/app/routes/vault/cluster/recovery/snapshots.ts @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { SystemListStorageRaftSnapshotLoadListEnum } from '@hashicorp/vault-client-typescript'; +import { SystemApiSystemListStorageRaftSnapshotLoadListEnum } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; import type Capabilities from 'vault/services/capabilities'; @@ -50,7 +50,7 @@ export default class RecoverySnapshotsRoute extends Route { // By default, the api service uses the current namespace context, so we'll need to specify otherwise. // Snapshot operations do not have this constraint. const { keys } = await this.api.sys.systemListStorageRaftSnapshotLoad( - SystemListStorageRaftSnapshotLoadListEnum.TRUE, + SystemApiSystemListStorageRaftSnapshotLoadListEnum.TRUE, this.api.buildHeaders({ namespace: '' }) ); return keys as string[]; diff --git a/ui/app/routes/vault/cluster/recovery/snapshots/load.ts b/ui/app/routes/vault/cluster/recovery/snapshots/load.ts index ead3119b5d..993f684bce 100644 --- a/ui/app/routes/vault/cluster/recovery/snapshots/load.ts +++ b/ui/app/routes/vault/cluster/recovery/snapshots/load.ts @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { SystemListStorageRaftSnapshotAutoConfigListEnum } from '@hashicorp/vault-client-typescript'; +import { SystemApiSystemListStorageRaftSnapshotAutoConfigListEnum } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; import type { Breadcrumb } from 'vault/vault/app-types'; @@ -34,7 +34,7 @@ export default class RecoverySnapshotsLoadRoute extends Route { try { const { keys } = await this.api.sys.systemListStorageRaftSnapshotAutoConfig( - SystemListStorageRaftSnapshotAutoConfigListEnum.TRUE + SystemApiSystemListStorageRaftSnapshotAutoConfigListEnum.TRUE ); configs = keys ?? []; } catch (e) { diff --git a/ui/lib/core/addon/components/kv-suggestion-input.ts b/ui/lib/core/addon/components/kv-suggestion-input.ts index 44ddcb35ea..bd325434af 100644 --- a/ui/lib/core/addon/components/kv-suggestion-input.ts +++ b/ui/lib/core/addon/components/kv-suggestion-input.ts @@ -10,7 +10,7 @@ import { action } from '@ember/object'; import { guidFor } from '@ember/object/internals'; import { run } from '@ember/runloop'; import { keyIsFolder, parentKeyForKey, keyWithoutParentKey } from 'core/utils/key-utils'; -import { KvV2ListListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKvV2ListListEnum } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; @@ -78,7 +78,11 @@ export default class KvSuggestionInputComponent extends Component { // This request can either list secrets at the mount root or for a specified :secret_path. // Since :secret_path already contains a trailing slash, e.g. /metadata/my-secret// // the request URL is sanitized by the api service to remove duplicate slashes. - const { keys } = await this.api.secrets.kvV2List(this.pathToSecret, backend, KvV2ListListEnum.TRUE); + const { keys } = await this.api.secrets.kvV2List( + this.pathToSecret, + backend, + SecretsApiKvV2ListListEnum.TRUE + ); // this will be used to filter the existing result set when the search term changes within the same path this._cachedSecrets = keys || []; return this._cachedSecrets; diff --git a/ui/lib/kmip/addon/routes/configuration.ts b/ui/lib/kmip/addon/routes/configuration.ts index 3687116405..1c98cfa462 100644 --- a/ui/lib/kmip/addon/routes/configuration.ts +++ b/ui/lib/kmip/addon/routes/configuration.ts @@ -21,11 +21,13 @@ export default class KmipConfigurationRoute extends Route { try { const { currentPath } = this.secretMountPath; - const { data } = await this.api.secrets.kmipReadConfiguration(currentPath); + // the spec changed and now the operation ids are the same for reading both the config and ca pem + const { data } = await this.api.secrets.kmipReadConfiguration_1(currentPath); const config = data as Record; const { secretsEngine } = this.modelFor('application') as KmipApplicationModel; try { - const { data } = await this.api.secrets.kmipReadCaPem(currentPath); + // this method now calls the same endpoint as the former kmipReadCaPem + const { data } = await this.api.secrets.kmipReadConfiguration(currentPath); const ca = data as Record; return { config: { ...config, ...ca }, secretsEngine }; } catch (error) { diff --git a/ui/lib/kmip/addon/routes/credentials/index.ts b/ui/lib/kmip/addon/routes/credentials/index.ts index 289db413a7..bf24f91379 100644 --- a/ui/lib/kmip/addon/routes/credentials/index.ts +++ b/ui/lib/kmip/addon/routes/credentials/index.ts @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { KmipListClientCertificatesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKmipListClientCertificatesListEnum } from '@hashicorp/vault-client-typescript'; import { paginate } from 'core/utils/paginate-list'; import type Controller from '@ember/controller'; @@ -40,7 +40,7 @@ export default class KmipCredentialsRoute extends Route { role_name as string, scope_name as string, currentPath, - KmipListClientCertificatesListEnum.TRUE + SecretsApiKmipListClientCertificatesListEnum.TRUE ); const credentials = keys ? paginate(keys, { page: Number(page) || 1, filter: pageFilter }) : []; // capabilities exist at root path, not for individual credentials diff --git a/ui/lib/kmip/addon/routes/scope/roles.ts b/ui/lib/kmip/addon/routes/scope/roles.ts index b83b6a7ec1..26a08c893d 100644 --- a/ui/lib/kmip/addon/routes/scope/roles.ts +++ b/ui/lib/kmip/addon/routes/scope/roles.ts @@ -6,7 +6,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { paginate } from 'core/utils/paginate-list'; -import { KmipListRolesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKmipListRolesListEnum } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; import type SecretMountPath from 'vault/services/secret-mount-path'; @@ -41,7 +41,7 @@ export default class KmipScopeRolesRoute extends Route { const { keys } = await this.api.secrets.kmipListRoles( scope as string, currentPath, - KmipListRolesListEnum.TRUE + SecretsApiKmipListRolesListEnum.TRUE ); const roles = keys ? paginate(keys, { page: Number(page) || 1, filter: pageFilter }) : []; // fetch capabilities for filtered scopes diff --git a/ui/lib/kmip/addon/routes/scopes/index.ts b/ui/lib/kmip/addon/routes/scopes/index.ts index 2390f2832d..3c64124828 100644 --- a/ui/lib/kmip/addon/routes/scopes/index.ts +++ b/ui/lib/kmip/addon/routes/scopes/index.ts @@ -6,7 +6,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { paginate } from 'core/utils/paginate-list'; -import { KmipListScopesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKmipListScopesListEnum } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; import type SecretMountPath from 'vault/services/secret-mount-path'; @@ -38,7 +38,10 @@ export default class KmipScopesRoute extends Route { const { secretsEngine } = this.modelFor('application') as KmipApplicationModel; try { - const { keys } = await this.api.secrets.kmipListScopes(currentPath, KmipListScopesListEnum.TRUE); + const { keys } = await this.api.secrets.kmipListScopes( + currentPath, + SecretsApiKmipListScopesListEnum.TRUE + ); const scopes = keys ? paginate(keys, { page: Number(page) || 1, filter: pageFilter }) : []; // fetch capabilities for filtered scopes const paths = scopes.map((scope) => diff --git a/ui/lib/kubernetes/addon/routes/overview.ts b/ui/lib/kubernetes/addon/routes/overview.ts index bfb6262863..714ea357f9 100644 --- a/ui/lib/kubernetes/addon/routes/overview.ts +++ b/ui/lib/kubernetes/addon/routes/overview.ts @@ -6,7 +6,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { ModelFrom } from 'vault/route'; -import { KubernetesListRolesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKubernetesListRolesListEnum } from '@hashicorp/vault-client-typescript'; import type { KubernetesApplicationModel } from './application'; import type SecretMountPath from 'vault/services/secret-mount-path'; @@ -30,7 +30,7 @@ export default class KubernetesOverviewRoute extends Route { const { promptConfig, secretsEngine } = this.modelFor('application') as KubernetesApplicationModel; const { keys } = await this.api.secrets - .kubernetesListRoles(currentPath, KubernetesListRolesListEnum.TRUE) + .kubernetesListRoles(currentPath, SecretsApiKubernetesListRolesListEnum.TRUE) .catch(() => ({ keys: [] })); return { diff --git a/ui/lib/kubernetes/addon/routes/roles/index.ts b/ui/lib/kubernetes/addon/routes/roles/index.ts index a2f8dcb0eb..76b2285a53 100644 --- a/ui/lib/kubernetes/addon/routes/roles/index.ts +++ b/ui/lib/kubernetes/addon/routes/roles/index.ts @@ -6,7 +6,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { ModelFrom } from 'vault/route'; -import { KubernetesListRolesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiKubernetesListRolesListEnum } from '@hashicorp/vault-client-typescript'; import type { KubernetesApplicationModel } from '../application'; import type ApiService from 'vault/services/api'; @@ -38,7 +38,7 @@ export default class KubernetesRolesRoute extends Route { const { pageFilter } = (transition.to?.queryParams || {}) as { pageFilter?: string }; const { keys } = await this.api.secrets.kubernetesListRoles( currentPath, - KubernetesListRolesListEnum.TRUE + SecretsApiKubernetesListRolesListEnum.TRUE ); const roles = pageFilter ? keys?.filter((key) => key.toLowerCase().includes(pageFilter.toLowerCase())) diff --git a/ui/lib/ldap/addon/components/page/overview.ts b/ui/lib/ldap/addon/components/page/overview.ts index 4b82913f8a..1a8bf7da22 100644 --- a/ui/lib/ldap/addon/components/page/overview.ts +++ b/ui/lib/ldap/addon/components/page/overview.ts @@ -9,8 +9,8 @@ import { service } from '@ember/service'; import { action } from '@ember/object'; import { restartableTask } from 'ember-concurrency'; import { - LdapLibraryListListEnum, - LdapLibraryListLibraryPathListEnum, + SecretsApiLdapLibraryListListEnum, + SecretsApiLdapLibraryListLibraryPathListEnum, } from '@hashicorp/vault-client-typescript'; import type { @@ -147,9 +147,9 @@ export default class LdapLibrariesPageComponent extends Component { ? await this.api.secrets.ldapLibraryListLibraryPath( pathToLibrary, currentPath, - LdapLibraryListLibraryPathListEnum.TRUE + SecretsApiLdapLibraryListLibraryPathListEnum.TRUE ) - : await this.api.secrets.ldapLibraryList(currentPath, LdapLibraryListListEnum.TRUE); + : await this.api.secrets.ldapLibraryList(currentPath, SecretsApiLdapLibraryListListEnum.TRUE); const libraries = keys?.map((name) => { diff --git a/ui/lib/ldap/addon/routes/libraries.ts b/ui/lib/ldap/addon/routes/libraries.ts index 2cf27ec78c..9f389a6e65 100644 --- a/ui/lib/ldap/addon/routes/libraries.ts +++ b/ui/lib/ldap/addon/routes/libraries.ts @@ -6,8 +6,8 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { - LdapLibraryListListEnum, - LdapLibraryListLibraryPathListEnum, + SecretsApiLdapLibraryListListEnum, + SecretsApiLdapLibraryListLibraryPathListEnum, } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; @@ -29,9 +29,9 @@ export default class LdapLibrariesRoute extends Route { ? await this.api.secrets.ldapLibraryListLibraryPath( path_to_library, currentPath, - LdapLibraryListLibraryPathListEnum.TRUE + SecretsApiLdapLibraryListLibraryPathListEnum.TRUE ) - : await this.api.secrets.ldapLibraryList(currentPath, LdapLibraryListListEnum.TRUE); + : await this.api.secrets.ldapLibraryList(currentPath, SecretsApiLdapLibraryListListEnum.TRUE); const libraries = keys?.map((name) => { diff --git a/ui/lib/ldap/addon/routes/overview.ts b/ui/lib/ldap/addon/routes/overview.ts index 22149d0e9a..d6aea8fc0e 100644 --- a/ui/lib/ldap/addon/routes/overview.ts +++ b/ui/lib/ldap/addon/routes/overview.ts @@ -7,8 +7,8 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { ModelFrom } from 'vault/route'; import { - LdapListStaticRolesListEnum, - LdapListDynamicRolesListEnum, + SecretsApiLdapListStaticRolesListEnum, + SecretsApiLdapListDynamicRolesListEnum, } from '@hashicorp/vault-client-typescript'; import type SecretMountPath from 'vault/services/secret-mount-path'; @@ -33,8 +33,8 @@ export default class LdapOverviewRoute extends Route { const { promptConfig, secretsEngine } = this.modelFor('application') as LdapApplicationModel; const { currentPath } = this.secretMountPath; const requests = [ - this.api.secrets.ldapListStaticRoles(currentPath, LdapListStaticRolesListEnum.TRUE), - this.api.secrets.ldapListDynamicRoles(currentPath, LdapListDynamicRolesListEnum.TRUE), + this.api.secrets.ldapListStaticRoles(currentPath, SecretsApiLdapListStaticRolesListEnum.TRUE), + this.api.secrets.ldapListDynamicRoles(currentPath, SecretsApiLdapListDynamicRolesListEnum.TRUE), ]; const results = await Promise.allSettled(requests); const roles = []; diff --git a/ui/lib/ldap/addon/routes/roles.ts b/ui/lib/ldap/addon/routes/roles.ts index 8f0016694f..029b2cb243 100644 --- a/ui/lib/ldap/addon/routes/roles.ts +++ b/ui/lib/ldap/addon/routes/roles.ts @@ -9,10 +9,10 @@ import { paginate, PaginateOptions } from 'core/utils/paginate-list'; import sortObjects from 'vault/utils/sort-objects'; import { fetchRoleCapabilities } from 'ldap/utils/capabilities-helper'; import { - LdapListStaticRolesListEnum, - LdapListDynamicRolesListEnum, - LdapListStaticRolePathListEnum, - LdapListRolePathListEnum, + SecretsApiLdapListStaticRolesListEnum, + SecretsApiLdapListDynamicRolesListEnum, + SecretsApiLdapListStaticRolePathListEnum, + SecretsApiLdapListRolePathListEnum, } from '@hashicorp/vault-client-typescript'; import type ApiService from 'vault/services/api'; @@ -39,12 +39,18 @@ export default class LdapRolesRoute extends Route { if (path) { requests = subType === 'static' - ? [this.api.secrets.ldapListStaticRolePath(currentPath, path, LdapListStaticRolePathListEnum.TRUE)] - : [this.api.secrets.ldapListRolePath(currentPath, path, LdapListRolePathListEnum.TRUE)]; + ? [ + this.api.secrets.ldapListStaticRolePath( + currentPath, + path, + SecretsApiLdapListStaticRolePathListEnum.TRUE + ), + ] + : [this.api.secrets.ldapListRolePath(currentPath, path, SecretsApiLdapListRolePathListEnum.TRUE)]; } else { requests = [ - this.api.secrets.ldapListStaticRoles(currentPath, LdapListStaticRolesListEnum.TRUE), - this.api.secrets.ldapListDynamicRoles(currentPath, LdapListDynamicRolesListEnum.TRUE), + this.api.secrets.ldapListStaticRoles(currentPath, SecretsApiLdapListStaticRolesListEnum.TRUE), + this.api.secrets.ldapListDynamicRoles(currentPath, SecretsApiLdapListDynamicRolesListEnum.TRUE), ]; } const results = await Promise.allSettled(requests); diff --git a/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts b/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts index dd86977ed6..826ecaee58 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts +++ b/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts @@ -20,7 +20,7 @@ import type { Breadcrumb, ValidationMap } from 'vault/vault/app-types'; import type { PkiGenerateRootResponse, PkiReadIssuerResponse, - PkiRotateRootExportedEnum, + SecretsApiPkiRotateRootExportedEnum, PkiRotateRootRequest, } from '@hashicorp/vault-client-typescript'; import type { ParsedCertificateData } from 'vault/vault/utils/parse-pki-cert'; @@ -113,7 +113,7 @@ export default class PagePkiIssuerRotateRootComponent extends Component { const { type } = this.newRootForm.data; try { this.newRoot = await this.api.secrets.pkiRotateRoot( - type as PkiRotateRootExportedEnum, + type as SecretsApiPkiRotateRootExportedEnum, this.secretMountPath.currentPath, data as PkiRotateRootRequest ); diff --git a/ui/lib/pki/addon/components/pki-generate-csr.ts b/ui/lib/pki/addon/components/pki-generate-csr.ts index d2ee45def4..153ab7f584 100644 --- a/ui/lib/pki/addon/components/pki-generate-csr.ts +++ b/ui/lib/pki/addon/components/pki-generate-csr.ts @@ -17,8 +17,8 @@ import type ApiService from 'vault/services/api'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type { ValidationMap } from 'vault/app-types'; import type { - PkiGenerateIntermediateExportedEnum, - PkiIssuersGenerateIntermediateExportedEnum, + SecretsApiPkiGenerateIntermediateExportedEnum, + SecretsApiPkiIssuersGenerateIntermediateExportedEnum, PkiGenerateIntermediateRequest, PkiGenerateIntermediateResponse, PkiIssuersGenerateIntermediateRequest, @@ -100,13 +100,13 @@ export default class PkiGenerateCsrComponent extends Component { generateCsr(canUseIssuer: boolean, data: PkiConfigGenerateForm['data']) { if (canUseIssuer) { return this.api.secrets.pkiIssuersGenerateIntermediate( - this.form.data.type as PkiIssuersGenerateIntermediateExportedEnum, + this.form.data.type as SecretsApiPkiIssuersGenerateIntermediateExportedEnum, this.secretMountPath.currentPath, data as PkiIssuersGenerateIntermediateRequest ); } else { return this.api.secrets.pkiGenerateIntermediate( - this.form.data.type as PkiGenerateIntermediateExportedEnum, + this.form.data.type as SecretsApiPkiGenerateIntermediateExportedEnum, this.secretMountPath.currentPath, data as PkiGenerateIntermediateRequest ); diff --git a/ui/lib/pki/addon/components/pki-generate-root.ts b/ui/lib/pki/addon/components/pki-generate-root.ts index 6031be6cbb..cb2ba52f93 100644 --- a/ui/lib/pki/addon/components/pki-generate-root.ts +++ b/ui/lib/pki/addon/components/pki-generate-root.ts @@ -20,13 +20,13 @@ import type ApiService from 'vault/services/api'; import type CapabilitiesService from 'vault/services/capabilities'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type { - PkiGenerateRootExportedEnum, - PkiIssuersGenerateRootExportedEnum, + SecretsApiPkiGenerateRootExportedEnum, + SecretsApiPkiIssuersGenerateRootExportedEnum, PkiGenerateRootRequest, PkiGenerateRootResponse, PkiIssuersGenerateRootRequest, PkiIssuersGenerateRootResponse, - PkiRotateRootExportedEnum, + SecretsApiPkiRotateRootExportedEnum, PkiRotateRootRequest, } from '@hashicorp/vault-client-typescript'; @@ -146,7 +146,7 @@ export default class PkiGenerateRootComponent extends Component { if (this.args.rotateCertData) { return this.api.secrets.pkiRotateRoot( - type as PkiRotateRootExportedEnum, + type as SecretsApiPkiRotateRootExportedEnum, currentPath, data as PkiRotateRootRequest ); @@ -154,13 +154,13 @@ export default class PkiGenerateRootComponent extends Component { const canUseIssuer = await this.fetchIssuerCapabilities(); if (canUseIssuer) { return this.api.secrets.pkiIssuersGenerateRoot( - type as PkiIssuersGenerateRootExportedEnum, + type as SecretsApiPkiIssuersGenerateRootExportedEnum, this.secretMountPath.currentPath, data as PkiIssuersGenerateRootRequest ); } else { return this.api.secrets.pkiGenerateRoot( - type as PkiGenerateRootExportedEnum, + type as SecretsApiPkiGenerateRootExportedEnum, this.secretMountPath.currentPath, data as PkiGenerateRootRequest ); diff --git a/ui/lib/pki/addon/components/pki-issuer-cross-sign.js b/ui/lib/pki/addon/components/pki-issuer-cross-sign.js index 4051292a74..40df3f7106 100644 --- a/ui/lib/pki/addon/components/pki-issuer-cross-sign.js +++ b/ui/lib/pki/addon/components/pki-issuer-cross-sign.js @@ -11,7 +11,7 @@ import { tracked } from '@glimmer/tracking'; import { waitFor } from '@ember/test-waiters'; import { parseCertificate } from 'vault/utils/parse-pki-cert'; import { addToArray } from 'vault/helpers/add-to-array'; -import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; /** * @module PkiIssuerCrossSign * PkiIssuerCrossSign components render from a parent issuer's details page to cross-sign an intermediate issuer (from a different mount). @@ -88,7 +88,7 @@ export default class PkiIssuerCrossSign extends Component { for (let row = 0; row < this.formData.length; row++) { const { intermediateMount, newCrossSignedIssuer } = this.formData[row]; const issuers = await this.api.secrets - .pkiListIssuers(intermediateMount, PkiListIssuersListEnum.TRUE) + .pkiListIssuers(intermediateMount, SecretsApiPkiListIssuersListEnum.TRUE) .then((response) => this.api.keyInfoToArray(response, 'issuer_id')) .catch(() => []); // for cross-signing error handling we want to record the list of issuers before the process starts diff --git a/ui/lib/pki/addon/decorators/check-issuers.js b/ui/lib/pki/addon/decorators/check-issuers.js index a166267ed9..d65fdebd81 100644 --- a/ui/lib/pki/addon/decorators/check-issuers.js +++ b/ui/lib/pki/addon/decorators/check-issuers.js @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; /** * the overview, roles, issuers, certificates, and key routes all need to be aware of the whether there is a config for the engine @@ -36,7 +36,7 @@ export function withConfig() { try { await this.api.secrets.pkiListIssuers( this.secretMountPath.currentPath, - PkiListIssuersListEnum.TRUE + SecretsApiPkiListIssuersListEnum.TRUE ); this.pkiMountHasConfig = true; } catch (e) { diff --git a/ui/lib/pki/addon/routes/certificates/index.js b/ui/lib/pki/addon/routes/certificates/index.js index 48f86d018a..6666526b5f 100644 --- a/ui/lib/pki/addon/routes/certificates/index.js +++ b/ui/lib/pki/addon/routes/certificates/index.js @@ -7,7 +7,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { withConfig } from 'pki/decorators/check-issuers'; import { getCliMessage } from 'pki/routes/overview'; -import { PkiListCertsListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListCertsListEnum } from '@hashicorp/vault-client-typescript'; import { paginate } from 'core/utils/paginate-list'; @withConfig() @@ -33,7 +33,7 @@ export default class PkiCertificatesIndexRoute extends Route { const page = Number(params.page) || 1; const { keys: certificates } = await this.api.secrets.pkiListCerts( this.secretMountPath.currentPath, - PkiListCertsListEnum.TRUE + SecretsApiPkiListCertsListEnum.TRUE ); model.certificates = paginate(certificates, { page }); } catch (e) { diff --git a/ui/lib/pki/addon/routes/issuers/index.js b/ui/lib/pki/addon/routes/issuers/index.js index 7a502dcdeb..c08fff2e21 100644 --- a/ui/lib/pki/addon/routes/issuers/index.js +++ b/ui/lib/pki/addon/routes/issuers/index.js @@ -6,7 +6,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { paginate } from 'core/utils/paginate-list'; -import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; import { verifyCertificates, parseCertificate } from 'vault/utils/parse-pki-cert'; export default class PkiIssuersListRoute extends Route { @@ -32,7 +32,7 @@ export default class PkiIssuersListRoute extends Route { try { const listResponse = await this.api.secrets.pkiListIssuers( this.secretMountPath.currentPath, - PkiListIssuersListEnum.TRUE + SecretsApiPkiListIssuersListEnum.TRUE ); // fetch full issuer data only if there are less than 10 issuers to avoid making too many requests if (listResponse.keys.length <= 10) { diff --git a/ui/lib/pki/addon/routes/keys/index.js b/ui/lib/pki/addon/routes/keys/index.js index b690ac86bd..1afd6ce9f5 100644 --- a/ui/lib/pki/addon/routes/keys/index.js +++ b/ui/lib/pki/addon/routes/keys/index.js @@ -7,7 +7,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { withConfig } from 'pki/decorators/check-issuers'; import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; -import { PkiListKeysListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListKeysListEnum } from '@hashicorp/vault-client-typescript'; import { paginate } from 'core/utils/paginate-list'; @withConfig() @@ -53,7 +53,7 @@ export default class PkiKeysIndexRoute extends Route { try { const response = await this.api.secrets.pkiListKeys( this.secretMountPath.currentPath, - PkiListKeysListEnum.TRUE + SecretsApiPkiListKeysListEnum.TRUE ); const keys = this.api.keyInfoToArray(response, 'key_id'); const capabilities = await this.fetchCapabilities(keys); diff --git a/ui/lib/pki/addon/routes/overview.js b/ui/lib/pki/addon/routes/overview.js index f0ddef9316..70e95d5824 100644 --- a/ui/lib/pki/addon/routes/overview.js +++ b/ui/lib/pki/addon/routes/overview.js @@ -8,9 +8,9 @@ import { service } from '@ember/service'; import { withConfig } from 'pki/decorators/check-issuers'; import { hash } from 'rsvp'; import { - PkiListCertsListEnum, - PkiListRolesListEnum, - PkiListIssuersListEnum, + SecretsApiPkiListCertsListEnum, + SecretsApiPkiListRolesListEnum, + SecretsApiPkiListIssuersListEnum, } from '@hashicorp/vault-client-typescript'; export const PKI_DEFAULT_EMPTY_STATE_MSG = @@ -32,7 +32,7 @@ export default class PkiOverviewRoute extends Route { try { const { keys } = await this.api.secrets.pkiListCerts( this.secretMountPath.currentPath, - PkiListCertsListEnum.TRUE + SecretsApiPkiListCertsListEnum.TRUE ); return keys; } catch (e) { @@ -47,7 +47,7 @@ export default class PkiOverviewRoute extends Route { try { const { keys } = await this.api.secrets.pkiListRoles( this.secretMountPath.currentPath, - PkiListRolesListEnum.TRUE + SecretsApiPkiListRolesListEnum.TRUE ); return keys; } catch (e) { @@ -62,7 +62,7 @@ export default class PkiOverviewRoute extends Route { try { const { keys } = await this.api.secrets.pkiListIssuers( this.secretMountPath.currentPath, - PkiListIssuersListEnum.TRUE + SecretsApiPkiListIssuersListEnum.TRUE ); return keys; } catch (e) { diff --git a/ui/lib/pki/addon/routes/roles/create.js b/ui/lib/pki/addon/routes/roles/create.js index 2930782217..30c6215f03 100644 --- a/ui/lib/pki/addon/routes/roles/create.js +++ b/ui/lib/pki/addon/routes/roles/create.js @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; import PkiRoleForm from 'vault/forms/secrets/pki/role'; export default class PkiRolesCreateRoute extends Route { @@ -16,7 +16,7 @@ export default class PkiRolesCreateRoute extends Route { const backend = this.secretMountPath.currentPath; let issuers = []; try { - const response = await this.api.secrets.pkiListIssuers(backend, PkiListIssuersListEnum.TRUE); + const response = await this.api.secrets.pkiListIssuers(backend, SecretsApiPkiListIssuersListEnum.TRUE); issuers = this.api.keyInfoToArray(response, 'issuer_id'); } catch (error) { if (error.response.status !== 404) { diff --git a/ui/lib/pki/addon/routes/roles/index.js b/ui/lib/pki/addon/routes/roles/index.js index b1da121667..2c5840c2f4 100644 --- a/ui/lib/pki/addon/routes/roles/index.js +++ b/ui/lib/pki/addon/routes/roles/index.js @@ -7,7 +7,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { withConfig } from 'pki/decorators/check-issuers'; import { getCliMessage } from 'pki/routes/overview'; -import { PkiListRolesListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListRolesListEnum } from '@hashicorp/vault-client-typescript'; import { paginate } from 'core/utils/paginate-list'; @withConfig() @@ -33,7 +33,7 @@ export default class PkiRolesIndexRoute extends Route { const page = Number(params.page) || 1; const { keys: roles } = await this.api.secrets.pkiListRoles( this.secretMountPath.currentPath, - PkiListRolesListEnum.TRUE + SecretsApiPkiListRolesListEnum.TRUE ); model.roles = paginate(roles, { page }); } catch (e) { diff --git a/ui/lib/pki/addon/routes/roles/role/edit.js b/ui/lib/pki/addon/routes/roles/role/edit.js index a20a4533ce..c5a9a94a09 100644 --- a/ui/lib/pki/addon/routes/roles/role/edit.js +++ b/ui/lib/pki/addon/routes/roles/role/edit.js @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; +import { SecretsApiPkiListIssuersListEnum } from '@hashicorp/vault-client-typescript'; import PkiRoleForm from 'vault/forms/secrets/pki/role'; export default class PkiRoleEditRoute extends Route { @@ -20,7 +20,7 @@ export default class PkiRoleEditRoute extends Route { let issuers = []; try { - const response = await this.api.secrets.pkiListIssuers(backend, PkiListIssuersListEnum.TRUE); + const response = await this.api.secrets.pkiListIssuers(backend, SecretsApiPkiListIssuersListEnum.TRUE); issuers = this.api.keyInfoToArray(response, 'issuer_id'); } catch (error) { if (error.response.status !== 404) { diff --git a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts index 9e6e2d8f13..b1c392480a 100644 --- a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts +++ b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts @@ -38,7 +38,7 @@ export default class SyncActivationModal extends Component { // for non-managed clusters the root namespace path is technically an empty string, otherwise we pass 'admin' if HVD managed. const namespace = this.flags.hvdManagedNamespaceRoot || ''; try { - yield this.api.sys.activationFlagsActivate_3(this.api.buildHeaders({ namespace })); + yield this.api.sys.activationFlagsActivate_4(this.api.buildHeaders({ namespace })); // must refresh and not transition because transition does not refresh the model from within a namespace yield this.router.refresh('vault.cluster'); } catch (error) { diff --git a/ui/lib/sync/addon/routes/secrets/destinations/index.ts b/ui/lib/sync/addon/routes/secrets/destinations/index.ts index 31af7113dc..3b8d19d3dc 100644 --- a/ui/lib/sync/addon/routes/secrets/destinations/index.ts +++ b/ui/lib/sync/addon/routes/secrets/destinations/index.ts @@ -5,7 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { SystemListSyncDestinationsListEnum } from '@hashicorp/vault-client-typescript'; +import { SystemApiSystemListSyncDestinationsListEnum } from '@hashicorp/vault-client-typescript'; import { listDestinationsTransform } from 'sync/utils/api-transforms'; import { paginate } from 'core/utils/paginate-list'; @@ -56,7 +56,9 @@ export default class SyncSecretsDestinationsIndexRoute extends Route { async model(params: SyncSecretsDestinationsIndexRouteParams) { const { name = '', type = '', page } = params; - const response = await this.api.sys.systemListSyncDestinations(SystemListSyncDestinationsListEnum.TRUE); + const response = await this.api.sys.systemListSyncDestinations( + SystemApiSystemListSyncDestinationsListEnum.TRUE + ); // transform and paginate destinations response const destinations = listDestinationsTransform(response, name, type); const paginatedDestinations = paginate(destinations, { page: Number(page) || 1 }); diff --git a/ui/lib/sync/addon/routes/secrets/overview.ts b/ui/lib/sync/addon/routes/secrets/overview.ts index 70a8dadd66..c3fa3bd957 100644 --- a/ui/lib/sync/addon/routes/secrets/overview.ts +++ b/ui/lib/sync/addon/routes/secrets/overview.ts @@ -6,8 +6,8 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { - SystemListSyncDestinationsListEnum, - SystemListSyncAssociationsListEnum, + SystemApiSystemListSyncDestinationsListEnum, + SystemApiSystemListSyncAssociationsListEnum, } from '@hashicorp/vault-client-typescript'; import { listDestinationsTransform } from 'sync/utils/api-transforms'; @@ -35,8 +35,12 @@ export default class SyncSecretsOverviewRoute extends Route { const requests = isActivated ? [ capabilitiesReq, - this.api.sys.systemListSyncAssociations(SystemListSyncAssociationsListEnum.TRUE).catch(() => []), - this.api.sys.systemListSyncDestinations(SystemListSyncDestinationsListEnum.TRUE).catch(() => []), + this.api.sys + .systemListSyncAssociations(SystemApiSystemListSyncAssociationsListEnum.TRUE) + .catch(() => []), + this.api.sys + .systemListSyncDestinations(SystemApiSystemListSyncDestinationsListEnum.TRUE) + .catch(() => []), ] : [capabilitiesReq, [], []]; diff --git a/ui/package.json b/ui/package.json index 1bd68e2bdd..3b80499227 100644 --- a/ui/package.json +++ b/ui/package.json @@ -226,7 +226,7 @@ "@hashicorp-internal/vault-reporting": "file:vault-reporting/0.8.0.tgz", "@hashicorp/design-system-components": "4.24.1", "@hashicorp/design-system-tokens": "3.0.0", - "@hashicorp/vault-client-typescript": "hashicorp/vault-client-typescript", + "@hashicorp/vault-client-typescript": "github:hashicorp/vault-client-typescript", "ember-auto-import": "2.10.0", "handlebars": "4.7.8", "posthog-js": "1.236.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 9aecfe284e..a65cbcc6cb 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: 3.0.0 version: 3.0.0 '@hashicorp/vault-client-typescript': - specifier: hashicorp/vault-client-typescript - version: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/e29e1596079a941e42dd87cf533d95575f88a67c + specifier: github:hashicorp/vault-client-typescript + version: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/8f2ac4276aab57ae2296cac1df82a8cb88e5e45b ember-auto-import: specifier: 2.10.0 version: 2.10.0(@glint/template@1.7.3)(webpack@5.94.0) @@ -1519,8 +1519,8 @@ packages: '@hashicorp/flight-icons@3.14.0': resolution: {integrity: sha512-nyLDApaZsAHpAf2sRNwYX1MnJQU9UI3euiwE6wHPl2l/+Yt8wba1oXkmWL/Ptc4QgJxxnRUUhf66jGcB/AIOyQ==} - '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/e29e1596079a941e42dd87cf533d95575f88a67c': - resolution: {tarball: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/e29e1596079a941e42dd87cf533d95575f88a67c} + '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/8f2ac4276aab57ae2296cac1df82a8cb88e5e45b': + resolution: {tarball: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/8f2ac4276aab57ae2296cac1df82a8cb88e5e45b} version: 0.0.0 '@humanwhocodes/config-array@0.13.0': @@ -7180,8 +7180,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -10582,7 +10582,7 @@ snapshots: '@hashicorp/flight-icons@3.14.0': {} - '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/e29e1596079a941e42dd87cf533d95575f88a67c': {} + '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/8f2ac4276aab57ae2296cac1df82a8cb88e5e45b': {} '@humanwhocodes/config-array@0.13.0': dependencies: @@ -17871,7 +17871,7 @@ snapshots: semver@7.7.2: {} - semver@7.7.3: {} + semver@7.7.4: {} send@0.19.0: dependencies: @@ -18881,7 +18881,7 @@ snapshots: espree: 9.6.1 esquery: 1.7.0 lodash: 4.17.23 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - supports-color From d8ef5fb9a59677895add0becf1032b9623c84676 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 10:46:10 -0700 Subject: [PATCH 011/468] Event Notifications For Leases (#11699) (#12106) * update expiration.go * Update expiration.go * remove error from SendEventInternal to avoid potentially leaking sensitive data * Create _11699.txt * Update _11699.txt * Update expiration.go * add tests * add other lease event notifications * Update go.mod * update to use constants for event notifications * Update expiration_test.go * updated so operations are using constants * remove lease_id from error log * Added nil check for receivedEvent * Update expiration.go * Update expiration_test.go * Update expiration.go * Added second type safety check * removed old unecessary field from test * Remove error metadata from lease events Stop passing error metadata to sendLeaseEvent; update changelog * Update expiration_test.go * Revert "removed old unecessary field from test" This reverts commit b75bc7e2f7506f8bf567ac98871f86dcfc08c264. * Update changelog/_11699.txt * Update vault/expiration.go * Small fixes for pr comments * Update expiration_test.go * Update expiration.go --------- Co-authored-by: Jaired Jawed Co-authored-by: Theron Voran --- changelog/_11699.txt | 3 ++ vault/expiration.go | 78 +++++++++++++++++++++++++++++++++++++++- vault/expiration_test.go | 61 +++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 changelog/_11699.txt diff --git a/changelog/_11699.txt b/changelog/_11699.txt new file mode 100644 index 0000000000..389a3e6079 --- /dev/null +++ b/changelog/_11699.txt @@ -0,0 +1,3 @@ +```release-note:improvement +events (enterprise): Add event notifications support for lease events. +``` diff --git a/vault/expiration.go b/vault/expiration.go index 7d7f94b354..c00c73dbed 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -33,9 +33,11 @@ import ( "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/helper/locksutil" "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault/eventbus" "github.com/hashicorp/vault/vault/observations" "github.com/hashicorp/vault/vault/quotas" uberAtomic "go.uber.org/atomic" + "google.golang.org/protobuf/types/known/structpb" ) const ( @@ -80,6 +82,23 @@ const ( outOfRetriesMessage = "out of retries" + // event notification types for leases + leaseEventTypeExpired = "lease/expired" + leaseEventTypeRevokeFailed = "lease/revoke_failed" + leaseEventTypeRevoked = "lease/revoked" + leaseEventTypeRenewFailed = "lease/renew_failed" + leaseEventTypeRenewed = "lease/renewed" + leaseEventTypeIssued = "lease/issued" + + // event notification operations for leases + leaseOperationExpire = "expire" + leaseOperationRevoke = "revoke" + leaseOperationRenew = "renew" + leaseOperationIssue = "issue" + + // lease metadata + leaseMetadataLeaseID = "lease_id" + // maximum number of irrevocable leases we return to the irrevocable lease // list API **without** the `force` flag set MaxIrrevocableLeasesToReturn = 10000 @@ -233,8 +252,16 @@ func (r *revocationJob) Execute() error { default: } + // send a lease event for expiration + le, err := r.m.loadEntry(revokeCtx, r.leaseID) + if err != nil { + r.m.logger.Error("failed to load lease for expiration event", "error", err) + } else if le != nil { + r.m.sendLeaseEvent(revokeCtx, le, leaseEventTypeExpired, leaseOperationExpire, false) + } + r.m.coreStateLock.RLock() - err := r.m.Revoke(revokeCtx, r.leaseID) + err = r.m.Revoke(revokeCtx, r.leaseID) r.m.coreStateLock.RUnlock() return err @@ -1045,6 +1072,7 @@ func (m *ExpirationManager) revokeCommon(ctx context.Context, leaseID string, fo // Revoke the entry if !skipToken || le.Auth == nil { if err := m.revokeEntry(ctx, le); err != nil { + m.sendLeaseEvent(ctx, le, leaseEventTypeRevokeFailed, leaseOperationRevoke, false) if !force { return err } @@ -1115,6 +1143,10 @@ func (m *ExpirationManager) revokeCommon(ctx context.Context, leaseID string, fo } } + if !skipToken { + m.sendLeaseEvent(ctx, le, leaseEventTypeRevoked, leaseOperationRevoke, true) + } + if m.logger.IsInfo() && !skipToken && m.logLeaseExpirations { m.logger.Info("revoked lease", "lease_id", leaseID) } @@ -1276,6 +1308,7 @@ func (m *ExpirationManager) Renew(ctx context.Context, leaseID string, increment // Check if the lease is renewable if _, err := le.renewable(); err != nil { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return nil, err } @@ -1303,12 +1336,14 @@ func (m *ExpirationManager) Renew(ctx context.Context, leaseID string, increment // Attempt to renew the entry resp, err := m.renewEntry(ctx, le, increment) if err != nil { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return nil, err } if resp == nil { return nil, nil } if resp.IsError() { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return &logical.Response{ Data: resp.Data, }, nil @@ -1377,6 +1412,8 @@ func (m *ExpirationManager) Renew(ctx context.Context, leaseID string, increment // Update the expiration time m.updatePending(le) + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewed, leaseOperationRenew, true) + // Return the response return resp, nil } @@ -1433,18 +1470,21 @@ func (m *ExpirationManager) RenewToken(ctx context.Context, req *logical.Request // Check if the lease is renewable. Note that this also checks for a nil // lease and errors in that case as well. if _, err := le.renewable(); err != nil { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } // Attempt to renew the auth entry resp, err := m.renewAuthEntry(ctx, req, le, increment) if err != nil { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return nil, err } if resp == nil { return nil, nil } if resp.IsError() { + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewFailed, leaseOperationRenew, false) return &logical.Response{ Data: resp.Data, }, nil @@ -1506,6 +1546,8 @@ func (m *ExpirationManager) RenewToken(ctx context.Context, req *logical.Request return nil, err } + m.sendLeaseEvent(ctx, le, leaseEventTypeRenewed, leaseOperationRenew, true) + retResp.Auth = resp.Auth return retResp, nil } @@ -1646,6 +1688,8 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, // Setup revocation timer if there is a lease m.updatePending(le) + m.sendLeaseEvent(ctx, le, leaseEventTypeIssued, leaseOperationIssue, true) + // We round here because the clock will have already started // ticking, so we'll end up always returning 299 instead of 300 or // 26399 instead of 26400, say, even if it's just a few @@ -1746,6 +1790,8 @@ func (m *ExpirationManager) RegisterAuth(ctx context.Context, te *logical.TokenE te.ExternalID = tok } + m.sendLeaseEvent(ctx, &le, leaseEventTypeIssued, leaseOperationIssue, true) + return nil } @@ -1921,6 +1967,36 @@ func (m *ExpirationManager) updatePending(le *leaseEntry) { m.updatePendingInternal(le) } +// sendLeaseEvent sends an event notification for a lease operations +func (m *ExpirationManager) sendLeaseEvent(ctx context.Context, le *leaseEntry, eventType string, operation string, modified bool) { + if le == nil || le.namespace == nil || m.core == nil || m.core.events == nil { + return + } + + ev, err := logical.NewEvent() + if err != nil { + m.logger.Error("Error creating lease event", "error", err) + return + } + + metadata := map[string]string{ + logical.EventMetadataPath: le.Path, + logical.EventMetadataOperation: operation, + logical.EventMetadataModified: strconv.FormatBool(modified), + leaseMetadataLeaseID: le.LeaseID, + } + + ev.Metadata = &structpb.Struct{Fields: make(map[string]*structpb.Value, len(metadata))} + for key, value := range metadata { + ev.Metadata.Fields[key] = structpb.NewStringValue(value) + } + + err = m.core.events.SendEventInternal(ctx, le.namespace, nil, logical.EventType(eventType), false, ev) + if err != nil && !errors.Is(err, eventbus.ErrNotStarted) { + m.logger.Error("Error sending lease event", "path", le.Path, "operation", operation, "error", err) + } +} + // updatePendingInternal is the locked version of updatePending; do not call // this without a write lock on m.pending func (m *ExpirationManager) updatePendingInternal(le *leaseEntry) { diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 1800596809..7977dd1626 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/eventlogger" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/metricsutil" @@ -1832,6 +1833,13 @@ func TestExpiration_RenewToken_NotRenewable(t *testing.T) { func TestExpiration_Renew(t *testing.T) { exp := mockExpiration(t) + ctx := namespace.RootContext(nil) + ch, cancel, err := exp.core.events.Subscribe(ctx, namespace.RootNamespace, leaseEventTypeRenewed, "") + if err != nil { + t.Fatal(err) + } + defer cancel() + noop := &NoopBackend{} _, barrier, _ := mockBarrier(t) view := NewBarrierView(barrier, "logical/") @@ -1885,6 +1893,8 @@ func TestExpiration_Renew(t *testing.T) { t.Fatalf("err: %v", err) } + expectedPath := req.Path + noop.Lock() defer noop.Unlock() @@ -1899,10 +1909,24 @@ func TestExpiration_Renew(t *testing.T) { if req.Operation != logical.RenewOperation { t.Fatalf("Bad: %v", req) } + + select { + case receivedEvent := <-ch: + assertLeaseRenewEvent(t, receivedEvent, leaseEventTypeRenewed, expectedPath, id) + case <-time.After(5 * time.Second): + t.Fatalf("timeout waiting for %s event", leaseEventTypeRenewed) + } } func TestExpiration_Renew_NotRenewable(t *testing.T) { exp := mockExpiration(t) + ctx := namespace.RootContext(nil) + ch, cancel, err := exp.core.events.Subscribe(ctx, namespace.RootNamespace, leaseEventTypeRenewFailed, "") + if err != nil { + t.Fatal(err) + } + defer cancel() + noop := &NoopBackend{} _, barrier, _ := mockBarrier(t) view := NewBarrierView(barrier, "logical/") @@ -1950,6 +1974,13 @@ func TestExpiration_Renew_NotRenewable(t *testing.T) { if len(noop.Requests) != 0 { t.Fatalf("Bad: %#v", noop.Requests) } + + select { + case receivedEvent := <-ch: + assertLeaseRenewEvent(t, receivedEvent, leaseEventTypeRenewFailed, req.Path, "") + case <-time.After(5 * time.Second): + t.Fatalf("timeout waiting for %s event", leaseEventTypeRenewFailed) + } } func TestExpiration_Renew_RevokeOnExpire(t *testing.T) { @@ -3507,3 +3538,33 @@ func TestExpiration_listIrrevocableLeases_includeAll(t *testing.T) { t.Errorf("bad lease count. expected %d, got %d", expectedNumLeases, numLeases) } } + +func assertLeaseRenewEvent(t *testing.T, receivedEvent *eventlogger.Event, expectedEventType, expectedPath, expectedLeaseID string) { + t.Helper() + + if receivedEvent == nil || receivedEvent.Payload == nil { + t.Fatal("missing event payload") + } + + received, ok := receivedEvent.Payload.(*logical.EventReceived) + if !ok || received == nil { + t.Fatalf("unexpected payload type: %T", receivedEvent.Payload) + } + if received.EventType != expectedEventType { + t.Fatalf("unexpected event type: %s", received.EventType) + } + if received.Event == nil || received.Event.Metadata == nil { + t.Fatal("missing event metadata") + } + + metadata := received.Event.Metadata.Fields + if metadata[logical.EventMetadataOperation].GetStringValue() != leaseOperationRenew { + t.Fatalf("unexpected operation: %s", metadata[logical.EventMetadataOperation].GetStringValue()) + } + if metadata[logical.EventMetadataPath].GetStringValue() != expectedPath { + t.Fatalf("unexpected path: %s", metadata[logical.EventMetadataPath].GetStringValue()) + } + if expectedLeaseID != "" && metadata[leaseMetadataLeaseID].GetStringValue() != expectedLeaseID { + t.Fatalf("unexpected lease id: %s", metadata[leaseMetadataLeaseID].GetStringValue()) + } +} From e8dc7c908e825df315e3c0aa09eb96d81fc57a8d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 12:22:37 -0700 Subject: [PATCH 012/468] Fix cert tests failing due to expired cert in test fixtures (#12564) (#12570) Also fix panic when vault is sealed Co-authored-by: Nick Cabatoff --- builtin/credential/cert/backend_test.go | 1231 +++++++++++++---- builtin/credential/cert/path_crls_test.go | 91 +- .../credential/cert/test-fixtures/cacert.pem | 20 - .../credential/cert/test-fixtures/cacert2crl | 12 - .../credential/cert/test-fixtures/cakey.pem | 27 - .../cert/test-fixtures/generate.txt | 67 - .../cert/test-fixtures/issuedcertcrl | 12 - .../cert/test-fixtures/keys/cert-alt-cn.pem | 19 - .../cert/test-fixtures/keys/cert.pem | 18 - .../cert/test-fixtures/keys/key.pem | 27 - .../cert/test-fixtures/keys/pkioutput | 74 - .../cert/test-fixtures/keys/rebuild-cert.md | 6 - .../cert/test-fixtures/noclientauthcert.pem | 19 - .../cert/test-fixtures/root/pkioutput | 74 - .../cert/test-fixtures/root/root.crl | 12 - .../cert/test-fixtures/root/rootcacert.pem | 20 - .../cert/test-fixtures/root/rootcacert.srl | 1 - .../cert/test-fixtures/root/rootcakey.pem | 27 - .../cert/test-fixtures/root/rootcawext.cnf | 16 - .../cert/test-fixtures/root/rootcawext.csr | 19 - .../test-fixtures/root/rootcawextcert.pem | 20 - .../cert/test-fixtures/root/rootcawextkey.pem | 28 - .../cert/test-fixtures/root/rootcawocert.pem | 21 - .../cert/test-fixtures/root/rootcawokey.pem | 27 - .../cert/test-fixtures/root/rootcawou.cnf | 18 - .../cert/test-fixtures/root/rootcawou.csr | 17 - .../cert/test-fixtures/root/rootcawoucert.pem | 19 - .../cert/test-fixtures/root/rootcawoukey.pem | 28 - .../cert/test-fixtures/testcacert1.pem | 20 - .../cert/test-fixtures/testcacert2.pem | 20 - .../cert/test-fixtures/testcakey1.pem | 27 - .../cert/test-fixtures/testcakey2.pem | 27 - .../cert/test-fixtures/testcert1.pem | 20 - .../cert/test-fixtures/testcert2.pem | 20 - .../cert/test-fixtures/testissuedcert4.pem | 22 - .../cert/test-fixtures/testissuedkey4.pem | 27 - .../cert/test-fixtures/testkey1.pem | 27 - .../cert/test-fixtures/testkey2.pem | 27 - vault/consumption_billing_util.go | 5 +- vault/core_metrics.go | 11 +- 40 files changed, 1058 insertions(+), 1165 deletions(-) delete mode 100644 builtin/credential/cert/test-fixtures/cacert.pem delete mode 100644 builtin/credential/cert/test-fixtures/cacert2crl delete mode 100644 builtin/credential/cert/test-fixtures/cakey.pem delete mode 100644 builtin/credential/cert/test-fixtures/generate.txt delete mode 100644 builtin/credential/cert/test-fixtures/issuedcertcrl delete mode 100644 builtin/credential/cert/test-fixtures/keys/cert-alt-cn.pem delete mode 100644 builtin/credential/cert/test-fixtures/keys/cert.pem delete mode 100644 builtin/credential/cert/test-fixtures/keys/key.pem delete mode 100644 builtin/credential/cert/test-fixtures/keys/pkioutput delete mode 100644 builtin/credential/cert/test-fixtures/keys/rebuild-cert.md delete mode 100644 builtin/credential/cert/test-fixtures/noclientauthcert.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/pkioutput delete mode 100644 builtin/credential/cert/test-fixtures/root/root.crl delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcacert.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcacert.srl delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcakey.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawext.cnf delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawext.csr delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawextcert.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawextkey.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawocert.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawokey.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawou.cnf delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawou.csr delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawoucert.pem delete mode 100644 builtin/credential/cert/test-fixtures/root/rootcawoukey.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcacert1.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcacert2.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcakey1.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcakey2.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcert1.pem delete mode 100644 builtin/credential/cert/test-fixtures/testcert2.pem delete mode 100644 builtin/credential/cert/test-fixtures/testissuedcert4.pem delete mode 100644 builtin/credential/cert/test-fixtures/testissuedkey4.pem delete mode 100644 builtin/credential/cert/test-fixtures/testkey1.pem delete mode 100644 builtin/credential/cert/test-fixtures/testkey2.pem diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index efad03179e..6a20a84f1b 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -12,6 +12,7 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "fmt" "io" @@ -25,6 +26,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "time" @@ -34,6 +36,7 @@ import ( "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/logical/pki" + "github.com/hashicorp/vault/helper/testhelpers/certhelpers" logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/framework" @@ -48,21 +51,24 @@ import ( "golang.org/x/net/http2" ) -const ( - serverCertPath = "test-fixtures/cacert.pem" - serverKeyPath = "test-fixtures/cakey.pem" - serverCAPath = serverCertPath +// Helper function to create custom X.509 extensions +func createCustomExtensions() []pkix.Extension { + // OID 2.1.1.1 with UTF8String value + ext1Value, _ := asn1.Marshal("A UTF8String Extension") + ext1 := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 1, 1, 1}, + Value: ext1Value, + } - testRootCACertPath1 = "test-fixtures/testcacert1.pem" - testRootCAKeyPath1 = "test-fixtures/testcakey1.pem" - testCertPath1 = "test-fixtures/testissuedcert4.pem" - testKeyPath1 = "test-fixtures/testissuedkey4.pem" - testIssuedCertCRL = "test-fixtures/issuedcertcrl" + // OID 2.1.1.2 with UTF8String value + ext2Value, _ := asn1.Marshal("A UTF8 Extension") + ext2 := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 1, 1, 2}, + Value: ext2Value, + } - testRootCACertPath2 = "test-fixtures/testcacert2.pem" - testRootCAKeyPath2 = "test-fixtures/testcakey2.pem" - testRootCertCRL = "test-fixtures/cacert2crl" -) + return []pkix.Extension{ext1, ext2} +} func generateTestCertAndConnState(t *testing.T, template *x509.Certificate, caKey *ecdsa.PrivateKey) (string, tls.ConnectionState, error) { t.Helper() @@ -477,6 +483,102 @@ func TestBackend_PermittedDNSDomainsIntermediateCA(t *testing.T) { } func TestBackend_MetadataBasedACLPolicy(t *testing.T) { + // Generate CA certificate + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + t.Fatal(err) + } + + caPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertBytes, + }) + + // Generate client certificate with custom extensions + clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + // Create custom extensions + customExts := createCustomExtensions() + + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + CommonName: "example.com", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + ExtraExtensions: customExts, + } + + clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + + clientCertPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: clientCertBytes, + }) + + clientKeyBytes, err := x509.MarshalECPrivateKey(clientKey) + if err != nil { + t.Fatal(err) + } + + clientKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: clientKeyBytes, + }) + + // Write certificates to temporary files for API client configuration + clientCertFile, err := ioutil.TempFile("", "client-cert-*.pem") + if err != nil { + t.Fatal(err) + } + defer os.Remove(clientCertFile.Name()) + if _, err := clientCertFile.Write(clientCertPEM); err != nil { + t.Fatal(err) + } + clientCertFile.Close() + + clientKeyFile, err := ioutil.TempFile("", "client-key-*.pem") + if err != nil { + t.Fatal(err) + } + defer os.Remove(clientKeyFile.Name()) + if _, err := clientKeyFile.Write(clientKeyPEM); err != nil { + t.Fatal(err) + } + clientKeyFile.Close() + // Start cluster with cert auth method enabled coreConfig := &vault.CoreConfig{ CredentialBackends: map[string]logical.Factory{ @@ -492,8 +594,6 @@ func TestBackend_MetadataBasedACLPolicy(t *testing.T) { vault.TestWaitActive(t, cores[0].Core) client := cores[0].Client - var err error - // Enable the cert auth method err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{ Type: "cert", @@ -541,16 +641,11 @@ path "kv/ext/{{identity.entity.aliases.%s.metadata.2-1-1-1}}" { t.Fatalf("err: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } - // Set the trusted certificate in the backend _, err = client.Logical().Write("auth/cert/certs/test", map[string]interface{}{ "display_name": "test", "policies": "metadata-based", - "certificate": string(ca), + "certificate": string(caPEM), "allowed_metadata_extensions": "2.1.1.1,1.2.3.45", }) if err != nil { @@ -583,8 +678,8 @@ path "kv/ext/{{identity.entity.aliases.%s.metadata.2-1-1-1}}" { // Set the client certificates config.ConfigureTLS(&api.TLSConfig{ CACertBytes: cluster.CACertPEM, - ClientCert: "test-fixtures/root/rootcawextcert.pem", - ClientKey: "test-fixtures/root/rootcawextkey.pem", + ClientCert: clientCertFile.Name(), + ClientKey: clientKeyFile.Name(), }) apiClient, err := api.NewClient(config) @@ -862,14 +957,19 @@ func TestBackend_RegisteredNonCA_CRL(t *testing.T) { t.Fatal(err) } - nonCACert, err := ioutil.ReadFile(testCertPath1) - if err != nil { - t.Fatal(err) - } + // Generate a self-signed certificate that can sign CRLs (needs to be a CA for CRL signing) + leafCert := certhelpers.NewCert(t, + certhelpers.CommonName("cert.example.com"), + certhelpers.DNS("cert.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + leafCertPEM := strings.TrimSpace(string(leafCert.Pem)) // Register the Non-CA certificate of the client key pair certData := map[string]interface{}{ - "certificate": nonCACert, + "certificate": leafCertPEM, "policies": "abc", "display_name": "cert1", "ttl": 10000, @@ -886,12 +986,14 @@ func TestBackend_RegisteredNonCA_CRL(t *testing.T) { t.Fatalf("err:%v resp:%#v", err, resp) } - // Connection state is presenting the client Non-CA cert and its key. - // This is exactly what is registered at the backend. - connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1) + // Create connection state with the leaf certificate + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(leafCert.Pem) + connState, err := testConnStateWithCert(leafCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state:%v", err) } + loginReq := &logical.Request{ Operation: logical.UpdateOperation, Storage: storage, @@ -906,13 +1008,28 @@ func TestBackend_RegisteredNonCA_CRL(t *testing.T) { t.Fatalf("err:%v resp:%#v", err, resp) } - // Register a CRL containing the issued client certificate used above. - issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL) + // Generate a CRL that revokes the leaf certificate + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + ThisUpdate: time.Now(), + NextUpdate: time.Now().Add(24 * time.Hour), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: leafCert.Template.SerialNumber, + RevocationTime: time.Now(), + }, + }, + }, leafCert.Template, leafCert.PrivKey.PrivKey) if err != nil { t.Fatal(err) } + crlPEM := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + crlData := map[string]interface{}{ - "crl": issuedCRL, + "crl": crlPEM, } crlReq := &logical.Request{ Operation: logical.UpdateOperation, @@ -960,13 +1077,19 @@ func TestBackend_CRLs(t *testing.T) { t.Fatal(err) } - clientCA1, err := ioutil.ReadFile(testRootCACertPath1) - if err != nil { - t.Fatal(err) - } + // Generate CA1 certificate + ca1Cert := certhelpers.NewCert(t, + certhelpers.CommonName("ca1.example.com"), + certhelpers.DNS("ca1.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + ca1CertPEM := strings.TrimSpace(string(ca1Cert.Pem)) + // Register the CA certificate of the client key pair certData := map[string]interface{}{ - "certificate": clientCA1, + "certificate": ca1CertPEM, "policies": "abc", "display_name": "cert1", "ttl": 10000, @@ -986,10 +1109,13 @@ func TestBackend_CRLs(t *testing.T) { // Connection state is presenting the client CA cert and its key. // This is exactly what is registered at the backend. - connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath1, testRootCAKeyPath1) + rootCAs1 := x509.NewCertPool() + rootCAs1.AppendCertsFromPEM(ca1Cert.Pem) + connState, err := testConnStateWithCert(ca1Cert.TLSCert, rootCAs1) if err != nil { t.Fatalf("error testing connection state:%v", err) } + loginReq := &logical.Request{ Operation: logical.UpdateOperation, Storage: storage, @@ -1005,7 +1131,14 @@ func TestBackend_CRLs(t *testing.T) { // Now, without changing the registered client CA cert, present from // the client side, a cert issued using the registered CA. - connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1) + issuedCert := certhelpers.NewCert(t, + certhelpers.CommonName("issued.example.com"), + certhelpers.DNS("issued.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca1Cert), + ) + + connState, err = testConnStateWithCert(issuedCert.TLSCert, rootCAs1) if err != nil { t.Fatalf("error testing connection state: %v", err) } @@ -1018,12 +1151,27 @@ func TestBackend_CRLs(t *testing.T) { } // Register a CRL containing the issued client certificate used above. - issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + ThisUpdate: time.Now(), + NextUpdate: time.Now().Add(24 * time.Hour), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: issuedCert.Template.SerialNumber, + RevocationTime: time.Now(), + }, + }, + }, ca1Cert.Template, ca1Cert.PrivKey.PrivKey) if err != nil { t.Fatal(err) } + crlPEM := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + crlData := map[string]interface{}{ - "crl": issuedCRL, + "crl": crlPEM, } crlReq := &logical.Request{ @@ -1047,18 +1195,25 @@ func TestBackend_CRLs(t *testing.T) { } // Register a different client CA certificate. - clientCA2, err := ioutil.ReadFile(testRootCACertPath2) - if err != nil { - t.Fatal(err) - } - certData["certificate"] = clientCA2 + ca2Cert := certhelpers.NewCert(t, + certhelpers.CommonName("ca2.example.com"), + certhelpers.DNS("ca2.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + ca2CertPEM := strings.TrimSpace(string(ca2Cert.Pem)) + + certData["certificate"] = ca2CertPEM resp, err = b.HandleRequest(context.Background(), certReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err:%v resp:%#v", err, resp) } // Test login using a different client CA cert pair. - connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath2, testRootCAKeyPath2) + rootCAs2 := x509.NewCertPool() + rootCAs2.AppendCertsFromPEM(ca2Cert.Pem) + connState, err = testConnStateWithCert(ca2Cert.TLSCert, rootCAs2) if err != nil { t.Fatalf("error testing connection state: %v", err) } @@ -1071,11 +1226,26 @@ func TestBackend_CRLs(t *testing.T) { } // Register a CRL containing the root CA certificate used above. - rootCRL, err := ioutil.ReadFile(testRootCertCRL) + rootCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(2), + ThisUpdate: time.Now(), + NextUpdate: time.Now().Add(24 * time.Hour), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: ca2Cert.Template.SerialNumber, + RevocationTime: time.Now(), + }, + }, + }, ca2Cert.Template, ca2Cert.PrivKey.PrivKey) if err != nil { t.Fatal(err) } - crlData["crl"] = rootCRL + rootCRLPEM := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: rootCRLBytes, + }) + + crlData["crl"] = rootCRLPEM resp, err = b.HandleRequest(context.Background(), crlReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err:%v resp:%#v", err, resp) @@ -1113,28 +1283,60 @@ func testFactory(t *testing.T) logical.Backend { // Test the certificates being registered to the backend func TestBackend_CertWrites(t *testing.T) { - // CA cert - ca1, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + // Generate CA cert + ca1Cert := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate non-CA cert signed by CA + ca2Cert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.Parent(ca1Cert), + ) + + // Generate non-CA cert without TLS web client authentication + // Create a certificate template without client auth (should fail registration) + ca3Key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - t.Fatalf("err: %v", err) + t.Fatal(err) } - // Non CA Cert - ca2, err := ioutil.ReadFile("test-fixtures/keys/cert.pem") + subjKeyID, err := certutil.GetSubjKeyID(ca3Key) if err != nil { - t.Fatalf("err: %v", err) + t.Fatal(err) } - // Non CA cert without TLS web client authentication - ca3, err := ioutil.ReadFile("test-fixtures/noclientauthcert.pem") + ca3Template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "no-client-auth", + }, + SubjectKeyId: subjKeyID, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, // Only server auth, no client auth + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: false, // Not a CA + } + ca3Bytes, err := x509.CreateCertificate(rand.Reader, ca3Template, ca3Template, ca3Key.Public(), ca3Key) if err != nil { - t.Fatalf("err: %v", err) + t.Fatal(err) } + ca3PEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: ca3Bytes, + } + ca3PEM := pem.EncodeToMemory(ca3PEMBlock) tc := logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "aaa", ca1, "foo", allowed{}, false), - testAccStepCert(t, "bbb", ca2, "foo", allowed{}, false), - testAccStepCert(t, "ccc", ca3, "foo", allowed{}, true), + testAccStepCert(t, "aaa", ca1Cert.Pem, "foo", allowed{}, false), + testAccStepCert(t, "bbb", ca2Cert.Pem, "foo", allowed{}, false), + testAccStepCert(t, "ccc", ca3PEM, "foo", allowed{}, true), }, } tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...) @@ -1143,30 +1345,46 @@ func TestBackend_CertWrites(t *testing.T) { // Test a client trusted by a CA func TestBackend_basic_CA(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate client certificate signed by CA + clientCert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.DNS("*.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(clientCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{}, false), testAccStepLogin(t, connState), - testAccStepCertLease(t, "web", ca, "foo"), - testAccStepCertTTL(t, "web", ca, "foo"), + testAccStepCertLease(t, "web", ca.Pem, "foo"), + testAccStepCertTTL(t, "web", ca.Pem, "foo"), testAccStepLogin(t, connState), - testAccStepCertMaxTTL(t, "web", ca, "foo"), + testAccStepCertMaxTTL(t, "web", ca.Pem, "foo"), testAccStepLogin(t, connState), - testAccStepCertNoLease(t, "web", ca, "foo"), + testAccStepCertNoLease(t, "web", ca.Pem, "foo"), testAccStepLoginDefaultLease(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "*.example.com"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{names: "*.example.com"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "*.invalid.com"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{names: "*.invalid.com"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -1174,26 +1392,140 @@ func TestBackend_basic_CA(t *testing.T) { // Test CRL behavior func TestBackend_Basic_CRLs(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + subjKeyID, err := certutil.GetSubjKeyID(caKey) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + SubjectKeyId: subjKeyID, + KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + caBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatal(err) + } + caPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + caPEM := pem.EncodeToMemory(caPEMBlock) + + // Generate client certificate signed by CA + clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + clientSubjKeyID, err := certutil.GetSubjKeyID(clientKey) + if err != nil { + t.Fatal(err) + } + clientTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "client", + }, + SubjectKeyId: clientSubjKeyID, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + } + clientBytes, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, clientKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + clientCert, err := x509.ParseCertificate(clientBytes) + if err != nil { + t.Fatal(err) + } + clientPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: clientBytes, + } + clientPEM := pem.EncodeToMemory(clientPEMBlock) + + clientKeyBytes, err := x509.MarshalECPrivateKey(clientKey) + if err != nil { + t.Fatal(err) + } + clientKeyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: clientKeyBytes, + } + clientKeyPEM := pem.EncodeToMemory(clientKeyPEMBlock) + + // Create TLS certificate for connection state + tlsCert, err := tls.X509KeyPair(clientPEM, clientKeyPEM) + if err != nil { + t.Fatal(err) + } + tlsCert.Leaf = clientCert + + // Create root CA pool + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caCert) + + // Create connection state + connState, err := testConnStateWithCert(tlsCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) + + // Generate CRL with the client certificate revoked + now := time.Now() + revokedCerts := []pkix.RevokedCertificate{ + { + SerialNumber: clientCert.SerialNumber, + RevocationTime: now, + }, } - crl, err := ioutil.ReadFile("test-fixtures/root/root.crl") - if err != nil { - t.Fatalf("err: %v", err) + crlTemplate := &x509.RevocationList{ + Number: big.NewInt(1), + ThisUpdate: now, + NextUpdate: now.Add(24 * time.Hour), + RevokedCertificates: revokedCerts, } + crlBytes, err := x509.CreateRevocationList(rand.Reader, crlTemplate, caCert, caKey) + if err != nil { + t.Fatal(err) + } + crlPEMBlock := &pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + } + crlPEM := pem.EncodeToMemory(crlPEMBlock) + + // Get the serial number as a string for checking in the CRL + expectedSerial := clientCert.SerialNumber.String() + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCertNoLease(t, "web", ca, "foo"), + testAccStepCertNoLease(t, "web", caPEM, "foo"), testAccStepLoginDefaultLease(t, connState), - testAccStepAddCRL(t, crl, connState), - testAccStepReadCRL(t, connState), + testAccStepAddCRL(t, crlPEM, connState), + testAccStepReadCRLWithSerial(t, connState, expectedSerial), testAccStepLoginInvalid(t, connState), testAccStepDeleteCRL(t, connState), testAccStepLoginDefaultLease(t, connState), @@ -1203,50 +1535,68 @@ func TestBackend_Basic_CRLs(t *testing.T) { // Test a self-signed client (root CA) that is trusted func TestBackend_basic_singleCert(t *testing.T) { - connState, err := testConnState("test-fixtures/root/rootcacert.pem", - "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") + // Generate self-signed CA certificate that will also be used as client cert + ca := certhelpers.NewCert(t, + certhelpers.CommonName("example.com"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + certhelpers.IP("127.0.0.1"), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state using the CA cert as both client and CA + connState, err := testConnStateWithCert(ca.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{names: "example.com"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{names: "invalid"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{ext: "1.2.3.4:invalid"}, false), testAccStepLoginInvalid(t, connState), }, }) } func TestBackend_common_name_singleCert(t *testing.T) { - connState, err := testConnState("test-fixtures/root/rootcacert.pem", - "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") + // Generate self-signed CA certificate that will also be used as client cert + ca := certhelpers.NewCert(t, + certhelpers.CommonName("example.com"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + certhelpers.IP("127.0.0.1"), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state using the CA cert as both client and CA + connState, err := testConnStateWithCert(ca.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{common_names: "example.com"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{common_names: "example.com"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{common_names: "invalid"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{common_names: "invalid"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), + testAccStepCert(t, "web", ca.Pem, "foo", allowed{ext: "1.2.3.4:invalid"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -1254,71 +1604,129 @@ func TestBackend_common_name_singleCert(t *testing.T) { // Test a self-signed client with custom ext (root CA) that is trusted func TestBackend_ext_singleCert(t *testing.T) { - connState, err := testConnState( - "test-fixtures/root/rootcawextcert.pem", - "test-fixtures/root/rootcawextkey.pem", - "test-fixtures/root/rootcacert.pem", - ) + // Generate self-signed CA certificate with custom X.509 extensions + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + subjKeyID, err := certutil.GetSubjKeyID(caKey) + if err != nil { + t.Fatal(err) + } + + // Create custom extensions + customExtensions := createCustomExtensions() + + caTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.com", + }, + SubjectKeyId: subjKeyID, + DNSNames: []string{"example.com"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature), + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + ExtraExtensions: customExtensions, + } + caBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatal(err) + } + caPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + caPEM := pem.EncodeToMemory(caPEMBlock) + + // Create TLS certificate for connection state + caKeyBytes, err := x509.MarshalECPrivateKey(caKey) + if err != nil { + t.Fatal(err) + } + caKeyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: caKeyBytes, + } + caKeyPEM := pem.EncodeToMemory(caKeyPEMBlock) + + tlsCert, err := tls.X509KeyPair(caPEM, caKeyPEM) + if err != nil { + t.Fatal(err) + } + tlsCert.Leaf = caCert + + // Create root CA pool + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caCert) + + // Create connection state + connState, err := testConnStateWithCert(tlsCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:A UTF8String Extension"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.45:*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:,2.1.1.2:*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{ext: "2.1.1.1:,2.1.1.2:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:A UTF8String Extension"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "1.2.3.45:*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:A UTF8String Extension"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "invalid", ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "1.2.3.45:*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "invalid", ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "invalid", ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000002*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000002*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000001*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000001*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.5.29.17:"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{names: "example.com", ext: "2.5.29.17:"}, false), testAccStepLogin(t, connState), testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: false}, connState), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, false), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{metadata_ext: "1.2.3.45"}, false), testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, false), testAccStepSetConfig(t, config{EnableIdentityAliasMetadata: true}, connState), testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: true}, connState), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false), testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, true), - testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{metadata_ext: "1.2.3.45"}, false), testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, true), testAccStepSetConfig(t, config{EnableMetadataOnFailures: true}, connState), testAccStepReadConfig(t, config{EnableMetadataOnFailures: true}, connState), @@ -1422,28 +1830,81 @@ func TestBackend_email_singleCert(t *testing.T) { // Test a self-signed client with OU (root CA) that is trusted func TestBackend_organizationalUnit_singleCert(t *testing.T) { - connState, err := testConnState( - "test-fixtures/root/rootcawoucert.pem", - "test-fixtures/root/rootcawoukey.pem", - "test-fixtures/root/rootcawoucert.pem", - ) + // Generate self-signed CA certificate with organizational unit + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + subjKeyID, err := certutil.GetSubjKeyID(caKey) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.com", + OrganizationalUnit: []string{"engineering"}, + }, + SubjectKeyId: subjKeyID, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature), + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + caBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatal(err) + } + caPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + caPEM := pem.EncodeToMemory(caPEMBlock) + + // Create TLS certificate for connection state + caKeyBytes, err := x509.MarshalECPrivateKey(caKey) + if err != nil { + t.Fatal(err) + } + caKeyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: caKeyBytes, + } + caKeyPEM := pem.EncodeToMemory(caKeyPEMBlock) + + tlsCert, err := tls.X509KeyPair(caPEM, caKeyPEM) + if err != nil { + t.Fatal(err) + } + tlsCert.Leaf = caCert + + // Create root CA pool + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caCert) + + // Create connection state + connState, err := testConnStateWithCert(tlsCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcawoucert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizational_units: "engineering"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "eng*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizational_units: "eng*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering,finance"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizational_units: "engineering,finance"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "foo"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizational_units: "foo"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -1452,32 +1913,81 @@ func TestBackend_organizationalUnit_singleCert(t *testing.T) { // TestBackend_organization_singleCert checks validation of the Organization (O) field on a self-signed cert that is // trusted func TestBackend_organization_singleCert(t *testing.T) { - connState, err := testConnState( - "test-fixtures/root/rootcawocert.pem", - "test-fixtures/root/rootcawokey.pem", - "test-fixtures/root/rootcawocert.pem", - // These have been generated to last 100 years with the following Vault PKI commands: - // $ vault -v secrets enable pki - // $ vault secrets tune -max-lease-ttl=876000h pki - // $ vault write pki/root/generate/exported common_name=example.com organization="example" ip_sans=127.0.0.1 ttl=875999h - ) + // Generate self-signed CA certificate with organization + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + subjKeyID, err := certutil.GetSubjKeyID(caKey) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.com", + Organization: []string{"example"}, + }, + SubjectKeyId: subjKeyID, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature), + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + caBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatal(err) + } + caPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + caPEM := pem.EncodeToMemory(caPEMBlock) + + // Create TLS certificate for connection state + caKeyBytes, err := x509.MarshalECPrivateKey(caKey) + if err != nil { + t.Fatal(err) + } + caKeyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: caKeyBytes, + } + caKeyPEM := pem.EncodeToMemory(caKeyPEMBlock) + + tlsCert, err := tls.X509KeyPair(caPEM, caKeyPEM) + if err != nil { + t.Fatal(err) + } + tlsCert.Leaf = caCert + + // Create root CA pool + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caCert) + + // Create connection state + connState, err := testConnStateWithCert(tlsCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcawocert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", allowed{organizations: "example"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizations: "example"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizations: "exa*"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizations: "exa*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizations: "example,otherspecimen"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizations: "example,otherspecimen"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", allowed{organizations: "foo"}, false), + testAccStepCert(t, "web", caPEM, "foo", allowed{organizations: "foo"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -1588,21 +2098,37 @@ func TestBackend_sameKey_differentCN(t *testing.T) { // Test against a collection of matching and non-matching rules func TestBackend_mixed_constraints(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate client certificate signed by CA + clientCert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.DNS("*.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(clientCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "1unconstrained", ca, "foo", allowed{}, false), - testAccStepCert(t, "2matching", ca, "foo", allowed{names: "*.example.com,whatever"}, false), - testAccStepCert(t, "3invalid", ca, "foo", allowed{names: "invalid"}, false), + testAccStepCert(t, "1unconstrained", ca.Pem, "foo", allowed{}, false), + testAccStepCert(t, "2matching", ca.Pem, "foo", allowed{names: "*.example.com,whatever"}, false), + testAccStepCert(t, "3invalid", ca.Pem, "foo", allowed{names: "invalid"}, false), testAccStepLogin(t, connState), // Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match testAccStepLoginWithName(t, connState, "2matching"), @@ -1613,11 +2139,30 @@ func TestBackend_mixed_constraints(t *testing.T) { // Test an untrusted client func TestBackend_untrusted(t *testing.T) { - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate client certificate signed by CA + clientCert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(clientCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: testFactory(t), Steps: []logicaltest.TestStep{ @@ -1637,20 +2182,40 @@ func TestBackend_different_cn(t *testing.T) { t.Fatal(err) } - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate first client certificate + cert1 := certhelpers.NewCert(t, + certhelpers.CommonName("example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Generate second client certificate with different CN + cert2 := certhelpers.NewCert(t, + certhelpers.CommonName("alt.example.com"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection states + connState, err := testConnStateWithCert(cert1.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - connState2, err := testConnState("test-fixtures/keys/cert-alt-cn.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + connState2, err := testConnStateWithCert(cert2.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/keys/cert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } name := "web" @@ -1658,7 +2223,7 @@ func TestBackend_different_cn(t *testing.T) { Operation: logical.UpdateOperation, Path: "certs/" + name, Data: map[string]interface{}{ - "certificate": string(ca), + "certificate": string(cert1.Pem), "policies": "foo", "display_name": name, "lease": 1000, @@ -1718,15 +2283,29 @@ func TestBackend_validCIDR(t *testing.T) { t.Fatal(err) } - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate client certificate signed by CA + clientCert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(clientCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } name := "web" boundCIDRs := []string{"127.0.0.1", "128.252.0.0/16"} @@ -1735,7 +2314,7 @@ func TestBackend_validCIDR(t *testing.T) { Operation: logical.UpdateOperation, Path: "certs/" + name, Data: map[string]interface{}{ - "certificate": string(ca), + "certificate": string(ca.Pem), "policies": "foo", "display_name": name, "allowed_names": "", @@ -1800,15 +2379,29 @@ func TestBackend_invalidCIDR(t *testing.T) { t.Fatal(err) } - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate client certificate signed by CA + clientCert := certhelpers.NewCert(t, + certhelpers.CommonName("test-client"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(clientCert.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatalf("err: %v", err) - } name := "web" @@ -1816,7 +2409,7 @@ func TestBackend_invalidCIDR(t *testing.T) { Operation: logical.UpdateOperation, Path: "certs/" + name, Data: map[string]interface{}{ - "certificate": string(ca), + "certificate": string(ca.Pem), "policies": "foo", "display_name": name, "allowed_names": "", @@ -1886,6 +2479,28 @@ func testAccStepReadCRL(t *testing.T, connState tls.ConnectionState) logicaltest } } +func testAccStepReadCRLWithSerial(t *testing.T, connState tls.ConnectionState, expectedSerial string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "crls/test", + ConnState: &connState, + Check: func(resp *logical.Response) error { + crlInfo := CRLInfo{} + err := mapstructure.Decode(resp.Data, &crlInfo) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(crlInfo.Serials) != 1 { + t.Fatalf("bad: expected CRL with length 1, got %d", len(crlInfo.Serials)) + } + if _, ok := crlInfo.Serials[expectedSerial]; !ok { + t.Fatalf("bad: expected serial number %s not found in CRL, got serials: %v", expectedSerial, crlInfo.Serials) + } + return nil + }, + } +} + func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, @@ -2363,15 +2978,37 @@ func Test_Renew(t *testing.T) { } b := lb.(*backend) - connState, err := testConnState("test-fixtures/testcert1.pem", - "test-fixtures/testkey1.pem", "test-fixtures/root/rootcacert.pem") + + // Generate CA certificate + ca := certhelpers.NewCert(t, + certhelpers.CommonName("test-ca"), + certhelpers.IsCA(true), + certhelpers.SelfSign(), + ) + + // Generate first client certificate signed by CA + clientCert1 := certhelpers.NewCert(t, + certhelpers.CommonName("test-client-1"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Generate second client certificate signed by CA (for testing cert change) + clientCert2 := certhelpers.NewCert(t, + certhelpers.CommonName("test-client-2"), + certhelpers.IP("127.0.0.1"), + certhelpers.Parent(ca), + ) + + // Create root CA pool for connection state + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(ca.Pem) + + // Create connection state with first certificate + connState, err := testConnStateWithCert(clientCert1.TLSCert, rootCAs) if err != nil { t.Fatalf("error testing connection state: %v", err) } - ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") - if err != nil { - t.Fatal(err) - } req := &logical.Request{ Connection: &logical.Connection{ @@ -2384,7 +3021,7 @@ func Test_Renew(t *testing.T) { fd := &framework.FieldData{ Raw: map[string]interface{}{ "name": "test", - "certificate": ca, + "certificate": ca.Pem, // Uppercase B should not cause an issue during renewal "token_policies": "foo,Bar", }, @@ -2428,8 +3065,7 @@ func Test_Renew(t *testing.T) { // Try changing the cert - this should fail goodConnState := connState - connState, err = testConnState("test-fixtures/testcert2.pem", - "test-fixtures/testkey2.pem", "test-fixtures/root/rootcacert.pem") + connState, err = testConnStateWithCert(clientCert2.TLSCert, rootCAs) req.Connection.ConnState = &connState if err != nil { t.Fatal(err) @@ -2584,16 +3220,11 @@ func TestBackend_CertUpgrade(t *testing.T) { // TestOCSPFailOpenWithBadIssuer validates we fail all different types of cert auth // login scenarios if we encounter an OCSP verification error func TestOCSPFailOpenWithBadIssuer(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") + caTLS, leafTLS, pemCa := createCertAndKey(t) + + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caTLS.Leaf) - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) connState, err := testConnStateWithCert(leafTLS, rootCAs) require.NoError(t, err, "error testing connection state: %v", err) @@ -2676,16 +3307,11 @@ func TestOCSPFailOpenWithBadIssuer(t *testing.T) { // TestOCSPWithMixedValidResponses validates the expected behavior of multiple OCSP servers configured, // with and without ocsp_query_all_servers enabled or disabled. func TestOCSPWithMixedValidResponses(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") + caTLS, leafTLS, pemCa := createCertAndKey(t) + + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caTLS.Leaf) - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) connState, err := testConnStateWithCert(leafTLS, rootCAs) require.NoError(t, err, "error testing connection state: %v", err) @@ -2755,16 +3381,11 @@ func TestOCSPWithMixedValidResponses(t *testing.T) { // TestOCSPFailOpenWithGoodResponse validates the expected behavior with multiple OCSP servers configured // one that returns a Good response the other is not available, along with the ocsp_fail_open in multiple modes func TestOCSPFailOpenWithGoodResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") + caTLS, leafTLS, pemCa := createCertAndKey(t) + + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caTLS.Leaf) - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) connState, err := testConnStateWithCert(leafTLS, rootCAs) require.NoError(t, err, "error testing connection state: %v", err) @@ -2871,16 +3492,11 @@ func TestOCSPFailOpenWithGoodResponse(t *testing.T) { // TestOCSPFailOpenWithRevokeResponse validates the expected behavior with multiple OCSP servers configured // one that returns a Revoke response the other is not available, along with the ocsp_fail_open in multiple modes func TestOCSPFailOpenWithRevokeResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") + caTLS, leafTLS, pemCa := createCertAndKey(t) + + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caTLS.Leaf) - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) connState, err := testConnStateWithCert(leafTLS, rootCAs) require.NoError(t, err, "error testing connection state: %v", err) @@ -2962,16 +3578,11 @@ func TestOCSPFailOpenWithRevokeResponse(t *testing.T) { // TestOCSPFailOpenWithUnknownResponse validates the expected behavior with multiple OCSP servers configured // one that returns an Unknown response the other is not available, along with the ocsp_fail_open in multiple modes func TestOCSPFailOpenWithUnknownResponse(t *testing.T) { - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) - caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem") - leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem") + caTLS, leafTLS, pemCa := createCertAndKey(t) + + rootCAs := x509.NewCertPool() + rootCAs.AddCert(caTLS.Leaf) - rootConfig := &rootcerts.Config{ - CAFile: caFile, - } - rootCAs, err := rootcerts.LoadCACerts(rootConfig) connState, err := testConnStateWithCert(leafTLS, rootCAs) require.NoError(t, err, "error testing connection state: %v", err) @@ -3066,9 +3677,7 @@ func TestOcspMaxRetriesUpdate(t *testing.T) { }) require.NoError(t, err, "failed creating backend") - caFile := "test-fixtures/root/rootcacert.pem" - pemCa, err := os.ReadFile(caFile) - require.NoError(t, err, "failed reading in file %s", caFile) + _, _, pemCa := createCertAndKey(t) data := map[string]interface{}{ "certificate": string(pemCa), @@ -3156,13 +3765,15 @@ func TestRecoverPartialLoad(t *testing.T) { require.True(t, ok, "somehow the backend was the wrong type") // There a single certificate in storage... - nonCACert, err := os.ReadFile(testCertPath1) - if err != nil { - t.Fatal(err) - } + // Generate a self-signed non-CA certificate + leafCert := certhelpers.NewCert(t, + certhelpers.CommonName("cert_a.example.com"), + certhelpers.SelfSign(), + ) + entry, err := logical.StorageEntryJSON("cert/cert_a", CertEntry{ Name: "cert_a", - Certificate: string(nonCACert), + Certificate: string(leafCert.Pem), }) if err != nil { t.Fatal(err) @@ -3229,3 +3840,91 @@ func createCa(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey) { return rootCa, rootCaKey } + +// createCertAndKey generates a CA and leaf certificate dynamically for testing +func createCertAndKey(t *testing.T) (caTLS tls.Certificate, leafTLS tls.Certificate, caPEM []byte) { + // Generate CA + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, "failed to generate CA key") + + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + require.NoError(t, err, "failed to create CA certificate") + + caCert, err := x509.ParseCertificate(caCertBytes) + require.NoError(t, err, "failed to parse CA certificate") + + caPEM = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertBytes, + }) + + caKeyBytes, err := x509.MarshalECPrivateKey(caKey) + require.NoError(t, err, "failed to marshal CA key") + + caKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: caKeyBytes, + }) + + // Create tls.Certificate for CA + caTLS, err = tls.X509KeyPair(caPEM, caKeyPEM) + require.NoError(t, err, "failed to create CA tls.Certificate") + caTLS.Leaf = caCert + + // Generate leaf certificate + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, "failed to generate leaf key") + + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + CommonName: "cert.example.com", + }, + DNSNames: []string{"cert.example.com", "localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + leafCertBytes, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCert, &leafKey.PublicKey, caKey) + require.NoError(t, err, "failed to create leaf certificate") + + leafCert, err := x509.ParseCertificate(leafCertBytes) + require.NoError(t, err, "failed to parse leaf certificate") + + leafCertPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: leafCertBytes, + }) + + leafKeyBytes, err := x509.MarshalECPrivateKey(leafKey) + require.NoError(t, err, "failed to marshal leaf key") + + leafKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: leafKeyBytes, + }) + + // Create tls.Certificate for leaf + leafTLS, err = tls.X509KeyPair(leafCertPEM, leafKeyPEM) + require.NoError(t, err, "failed to create leaf tls.Certificate") + leafTLS.Leaf = leafCert + + return caTLS, leafTLS, caPEM +} diff --git a/builtin/credential/cert/path_crls_test.go b/builtin/credential/cert/path_crls_test.go index 837566f9a8..df33c0de39 100644 --- a/builtin/credential/cert/path_crls_test.go +++ b/builtin/credential/cert/path_crls_test.go @@ -5,12 +5,16 @@ package cert import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/pem" "fmt" - "io/ioutil" "math/big" + "net" "net/http" "net/http/httptest" "net/url" @@ -55,14 +59,87 @@ func TestCRLFetch(t *testing.T) { if err != nil { t.Fatalf("error: %s", err) } - connState, err := testConnState("test-fixtures/keys/cert.pem", - "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") + + // Generate CA certificate + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - caPEM, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) require.NoError(t, err) - caKeyPEM, err := ioutil.ReadFile("test-fixtures/keys/key.pem") + + caCert, err := x509.ParseCertificate(caCertBytes) + require.NoError(t, err) + + caPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertBytes, + }) + + caKeyBytes, err := x509.MarshalECPrivateKey(caKey) + require.NoError(t, err) + + caKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: caKeyBytes, + }) + + // Generate client certificate + clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + CommonName: "test.example.com", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost", "test.example.com"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientKey.PublicKey, caKey) + require.NoError(t, err) + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: clientCertBytes, + }) + + clientKeyBytes, err := x509.MarshalECPrivateKey(clientKey) + require.NoError(t, err) + + clientKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: clientKeyBytes, + }) + + // Create tls.Certificate from PEM data + tlsCert, err := tls.X509KeyPair(certPEM, clientKeyPEM) + require.NoError(t, err) + + // Create CA cert pool + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(caPEM) + + // Create connection state with generated certificates + connState, err := testConnStateWithCert(tlsCert, rootCAs) require.NoError(t, err) - certPEM, err := ioutil.ReadFile("test-fixtures/keys/cert.pem") caBundle, err := certutil.ParsePEMBundle(string(caPEM)) require.NoError(t, err) @@ -80,7 +157,7 @@ func TestCRLFetch(t *testing.T) { Number: big.NewInt(1), ThisUpdate: time.Now(), NextUpdate: time.Now().Add(50 * time.Millisecond), - SignatureAlgorithm: x509.SHA1WithRSA, + SignatureAlgorithm: x509.ECDSAWithSHA256, } var crlBytesLock sync.Mutex diff --git a/builtin/credential/cert/test-fixtures/cacert.pem b/builtin/credential/cert/test-fixtures/cacert.pem deleted file mode 100644 index 9d9a3859e5..0000000000 --- a/builtin/credential/cert/test-fixtures/cacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUXiEDuecwua9+j1XHLnconxQ/JBcwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwMzU4WhgPMjA2 -NjA0MjAxNjA0MjhaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwWPjnTqnkc6acah+wWLmdTK0oCrf2687XVhx -VP3IN897TYzkaBQ2Dn1UM2VEL71sE3OZSVm0UWs5n7UqRuDp6mvkvrT2q5zgh/bV -zg9ZL1AI5H7dY2Rsor95I849ymFpXZooMgNtIQLxIeleBwzTnVSkFl8RqKM7NkjZ -wvBafQEjSsYk9050Bu0GMLgFJYRo1LozJLbwIs5ykG5F5PWTMfRvLCgLBzixPb75 -unIJ29nL0yB7zzUdkM8CG1EX8NkjGLEnpRnPa7+RMf8bd10v84cr0JFCUQmoabks -sqVyA825/1we2r5Y8blyXZVIr2lcPyGocLDxz1qT1MqxrNQIywIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBTo2I+W -3Wb2MBe3OWuj5qCbafavMB8GA1UdIwQYMBaAFBTo2I+W3Wb2MBe3OWuj5qCbafav -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQAyjJzDMzf28yMgiu//2R6LD3+zuLHlfX8+p5JB7WDBT7CgSm89gzMRtD2DvqZQ -6iLbZv/x7Td8bdLsOKf3LDCkZyOygJ0Sr9+6YZdc9heWO8tsO/SbcLhj9/vK8YyV -5fJo+vECW8I5zQLeTKfPqJtTU0zFspv0WYCB96Hsbhd1hTfHmVgjBoxi0YuduAa8 -3EHuYPfTYkO3M4QJCoQ+3S6LXSTDqppd1KGAy7QhRU6shd29EpSVxhgqZ+CIOpZu -3RgPOgPqfqcOD/v/SRPqhRf+P5O5Dc/N4ZXTZtfJbaY0qE+smpeQUskVQ2TrSqha -UYpNk7+toZW3Gioo0lBD3gH2 ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/cacert2crl b/builtin/credential/cert/test-fixtures/cacert2crl deleted file mode 100644 index 82db7a3f6c..0000000000 --- a/builtin/credential/cert/test-fixtures/cacert2crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtteXZhdWx0LmNvbRcN -MTYwNTAyMTYxNDMzWhcNMTYwNTA1MTYxNDMzWjArMCkCFCXxxcbS0ATpI2PYrx8d -ACLEQ3B9FxExNjA1MDIxMjE0MzMtMDQwMKAjMCEwHwYDVR0jBBgwFoAUwsRNYCw4 -U2won66rMKEJm8inFfgwDQYJKoZIhvcNAQELBQADggEBAD/VvoRK4eaEDzG7Z95b -fHL5ubJGkyvkp8ruNu+rfQp8NLgFVvY6a93Hz7WLOhACkKIWJ63+/4vCfDi5uU0B -HW2FICHdlSQ+6DdGJ6MrgujALlyT+69iF+fPiJ/M1j/N7Am8XPYYcfNdSK6CHtfg -gHNB7E+ubBA7lIw7ucIkoiJjXrSWSXTs9/GzLUImiXJAKQ+JzPYryIsGKXKAwgHh -HB56BnJ2vOs7+6UxQ6fjKTMxYdNgoZ34MhkkxNNhylrEndO6XUvUvC1f/1p1wlzy -xTq2MrMfJHJyu08rkrD+kwMPH2uoVwKyDhXdRBP0QrvQwOsvNEhW8LTKwLWkK17b -fEI= ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/cakey.pem b/builtin/credential/cert/test-fixtures/cakey.pem deleted file mode 100644 index ecba4754cd..0000000000 --- a/builtin/credential/cert/test-fixtures/cakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAwWPjnTqnkc6acah+wWLmdTK0oCrf2687XVhxVP3IN897TYzk -aBQ2Dn1UM2VEL71sE3OZSVm0UWs5n7UqRuDp6mvkvrT2q5zgh/bVzg9ZL1AI5H7d -Y2Rsor95I849ymFpXZooMgNtIQLxIeleBwzTnVSkFl8RqKM7NkjZwvBafQEjSsYk -9050Bu0GMLgFJYRo1LozJLbwIs5ykG5F5PWTMfRvLCgLBzixPb75unIJ29nL0yB7 -zzUdkM8CG1EX8NkjGLEnpRnPa7+RMf8bd10v84cr0JFCUQmoabkssqVyA825/1we -2r5Y8blyXZVIr2lcPyGocLDxz1qT1MqxrNQIywIDAQABAoIBAD1pBd9ov8t6Surq -sY2hZUM0Hc16r+ln5LcInbx6djjaxvHiWql+OYgyXimP764lPYuTuspjFPKB1SOU -+N7XDxCkwFeayXXHdDlYtZ4gm5Z9mMVOT+j++8xWdxZaqJ56fmX9zOPM2LuR3paB -L52Xgh9EwHJmMApYAzaCvbu8bU+iHeNTW80xabxQrp9VCu/A1BXUX06jK4T+wmjZ -kDA82uQp3dCOF1tv/10HgwqkJj6/1jjM0XUzUZR6iV85S6jrA7wD7gDDeqNO8YHN -08YMRgTKk4pbA7AqoC5xbL3gbSjsjyw48KRq0FkdkjsgV0PJZRMUU9fv9puDa23K -WRPa8LECgYEAyeth5bVH8FXnVXIAAFU6W0WdgCK3VakhjItLw0eoxshuTwbVq64w -CNOB8y1pfP83WiJjX3qRG43NDW07X69J57YKtCCb6KICVUPmecgYZPkmegD1HBQZ -5+Aak+5pIUQuycQ0t65yHGu4Jsju05gEFgdzydFjNANgiPxRzZxzAkkCgYEA9S+y -ZR063oCQDg/GhMLCx19nCJyU44Figh1YCD6kTrsSTECuRpQ5B1F9a+LeZT2wnYxv -+qMvvV+lfVY73f5WZ567u2jSDIsCH34p4g7sE25lKwo+Lhik6EtOehJFs2ZUemaT -Ym7EjqWlC1whrG7P4MnTGzPOVNAGAxsGPtT58nMCgYAs/R8A2VU//UPfy9ioOlUY -RPiEtjd3BIoPEHI+/lZihAHf5bvx1oupS8bmcbXRPeQNVyAhA+QU6ZFIbpAOD7Y9 -xFe6LpHOUVqHuOs/MxAMX17tTA1QxkHHYi1JzJLr8I8kMW01h86w+mc7bQWZa4Nt -jReFXfvmeOInY2CumS8e0QKBgC23ow/vj1aFqla04lNG7YK3a0LTz39MVM3mItAG -viRgBV1qghRu9uNCcpx3RPijtBbsZMTbQL+S4gyo06jlD79qfZ7IQMJN+SteHvkj -xykoYHzSAB4gQj9+KzffyFdXMVFRZxHnjYb7o/amSzEXyHMlrtNXqZVu5HAXzeZR -V/m5AoGAAStS43Q7qSJSMfMBITKMdKlqCObnifD77WeR2WHGrpkq26300ggsDpMS -UTmnAAo77lSMmDsdoNn2XZmdeTu1CPoQnoZSE5CqPd5GeHA/hhegVCdeYxSXZJoH -Lhiac+AhCEog/MS1GmVsjynD7eDGVFcsJ6SWuam7doKfrpPqPnE= ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/generate.txt b/builtin/credential/cert/test-fixtures/generate.txt deleted file mode 100644 index 5b888ee778..0000000000 --- a/builtin/credential/cert/test-fixtures/generate.txt +++ /dev/null @@ -1,67 +0,0 @@ -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h ip_sans=127.0.0.1 -vi cacert.pem -vi cakey.pem - -vaultcert.hcl -backend "inmem" { -} -disable_mlock = true -default_lease_ttl = "700h" -max_lease_ttl = "768h" -listener "tcp" { - address = "127.0.0.1:8200" - tls_cert_file = "./cacert.pem" - tls_key_file = "./cakey.pem" -} -======================================== -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h max_ttl=438000h ip_sans=127.0.0.1 -vi testcacert1.pem -vi testcakey1.pem -vi testcaserial1 - -vault write pki/config/urls issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl" -vault write pki/roles/myvault-dot-com allowed_domains=myvault.com allow_subdomains=true ttl=437999h max_ttl=438000h allow_ip_sans=true - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial1 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedcert2.pem -vi testissuedkey2.pem -vi testissuedserial2 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial3 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedcert4.pem -vi testissuedkey4.pem -vi testissuedserial4 - -vault write pki/issue/myvault-dot-com common_name=cert.myvault.com format=pem ip_sans=127.0.0.1 -vi testissuedserial5 - -vault write pki/revoke serial_number=$(cat testissuedserial2) -vault write pki/revoke serial_number=$(cat testissuedserial4) -curl -XGET "http://127.0.0.1:8200/v1/pki/crl/pem" -H "x-vault-token:123" > issuedcertcrl -openssl crl -in issuedcertcrl -noout -text - -======================================== -export VAULT_ADDR='http://127.0.0.1:8200' -vault mount pki -vault mount-tune -max-lease-ttl=438000h pki -vault write pki/root/generate/exported common_name=myvault.com ttl=438000h ip_sans=127.0.0.1 -vi testcacert2.pem -vi testcakey2.pem -vi testcaserial2 -vi testcacert2leaseid - -vault write pki/config/urls issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl" -vault revoke $(cat testcacert2leaseid) - -curl -XGET "http://127.0.0.1:8200/v1/pki/crl/pem" -H "x-vault-token:123" > cacert2crl -openssl crl -in cacert2crl -noout -text diff --git a/builtin/credential/cert/test-fixtures/issuedcertcrl b/builtin/credential/cert/test-fixtures/issuedcertcrl deleted file mode 100644 index 45e9a98ecd..0000000000 --- a/builtin/credential/cert/test-fixtures/issuedcertcrl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIB2TCBwjANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtteXZhdWx0LmNvbRcN -MTYwNTAyMTYxMTA4WhcNMTYwNTA1MTYxMTA4WjBWMCkCFAS6oenLRllQ1MRYcSV+ -5ukv2563FxExNjA1MDIxMjExMDgtMDQwMDApAhQaQdPJfbIwE3q4nyYp60lVnZaE -5hcRMTYwNTAyMTIxMTA1LTA0MDCgIzAhMB8GA1UdIwQYMBaAFOuKvPiUG06iHkRX -AOeMiUdBfHFyMA0GCSqGSIb3DQEBCwUAA4IBAQBD2jkeOAmkDdYkAXbmjLGdHaQI -WMS/M+wtFnHVIDVQEmUmj/KPsrkshTZv2UgCHIxBha6y+kXUMQFMg6FwriDTB170 -WyJVDVhGg2WjiQjnzrzEI+iOmcpx60sPPXE63J/Zxo4QS5M62RTXRq3909HQTFI5 -f3xf0pog8mOrv5uQxO1SACP6YFtdDE2dGOVwoIPuNMTY5vijnj8I9dAw8VrbdoBX -m/Ky56kT+BpmVWHKwQd1nEcP/RHSKbZwwJzJG0BoGM8cvzjITtBmpEF+OZcea81x -p9XJkpfFeiVIgzxks3zTeuQjLF8u+MDcdGt0ztHEbkswjxuk1cCovZe2GFr4 ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/keys/cert-alt-cn.pem b/builtin/credential/cert/test-fixtures/keys/cert-alt-cn.pem deleted file mode 100644 index 06975732de..0000000000 --- a/builtin/credential/cert/test-fixtures/keys/cert-alt-cn.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDFjCCAf6gAwIBAgIJAJIiPq+77hewMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV -BAMTC2V4YW1wbGUuY29tMCAXDTI1MDYxODE5MzEzM1oYDzIwNTAwNjE5MTkzMTMz -WjAWMRQwEgYDVQQDDAtub3RmYWtlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBALGcdEr6/NmCaRaSMuHyTVuXygOgfJocUk0QFZ8oAH7XrU/Kbh35 -WNLLO2Ol9jl47FND0Z/tT7MLYr4Sovni7YFpFBfXBWBRp0oDJXemVkYTYXmYQOmX -0JnjPwRdqWuRytkgxd+pY4Iy/U/p1q67tlL9AQqB8ywJomGDIiFqZYnTJjDuTU0R -t5PtwDdE5H2pAeiVc4yxoU3F3TnFRDF5HkFaiTALhqLlOXaJ8qrZ2h7iCRrlHcue -GA51/OrcYTkIQL+q+3R182cUuKZi78PFCNa7En/xi3FFRgfufgABheYrhwtzdg05 -eMc9xXDeAm+MPaW2kO/pgs8/mp6oiX8q9p8CAwEAAaNlMGMwIQYDVR0RBBowGIIQ -Y2VydC5leGFtcGxlLmNvbYcEfwAAATAdBgNVHQ4EFgQUmuMiWgeoyPlkEyXcJ7Og -qJotlJkwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+ok5swDQYJKoZIhvcN -AQELBQADggEBANYE2poNMZakvN2a8hGbVHNXCR4+j5dE/nIw0ZW2PPv4CuNJ3whN -G15MT790k9DiNNecJBP32jwAOedIWKr3aqasur4eXA8xhN1nbtJODWxXYaV3USs7 -C1Mu9DV3aRiyFhEv6Va6RL4gCnOqOZdXG1+dAkughBKrRNJAMo5Wop/VC8rkSssO -NpQBt7ROIJjyTXrHQq23HR/uVv511AE5hDMDzxaVbsslBuzhD4nHOJ/Y/LWcXHJ1 -8W6S1vgSES2lo8U5tthSE4WyYW6kH6r2hBrK4ESdpdAUfO/lVNCe5KeYYWRwzMOw -oj9OOxBsjSX5C368IWM4sAn08mYJUUmq3i4= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/keys/cert.pem b/builtin/credential/cert/test-fixtures/keys/cert.pem deleted file mode 100644 index 5b7fa1aed0..0000000000 --- a/builtin/credential/cert/test-fixtures/keys/cert.pem +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC2zCCAcOgAwIBAgIJAJIiPq+77hewMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV -BAMTC2V4YW1wbGUuY29tMCAXDTI1MDEwNjE0MzgzMloYDzIwNTAwMTA3MTQzODMy -WjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftet -T8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNh -eZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMm -MO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJ -GuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuH -C3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABoyUwIzAhBgNVHREE -GjAYghBjZXJ0LmV4YW1wbGUuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQB/ -0M2jZ8cZJW23s1xpMDS5u2ScrW4QdpVsPbuBu5dxi3SNx7MK0CbvcNVUEZE0WV6b -rCYvYS0+SBi0skudHRV7IeRADPcvzbXY/AdFktWt0adtQ/5B/DKeZIRrnhGtlzhD -m8b3TTnKLoGdV7iS5HO8emvlzaihY/5PjObkztLRLLDRmBAOwYv4z/xBhEqZJRV1 -Ztywy/Qy5srNJug+sHmj8JlBldob/Ohk7Eon04XvXMuCIBptPG/QytnmgGbDGghD -WO/HpCWBh6GHrwzQtof8y7Upxi16i5DSiFbRwNXgRyST4W/ChpZoggvOJ/RI4o2g -5serAZLPfBGztdRbTef2 ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/keys/key.pem b/builtin/credential/cert/test-fixtures/keys/key.pem deleted file mode 100644 index add982002a..0000000000 --- a/builtin/credential/cert/test-fixtures/keys/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/keys/pkioutput b/builtin/credential/cert/test-fixtures/keys/pkioutput deleted file mode 100644 index 526ff03167..0000000000 --- a/builtin/credential/cert/test-fixtures/keys/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/issue/example-dot-com/d8214077-9976-8c68-9c07-6610da30aea4 -lease_duration 279359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw -MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS -TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn -SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi -YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5 -donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG -B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1 -MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e -HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o -k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x -OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A -AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br -aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs -X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4 -aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA -KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN -QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj -xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk= ------END CERTIFICATE----- -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu -HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA -6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N -TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd -y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2 -DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX -9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF -RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd -rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI -5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7 -oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ -GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb -VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR -akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI -FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy -efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh -r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ -0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp -FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR -kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT -UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3 -xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W -injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU -2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3 -gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4= ------END RSA PRIVATE KEY----- -private_key_type rsa diff --git a/builtin/credential/cert/test-fixtures/keys/rebuild-cert.md b/builtin/credential/cert/test-fixtures/keys/rebuild-cert.md deleted file mode 100644 index 6a69ff78e4..0000000000 --- a/builtin/credential/cert/test-fixtures/keys/rebuild-cert.md +++ /dev/null @@ -1,6 +0,0 @@ -To rebuild the cert.pem within this folder run the following commands - -```shell -$ openssl x509 -in cert.pem -signkey key.pem -x509toreq -out cert.csr -$ openssl x509 -req -in cert.csr -CA ../root/rootcacert.pem -CAkey ../root/rootcakey.pem -CAcreateserial -out cert.pem -days 9132 -sha256 -extensions v3_req -extfile <(echo "[v3_req]\nsubjectAltName=DNS:cert.example.com,IP:127.0.0.1") -``` diff --git a/builtin/credential/cert/test-fixtures/noclientauthcert.pem b/builtin/credential/cert/test-fixtures/noclientauthcert.pem deleted file mode 100644 index 3948f22032..0000000000 --- a/builtin/credential/cert/test-fixtures/noclientauthcert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGTCCAgGgAwIBAgIBBDANBgkqhkiG9w0BAQUFADBxMQowCAYDVQQDFAEqMQsw -CQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hhbG5h -eWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFVmF1 -bHQwHhcNMTYwMjI5MjE0NjE2WhcNMjEwMjI3MjE0NjE2WjBxMQowCAYDVQQDFAEq -MQswCQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hh -bG5heWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMF -VmF1bHQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMfRkLfIGHt1r2jjnV0N -LqRCu3oB+J1dqpM03vQt3qzIiqtuQuIA2ba7TJm2HwU3W3+rtfFcS+hkBR/LZM+u -cBPB+9b9+7i08vHjgy2P3QH/Ebxa8j1v7JtRMT2qyxWK8NlT/+wZSH82Cr812aS/ -zNT56FbBo2UAtzpqeC4eiv6NAgMBAAGjQDA+MAkGA1UdEwQCMAAwCwYDVR0PBAQD -AgXgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZI -hvcNAQEFBQADggEBAG2mUwsZ6+R8qqyNjzMk7mgpsRZv9TEl6c1IiQdyjaCOPaYH -vtZpLX20um36cxrLuOUtZLllG/VJEhRZW5mXWxuOk4QunWMBXQioCDJG1ktcZAcQ -QqYv9Dzy2G9lZHjLztEac37T75RXW7OEeQREgwP11c8sQYiS9jf+7ITYL7nXjoKq -gEuH0h86BOH2O/BxgMelt9O0YCkvkLLHnE27xuNelRRZcBLSuE1GxdUi32MDJ+ff -25GUNM0zzOEaJAFE/USUBEdQqN1gvJidNXkAiMtIK7T8omQZONRaD2ZnSW8y2krh -eUg+rKis9RinqFlahLPfI5BlyQsNMEnsD07Q85E= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/pkioutput b/builtin/credential/cert/test-fixtures/root/pkioutput deleted file mode 100644 index 312ae18dea..0000000000 --- a/builtin/credential/cert/test-fixtures/root/pkioutput +++ /dev/null @@ -1,74 +0,0 @@ -Key Value -lease_id pki/root/generate/exported/7bf99d76-dd3e-2c5b-04ce-5253062ad586 -lease_duration 315359999 -lease_renewable false -certificate -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -expiration 1.772072879e+09 -issuing_ca -----BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- -private_key -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- -private_key_type rsa -serial_number 6f:98:9d:f8:67:1a:31:e3:27:60:1b:f7:32:f7:53:19:68:a0:c8:9d diff --git a/builtin/credential/cert/test-fixtures/root/root.crl b/builtin/credential/cert/test-fixtures/root/root.crl deleted file mode 100644 index a80c9e4117..0000000000 --- a/builtin/credential/cert/test-fixtures/root/root.crl +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN X509 CRL----- -MIIBrjCBlzANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbRcN -MTYwMjI5MDIyOTE3WhcNMjUwMTA1MTAyOTE3WjArMCkCFG+YnfhnGjHjJ2Ab9zL3 -UxlooMidFxExNjAyMjgyMTI5MTctMDUwMKAjMCEwHwYDVR0jBBgwFoAUncSzT/6H -MexyuiU9/7EgHu+ok5swDQYJKoZIhvcNAQELBQADggEBAG9YDXpNe4LJroKZmVCn -HqMhW8eyzyaPak2nPPGCVUnc6vt8rlBYQU+xlBizD6xatZQDMPgrT8sBl9W3ysXk -RUlliHsT/SHddMz5dAZsBPRMJ7pYWLTx8jI4w2WRfbSyI4bY/6qTRNkEBUv+Fk8J -xvwB89+EM0ENcVMhv9ghsUA8h7kOg673HKwRstLDAzxS/uLmEzFjj8SV2m5DbV2Y -UUCKRSV20/kxJMIC9x2KikZhwOSyv1UE1otD+RQvbfAoZPUDmvp2FR/E0NGjBBOg -1TtCPRrl63cjqU3s8KQ4uah9Vj+Cwcu9n/yIKKtNQq4NKHvagv8GlUsoJ4BdAxCw -IA0= ------END X509 CRL----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcacert.pem b/builtin/credential/cert/test-fixtures/root/rootcacert.pem deleted file mode 100644 index dcb307a140..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcacert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw -MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 -Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 -z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x -AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb -6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH -SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx -7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc -BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA -wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 -U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa -cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N -ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ -t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk -zehNe5dFTjFpylg1o6b8Ow== ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcacert.srl b/builtin/credential/cert/test-fixtures/root/rootcacert.srl deleted file mode 100644 index 1c85d6318e..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcacert.srl +++ /dev/null @@ -1 +0,0 @@ -92223EAFBBEE17AF diff --git a/builtin/credential/cert/test-fixtures/root/rootcakey.pem b/builtin/credential/cert/test-fixtures/root/rootcakey.pem deleted file mode 100644 index e950da5ba3..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcakey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p -t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 -BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w -/2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv -0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi -18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb -ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn -8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f -nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 -2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t -grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc -bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 -0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN -ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf -lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 -lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj -AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG -ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib -thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU -4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb -iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO -tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y -LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc -4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX -OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= ------END RSA PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf b/builtin/credential/cert/test-fixtures/root/rootcawext.cnf deleted file mode 100644 index 77e8258e10..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf +++ /dev/null @@ -1,16 +0,0 @@ -[ req ] -default_bits = 2048 -encrypt_key = no -prompt = no -default_md = sha256 -req_extensions = req_v3 -distinguished_name = dn - -[ dn ] -CN = example.com - -[ req_v3 ] -2.1.1.1=ASN1:UTF8String:A UTF8String Extension -2.1.1.2=ASN1:UTF8:A UTF8 Extension -2.1.1.3=ASN1:IA5:An IA5 Extension -2.1.1.4=ASN1:VISIBLE:A Visible Extension diff --git a/builtin/credential/cert/test-fixtures/root/rootcawext.csr b/builtin/credential/cert/test-fixtures/root/rootcawext.csr deleted file mode 100644 index 55e22eedeb..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawext.csr +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIDAzCCAesCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDM2PrLyK/wVQIcnK362ZylDrIVMjFQzps/0AxM -ke+8MNPMArBlSAhnZus6qb0nN0nJrDLkHQgYqnSvK9N7VUv/xFblEcOLBlciLhyN -Wkm92+q/M/xOvUVmnYkN3XgTI5QNxF7ZWDFHmwCNV27RraQZou0hG7yvyoILLMQE -3MnMCNM1nZ9JIuBMcRsZLGqQ1XNaQljboRVIUjimzkcfYyTruhLosTIbwForp78J -MzHHqVjtLJXPqUnRMS7KhGMj1f2mIswQzCv6F2PWEzNBbP4Gb67znKikKDs0RgyL -RyfizFNFJSC58XntK8jwHK1D8W3UepFf4K8xNFnhPoKWtWfJAgMBAAGggacwgaQG -CSqGSIb3DQEJDjGBljCBkzAcBgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATAf -BgNRAQEEGAwWQSBVVEY4U3RyaW5nIEV4dGVuc2lvbjAZBgNRAQIEEgwQQSBVVEY4 -IEV4dGVuc2lvbjAZBgNRAQMEEhYQQW4gSUE1IEV4dGVuc2lvbjAcBgNRAQQEFRoT -QSBWaXNpYmxlIEV4dGVuc2lvbjANBgkqhkiG9w0BAQsFAAOCAQEAtYjewBcqAXxk -tDY0lpZid6ZvfngdDlDZX0vrs3zNppKNe5Sl+jsoDOexqTA7HQA/y1ru117sAEeB -yiqMeZ7oPk8b3w+BZUpab7p2qPMhZypKl93y/jGXGscc3jRbUBnym9S91PSq6wUd -f2aigSqFc9+ywFVdx5PnnZUfcrUQ2a+AweYEkGOzXX2Ga+Ige8grDMCzRgCoP5cW -kM5ghwZp5wYIBGrKBU9iDcBlmnNhYaGWf+dD00JtVDPNn2bJnCsJHIO0nklZgnrS -fli8VQ1nYPkONdkiRYLt6//6at1iNDoDgsVCChtlVkLpxFIKcDFUHlffZsc1kMFI -HTX579k8hA== ------END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem b/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem deleted file mode 100644 index 2c8591735f..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawextcert.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDRjCCAi6gAwIBAgIJAJIiPq+77hejMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV -BAMTC2V4YW1wbGUuY29tMB4XDTE3MTEyOTE5MTgwM1oXDTI3MTEyNzE5MTgwM1ow -FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDM2PrLyK/wVQIcnK362ZylDrIVMjFQzps/0AxMke+8MNPMArBlSAhn -Zus6qb0nN0nJrDLkHQgYqnSvK9N7VUv/xFblEcOLBlciLhyNWkm92+q/M/xOvUVm -nYkN3XgTI5QNxF7ZWDFHmwCNV27RraQZou0hG7yvyoILLMQE3MnMCNM1nZ9JIuBM -cRsZLGqQ1XNaQljboRVIUjimzkcfYyTruhLosTIbwForp78JMzHHqVjtLJXPqUnR -MS7KhGMj1f2mIswQzCv6F2PWEzNBbP4Gb67znKikKDs0RgyLRyfizFNFJSC58Xnt -K8jwHK1D8W3UepFf4K8xNFnhPoKWtWfJAgMBAAGjgZYwgZMwHAYDVR0RBBUwE4IL -ZXhhbXBsZS5jb22HBH8AAAEwHwYDUQEBBBgMFkEgVVRGOFN0cmluZyBFeHRlbnNp -b24wGQYDUQECBBIMEEEgVVRGOCBFeHRlbnNpb24wGQYDUQEDBBIWEEFuIElBNSBF -eHRlbnNpb24wHAYDUQEEBBUaE0EgVmlzaWJsZSBFeHRlbnNpb24wDQYJKoZIhvcN -AQELBQADggEBAGU/iA6saupEaGn/veVNCknFGDL7pst5D6eX/y9atXlBOdJe7ZJJ -XQRkeHJldA0khVpzH7Ryfi+/25WDuNz+XTZqmb4ppeV8g9amtqBwxziQ9UUwYrza -eDBqdXBaYp/iHUEHoceX4F44xuo80BIqwF0lD9TFNUFoILnF26ajhKX0xkGaiKTH -6SbjBfHoQVMzOHokVRWregmgNycV+MAI9Ne9XkIZvdOYeNlcS9drZeJI3szkiaxB -WWaWaAr5UU2Z0yUCZnAIDMRcIiUbSEjIDz504sSuCzTctMOxWZu0r/0UrXRzwZZi -HAaKm3MUmBh733ChP4rTB58nr5DEr5rJ9P8= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem b/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem deleted file mode 100644 index 3f8d8ebed9..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawextkey.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDM2PrLyK/wVQIc -nK362ZylDrIVMjFQzps/0AxMke+8MNPMArBlSAhnZus6qb0nN0nJrDLkHQgYqnSv -K9N7VUv/xFblEcOLBlciLhyNWkm92+q/M/xOvUVmnYkN3XgTI5QNxF7ZWDFHmwCN -V27RraQZou0hG7yvyoILLMQE3MnMCNM1nZ9JIuBMcRsZLGqQ1XNaQljboRVIUjim -zkcfYyTruhLosTIbwForp78JMzHHqVjtLJXPqUnRMS7KhGMj1f2mIswQzCv6F2PW -EzNBbP4Gb67znKikKDs0RgyLRyfizFNFJSC58XntK8jwHK1D8W3UepFf4K8xNFnh -PoKWtWfJAgMBAAECggEAW7hLkzMok9N8PpNo0wjcuor58cOnkSbxHIFrAF3XmcvD -CXWqxa6bFLFgYcPejdCTmVkg8EKPfXvVAxn8dxyaCss+nRJ3G6ibGxLKdgAXRItT -cIk2T4svp+KhmzOur+MeR4vFbEuwxP8CIEclt3yoHVJ2Gnzw30UtNRO2MPcq48/C -ZODGeBqUif1EGjDAvlqu5kl/pcDBJ3ctIZdVUMYYW4R9JtzKsmwhX7CRCBm8k5hG -2uzn8AKwpuVtfWcnX59UUmHGJ8mjETuNLARRAwWBWhl8f7wckmi+PKERJGEM2QE5 -/Voy0p22zmQ3waS8LgiI7YHCAEFqjVWNziVGdR36gQKBgQDxkpfkEsfa5PieIaaF -iQOO0rrjEJ9MBOQqmTDeclmDPNkM9qvCF/dqpJfOtliYFxd7JJ3OR2wKrBb5vGHt -qIB51Rnm9aDTM4OUEhnhvbPlERD0W+yWYXWRvqyHz0GYwEFGQ83h95GC/qfTosqy -LEzYLDafiPeNP+DG/HYRljAxUwKBgQDZFOWHEcZkSFPLNZiksHqs90OR2zIFxZcx -SrbkjqXjRjehWEAwgpvQ/quSBxrE2E8xXgVm90G1JpWzxjUfKKQRM6solQeEpnwY -kCy2Ozij/TtbLNRlU65UQ+nMto8KTSIyJbxxdOZxYdtJAJQp1FJO1a1WC11z4+zh -lnLV1O5S8wKBgQCDf/QU4DBQtNGtas315Oa96XJ4RkUgoYz+r1NN09tsOERC7UgE -KP2y3JQSn2pMqE1M6FrKvlBO4uzC10xLja0aJOmrssvwDBu1D8FtA9IYgJjFHAEG -v1i7lJrgdu7TUtx1flVli1l3gF4lM3m5UaonBrJZV7rB9iLKzwUKf8IOJwKBgFt/ -QktPA6brEV56Za8sr1hOFA3bLNdf9B0Tl8j4ExWbWAFKeCu6MUDCxsAS/IZxgdeW -AILovqpC7CBM78EFWTni5EaDohqYLYAQ7LeWeIYuSyFf4Nogjj74LQha/iliX4Jx -g17y3dp2W34Gn2yOEG8oAxpcSfR54jMnPZnBWP5fAoGBAMNAd3oa/xq9A5v719ik -naD7PdrjBdhnPk4egzMDv54y6pCFlvFbEiBduBWTmiVa7dSzhYtmEbri2WrgARlu -vkfTnVH9E8Hnm4HTbNn+ebxrofq1AOAvdApSoslsOP1NT9J6zB89RzChJyzjbIQR -Gevrutb4uO9qpB1jDVoMmGde ------END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawocert.pem b/builtin/credential/cert/test-fixtures/root/rootcawocert.pem deleted file mode 100644 index 14536c16ae..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawocert.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDYjCCAkqgAwIBAgIUM4ANPdEihcrpgryBFpBr3QCr2a4wDQYJKoZIhvcNAQEL -BQAwKDEQMA4GA1UEChMHZXhhbXBsZTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wIBcN -MjUwOTE4MTcxMjUwWhgPMjEyNTA4MjUxNjEzMjBaMCgxEDAOBgNVBAoTB2V4YW1w -bGUxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEA45oJCYBM7SI4KwfAQHTRQnJt5PGap0s/ckdoozjulpDRE45OKT3Q -Dbc6mYXgBXVCpcHjVweqptPUetoWUwpPppgmbj4/MvaPWFebskspE9i/W85m9SOk -s1V8zD73Apf3m2l6v3VyZ1hGHaM1uLTcRj47qR+dF6naU76QWjIObKqFDBk24kDD -Ktr6IEVF44GERqfson+u40NqQE/ZnqTk5jnBEWKf3Lp/NNlmrKb3EZ1jkPhCNE9N -IsShs5qg+3y0PNut4wCE5WXaYCIIk7ydn8s3dJO6cMSkCZpAvyXz1Af0oSh+aqKH -Y1+uudfu8sheRCRd4u0J+4Q27J9GpQeOrwIDAQABo4GBMH8wDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPTXC7WMp/TjZ2OyfotP13k6 -5MDCMB8GA1UdIwQYMBaAFPTXC7WMp/TjZ2OyfotP13k65MDCMBwGA1UdEQQVMBOC -C2V4YW1wbGUuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAZ+sdJdmeZznMu -W7LAt/yPhf1gIMReL3WTRi9zAxjOyJCmMOcqGbSWX+iYah5fm16ehmfaenZjli9e -JEgfsD9PlZgopwrThFB6haW3O+9WpMAcQ4gbJTQMpV0M7wLcj3SNtnY3jaQ9AB5X -ys0nc146wVaJgUZycvCkOwTDo6JZ0Z1F5qbNI82bUSZdkXC1MgyHI0uFfmSloXi/ -jTdv9o1zjsYEjckzqVuGzuGXT5zl96WBE4RofJhzRPRefccnYJQ3aitzuzxbl3J/ -FtiW3HYBupXfXr46gXncuWfB3MNDzxftf1OeDfYdT7Een5zV8PdIqoFOvIl51ZC1 -/lVyaDYd ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/root/rootcawokey.pem b/builtin/credential/cert/test-fixtures/root/rootcawokey.pem deleted file mode 100644 index 0bbe2bf496..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawokey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA45oJCYBM7SI4KwfAQHTRQnJt5PGap0s/ckdoozjulpDRE45O -KT3QDbc6mYXgBXVCpcHjVweqptPUetoWUwpPppgmbj4/MvaPWFebskspE9i/W85m -9SOks1V8zD73Apf3m2l6v3VyZ1hGHaM1uLTcRj47qR+dF6naU76QWjIObKqFDBk2 -4kDDKtr6IEVF44GERqfson+u40NqQE/ZnqTk5jnBEWKf3Lp/NNlmrKb3EZ1jkPhC -NE9NIsShs5qg+3y0PNut4wCE5WXaYCIIk7ydn8s3dJO6cMSkCZpAvyXz1Af0oSh+ -aqKHY1+uudfu8sheRCRd4u0J+4Q27J9GpQeOrwIDAQABAoIBACR+Miy/0ZXEAtWD -bKPpFxRcXJp00qM4QXgFUxW4ryidF6jXDFk4e/92/YJYIM8/Oexx5g2yQP52wH7i -MOonoRXJF4Bdoqx9NAaqJWC1BGUWP7hso71ydZn7fwMQpXJZA257vx6rqig/0x41 -aQuwlBD/MXmwg/OjXEpJJ8QOepmZgEte2stFJOF1dxnWwRk3KDFsAIukYdxkzb/Z -oaLDetDj678t71ab6EPToPDRAzu9zIzyP9KhbirPepLOTrwfEU552oSYs4THU0+z -PJy/vMKuEa/M56a8WoyV5S+Q/t9VgTkUUgqpEsodXvsd3el7xOquuVTcQyRLTXqB -SrP0rSECgYEA8oVNGgxMk9vTXbEFxWqLIEkDU8h3A9e0IS19VqBXEML5YKNAh5xh -zar+xqlkIXWTqD0qFIgtE7ggB+Gu7Gb7lGrI1bj2tLAMlaEImqvT9ALtSzsiIK1L -8KpNBdPkOlWlG2OA3fU0m0TynjtwOaxHGRVmf67BEG6v/wWk77q97lsCgYEA8EB1 -n0TaW2PTqCU9F2wwuoML8XEn0L0+J7rXJy+vC3rjr8asglxVNndXV3/Z+jtwupQN -y1MIxUA2WghGr40N53UHnQzSjn3IKfi6Nqb/uKiW34KppcI0W3MrqhLEoytyhj+Q -8UTryrajaiKvNgWo+J/8oAqFoNuCOFhwa0ByuT0CgYBS4WVpGnztJvoEEeRUBEZJ -oUomzuKFiKkBkac8/IzkqI1LDl+WOMZf4CkzwV375U+x9j00SRmGnK0tpF4AYm1l -2lyKVazSMTwLwr3LBh/oSzvHMw1Ft5O1Sq4J6NEdcnl7c7Ttpcf1rElx9AQ1YX/m -vZ6K0jEeqYUyFT65wsr38wKBgQDVKhwyqEClfbk6I3BE6/WARu2914xgJMiVL63e -UuyY3vxN5ZUCRTJGFTUlqYaaA0tOADcNBCtv+D1BPL6a3ChOCQQsUEgxrWB//PQb -saiLCupyfdhP/jO+QD2ptOVLcS03+AZ+S4x6W/o6HXQgFn2Ju0nGJg/SXXD41V9J -ifFAcQKBgH692iebaAhWHgEbYNlIdpwZh+hhraOVp6D+xi+2j0TbZi5gZtsBzzkK -RTlU6rmgYgUOlURYM7uysFcddB2pgraSrFigQ6J7LHRpowaJxyHC9yQbQ4Y0UjgK -Mrh68/EHSDhm1M0J+CwleLkYNfW+xE5TCABgvv5mLl1hIOa3tyYq ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/root/rootcawou.cnf b/builtin/credential/cert/test-fixtures/root/rootcawou.cnf deleted file mode 100644 index be11c33a17..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawou.cnf +++ /dev/null @@ -1,18 +0,0 @@ -[ req ] -default_bits = 2048 -encrypt_key = no -prompt = no -default_md = sha256 -distinguished_name = dn -req_extensions = req_v3 - -[ req_v3 ] -subjectAltName = @alt_names - -[ dn ] -CN = example.com -OU = engineering - -[ alt_names ] -IP.1 = 127.0.0.1 -email = valid@example.com diff --git a/builtin/credential/cert/test-fixtures/root/rootcawou.csr b/builtin/credential/cert/test-fixtures/root/rootcawou.csr deleted file mode 100644 index d72579b76c..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawou.csr +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICpjCCAY4CAQAwLDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFDASBgNVBAsMC2Vu -Z2luZWVyaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsI4ZJWfQ -mI/3qfacas7O260Iii06oTP4GoQ5QpAYvcfWKKnkXagd0fBl+hfpnrK6ojYY71Jt -cMstVdff2Wc5D3bnQ8Hikb1TMhdAAtZDUW4QbeWAXJ4mkDq1ARRcbTvK121bmDQp -1efepohe0mDxNCruGSHpqfayC6LOkk7XZ73VAOcPPV5OOpY8el7quUdfvElxn0vH -KBVlFRBBW2fbY5EAHDMkmBjWr0ofpwb+vhSuQlOZgsbd20mjDwSYIbywG0tAEOoj -pLI0pOQV5msdfbqmKYE6ZmUeL/Q/pZjYh5uxFUZ4aMD/STDaeq7GdYQYcm17WL+N -ceal9+gKceJSiQIDAQABoDUwMwYJKoZIhvcNAQkOMSYwJDAiBgNVHREEGzAZhwR/ -AAABgRF2YWxpZEBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAf1tnXgX1 -/1p2MAxHhcil5/lsOMgHWU5dRL6KjK2cepuBpfzlCbxFtvnsj9WHx46f9Q/xbqy+ -1A2TJIBUWxK+Eji//WJxbDsi7fmV5VQlpG7+sEa7yin3KobfMd84nDIYP8wLF1Fq -HhRf7ZjIDh3zTgBosvIIjGEyABrouGYm4Nl409I09MftGXK/5TLJkgm6sxcJCAHG -BMm8IFaI0VN5QFIHKvJ/1oQLpLV+gvtR6jAM/99LXc0SXmFn0Jcy/mE/hxJXJigW -dDOblgjliJo0rWwHK4gfsgpMbHjJiG70g0XHtTpBW+i/NyuPnc8RYzBIJv+4sks+ -hWSmn6/IL46qTg== ------END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem b/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem deleted file mode 100644 index fe0f227543..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawoucert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIJAMAMmdiZi5G/MA0GCSqGSIb3DQEBCwUAMCwxFDASBgNV -BAMMC2V4YW1wbGUuY29tMRQwEgYDVQQLDAtlbmdpbmVlcmluZzAeFw0xODA5MDEx -NDM0NTVaFw0yODA4MjkxNDM0NTVaMCwxFDASBgNVBAMMC2V4YW1wbGUuY29tMRQw -EgYDVQQLDAtlbmdpbmVlcmluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALCOGSVn0JiP96n2nGrOztutCIotOqEz+BqEOUKQGL3H1iip5F2oHdHwZfoX -6Z6yuqI2GO9SbXDLLVXX39lnOQ9250PB4pG9UzIXQALWQ1FuEG3lgFyeJpA6tQEU -XG07ytdtW5g0KdXn3qaIXtJg8TQq7hkh6an2sguizpJO12e91QDnDz1eTjqWPHpe -6rlHX7xJcZ9LxygVZRUQQVtn22ORABwzJJgY1q9KH6cG/r4UrkJTmYLG3dtJow8E -mCG8sBtLQBDqI6SyNKTkFeZrHX26pimBOmZlHi/0P6WY2IebsRVGeGjA/0kw2nqu -xnWEGHJte1i/jXHmpffoCnHiUokCAwEAAaMmMCQwIgYDVR0RBBswGYcEfwAAAYER -dmFsaWRAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAHATSjW20P7+6en0 -Oq/n/R/i+aCgzcxIWSgf3dhOyxGfBW6svSg8ZtBQFEZZHqIRSXZX89zz25+mvwqi -kGRJKKzD/KDd2v9C5+H3DSuu9CqClVtpjF2XLvRHnuclBIrwvyijRcqa2GCTA9YZ -sOfVVGQYobDbtRCgTwWkEpU9RrZWWoD8HAYMkxFc1Cs/vJconeAaQDPEIZx9wnAN -4r/F5143rn5dyhbYehz1/gykL3K0v7s4U5NhaSACE2AiQ+63vhAEd5xt9WPKAAGY -zEyK4b/qPO88mxLr3A/rdzzt1UYAwT38kXA7aV82AH1J8EaCr7tLnXzyLXiEsI4E -BOrHBgU= ------END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem b/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem deleted file mode 100644 index 166317294d..0000000000 --- a/builtin/credential/cert/test-fixtures/root/rootcawoukey.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwjhklZ9CYj/ep -9pxqzs7brQiKLTqhM/gahDlCkBi9x9YoqeRdqB3R8GX6F+mesrqiNhjvUm1wyy1V -19/ZZzkPdudDweKRvVMyF0AC1kNRbhBt5YBcniaQOrUBFFxtO8rXbVuYNCnV596m -iF7SYPE0Ku4ZIemp9rILos6STtdnvdUA5w89Xk46ljx6Xuq5R1+8SXGfS8coFWUV -EEFbZ9tjkQAcMySYGNavSh+nBv6+FK5CU5mCxt3bSaMPBJghvLAbS0AQ6iOksjSk -5BXmax19uqYpgTpmZR4v9D+lmNiHm7EVRnhowP9JMNp6rsZ1hBhybXtYv41x5qX3 -6Apx4lKJAgMBAAECggEAF1Jd7fv9qPlzfKcP2GgDGS+NLjt1QDAOOOp4aduA+Si5 -mFuAyAJaFg5MWjHocUcosh61Qn+/5yNflLRUZHJnLizFtcSZuiipIbfCg91rvQjt -8KZdQ168t1aZ7E+VOfSpAbX3YG6bjB754UOoSt/1XK/DDdzV8dadhD34TYlOmOxZ -MMnIRERqa+IBSn90TONWPyY3ELSpaiCkz1YZpp6g9RnTACZKLwzBMSunNO5qbEfH -TWlk5o14DZ3zRu5gLT5wy3SGfzm2M+qi8afQq1MT2I6opXj4KU3c64agjNUBYTq7 -S2YWmw6yrqPzxcg0hOz9H6djCx2oen/UxM2z4uoE1QKBgQDlHIFQcVTWEmxhy5yp -uV7Ya5ubx6rW4FnCgh5lJ+wWuSa5TkMuBr30peJn0G6y0I0J1El4o3iwLD/jxwHb -BIJTB1z5fBo3K7lhpZLuRFSWe9Mcd/Aj2pFcy5TqaIV9x8bgVAMVOoZAq9muiEog -zIWVWrVF6FDuFgRMRegNDej6pwKBgQDFRpNQMscPpH+x6xeS0E8boZKnHyuJUZQZ -kfEmnHQuTYmmHS4kXSnJhjODa53YddknTrPrHOvddDDYAaulyyYitPemubYQzBog -MyIgaeFSw/eHrcr/8g4QTohRFcI71xnKRmHvQZb8UflFJkqsqil6WZ6FJiC+STcn -Qdnhol9fTwKBgQCZtGDw1cdjgqKhjVcB6nG94ZtYjECJvaOaQW8g0AKsT/SxttaN -B0ri2XMl0IijgBROttO/knQCRP1r03PkOocwKq1uVprDzpqk7s6++KqC9nlwDOrX -Muf4iD/UbuC3vJIop1QWJtgwhNoaJCcPEAbCZ0Nbrfq1b6Hchb2jHGTj2wKBgHJo -DpDJEeaBeMi+1SoAgpA8sKcZDY+SbvgxShAhVcNwli5u586Q9OX5XTCPHbhmB+yi -2Pa2DBefBaCPv3LkEJa6KpFXTD4Lj+8ymE0B+nmcSpY19O9f+kX8tVOI8d7wTPWg -wbUWbbCg/ZXbshzWhj19cdA4H28bWM/8gZY4K2VDAoGBAMYsNhKdu9ON/7vaLijh -kai2tQLObYqDV6OAzdYm1gopmTTLcxQ6jP6aQlyw1ie51ms/hFozmNkGkaQGD8pp -751Lv3prQz/lDaZeQfKANNN1tpz/QqUOu2di9secMmodxXkwcLzcEKjWPDTuPhcO -VODU1hC5oj8yGFInoDLL2B0K ------END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/testcacert1.pem b/builtin/credential/cert/test-fixtures/testcacert1.pem deleted file mode 100644 index ab8bf9e931..0000000000 --- a/builtin/credential/cert/test-fixtures/testcacert1.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUfIKsF2VPT7sdFcKOHJH2Ii6K4MwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwNTQyWhgPMjA2 -NjA0MjAxNjA2MTJaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuOimEXawD2qBoLCFP3Skq5zi1XzzcMAJlfdS -xz9hfymuJb+cN8rB91HOdU9wQCwVKnkUtGWxUnMp0tT0uAZj5NzhNfyinf0JGAbP -67HDzVZhGBHlHTjPX0638yaiUx90cTnucX0N20SgCYct29dMSgcPl+W78D3Jw3xE -JsHQPYS9ASe2eONxG09F/qNw7w/RO5/6WYoV2EmdarMMxq52pPe2chtNMQdSyOUb -cCcIZyk4QVFZ1ZLl6jTnUPb+JoCx1uMxXvMek4NF/5IL0Wr9dw2gKXKVKoHDr6SY -WrCONRw61A5Zwx1V+kn73YX3USRlkufQv/ih6/xThYDAXDC9cwIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOuKvPiU -G06iHkRXAOeMiUdBfHFyMB8GA1UdIwQYMBaAFOuKvPiUG06iHkRXAOeMiUdBfHFy -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQBcN/UdAMzc7UjRdnIpZvO+5keBGhL/vjltnGM1dMWYHa60Y5oh7UIXF+P1RdNW -n7g80lOyvkSR15/r1rDkqOK8/4oruXU31EcwGhDOC4hU6yMUy4ltV/nBoodHBXNh -MfKiXeOstH1vdI6G0P6W93Bcww6RyV1KH6sT2dbETCw+iq2VN9CrruGIWzd67UT/ -spe/kYttr3UYVV3O9kqgffVVgVXg/JoRZ3J7Hy2UEXfh9UtWNanDlRuXaZgE9s/d -CpA30CHpNXvKeyNeW2ktv+2nAbSpvNW+e6MecBCTBIoDSkgU8ShbrzmDKVwNN66Q -5gn6KxUPBKHEtNzs5DgGM7nq ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcacert2.pem b/builtin/credential/cert/test-fixtures/testcacert2.pem deleted file mode 100644 index a8fe6c4806..0000000000 --- a/builtin/credential/cert/test-fixtures/testcacert2.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPjCCAiagAwIBAgIUJfHFxtLQBOkjY9ivHx0AIsRDcH0wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYxMjI5WhgPMjA2 -NjA0MjAxNjEyNTlaMBYxFDASBgNVBAMTC215dmF1bHQuY29tMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqj8ANjAGrg5BgUb3owGwUHlMYDxljMdwroA/ -Bv76ESjomj1zCyVtoJxlDZ8m9VcKQldk5ashFNuY+Ms9FrJ1YsePvsfStNe37C26 -2uldDToh5rm7K8uwp/bQiErwM9QZMCVYCPEH8QgETPg9qWnikDFLMqcLBNbIiXVL -alxEYgA1Qt6+ayMvoS35288hFdZj6a0pCF0+zMHORZxloPhkXWnZLp5lWBiunSJG -0kVz56TjF+oY0L74iW4y3x2805biisGvFqgpZJW8/hLw/kDthNylNTzEqBktsctQ -BXpSMcwG3woJ0uZ8cH/HA/m0VDeIA77UisXnlLiQDpdB7U7QPwIDAQABo4GBMH8w -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMLETWAs -OFNsKJ+uqzChCZvIpxX4MB8GA1UdIwQYMBaAFMLETWAsOFNsKJ+uqzChCZvIpxX4 -MBwGA1UdEQQVMBOCC215dmF1bHQuY29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB -AQCRlFb6bZDrq3NkoZF9evls7cT41V3XCdykMA4K9YRgDroZ5psanSvYEnSrk9cU -Y7sVYW7b8qSRWkLZrHCAwc2V0/i5F5j4q9yVnWaTZ+kOVCFYCI8yUS7ixRQdTLNN -os/r9dcRSzzTEqoQThAzn571yRcbJHzTjda3gCJ5F4utYUBU2F9WK+ukW9nqfepa -ju5vEEGDuL2+RyApzL0nGzMUkCdBcK82QBksTlElPnbICbJZWUUMTZWPaZ7WGDDa -Pj+pWMXiDQmzIuzgXUCNtQL6lEv4tQwGYRHjjPmhgJP4sr6Cyrj4G0iljrqM+z/3 -gLyJOlNU8c5x02/C1nFDDa14 ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcakey1.pem b/builtin/credential/cert/test-fixtures/testcakey1.pem deleted file mode 100644 index 05211bad1c..0000000000 --- a/builtin/credential/cert/test-fixtures/testcakey1.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAuOimEXawD2qBoLCFP3Skq5zi1XzzcMAJlfdSxz9hfymuJb+c -N8rB91HOdU9wQCwVKnkUtGWxUnMp0tT0uAZj5NzhNfyinf0JGAbP67HDzVZhGBHl -HTjPX0638yaiUx90cTnucX0N20SgCYct29dMSgcPl+W78D3Jw3xEJsHQPYS9ASe2 -eONxG09F/qNw7w/RO5/6WYoV2EmdarMMxq52pPe2chtNMQdSyOUbcCcIZyk4QVFZ -1ZLl6jTnUPb+JoCx1uMxXvMek4NF/5IL0Wr9dw2gKXKVKoHDr6SYWrCONRw61A5Z -wx1V+kn73YX3USRlkufQv/ih6/xThYDAXDC9cwIDAQABAoIBAG3bCo7ljMQb6tel -CAUjL5Ilqz5a9ebOsONABRYLOclq4ePbatxawdJF7/sSLwZxKkIJnZtvr2Hkubxg -eOO8KC0YbVS9u39Rjc2QfobxHfsojpbWSuCJl+pvwinbkiUAUxXR7S/PtCPJKat/ -fGdYCiMQ/tqnynh4vR4+/d5o12c0KuuQ22/MdEf3GOadUamRXS1ET9iJWqla1pJW -TmzrlkGAEnR5PPO2RMxbnZCYmj3dArxWAnB57W+bWYla0DstkDKtwg2j2ikNZpXB -nkZJJpxR76IYD1GxfwftqAKxujKcyfqB0dIKCJ0UmfOkauNWjexroNLwaAOC3Nud -XIxppAECgYEA1wJ9EH6A6CrSjdzUocF9LtQy1LCDHbdiQFHxM5/zZqIxraJZ8Gzh -Q0d8JeOjwPdG4zL9pHcWS7+x64Wmfn0+Qfh6/47Vy3v90PIL0AeZYshrVZyJ/s6X -YkgFK80KEuWtacqIZ1K2UJyCw81u/ynIl2doRsIbgkbNeN0opjmqVTMCgYEA3CkW -2fETWK1LvmgKFjG1TjOotVRIOUfy4iN0kznPm6DK2PgTF5DX5RfktlmA8i8WPmB7 -YFOEdAWHf+RtoM/URa7EAGZncCWe6uggAcWqznTS619BJ63OmncpSWov5Byg90gJ -48qIMY4wDjE85ypz1bmBc2Iph974dtWeDtB7dsECgYAyKZh4EquMfwEkq9LH8lZ8 -aHF7gbr1YeWAUB3QB49H8KtacTg+iYh8o97pEBUSXh6hvzHB/y6qeYzPAB16AUpX -Jdu8Z9ylXsY2y2HKJRu6GjxAewcO9bAH8/mQ4INrKT6uIdx1Dq0OXZV8jR9KVLtB -55RCfeLhIBesDR0Auw9sVQKBgB0xTZhkgP43LF35Ca1btgDClNJGdLUztx8JOIH1 -HnQyY/NVIaL0T8xO2MLdJ131pGts+68QI/YGbaslrOuv4yPCQrcS3RBfzKy1Ttkt -TrLFhtoy7T7HqyeMOWtEq0kCCs3/PWB5EIoRoomfOcYlOOrUCDg2ge9EP4nyVVz9 -hAGBAoGBAJXw/ufevxpBJJMSyULmVWYr34GwLC1OhSE6AVVt9JkIYnc5L4xBKTHP -QNKKJLmFmMsEqfxHUNWmpiHkm2E0p37Zehui3kywo+A4ybHPTua70ZWQfZhKxLUr -PvJa8JmwiCM7kO8zjOv+edY1mMWrbjAZH1YUbfcTHmST7S8vp0F3 ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcakey2.pem b/builtin/credential/cert/test-fixtures/testcakey2.pem deleted file mode 100644 index c2e3763e23..0000000000 --- a/builtin/credential/cert/test-fixtures/testcakey2.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAqj8ANjAGrg5BgUb3owGwUHlMYDxljMdwroA/Bv76ESjomj1z -CyVtoJxlDZ8m9VcKQldk5ashFNuY+Ms9FrJ1YsePvsfStNe37C262uldDToh5rm7 -K8uwp/bQiErwM9QZMCVYCPEH8QgETPg9qWnikDFLMqcLBNbIiXVLalxEYgA1Qt6+ -ayMvoS35288hFdZj6a0pCF0+zMHORZxloPhkXWnZLp5lWBiunSJG0kVz56TjF+oY -0L74iW4y3x2805biisGvFqgpZJW8/hLw/kDthNylNTzEqBktsctQBXpSMcwG3woJ -0uZ8cH/HA/m0VDeIA77UisXnlLiQDpdB7U7QPwIDAQABAoIBADivQ2XHdeHsUzk1 -JOz8efVBfgGo+nL2UPl5MAMnUKH4CgKZJT3311mb2TXA4RrdQUg3ixvBcAFe4L8u -BIgTIWyjX6Q5KloWXWHhFA8hll76FSGag8ygRJCYaHSI5xOKslxKgtZvUqKZdb0f -BoDrBYnXL9+MqOmSjjDegh7G2+n49n774Z2VVR47TZTBB5LCWDWj4AtEcalgwlvw -d5yL/GU/RfCkXCjCeie1pInp3eCMUI9jlvbe/vyaoFq2RiaJw1LSlJLXZBMYzaij -XkgMtRsr5bf0Tg2z3SPiaa9QZogfVLqHWAt6RHZf9Keidtiho+Ad6/dzJu+jKDys -Z6cthOECgYEAxMUCIYKO74BtPRN2r7KxbSjHzFsasxbfwkSg4Qefd4UoZJX2ShlL -cClnef3WdkKxtShJhqEPaKTYTrfgM+iz/a9+3lAFnS4EZawSf3YgXXslVTory0Da -yPQZKxX6XsupaLl4s13ehw/D0qfdxWVYaiFad3ePEE4ytmSkMMHLHo8CgYEA3X4a -jMWVbVv1W1lj+LFcg7AhU7lHgla+p7NI4gHw9V783noafnW7/8pNF80kshYo4u0g -aJRwaU/Inr5uw14eAyEjB4X7N8AE5wGmcxxS2uluGG6r3oyQSJBqktGnLwyTfcfC -XrfsGJza2BRGF4Mn8SFb7WtCl3f1qu0hTF+mC1ECgYB4oA1eXZsiV6if+H6Z1wHN -2WIidPc5MpyZi1jUmse3jXnlr8j8Q+VrLPayYlpGxTwLwlbQoYvAqs2v9CkNqWot -6pfr0UKfyMYJTiNI4DGXHRcV2ENgprF436tOLnr+AfwopwrHapQwWAnD6gSaLja1 -WR0Mf87EQCv2hFvjR+otIQKBgQCLyvJQ1MeZzQdPT1zkcnSUfM6b+/1hCwSr7WDb -nCQLiZcJh4E/PWmZaII9unEloQzPJKBmwQEtxng1kLVxwu4oRXrJXcuPhTbS4dy/ -HCpDFj8xVnBNNuQ9mEBbR80/ya0xHqnThDuT0TPiWvFeF55W9xoA/8h4tvKrnZx9 -ioTO8QKBgCMqRa5pHb+vCniTWUTz9JZRnRsdq7fRSsJHngMe5gOR4HylyAmmqKrd -kEXfkdu9TH2jxSWcZbHUPVwKfOUqQUZMz0pml0DIs1kedUDFanTZ8Rgg5SGUHBW0 -5bNCq64tKMmw6GiicaAGqd04OPo85WD9h8mPhM1Jdv/UmTV+HFAr ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcert1.pem b/builtin/credential/cert/test-fixtures/testcert1.pem deleted file mode 100644 index ae39599d14..0000000000 --- a/builtin/credential/cert/test-fixtures/testcert1.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDVzCCAj+gAwIBAgIUKJtTQMEcL+SjIDyoqDFZ/JQFAIwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wIBcNMjYwMTA5MjEyMzM2WhgPMjA3 -NTEwMDYxMzI0MDZaMBsxGTAXBgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0Il8itdPokxLUfLylUx2BU0keMVc1 -ADkQLVSRbn8PadSDp6bs9EBe2vhWM0kfs/OyKC9Y1AMd7dubmj+KYemOpEjnayoi -ym/RB767b9JgphnkbvR13V7qUVkWGdiXVBbultTaSpNNSUTPkhQoxYgl3VM9G+Vc -7hXJ4u7/SyhqA5R2HuPAse4HQG2HBvHU5qsmSEfZijQNR6YpL/qfCki3Fj7G4ZA0 -6QGVYltcAKmsBC1KtIxlxbxMzdkkiQrzaq1otEGR1Db9ScLFz1Kf5APT5lIoOC4T -qwn9KRA10wkTL0yCYO0LkrHbmEFcagZBq0I8tfan2TMkOQdMLrhk0U2jAgMBAAGj -gZUwgZIwDgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF -BQcDAjAdBgNVHQ4EFgQUAsz+3ch3eUMLIOBSaBHNvkuotpwwHwYDVR0jBBgwFoAU -ncSzT/6HMexyuiU9/7EgHu+ok5swIQYDVR0RBBowGIIQdGVzdC5leGFtcGxlLmNv -bYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAeQpN2vLKiTbLw1JhpvIlUzOftWU/ -tykJDMuTOP2QBTPq9pRvofE5Ij/nCZj2z7+Ui3M/RsuziCA2qzlT5voqL3cQZ6FJ -xYm+oUlIyw+88/ArcSUZmWaA/dtkvn0CUTImSgYKswlzCBABISGGzCC5xPkhLKfa -lVB6jQFmSyOXmeSKwA1KXOhykzUVvFvhjMJidWj12rsB6jzixLCtCuhPgXol801w -X76Ul8bKtVaZOLyzUd/eWcd3qSVi7jvuTtC368pV9CsIzanXAG+rfmOXQhMh48C+ -4IQFEXjf9u18KgQrk3UCwN4bhIwzyNAd90A3rdiv0a86jZjmOtPJZzypEw== ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testcert2.pem b/builtin/credential/cert/test-fixtures/testcert2.pem deleted file mode 100644 index c6defccc02..0000000000 --- a/builtin/credential/cert/test-fixtures/testcert2.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDVzCCAj+gAwIBAgIUegOuC0oLkgJXvcRbbqq6HaU4b48wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wIBcNMjYwMTA5MjEzODM1WhgPMjA3 -NTEwMDYxMzM5MDVaMBsxGTAXBgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwzWVwpgL1ezR+nphVdcSJpAXDIqh0 -ByClMi9Zr9iDEPnSAoLAaXKkhn81IyydsL2RHbt+jjUaGjBfd8Y4ib/0abiGN6fo -R1WNIVRE6tYMJZ6WntWyPsGrqPCpLU9PTrovGGqV1u498AJPJPmPYHvEoceHUEbS -e9qlsQis+hjd9CuQfXRYhhcBjBZQo6IQ/9ucS5VPF2M7+c+6TjlnHiO+UPKIwEtr -74SRQngG4jMVYwG8GLDBB6rLlqhoUqwxl1ld4UnIitbb7uiG0lnT4liT0l11pjVy -1DxMM5g6h2tIIqNECriwAD1DWe28GVmi59EPwBT6yPt/1QkGcj1ukPsJAgMBAAGj -gZUwgZIwDgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF -BQcDAjAdBgNVHQ4EFgQUidElpxqnvZelSOfHrdHBDj4WOYowHwYDVR0jBBgwFoAU -ncSzT/6HMexyuiU9/7EgHu+ok5swIQYDVR0RBBowGIIQdGVzdC5leGFtcGxlLmNv -bYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAM0rhxChmXvCNOZLmeoijW8u3Piw4 -HrYNKvubgoWlhSaJTyvqHyNtgvIDoR4nLwlTe0hfww32gSueIqRcmmrH+0S1406k -Z88bgFFN1pNRR0yNhu9obxAbHY/nK8/ODeJF8h+waqhfWw6JRWxV3+8IprqAL+F8 -Oz5gJ1JpMNR7SfgZgNMDHnXdYt/XyDGWdzIG9L8xJ8X6wPZRmYYWKIfuFZWijoav -9LReF31VbXrEn/Qlb7JO5lfPFmmasXzkCJC8DlnWVFhsh/iyfxDo35kc4WlQ371K -KRZco41pVge0ExY69IENa8dqL882hrU+g3UTo09MCMpuNUYwmS4wrmEOgA== ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testissuedcert4.pem b/builtin/credential/cert/test-fixtures/testissuedcert4.pem deleted file mode 100644 index 5bffd67779..0000000000 --- a/builtin/credential/cert/test-fixtures/testissuedcert4.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIUBLqh6ctGWVDUxFhxJX7m6S/bnrcwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wIBcNMTYwNTAyMTYwOTI2WhgPMjA2 -NjA0MjAxNTA5NTZaMBsxGTAXBgNVBAMTEGNlcnQubXl2YXVsdC5jb20wggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY3gPB29kkdbu0mPO6J0efagQhSiXB -9OyDuLf5sMk6CVDWVWal5hISkyBmw/lXgF7qC2XFKivpJOrcGQd5Ep9otBqyJLzI -b0IWdXuPIrVnXDwcdWr86ybX2iC42zKWfbXgjzGijeAVpl0UJLKBj+fk5q6NvkRL -5FUL6TRV7Krn9mrmnrV9J5IqV15pTd9W2aVJ6IqWvIPCACtZKulqWn4707uy2X2W -1Stq/5qnp1pDshiGk1VPyxCwQ6yw3iEcgecbYo3vQfhWcv7Q8LpSIM9ZYpXu6OmF -+czqRZS9gERl+wipmmrN1MdYVrTuQem21C/PNZ4jo4XUk1SFx6JrcA+lAgMBAAGj -gfUwgfIwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSe -Cl9WV3BjGCwmS/KrDSLRjfwyqjAfBgNVHSMEGDAWgBTrirz4lBtOoh5EVwDnjIlH -QXxxcjA7BggrBgEFBQcBAQQvMC0wKwYIKwYBBQUHMAKGH2h0dHA6Ly8xMjcuMC4w -LjE6ODIwMC92MS9wa2kvY2EwIQYDVR0RBBowGIIQY2VydC5teXZhdWx0LmNvbYcE -fwAAATAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vMTI3LjAuMC4xOjgyMDAvdjEv -cGtpL2NybDANBgkqhkiG9w0BAQsFAAOCAQEAWGholPN8buDYwKbUiDavbzjsxUIX -lU4MxEqOHw7CD3qIYIauPboLvB9EldBQwhgOOy607Yvdg3rtyYwyBFwPhHo/hK3Z -6mn4hc6TF2V+AUdHBvGzp2dbYLeo8noVoWbQ/lBulggwlIHNNF6+a3kALqsqk1Ch -f/hzsjFnDhAlNcYFgG8TgfE2lE/FckvejPqBffo7Q3I+wVAw0buqiz5QL81NOT+D -Y2S9LLKLRaCsWo9wRU1Az4Rhd7vK5SEMh16jJ82GyEODWPvuxOTI1MnzfnbWyLYe -TTp6YBjGMVf1I6NEcWNur7U17uIOiQjMZ9krNvoMJ1A/cxCoZ98QHgcIPg== ------END CERTIFICATE----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testissuedkey4.pem b/builtin/credential/cert/test-fixtures/testissuedkey4.pem deleted file mode 100644 index 58e7f8df73..0000000000 --- a/builtin/credential/cert/test-fixtures/testissuedkey4.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA2N4DwdvZJHW7tJjzuidHn2oEIUolwfTsg7i3+bDJOglQ1lVm -peYSEpMgZsP5V4Be6gtlxSor6STq3BkHeRKfaLQasiS8yG9CFnV7jyK1Z1w8HHVq -/Osm19oguNsyln214I8xoo3gFaZdFCSygY/n5Oaujb5ES+RVC+k0Veyq5/Zq5p61 -fSeSKldeaU3fVtmlSeiKlryDwgArWSrpalp+O9O7stl9ltUrav+ap6daQ7IYhpNV -T8sQsEOssN4hHIHnG2KN70H4VnL+0PC6UiDPWWKV7ujphfnM6kWUvYBEZfsIqZpq -zdTHWFa07kHpttQvzzWeI6OF1JNUhceia3APpQIDAQABAoIBAQCH3vEzr+3nreug -RoPNCXcSJXXY9X+aeT0FeeGqClzIg7Wl03OwVOjVwl/2gqnhbIgK0oE8eiNwurR6 -mSPZcxV0oAJpwiKU4T/imlCDaReGXn86xUX2l82KRxthNdQH/VLKEmzij0jpx4Vh -bWx5SBPdkbmjDKX1dmTiRYWIn/KjyNPvNvmtwdi8Qluhf4eJcNEUr2BtblnGOmfL -FdSu+brPJozpoQ1QdDnbAQRgqnh7Shl0tT85whQi0uquqIj1gEOGVjmBvDDnL3GV -WOENTKqsmIIoEzdZrql1pfmYTk7WNaD92bfpN128j8BF7RmAV4/DphH0pvK05y9m -tmRhyHGxAoGBAOV2BBocsm6xup575VqmFN+EnIOiTn+haOvfdnVsyQHnth63fOQx -PNtMpTPR1OMKGpJ13e2bV0IgcYRsRkScVkUtoa/17VIgqZXffnJJ0A/HT67uKBq3 -8o7RrtyK5N20otw0lZHyqOPhyCdpSsurDhNON1kPVJVYY4N1RiIxfut/AoGBAPHz -HfsJ5ZkyELE9N/r4fce04lprxWH+mQGK0/PfjS9caXPhj/r5ZkVMvzWesF3mmnY8 -goE5S35TuTvV1+6rKGizwlCFAQlyXJiFpOryNWpLwCmDDSzLcm+sToAlML3tMgWU -jM3dWHx3C93c3ft4rSWJaUYI9JbHsMzDW6Yh+GbbAoGBANIbKwxh5Hx5XwEJP2yu -kIROYCYkMy6otHLujgBdmPyWl+suZjxoXWoMl2SIqR8vPD+Jj6mmyNJy9J6lqf3f -DRuQ+fEuBZ1i7QWfvJ+XuN0JyovJ5Iz6jC58D1pAD+p2IX3y5FXcVQs8zVJRFjzB -p0TEJOf2oqORaKWRd6ONoMKvAoGALKu6aVMWdQZtVov6/fdLIcgf0pn7Q3CCR2qe -X3Ry2L+zKJYIw0mwvDLDSt8VqQCenB3n6nvtmFFU7ds5lvM67rnhsoQcAOaAehiS -rl4xxoJd5Ewx7odRhZTGmZpEOYzFo4odxRSM9c30/u18fqV1Mm0AZtHYds4/sk6P -aUj0V+kCgYBMpGrJk8RSez5g0XZ35HfpI4ENoWbiwB59FIpWsLl2LADEh29eC455 -t9Muq7MprBVBHQo11TMLLFxDIjkuMho/gcKgpYXCt0LfiNm8EZehvLJUXH+3WqUx -we6ywrbFCs6LaxaOCtTiLsN+GbZCatITL0UJaeBmTAbiw0KQjUuZPQ== ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testkey1.pem b/builtin/credential/cert/test-fixtures/testkey1.pem deleted file mode 100644 index 6d81857a63..0000000000 --- a/builtin/credential/cert/test-fixtures/testkey1.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAtCJfIrXT6JMS1Hy8pVMdgVNJHjFXNQA5EC1UkW5/D2nUg6em -7PRAXtr4VjNJH7PzsigvWNQDHe3bm5o/imHpjqRI52sqIspv0Qe+u2/SYKYZ5G70 -dd1e6lFZFhnYl1QW7pbU2kqTTUlEz5IUKMWIJd1TPRvlXO4VyeLu/0soagOUdh7j -wLHuB0Bthwbx1OarJkhH2Yo0DUemKS/6nwpItxY+xuGQNOkBlWJbXACprAQtSrSM -ZcW8TM3ZJIkK82qtaLRBkdQ2/UnCxc9Sn+QD0+ZSKDguE6sJ/SkQNdMJEy9MgmDt -C5Kx25hBXGoGQatCPLX2p9kzJDkHTC64ZNFNowIDAQABAoIBAAalZgEv2DuygXVZ -jNREtsf4vK/ific0dOaF5aLgAswcyXx6CQyhDmbxiUwU5FPJHeqq1ORgHiVSi1G4 -ZTPD3QwoP5BaQdm6wlliAcWEoKx0NGxbM6XNnxziF3lbRsR+k8IFyqCrM7gcRe+q -ohfHAfjzq4iLqPC+0Ar81niQ21Ld8xgck3t+UWtgoYdLAyA8aIa43k+I3wHEPhNv -+1w+/F/dOKcmCMBrnh3ff6mot/6J31MOecP8tU1ss5nWyJ3B+3tdHjETIcWWFhtG -w5XXlP+e39C/Haq8+h+3AgsCPVoLfVAEMDtwyax+a9S7tEie+F09ENecRPlDJLQi -cepy+uECgYEA0NQrwRFJGmaTi5zsgwXu04kbsx8vCdR0SlJeE++s7zhwH8SD66np -ZxjiAHTu6OxlIkxLR0C+YKKpP2c9sq2jpj4saJzxHtv+E9N6qSuPj1ULQ/yKQhog -y9bIBpznuTaK1k2nJ8naWoYJuiFN3LQhMmGyVRr4+MSbY0FkzWNRKVkCgYEA3NLl -1qlKPRvn3Q1oT4y9vkvtlVINZael2aZS+zM+yuZyZoo7ximOeYIMJ/YQGH1b7ygl -bLEnaCS+OBFRA4RkAwAeO9I7g57gJjoFKo4KCyF9T9oPEa5NYXAohvLhiJRZJgG/ -dhpwcQgMRUGC9nzj8wk8KJJ5rnWf+T18afVWE1sCgYEAztpr4NmTdRBIdJHjgUGe -OXFlu79W48DL1FbUk6Dkxy07e2w4VHbBGPt/2n35rUWERD4YjyLlsWlOhtxoNBZl -tSV+7b0P5sZ5XgAsT2gz0wGloBmGhkXFWMSO7GX97uvFCNRwkCwVG3gMKJAWxVi0 -TWiSslR+bESrutyq0fvgCDkCgYBCQqImvFuDZKk5QjmnjRKuVDgxExLkCt8QJQFH -UQQpe+ad8CKpfnS67xPYtdP0lUENzR0VtT6e2E+foUqO5J3h7Jol1xp2jyixL723 -HDHVTzI70LGu239qmm3+uEiGZAUwC1w5Awv0Trbn3RWAAs+fcIj1n6YVfEQJVLLN -VImEewKBgALnn48dFKSRPWcYJKnbWI5ujsggUZmabuWJKwiHeoTgJ8o8yFtWrFIW -+GVPqL2ZrKhrF+sYqbVX6yjF+0GzeGIjtaW9DifvimJ2kyhyncUEomDxRsl22/LG -P3w7PM0YzvAuo6muYqqhljhNpZUBRmCvpBaqsNwoJc04cYhfFvhn ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/builtin/credential/cert/test-fixtures/testkey2.pem b/builtin/credential/cert/test-fixtures/testkey2.pem deleted file mode 100644 index c8b16f98d2..0000000000 --- a/builtin/credential/cert/test-fixtures/testkey2.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAsM1lcKYC9Xs0fp6YVXXEiaQFwyKodAcgpTIvWa/YgxD50gKC -wGlypIZ/NSMsnbC9kR27fo41GhowX3fGOIm/9Gm4hjen6EdVjSFUROrWDCWelp7V -sj7Bq6jwqS1PT066LxhqldbuPfACTyT5j2B7xKHHh1BG0nvapbEIrPoY3fQrkH10 -WIYXAYwWUKOiEP/bnEuVTxdjO/nPuk45Zx4jvlDyiMBLa++EkUJ4BuIzFWMBvBiw -wQeqy5aoaFKsMZdZXeFJyIrW2+7ohtJZ0+JYk9JddaY1ctQ8TDOYOodrSCKjRAq4 -sAA9Q1ntvBlZoufRD8AU+sj7f9UJBnI9bpD7CQIDAQABAoIBAATt2wbEmBdGNbDU -hNh4GbBBLohx0Eq40qYMY8HPO1zvFZnvaDwLTI8N8WC2v/UPAv/3WV12Q0B8q63T -sebsXznGE0cJqPCawYW3YMkxl2FcEKNwHvLS2VUq9veue9QxFJOAzaLrDLYVGYlh -lWQUC2tQW9bXy/utCIvfR0fESrpwWwQgjFJQJnoiWs9RYG/crpaIZUZOsGcWWGwI -6qUKO/aqL0zVcb6+grwtppCgJKx7jx0bXj8LkhTUxzxx8IW0YK/lvJ/wxhfy5242 -DJLD2hBmxpW4FiAss26jKkaznzrLVrCUYWDSa//11dYnTZMNL8N5zqhYmnrnVd4N -Yju6/1ECgYEA1oHxreRvmYgYkMqY6r4qHT/cTHgQc0zwL1OXqIZrYFgHQqQ1VrHg -bdhGMXJAPhEtuPuVPqikv9bdVfvhf6bt3h5bGYnz3cKpi+ud70c88hOQCr33CCVN -54JQC0UDi7EQRqAhGaLpFah4S46kbLzpuPTsKpUR8lic/aAwdVV6o5kCgYEA0wBZ -zECi1yzwdKspoJwUJwdfSU1JJSwS2uWSMxRhkY1Avg8RNMQcb+3gM7Jt+BTL0dy7 -Fxr8kQ235NfY8rnJz0auNFpYjIRDb0KI82THDQovg+EvhdD5FaOJk5kS4jYfe2N4 -Iy6szO9NweoLO3xmZcYECuGtLqq4pVOxLqkfuPECgYEAi5LjwaU45GqEqXnaBCwW -VQ/fdTZOZeezBOhcbwB/z6GXn8ofFrkI8hBeo//WQ0yENrAkjS/IezcAr9kEAj6I -2hVga36y2iG2ll+KVU5CHrWR7RtsKLW1OiU1lg+i3fspPvskbnztMvV6yJcY79QA -NCPRo2d51PnJtNHNlhs3gEkCgYEAtXY1xA1KfldtrEiPkkroofAbKIVJBKj0xkBt -DXTXvD+IkGuQ1ppaAoDHMm6fWJ059JAqbmKNF4p+vlZLg+P4BUS6CNgyExakkAje -ksP20+YQmxCMuD7SGKP+a2tX7Cezx3/yD//SKKUdcEmBw3Tm81vqmhkfwWSdS8HA -PWrBl2ECgYAeZYrxrwWkelcbhJsxkOgeQhJkfUdv/VuDLT5pcJlH5jjvaFsEWDZW -wCtQz2KLrEK1u/HnfmjXgRURvDgf1qddGD/BV83AnBIHtXD9ia8dtbt/4rY+KaYU -/ROg406oZ7m55UhsUtuQkNAcAG7IvhHDGCp9dY2QaheeXpHvcDxe+w== ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 1ffd36fbd1..aa96e901dd 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -254,7 +254,10 @@ func (c *Core) UpdateMaxRoleAndManagedKeyCounts(ctx context.Context, localPathPr defer cb.BillingStorageLock.Unlock() local := localPathPrefix == billing.LocalPrefix - currentRoleCounts, currentManagedKeyCounts := c.getRoleAndManagedKeyCountsInternal(local, !local, true) + currentRoleCounts, currentManagedKeyCounts, err := c.getRoleAndManagedKeyCountsInternal(local, !local, true) + if err != nil { + return nil, nil, err + } // get max role counts maxRoleCounts, err := c.updateMaxRoleCounts(ctx, currentRoleCounts, localPathPrefix, currentMonth) diff --git a/vault/core_metrics.go b/vault/core_metrics.go index 54263197f4..4a3c0f468d 100644 --- a/vault/core_metrics.go +++ b/vault/core_metrics.go @@ -852,10 +852,9 @@ type ManagedKeyCounts struct { // includeReplicated determines if replicated mounts are included // officialPluginsOnly determines if this function should include only plugins that are official, // which would exclude, for example, a custom built version of these plugins. -func (c *Core) getRoleAndManagedKeyCountsInternal(includeLocal bool, includeReplicated bool, officialPluginsOnly bool) (*RoleCounts, *ManagedKeyCounts) { +func (c *Core) getRoleAndManagedKeyCountsInternal(includeLocal bool, includeReplicated bool, officialPluginsOnly bool) (*RoleCounts, *ManagedKeyCounts, error) { if c.Sealed() { - c.logger.Debug("core is sealed, cannot access mounts table") - return nil, nil + return nil, nil, fmt.Errorf("core is sealed, cannot access mounts table") } ctx := namespace.RootContext(c.activeContext) @@ -988,16 +987,16 @@ func (c *Core) getRoleAndManagedKeyCountsInternal(includeLocal bool, includeRepl } } - return &roles, &keyCounts + return &roles, &keyCounts, nil } func (c *Core) GetRoleCounts() *RoleCounts { - roleCounts, _ := c.getRoleAndManagedKeyCountsInternal(true, true, false) + roleCounts, _, _ := c.getRoleAndManagedKeyCountsInternal(true, true, false) return roleCounts } func (c *Core) GetRoleCountsForCluster() *RoleCounts { - roleCounts, _ := c.getRoleAndManagedKeyCountsInternal(true, c.isPrimary(), false) + roleCounts, _, _ := c.getRoleAndManagedKeyCountsInternal(true, c.isPrimary(), false) return roleCounts } From ed74e05cc5832f04d2c1076022e2f4971df0b630 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 13:35:03 -0700 Subject: [PATCH 013/468] Enhance sys/seal-backend-status to show all seals (#12575) (#12578) * Enhance sys/seal-backend-status to show all seals. Make GetSealBackendStatus return information about all seals, not just those configured (successfully initialized). Return information about the shamir seal when in seal migration mode. Include new fields for each seal: configured (successfully initialized) and disabled. * Add changelog entry. Co-authored-by: Victor Rodriguez Rizo --- changelog/_12575.txt | 3 +++ vault/logical_system.go | 24 +++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 changelog/_12575.txt diff --git a/changelog/_12575.txt b/changelog/_12575.txt new file mode 100644 index 0000000000..5e3b95af9e --- /dev/null +++ b/changelog/_12575.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core/seal: Enhance sys/seal-backend-status to provide more information about seal backends. +``` diff --git a/vault/logical_system.go b/vault/logical_system.go index 56b290f3f7..d018091d14 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -5513,6 +5513,8 @@ type SealStatusResponse struct { type SealBackendStatus struct { Name string `json:"name"` + Configured bool `json:"configured"` + Disabled bool `json:"disabled"` Healthy bool `json:"healthy"` UnhealthySince string `json:"unhealthy_since,omitempty"` } @@ -5642,10 +5644,12 @@ func (c *Core) GetSealBackendStatus(ctx context.Context) (*SealBackendStatusResp if a, ok := c.seal.(*autoSeal); ok { r.Healthy = c.seal.Healthy() var uhMin time.Time - for _, sealWrapper := range a.GetConfiguredSealWrappersByPriority() { + for _, sealWrapper := range a.GetAllSealWrappersByPriority() { b := SealBackendStatus{ - Name: sealWrapper.Name, - Healthy: sealWrapper.IsHealthy(), + Name: sealWrapper.Name, + Configured: sealWrapper.Configured, + Healthy: sealWrapper.IsHealthy(), + Disabled: sealWrapper.Disabled, } if !sealWrapper.IsHealthy() { lastSeenHealthy := sealWrapper.LastSeenHealthy() @@ -5661,11 +5665,21 @@ func (c *Core) GetSealBackendStatus(ctx context.Context) (*SealBackendStatusResp if !uhMin.IsZero() { r.UnhealthySince = uhMin.String() } + if c.IsInSealMigrationMode(false) { + r.Backends = append(r.Backends, SealBackendStatus{ + Name: "shamir", + Configured: true, + Healthy: true, + Disabled: true, + }) + } } else { r.Backends = []SealBackendStatus{ { - Name: "shamir", // "default?" - Healthy: true, + Name: "shamir", + Configured: true, + Healthy: true, + Disabled: false, }, } r.Healthy = true From 82072019a30017942e8e389b9f1a9547fadf07eb Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 13:36:50 -0700 Subject: [PATCH 014/468] add additional fields to alias proto (#12533) (#12561) Co-authored-by: davidadeleon <56207066+davidadeleon@users.noreply.github.com> Co-authored-by: davidadeleon --- helper/identity/types.pb.go | 226 ++++++++++++++++++++---------------- helper/identity/types.proto | 8 ++ 2 files changed, 133 insertions(+), 101 deletions(-) diff --git a/helper/identity/types.pb.go b/helper/identity/types.pb.go index 98da732158..060965fccf 100644 --- a/helper/identity/types.pb.go +++ b/helper/identity/types.pb.go @@ -540,7 +540,13 @@ type Alias struct { // ensuring that authoritative lifecycle operations (like updates and deletes by // an IGA) can only be performed by the client that owns the resource. // @inject_tag: sentinel:"-" - ScimClientID string `protobuf:"bytes,15,opt,name=scim_client_id,json=scimClientId,proto3" json:"scim_client_id,omitempty" sentinel:"-"` + ScimClientID string `protobuf:"bytes,15,opt,name=scim_client_id,json=scimClientId,proto3" json:"scim_client_id,omitempty" sentinel:"-"` + // external_id is the unique external identifier for an entity managed via + // an external IDP. This field does not always map 1:1 to a claim external_id. + // This mapping is done via configuration. + ExternalID string `protobuf:"bytes,16,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"` + // issuer is the issuer claim for this alias + Issuer string `protobuf:"bytes,17,opt,name=issuer,proto3" json:"issuer,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -680,6 +686,20 @@ func (x *Alias) GetScimClientID() string { return "" } +func (x *Alias) GetExternalID() string { + if x != nil { + return x.ExternalID + } + return "" +} + +func (x *Alias) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + // ScimClient defines the stored configuration for a single SCIM client. // This configuration links a client's identity within Vault to its specific // role and capabilities within the SCIM server. @@ -1119,7 +1139,7 @@ var file_helper_identity_types_proto_rawDesc = string([]byte{ 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x87, 0x06, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0xc0, 0x06, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, @@ -1159,106 +1179,110 @@ var file_helper_identity_types_proto_rawDesc = string([]byte{ 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x63, 0x69, 0x6d, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x63, 0x69, - 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x41, 0x0a, 0x13, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, + 0x65, 0x72, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x41, 0x0a, 0x13, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x0a, 0x53, 0x63, 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, + 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x12, + 0x34, 0x0a, 0x16, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, + 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x14, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x50, 0x72, 0x69, 0x6e, + 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x12, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x22, 0x88, 0x05, 0x0a, 0x12, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x37, 0x0a, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x46, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, + 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, + 0x0a, 0x11, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, + 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, + 0x0a, 0x0b, 0x6d, 0x66, 0x61, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0a, 0x6d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, + 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, + 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x0a, 0x53, 0x63, - 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, - 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x47, 0x72, - 0x61, 0x6e, 0x74, 0x50, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x14, - 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x61, 0x6c, 0x69, 0x61, - 0x73, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1f, - 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x49, 0x64, 0x22, 0x88, 0x05, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, - 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x08, 0x70, 0x65, 0x72, - 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, - 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3f, - 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, - 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x08, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, - 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0b, 0x6d, 0x66, 0x61, 0x5f, 0x73, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, - 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6d, 0x66, 0x61, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf9, 0x03, - 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x16, 0x6d, 0x65, 0x72, 0x67, 0x65, - 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x46, - 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x21, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, + 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf9, 0x03, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, + 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, + 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, + 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, + 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x33, 0x0a, 0x16, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x13, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x49, 0x64, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/helper/identity/types.proto b/helper/identity/types.proto index 96e505483c..0903ce4a75 100644 --- a/helper/identity/types.proto +++ b/helper/identity/types.proto @@ -262,6 +262,14 @@ message Alias { // an IGA) can only be performed by the client that owns the resource. // @inject_tag: sentinel:"-" string scim_client_id = 15; + + // external_id is the unique external identifier for an entity managed via + // an external IdP. This field does not always map 1:1 to a claim external_id. + // This mapping is done via configuration. + string external_id = 16; + + // issuer is the issuer claim for this alias + string issuer = 17; } // ScimClient defines the stored configuration for a single SCIM client. From 57e47f4546c9ea78c2e43d3d2e70adacbebacb66 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 13:38:05 -0700 Subject: [PATCH 015/468] adding sealing test & seal permissions (#12566) (#12569) Co-authored-by: Dan Rivera --- ui/e2e/policies/superuser.hcl | 4 ++++ ui/e2e/tests/superuser/seal.spec.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 ui/e2e/tests/superuser/seal.spec.ts diff --git a/ui/e2e/policies/superuser.hcl b/ui/e2e/policies/superuser.hcl index 2c64d21233..589857650d 100644 --- a/ui/e2e/policies/superuser.hcl +++ b/ui/e2e/policies/superuser.hcl @@ -3,4 +3,8 @@ path "*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} +// needed permissions to be able to seal vault +path "sys/seal" { + capabilities = ["sudo", "update"] } \ No newline at end of file diff --git a/ui/e2e/tests/superuser/seal.spec.ts b/ui/e2e/tests/superuser/seal.spec.ts new file mode 100644 index 0000000000..142c511381 --- /dev/null +++ b/ui/e2e/tests/superuser/seal.spec.ts @@ -0,0 +1,15 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('sealing workflow', async ({ page }) => { + await page.goto('dashboard'); + await page.getByRole('link', { name: 'Resilience and recovery' }).click(); + await page.getByRole('link', { name: 'Seal Vault' }).click(); + await page.getByRole('button', { name: 'Seal' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.getByText('Vault is sealed')).toBeVisible(); +}); From 5f77aa78fccf65721f815825062ea93e48a30ec7 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 14:19:16 -0700 Subject: [PATCH 016/468] VAULT-42759: Fix logic around setting updated_at field in billing endpoint, enhance tests coverage for the endpoint (#12454) (#12584) * fix updated_at logic for previous month, add tests * improvement: separate out metric names into consts * wholistically cover all metrics in billing api test * add actual totp data in the ent test * fix more wording errors * feedback: remove consts and use metric names directly * fix a test * simplify the logic around refreshing data * simplify the logic by centralizing the atomic tracker interacting inside updateBillingMetrics method, fix the logic inside the endpoint, add tests * miner fixes * feedback: set time tracker to zero at set up and at start of month to indicate data has not been updated yet, update test * attempt to fix deadlock by using statelock free version of update billing metrics method * remove unnecessary locks inside request handling * remove duplicate methods - instead create 2 wrappers around the method one with lock and one without * add a new prefix and methods to store and retrieve last update time * add comments to explain local prefix behavior for the update method * replace atomic tracker with storage methods * add method level tests for the update time storage methods * add external tests to verify perf replicated cluster independelty track last update time now * normalize time to utc before storing to storage, fix comments * code scanql feedback: remove logging of raw error to prevent leakage * feedback: reorganize and refactor update billing metric method wrappers * feedback: add go doc to the get method * feedback: retrieve stored update time for last month, instead of always putting end of month inside computeUpdatedTime * use equal test instead of within duration inside util tests * use require equal inside external tests too * use end of the requested month inside the endpoint for past months * update tests * add a new test case for when time is not stored in storage * fix a bug: add nil check before passing role counts and managed key counts to update method * feedback: remove update call of of time inside setup billing * Update vault/consumption_billing_util.go * Update vault/logical_system_use_case_billing.go * Update vault/logical_system_use_case_billing.go * comment fix * feedback: do not allow refresh on perf standby, add a warning and just retrieve stored data * add tests --------- Co-authored-by: Amir Aslamov Co-authored-by: divyaac --- api/sys_billing_test.go | 105 +++++-- vault/billing/billing_counts.go | 6 +- vault/consumption_billing.go | 22 +- vault/consumption_billing_util.go | 83 ++++++ vault/consumption_billing_util_test.go | 45 +++ vault/logical_system_use_case_billing.go | 261 ++++++++---------- ...ogical_system_use_case_billing_pki_test.go | 3 +- vault/logical_system_use_case_billing_test.go | 143 +++++++++- 8 files changed, 479 insertions(+), 189 deletions(-) diff --git a/api/sys_billing_test.go b/api/sys_billing_test.go index e84f6be994..7b816b3831 100644 --- a/api/sys_billing_test.go +++ b/api/sys_billing_test.go @@ -33,32 +33,45 @@ func TestSys_BillingOverview(t *testing.T) { currentMonth := resp.Months[0] require.Equal(t, "2026-01", currentMonth.Month) require.Equal(t, "2026-01-14T10:49:00Z", currentMonth.UpdatedAt) - require.Len(t, currentMonth.UsageMetrics, 4) + require.Len(t, currentMonth.UsageMetrics, 8, "should have all 8 metrics") - // Verify static_secrets metric - staticSecretsMetric := currentMonth.UsageMetrics[0] - require.Equal(t, "static_secrets", staticSecretsMetric.MetricName) - require.NotNil(t, staticSecretsMetric.MetricData) + // Create a map to verify all expected metrics are present + metricsMap := make(map[string]UsageMetric) + for _, metric := range currentMonth.UsageMetrics { + metricsMap[metric.MetricName] = metric + } + + // Verify all expected metrics are present + expectedMetrics := []string{ + "static_secrets", + "dynamic_roles", + "auto_rotated_roles", + "kmip", + "external_plugins", + "data_protection_calls", + "pki_units", + "managed_keys", + } + + for _, metricName := range expectedMetrics { + metric, exists := metricsMap[metricName] + require.True(t, exists, "metric %s should be present", metricName) + require.NotNil(t, metric.MetricData, "metric_data should not be nil for %s", metricName) + } + + // Verify specific metric structures + staticSecretsMetric := metricsMap["static_secrets"] require.Contains(t, staticSecretsMetric.MetricData, "total") require.Contains(t, staticSecretsMetric.MetricData, "metric_details") - // Verify kmip metric - kmipMetric := currentMonth.UsageMetrics[1] - require.Equal(t, "kmip", kmipMetric.MetricName) - require.NotNil(t, kmipMetric.MetricData) + kmipMetric := metricsMap["kmip"] require.Contains(t, kmipMetric.MetricData, "used_in_month") require.Equal(t, true, kmipMetric.MetricData["used_in_month"]) - // Verify pki_units metric - pkiMetric := currentMonth.UsageMetrics[2] - require.Equal(t, "pki_units", pkiMetric.MetricName) - require.NotNil(t, pkiMetric.MetricData) + pkiMetric := metricsMap["pki_units"] require.Contains(t, pkiMetric.MetricData, "total") - // Verify managed_keys metric - managedKeysMetric := currentMonth.UsageMetrics[3] - require.Equal(t, "managed_keys", managedKeysMetric.MetricName) - require.NotNil(t, managedKeysMetric.MetricData) + managedKeysMetric := metricsMap["managed_keys"] require.Contains(t, managedKeysMetric.MetricData, "total") require.Contains(t, managedKeysMetric.MetricData, "metric_details") @@ -102,12 +115,70 @@ const billingOverviewResponse = `{ ] } }, + { + "metric_name": "dynamic_roles", + "metric_data": { + "total": 15, + "metric_details": [ + { + "type": "aws_dynamic", + "count": 5 + }, + { + "type": "azure_dynamic", + "count": 5 + }, + { + "type": "database_dynamic", + "count": 5 + } + ] + } + }, + { + "metric_name": "auto_rotated_roles", + "metric_data": { + "total": 10, + "metric_details": [ + { + "type": "aws_static", + "count": 5 + }, + { + "type": "azure_static", + "count": 5 + } + ] + } + }, { "metric_name": "kmip", "metric_data": { "used_in_month": true } }, + { + "metric_name": "external_plugins", + "metric_data": { + "total": 3 + } + }, + { + "metric_name": "data_protection_calls", + "metric_data": { + "total": 100, + "metric_details": [ + { + "type": "transit", + "count": 50 + }, + { + "type": "transform", + "count": 50 + } + ] + } + }, { "metric_name": "pki_units", "metric_data": { diff --git a/vault/billing/billing_counts.go b/vault/billing/billing_counts.go index 4c03a55a6a..1e43a9d9f1 100644 --- a/vault/billing/billing_counts.go +++ b/vault/billing/billing_counts.go @@ -28,6 +28,7 @@ const ( ThirdPartyPluginsPrefix = "thirdPartyPluginCounts/" KmipEnabledPrefix = "kmipEnabled/" PkiDurationAdjustedCountPrefix = "normalizedCertsIssued/" + MetricsLastUpdatedAtPrefix = "metricsLastUpdatedAt/" BillingWriteInterval = 10 * time.Minute // pluginCountsSendTimeout is the timeout for sending plugin counts to the active node @@ -49,11 +50,6 @@ type ConsumptionBilling struct { // KmipSeenEnabledThisMonth tracks whether KMIP has been enabled during the current billing month. // This is used to avoid scanning all mounts every 10 minutes for KMIP billing detection. KmipSeenEnabledThisMonth atomic.Bool - - // LastMetricsUpdate tracks when billing metrics were last updated, either by the background worker - // or by the billing endpoint API call. This timestamp is used by the billing overview endpoint to - // indicate data freshness. - LastMetricsUpdate atomic.Value } type BillingConfig struct { diff --git a/vault/consumption_billing.go b/vault/consumption_billing.go index e26417e9bd..931dccb452 100644 --- a/vault/consumption_billing.go +++ b/vault/consumption_billing.go @@ -33,6 +33,7 @@ func (c *Core) setupConsumptionBilling(ctx context.Context) error { Logger: logger, } c.consumptionBillingLock.Unlock() + c.postUnsealFuncs = append(c.postUnsealFuncs, func() { c.consumptionBillingMetricsWorker(ctx) // Start the perf standby plugin counts worker if this is a perf standby @@ -113,6 +114,8 @@ func (c *Core) HandleStartOfMonth(ctx context.Context, currentMonth time.Time) { if err := c.resetInMemoryBillingMetrics(); err != nil { c.logger.Error("error resetting in memory billing metrics", "error", err) } + // Reset the metrics last update time to zero time to indicate new month data hasn't been updated yet + c.UpdateMetricsLastUpdateTime(ctx, currentMonth, time.Time{}) } func (c *Core) deletePreviousMonthBillingMetrics(ctx context.Context, currentMonth time.Time) error { @@ -153,7 +156,8 @@ func (c *Core) resetInMemoryBillingMetrics() error { return nil } -func (c *Core) updateBillingMetrics(ctx context.Context, currentMonth time.Time) error { +// updateBillingMetricsLocked must be called with stateLock already held. +func (c *Core) updateBillingMetricsLocked(ctx context.Context, currentMonth time.Time) error { // Check if systemBarrierView is initialized c.mountsLock.RLock() initialized := c.systemBarrierView != nil @@ -162,11 +166,11 @@ func (c *Core) updateBillingMetrics(ctx context.Context, currentMonth time.Time) if !initialized { return nil } - if c.PerfStandby() { + if c.perfStandby { // We do not update billing metrics on performance standbys // Instead we send any in memory counts to the primary. This doesn't apply // to role counts, but will be used for other metrics - } else if standby, _ := c.Standby(); standby { + } else if c.standby { // Do nothing if we are a standby. All requests get forwarded anyway } else { // The active node will need to flush max role counts to storage @@ -180,11 +184,21 @@ func (c *Core) updateBillingMetrics(ctx context.Context, currentMonth time.Time) c.logger.Info("updated cluster data protection call counts", "prefix", billing.LocalPrefix, "currentMonth", currentMonth) } - c.consumptionBilling.LastMetricsUpdate.Store(time.Now().UTC()) + // Store the last metrics update time. This is used to determine the freshness of the billing data. + // We store this on the active node only, since this is the node that updates the billing metrics. + // The standby nodes will replicate this value, so it will be available on all nodes, but we avoid + // having all nodes write to this value to avoid write conflicts. + c.UpdateMetricsLastUpdateTime(ctx, currentMonth, time.Now().UTC()) } return nil } +func (c *Core) updateBillingMetrics(ctx context.Context, currentMonth time.Time) error { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + return c.updateBillingMetricsLocked(ctx, currentMonth) +} + func (c *Core) UpdateReplicatedHWMMetrics(ctx context.Context, currentMonth time.Time) error { _, _, err := c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.ReplicatedPrefix, currentMonth) if err != nil { diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index aa96e901dd..33ca3abd39 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -10,6 +10,7 @@ import ( "strconv" "time" + "github.com/hashicorp/vault/helper/timeutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault/billing" ) @@ -259,6 +260,14 @@ func (c *Core) UpdateMaxRoleAndManagedKeyCounts(ctx context.Context, localPathPr return nil, nil, err } + // Add nil checks before dereferencing + if currentRoleCounts == nil { + currentRoleCounts = &RoleCounts{} + } + if currentManagedKeyCounts == nil { + currentManagedKeyCounts = &ManagedKeyCounts{} + } + // get max role counts maxRoleCounts, err := c.updateMaxRoleCounts(ctx, currentRoleCounts, localPathPrefix, currentMonth) if err != nil { @@ -684,3 +693,77 @@ func (c *Core) storePkiDurationAdjustedCountLocked(ctx context.Context, localPat return nil } + +// storeMetricsLastUpdateTimeLocked must be called with BillingStorageLock held +func (c *Core) storeMetricsLastUpdateTimeLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, updateTime time.Time) error { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.MetricsLastUpdatedAtPrefix) + entry := &logical.StorageEntry{ + Key: billingPath, + Value: []byte(updateTime.Format(time.RFC3339)), + } + view, ok := c.GetBillingSubView() + if !ok { + return nil + } + return view.Put(ctx, entry) +} + +// getMetricsLastUpdateTimeLocked retrieves timestamp of the last billing metrics update for the given month. If the value does not exist, the 0 timestamp will be returned. +func (c *Core) getMetricsLastUpdateTimeLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (time.Time, error) { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.MetricsLastUpdatedAtPrefix) + view, ok := c.GetBillingSubView() + if !ok { + return time.Time{}, nil + } + entry, err := view.Get(ctx, billingPath) + if err != nil { + return time.Time{}, err + } + if entry == nil { + return time.Time{}, nil + } + updateTime, err := time.Parse(time.RFC3339, string(entry.Value)) + if err != nil { + return time.Time{}, err + } + return updateTime, nil +} + +func (c *Core) GetMetricsLastUpdateTime(ctx context.Context, currentMonth time.Time) (time.Time, error) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return time.Time{}, ErrConsumptionBillingNotInitialized + } + + // Normalize month to UTC start-of-month to avoid timezone/midnight mismatches + normalizedMonth := timeutil.StartOfMonth(currentMonth.UTC()) + + cb.BillingStorageLock.RLock() + defer cb.BillingStorageLock.RUnlock() + return c.getMetricsLastUpdateTimeLocked(ctx, billing.LocalPrefix, normalizedMonth) +} + +// UpdateMetricsLastUpdateTime updates the last update time for billing metrics for the given month, and returns the value that was stored. +// Note that this last metrics update time is per cluster. It does NOT de-duplicate across clusters. For that reason, +// we will always store the time at the "local" prefix. +func (c *Core) UpdateMetricsLastUpdateTime(ctx context.Context, currentMonth, updateTime time.Time) error { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return ErrConsumptionBillingNotInitialized + } + + // Normalize month to UTC start-of-month and ensure updateTime is in UTC + normalizedMonth := timeutil.StartOfMonth(currentMonth.UTC()) + updateTime = updateTime.UTC() + + cb.BillingStorageLock.Lock() + defer cb.BillingStorageLock.Unlock() + + return c.storeMetricsLastUpdateTimeLocked(ctx, billing.LocalPrefix, normalizedMonth, updateTime) +} diff --git a/vault/consumption_billing_util_test.go b/vault/consumption_billing_util_test.go index 094b48cbcb..b0d8c60d40 100644 --- a/vault/consumption_billing_util_test.go +++ b/vault/consumption_billing_util_test.go @@ -399,6 +399,51 @@ func TestHWMKvSecretsCounts(t *testing.T) { require.Equal(t, 5, counts) } +// TestStoreAndGetMetricsLastUpdateTimeLocked tests the store/get helpers +// that operate under the BillingStorageLock +func TestStoreAndGetMetricsLastUpdateTimeLocked(t *testing.T) { + coreConfig := &CoreConfig{} + core, _, _ := TestCoreUnsealedWithConfig(t, coreConfig) + + ctx := namespace.RootContext(context.Background()) + month := time.Now() + updateTime := time.Now().UTC().Truncate(time.Second) + + // Acquire billing storage lock as required by the helper contract + core.consumptionBilling.BillingStorageLock.Lock() + defer core.consumptionBilling.BillingStorageLock.Unlock() + + // Store under local prefix and verify + err := core.storeMetricsLastUpdateTimeLocked(ctx, billing.LocalPrefix, month, updateTime) + require.NoError(t, err) + + got, err := core.getMetricsLastUpdateTimeLocked(ctx, billing.LocalPrefix, month) + require.NoError(t, err) + require.Equal(t, updateTime.Format(time.RFC3339), got.Format(time.RFC3339)) + + // Ensure other prefix returns zero when not set + gotReplicated, err := core.getMetricsLastUpdateTimeLocked(ctx, billing.ReplicatedPrefix, month) + require.NoError(t, err) + require.True(t, gotReplicated.IsZero(), "replicated prefix should have no stored timestamp") +} + +// TestUpdateAndGetMetricsLastUpdateTime tests the public Update/Get helpers for the metrics last update time +func TestUpdateAndGetMetricsLastUpdateTime(t *testing.T) { + coreConfig := &CoreConfig{} + core, _, _ := TestCoreUnsealedWithConfig(t, coreConfig) + + ctx := namespace.RootContext(context.Background()) + month := time.Now() + updateTime := time.Now().UTC().Truncate(time.Second) + + err := core.UpdateMetricsLastUpdateTime(ctx, month, updateTime) + require.NoError(t, err) + + got, err := core.GetMetricsLastUpdateTime(ctx, month) + require.NoError(t, err) + require.Equal(t, updateTime.Format(time.RFC3339), got.Format(time.RFC3339)) +} + // TestStoreAndGetMaxTotpKeyCounts verifies that we can store and retrieve the HWM totp key counts correctly func TestStoreAndGetMaxTotpKeyCounts(t *testing.T) { coreConfig := &CoreConfig{ diff --git a/vault/logical_system_use_case_billing.go b/vault/logical_system_use_case_billing.go index a877efc5ce..3a463f7821 100644 --- a/vault/logical_system_use_case_billing.go +++ b/vault/logical_system_use_case_billing.go @@ -15,7 +15,11 @@ import ( "github.com/hashicorp/vault/vault/billing" ) -const pkiDurationAjustedCountMetricName = "pki_units" +const ( + WarningRefreshIgnoredOnStandby = "refresh_data parameter is supported only on the active node. " + + "Since this parameter was set on a performance standby, the billing data was not refreshed " + + "and retrieved from storage without update." +) func (b *SystemBackend) useCaseConsumptionBillingPaths() []*framework.Path { return []*framework.Path{ @@ -64,6 +68,16 @@ func (b *SystemBackend) handleUseCaseConsumption(ctx context.Context, req *logic currentMonth := time.Now() previousMonth := timeutil.StartOfPreviousMonth(currentMonth) + warnings := make([]string, 0) + + // Check if this is a performance standby and if refreshData is true, + // and add a warning that refresh will be ignored in this case. + // We do not need to hold stateLock here since HandleRequest is already holding this lock. + if refreshData && b.Core.perfStandby { + warnings = append(warnings, WarningRefreshIgnoredOnStandby) + refreshData = false + } + // Refresh data only if explicitly requested and for current month currentMonthData, err := b.buildMonthBillingData(ctx, currentMonth, refreshData) if err != nil { @@ -83,34 +97,45 @@ func (b *SystemBackend) handleUseCaseConsumption(ctx context.Context, req *logic } return &logical.Response{ - Data: resp, + Data: resp, + Warnings: warnings, }, nil } // buildMonthBillingData constructs billing data for a specific month func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Time, refreshData bool) (map[string]interface{}, error) { + currentMonth := timeutil.StartOfMonth(time.Now().UTC()) + // Check if the billing metrics need to be refreshed. We're running + // under the core stateLock during request handling,so call the no-lock helper to + // avoid recursive locking. + if refreshData { + if err := b.Core.updateBillingMetricsLocked(ctx, currentMonth); err != nil { + return nil, fmt.Errorf("error refreshing billing metrics: %w", err) + } + } + // Retrieve all billing metrics - combinedRoleCounts, combinedManagedKeyCounts, err := b.Core.getRoleAndManagedKeyCounts(ctx, month, refreshData) + combinedRoleCounts, combinedManagedKeyCounts, err := b.Core.getRoleAndManagedKeyCounts(ctx, month) if err != nil { return nil, err } - combinedKvCounts, err := b.Core.getKvCounts(ctx, month, refreshData) + combinedKvCounts, err := b.Core.getKvCounts(ctx, month) if err != nil { return nil, err } - transitCounts, transformCounts, err := b.Core.getDataProtectionCounts(ctx, month, refreshData) + transitCounts, transformCounts, err := b.Core.getDataProtectionCounts(ctx, month) if err != nil { return nil, err } - kmipEnabled, err := b.Core.getKmipStatus(ctx, month, refreshData) + kmipEnabled, err := b.Core.getKmipStatus(ctx, month) if err != nil { return nil, err } - thirdPartyPluginCounts, err := b.Core.getThirdPartyPluginCounts(ctx, month, refreshData) + thirdPartyPluginCounts, err := b.Core.getThirdPartyPluginCounts(ctx, month) if err != nil { return nil, err } @@ -185,27 +210,7 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti }, }) - // Determine updated_at timestamp based on whether data was refreshed - var dataUpdatedAt time.Time - if refreshData { - // Data was just refreshed, use current time and update the stored timestamp - dataUpdatedAt = time.Now().UTC() - b.Core.consumptionBilling.LastMetricsUpdate.Store(dataUpdatedAt) - } else { - // Data was not refreshed, use the last time metrics were updated by the background worker - lastUpdate := b.Core.consumptionBilling.LastMetricsUpdate.Load() - if lastUpdate != nil { - if t, ok := lastUpdate.(time.Time); ok && !t.IsZero() { - dataUpdatedAt = t - } else { - // Fallback to end of month if timestamp not available - dataUpdatedAt = timeutil.StartOfMonth(month.AddDate(0, 1, 0)).Add(-time.Second).UTC() - } - } else { - // Fallback to end of month if timestamp not available - dataUpdatedAt = timeutil.StartOfMonth(month.AddDate(0, 1, 0)).Add(-time.Second).UTC() - } - } + dataUpdatedAt := b.Core.computeUpdatedAt(ctx, month, currentMonth) monthStr := month.Format("2006-01") @@ -216,6 +221,40 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti }, nil } +// computeUpdatedAt determines the appropriate updated_at timestamp for billing data +func (c *Core) computeUpdatedAt(ctx context.Context, month, currentMonth time.Time) time.Time { + var dataUpdatedAt time.Time + isCurrentMonth := timeutil.StartOfMonth(month).Equal(currentMonth) + if isCurrentMonth { + // Use the last time metrics were updated. If it is zero, it means the data has not + // been updated yet for the current month. + lastUpdate, err := c.GetMetricsLastUpdateTime(ctx, currentMonth) + if err != nil { + // Avoid logging raw error contents which may include sensitive information. + c.logger.Error("error retrieving last metrics update time") + return time.Time{} + } + dataUpdatedAt = lastUpdate + } else { + // Check presence of a stored metrics timestamp for the previous month. + // If present, return the canonical end-of-month for the requested + // `month`. The stored timestamp acts strictly as a + // presence indicator. + previousMonthStart := timeutil.StartOfPreviousMonth(currentMonth) + previousMonthTimestamp, err := c.GetMetricsLastUpdateTime(ctx, previousMonthStart) + + // The previous month has not been updated yet. + if err != nil || previousMonthTimestamp.IsZero() { + return time.Time{} + } + + // Use requested month's canonical end-of-month. + dataUpdatedAt = timeutil.EndOfMonth(month.UTC()) + } + + return dataUpdatedAt +} + // buildDynamicRolesMetric creates the dynamic_roles metric from role counts. func buildDynamicRolesMetric(counts *RoleCounts) map[string]interface{} { total := 0 @@ -342,7 +381,7 @@ func (b *SystemBackend) buildPkiBillingMetric(ctx context.Context, month time.Ti } return map[string]interface{}{ - "metric_name": pkiDurationAjustedCountMetricName, + "metric_name": "pki_units", "metric_data": map[string]interface{}{ "total": count, }, @@ -350,61 +389,38 @@ func (b *SystemBackend) buildPkiBillingMetric(ctx context.Context, month time.Ti } // getRoleCounts retrieves and combines role and managed key counts from replicated and local storage -func (c *Core) getRoleAndManagedKeyCounts(ctx context.Context, month time.Time, updateCounts bool) (*RoleCounts, *ManagedKeyCounts, error) { +func (c *Core) getRoleAndManagedKeyCounts(ctx context.Context, month time.Time) (*RoleCounts, *ManagedKeyCounts, error) { var replicatedRoleCounts *RoleCounts - var replicatedManagedKeyCounts *ManagedKeyCounts replicatedTotpHWMValue := 0 replicatedKmseHWMValue := 0 var err error if c.isPrimary() { - if updateCounts { - replicatedRoleCounts, replicatedManagedKeyCounts, err = c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error updating replicated max role and managed key counts: %w", err) - } - replicatedTotpHWMValue = replicatedManagedKeyCounts.TotpKeys - replicatedKmseHWMValue = replicatedManagedKeyCounts.KmseKeys - } else { - replicatedRoleCounts, err = c.GetStoredHWMRoleCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving replicated max role counts: %w", err) - } - replicatedTotpHWMValue, err = c.GetStoredHWMTotpCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving replicated max managed key count: %w", err) - } - replicatedKmseHWMValue, err = c.GetStoredHWMKmseCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving replicated max kmse key count: %w", err) - } + replicatedRoleCounts, err = c.GetStoredHWMRoleCounts(ctx, billing.ReplicatedPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving replicated max role counts: %w", err) + } + replicatedTotpHWMValue, err = c.GetStoredHWMTotpCounts(ctx, billing.ReplicatedPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving replicated max managed key count: %w", err) + } + replicatedKmseHWMValue, err = c.GetStoredHWMKmseCounts(ctx, billing.ReplicatedPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving replicated max kmse key count: %w", err) } } - var localRoleCounts *RoleCounts - var localManagedKeyCounts *ManagedKeyCounts - localTotpHWMValue := 0 - localKmseHWMValue := 0 - if updateCounts { - localRoleCounts, localManagedKeyCounts, err = c.UpdateMaxRoleAndManagedKeyCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error updating local max role and managed key counts: %w", err) - } - localTotpHWMValue = localManagedKeyCounts.TotpKeys - localKmseHWMValue = localManagedKeyCounts.KmseKeys - } else { - localRoleCounts, err = c.GetStoredHWMRoleCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving local max role counts: %w", err) - } - localTotpHWMValue, err = c.GetStoredHWMTotpCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving local max totp key count: %w", err) - } - localKmseHWMValue, err = c.GetStoredHWMKmseCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return nil, nil, fmt.Errorf("error retrieving local max kmse key count: %w", err) - } + localRoleCounts, err := c.GetStoredHWMRoleCounts(ctx, billing.LocalPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving local max role counts: %w", err) + } + localTotpHWMValue, err := c.GetStoredHWMTotpCounts(ctx, billing.LocalPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving local max totp key count: %w", err) + } + localKmseHWMValue, err := c.GetStoredHWMKmseCounts(ctx, billing.LocalPrefix, month) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving local max kmse key count: %w", err) } combinedManagedKeyCounts := &ManagedKeyCounts{ @@ -416,35 +432,20 @@ func (c *Core) getRoleAndManagedKeyCounts(ctx context.Context, month time.Time, } // getKvCounts retrieves and combines KV secret counts from replicated and local storage -func (c *Core) getKvCounts(ctx context.Context, month time.Time, updateCounts bool) (int, error) { +func (c *Core) getKvCounts(ctx context.Context, month time.Time) (int, error) { var replicatedKvCounts int var err error if c.isPrimary() { - if updateCounts { - replicatedKvCounts, err = c.UpdateMaxKvCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return 0, fmt.Errorf("error updating replicated max kv counts: %w", err) - } - } else { - replicatedKvCounts, err = c.GetStoredHWMKvCounts(ctx, billing.ReplicatedPrefix, month) - if err != nil { - return 0, fmt.Errorf("error retrieving replicated max kv counts: %w", err) - } + replicatedKvCounts, err = c.GetStoredHWMKvCounts(ctx, billing.ReplicatedPrefix, month) + if err != nil { + return 0, fmt.Errorf("error retrieving replicated max kv counts: %w", err) } } - var localKvCounts int - if updateCounts { - localKvCounts, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return 0, fmt.Errorf("error updating local max kv counts: %w", err) - } - } else { - localKvCounts, err = c.GetStoredHWMKvCounts(ctx, billing.LocalPrefix, month) - if err != nil { - return 0, fmt.Errorf("error retrieving local max kv counts: %w", err) - } + localKvCounts, err := c.GetStoredHWMKvCounts(ctx, billing.LocalPrefix, month) + if err != nil { + return 0, fmt.Errorf("error retrieving local max kv counts: %w", err) } return replicatedKvCounts + localKvCounts, nil @@ -453,68 +454,34 @@ func (c *Core) getKvCounts(ctx context.Context, month time.Time, updateCounts bo // getDataProtectionCounts retrieves Transit and Transform call counts // Data protection call counts are stored at local path only // Each cluster tracks its own total requests to avoid double counting -func (c *Core) getDataProtectionCounts(ctx context.Context, month time.Time, updateCounts bool) (uint64, uint64, error) { - var transitCounts, transformCounts uint64 - var err error - - if updateCounts { - transitCounts, err = c.UpdateTransitCallCounts(ctx, month) - if err != nil { - return 0, 0, fmt.Errorf("error updating local transit call counts: %w", err) - } - transformCounts, err = c.UpdateTransformCallCounts(ctx, month) - if err != nil { - return 0, 0, fmt.Errorf("error updating local transform call counts: %w", err) - } - } else { - transitCounts, err = c.GetStoredTransitCallCounts(ctx, month) - if err != nil { - return 0, 0, fmt.Errorf("error retrieving local transit call counts: %w", err) - } - transformCounts, err = c.GetStoredTransformCallCounts(ctx, month) - if err != nil { - return 0, 0, fmt.Errorf("error retrieving local transform call counts: %w", err) - } +func (c *Core) getDataProtectionCounts(ctx context.Context, month time.Time) (uint64, uint64, error) { + transitCounts, err := c.GetStoredTransitCallCounts(ctx, month) + if err != nil { + return 0, 0, fmt.Errorf("error retrieving local transit call counts: %w", err) + } + transformCounts, err := c.GetStoredTransformCallCounts(ctx, month) + if err != nil { + return 0, 0, fmt.Errorf("error retrieving local transform call counts: %w", err) } return transitCounts, transformCounts, nil } // getKmipStatus retrieves KMIP enabled status (always stored at local path) -func (c *Core) getKmipStatus(ctx context.Context, month time.Time, updateCounts bool) (bool, error) { - var kmipEnabled bool - var err error - - if updateCounts { - kmipEnabled, err = c.UpdateKmipEnabled(ctx, month) - if err != nil { - return false, fmt.Errorf("error updating KMIP enabled status: %w", err) - } - } else { - kmipEnabled, err = c.GetStoredKmipEnabled(ctx, month) - if err != nil { - return false, fmt.Errorf("error retrieving KMIP enabled status: %w", err) - } +func (c *Core) getKmipStatus(ctx context.Context, month time.Time) (bool, error) { + kmipEnabled, err := c.GetStoredKmipEnabled(ctx, month) + if err != nil { + return false, fmt.Errorf("error retrieving KMIP enabled status: %w", err) } return kmipEnabled, nil } // getThirdPartyPluginCounts retrieves third-party plugin counts (always stored at local path) -func (c *Core) getThirdPartyPluginCounts(ctx context.Context, month time.Time, updateCounts bool) (int, error) { - var thirdPartyPluginCounts int - var err error - - if updateCounts { - thirdPartyPluginCounts, err = c.UpdateMaxThirdPartyPluginCounts(ctx, month) - if err != nil { - return 0, fmt.Errorf("error updating third-party plugin counts: %w", err) - } - } else { - thirdPartyPluginCounts, err = c.GetStoredThirdPartyPluginCounts(ctx, month) - if err != nil { - return 0, fmt.Errorf("error retrieving third-party plugin counts: %w", err) - } +func (c *Core) getThirdPartyPluginCounts(ctx context.Context, month time.Time) (int, error) { + thirdPartyPluginCounts, err := c.GetStoredThirdPartyPluginCounts(ctx, month) + if err != nil { + return 0, fmt.Errorf("error retrieving third-party plugin counts: %w", err) } return thirdPartyPluginCounts, nil diff --git a/vault/logical_system_use_case_billing_pki_test.go b/vault/logical_system_use_case_billing_pki_test.go index 81748ed2ea..34aa98f741 100644 --- a/vault/logical_system_use_case_billing_pki_test.go +++ b/vault/logical_system_use_case_billing_pki_test.go @@ -90,8 +90,7 @@ func TestGeneratePkiBillingMetric(t *testing.T) { overview, err := backend.buildPkiBillingMetric(ctx, month) require.NoError(t, err) - // Verify it uses the constant pkiDurationAjustedCountMetricName - require.Equal(t, pkiDurationAjustedCountMetricName, overview["metric_name"]) + // Verify it uses the right metric name require.Equal(t, "pki_units", overview["metric_name"]) }) } diff --git a/vault/logical_system_use_case_billing_test.go b/vault/logical_system_use_case_billing_test.go index c0bec91b18..bc5d2eceba 100644 --- a/vault/logical_system_use_case_billing_test.go +++ b/vault/logical_system_use_case_billing_test.go @@ -396,6 +396,11 @@ func TestSystemBackend_BillingOverview_PreviousMonth(t *testing.T) { c.consumptionBilling.BillingStorageLock.Unlock() require.NoError(t, err) + // Store metrics last update timestamp for previous month so it's detected as having data + testUpdateTime := time.Date(previousMonth.Year(), previousMonth.Month(), 15, 12, 0, 0, 0, time.UTC) + err = c.UpdateMetricsLastUpdateTime(ctx, previousMonth, testUpdateTime) + require.NoError(t, err) + // Make a request to the billing overview endpoint req := logical.TestRequest(t, logical.ReadOperation, "billing/overview") resp, err := b.HandleRequest(ctx, req) @@ -422,8 +427,8 @@ func TestSystemBackend_BillingOverview_PreviousMonth(t *testing.T) { require.NoError(t, err) // The updated_at for previous month should be at the end of that month - expectedEndOfMonth := timeutil.StartOfMonth(previousMonth.AddDate(0, 1, 0)).Add(-time.Second) - require.WithinDuration(t, expectedEndOfMonth, parsedTime, time.Minute) + expectedEndOfMonth := timeutil.EndOfMonth(previousMonth).UTC() + require.Equal(t, expectedEndOfMonth, parsedTime) } // TestSystemBackend_BillingOverview_EmptyMetrics verifies that the billing overview @@ -508,12 +513,12 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) { case "pki_units": total, ok := metricData["total"].(float64) require.True(t, ok, "pki_units total should be float64") - require.Equal(t, float64(0), total, "data_protection_calls total should be 0") + require.Equal(t, float64(0), total, "pki units total should be 0") case "managed_keys": total, ok := metricData["total"].(int) require.True(t, ok, "managed_keys total should be float64") - require.Equal(t, int(0), total, "data_protection_calls total should be 0") + require.Equal(t, int(0), total, "managed keys total should be 0") details, ok := metricData["metric_details"].([]map[string]interface{}) require.True(t, ok, "%s metric_details should be array", metricName) require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName) @@ -598,7 +603,7 @@ func TestSystemBackend_BillingOverview_UpdatedAtTimestamp(t *testing.T) { c, b, _ := testCoreSystemBackend(t) ctx := namespace.RootContext(nil) - // First, call with refresh_data set to set the LastMetricsUpdate timestamp + // First, call with refresh_data set to set the metrics last update timestamp req := logical.TestRequest(t, logical.ReadOperation, "billing/overview") req.Data["refresh_data"] = true resp, err := b.HandleRequest(ctx, req) @@ -612,18 +617,29 @@ func TestSystemBackend_BillingOverview_UpdatedAtTimestamp(t *testing.T) { currentMonth, ok := months[0].(map[string]interface{}) require.True(t, ok) - // Get the updated_at timestamp from the first call + previousMonth, ok := months[1].(map[string]interface{}) + require.True(t, ok) + + // Get the updated_at timestamp from the first call (current month) firstUpdatedAt, ok := currentMonth["updated_at"].(string) require.True(t, ok) firstTime, err := time.Parse(time.RFC3339, firstUpdatedAt) require.NoError(t, err) - // Verify LastMetricsUpdate was set - lastUpdate := c.consumptionBilling.LastMetricsUpdate.Load() - require.NotNil(t, lastUpdate, "LastMetricsUpdate should be set after refresh") - storedTime, ok := lastUpdate.(time.Time) + // Verify the metrics last update time was set + lastUpdate, err := c.GetMetricsLastUpdateTime(ctx, time.Now().UTC()) + require.NoError(t, err) + require.Equal(t, firstTime, lastUpdate, "stored timestamp should match response timestamp") + + // Verify previous month timestamp is zero time (no data stored for previous month) + prevMonthUpdatedAt, ok := previousMonth["updated_at"].(string) require.True(t, ok) - require.WithinDuration(t, firstTime, storedTime, time.Second, "stored timestamp should match response timestamp") + prevMonthTime, err := time.Parse(time.RFC3339, prevMonthUpdatedAt) + require.NoError(t, err) + + // Previous month should be zero time since we haven't stored any data for it + require.True(t, prevMonthTime.IsZero(), + "previous month updated_at should be zero time when no data is stored") // Wait a moment to ensure time difference time.Sleep(100 * time.Millisecond) @@ -642,17 +658,116 @@ func TestSystemBackend_BillingOverview_UpdatedAtTimestamp(t *testing.T) { currentMonth, ok = months[0].(map[string]interface{}) require.True(t, ok) - // Get the updated_at timestamp from the second call + previousMonth, ok = months[1].(map[string]interface{}) + require.True(t, ok) + + // Get the updated_at timestamp from the second call (current month) secondUpdatedAt, ok := currentMonth["updated_at"].(string) require.True(t, ok) secondTime, err := time.Parse(time.RFC3339, secondUpdatedAt) require.NoError(t, err) // The timestamp should be the same as the first call because we didn't refresh the data - require.WithinDuration(t, firstTime, secondTime, time.Second, - "updated_at without refresh should use stored LastMetricsUpdate timestamp") + require.Equal(t, firstTime, secondTime, + "updated_at without refresh should use stored metrics last update timestamp") // Verify the timestamps are equal require.Equal(t, firstUpdatedAt, secondUpdatedAt, "updated_at without refresh should be identical to the stored timestamp") + + // Verify previous month timestamp remains the same (zero time) + secondPrevMonthUpdatedAt, ok := previousMonth["updated_at"].(string) + require.True(t, ok) + require.Equal(t, prevMonthUpdatedAt, secondPrevMonthUpdatedAt, + "previous month updated_at should remain zero time") +} + +// TestSystemBackend_BillingOverview_UpdatedAtTimestamp_NoStoredTimestamp tests the behavior +// when the metrics last update time is zero time (background worker hasn't run yet) +func TestSystemBackend_BillingOverview_UpdatedAtTimestamp_NoStoredTimestamp(t *testing.T) { + c, b, _ := testCoreSystemBackend(t) + ctx := namespace.RootContext(nil) + + // Verify the metrics last update time is zero time initially + lastUpdate, err := c.GetMetricsLastUpdateTime(ctx, time.Now().UTC()) + require.NoError(t, err) + require.True(t, lastUpdate.IsZero(), "metrics last update time should be zero time initially") + + // Call without refresh_data when timestamp is zero + req := logical.TestRequest(t, logical.ReadOperation, "billing/overview") + req.Data["refresh_data"] = false + resp, err := b.HandleRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + months, ok := resp.Data["months"].([]interface{}) + require.True(t, ok) + require.Len(t, months, 2) + + currentMonth, ok := months[0].(map[string]interface{}) + require.True(t, ok) + + // Get the updated_at timestamp + updatedAt, ok := currentMonth["updated_at"].(string) + require.True(t, ok) + updatedTime, err := time.Parse(time.RFC3339, updatedAt) + require.NoError(t, err) + + // Verify it's zero time to indicate data hasn't been updated yet + require.True(t, updatedTime.IsZero(), + "updated_at should be zero time when the metrics last update time is zero") + + // Verify previous month is also zero time (no stored timestamp for previous month) + previousMonth, ok := months[1].(map[string]interface{}) + require.True(t, ok) + prevMonthUpdatedAt, ok := previousMonth["updated_at"].(string) + require.True(t, ok) + prevMonthTime, err := time.Parse(time.RFC3339, prevMonthUpdatedAt) + require.NoError(t, err) + + // Previous month should also be zero time since no timestamp is stored + require.True(t, prevMonthTime.IsZero(), + "previous month updated_at should be zero time when no stored timestamp exists") +} + +// TestSystemBackend_BillingOverview_PreviousMonth_WithError tests the behavior +// when retrieving the previous month's timestamp fails with an error. +// This ensures the endpoint gracefully handles storage errors by returning zero time. +func TestSystemBackend_BillingOverview_PreviousMonth_WithError(t *testing.T) { + c, b, _ := testCoreSystemBackend(t) + ctx := namespace.RootContext(nil) + + // Store some data for previous month + previousMonth := timeutil.StartOfPreviousMonth(time.Now()) + + // Store counts but intentionally do NOT store the metrics last update timestamp + // This simulates a scenario where data exists but timestamp retrieval might fail + c.consumptionBilling.BillingStorageLock.Lock() + err := c.storeMaxKvCountsLocked(ctx, 5, "local/", previousMonth) + c.consumptionBilling.BillingStorageLock.Unlock() + require.NoError(t, err) + + // Make a request to the billing overview endpoint + req := logical.TestRequest(t, logical.ReadOperation, "billing/overview") + resp, err := b.HandleRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + months, ok := resp.Data["months"].([]interface{}) + require.True(t, ok) + require.Len(t, months, 2) + + // Check previous month data + previousMonthData, ok := months[1].(map[string]interface{}) + require.True(t, ok) + + // Verify updated_at is zero time when no timestamp is stored + updatedAt, ok := previousMonthData["updated_at"].(string) + require.True(t, ok) + parsedTime, err := time.Parse(time.RFC3339, updatedAt) + require.NoError(t, err) + + // Should be zero time since no timestamp was stored for previous month + require.True(t, parsedTime.IsZero(), + "previous month updated_at should be zero time when timestamp is not stored") } From 8f1019d4e7096784bd423dc6a65e221fdd87d321 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 26 Feb 2026 17:19:25 -0700 Subject: [PATCH 017/468] [SECVULN-37949] UI: add pnpm overrides for ajv and markdown-it libraries to address open security vulns (#12559) (#12590) Co-authored-by: Shannon Roberts (Beagin) --- ui/package.json | 3 + ui/pnpm-lock.yaml | 141 ++++++++++++++++++++++++++-------------------- 2 files changed, 83 insertions(+), 61 deletions(-) diff --git a/ui/package.json b/ui/package.json index 3b80499227..15ef309225 100644 --- a/ui/package.json +++ b/ui/package.json @@ -178,6 +178,8 @@ "@babel/runtime": "7.27.0", "@embroider/macros": "1.15.0", "@messageformat/runtime": "3.0.2", + "ajv@6.12.6": "6.14.0", + "ajv@8.17.1": "8.18.0", "ansi-html": "0.0.8", "async": "2.6.4", "braces": "3.0.3", @@ -187,6 +189,7 @@ "ini": "1.3.8", "json5": "1.0.2", "kind-of": "6.0.3", + "markdown-it": "14.1.1", "micromatch": "4.0.8", "prismjs": "1.30.0", "qs": "6.14.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index a65cbcc6cb..80ca695754 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -8,6 +8,8 @@ overrides: '@babel/runtime': 7.27.0 '@embroider/macros': 1.15.0 '@messageformat/runtime': 3.0.2 + ajv@6.12.6: 6.14.0 + ajv@8.17.1: 8.18.0 ansi-html: 0.0.8 async: 2.6.4 braces: 3.0.3 @@ -17,6 +19,7 @@ overrides: ini: 1.3.8 json5: 1.0.2 kind-of: 6.0.3 + markdown-it: 14.1.1 micromatch: 4.0.8 prismjs: 1.30.0 qs: 6.14.1 @@ -2310,18 +2313,18 @@ packages: ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: - ajv: ^6.9.1 + ajv: 6.14.0 ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: - ajv: ^8.8.2 + ajv: 8.18.0 - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} amd-name-resolver@0.0.6: resolution: {integrity: sha512-W2trar3LgeKV/yB6ZRD3Iw7MlhrKjLMVSNAatWNNYsn4w+iSfbmA66VB+jQjVIfvzHPZicnHObAvflMkoVtjAQ==} @@ -5752,9 +5755,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} - linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -5941,19 +5941,15 @@ packages: resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} peerDependencies: '@types/markdown-it': '*' - markdown-it: '*' + markdown-it: 14.1.1 markdown-it-terminal@0.4.0: resolution: {integrity: sha512-NeXtgpIK6jBciHTm9UhiPnyHDdqyVIdRPJ+KdQtZaf/wR74gvhCNbw5li4TYsxRp5u3ZoHEF4DwpECeZqyCw+w==} peerDependencies: - markdown-it: '>= 13.0.0' + markdown-it: 14.1.1 - markdown-it@13.0.2: - resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} - hasBin: true - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true markdown-table@2.0.0: @@ -7830,9 +7826,6 @@ packages: resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} engines: {node: '>=12.17'} - uc.micro@1.0.6: - resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -10252,7 +10245,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.1 espree: 9.6.1 globals: 13.24.0 @@ -11071,6 +11064,26 @@ snapshots: - '@babel/core' - supports-color + '@types/ember@4.0.11': + dependencies: + '@types/ember__application': 4.0.11(@babel/core@7.26.10) + '@types/ember__array': 4.0.10(@babel/core@7.26.10) + '@types/ember__component': 4.0.22 + '@types/ember__controller': 4.0.12(@babel/core@7.26.10) + '@types/ember__debug': 4.0.8(@babel/core@7.26.10) + '@types/ember__engine': 4.0.11(@babel/core@7.26.10) + '@types/ember__error': 4.0.6 + '@types/ember__object': 4.0.12(@babel/core@7.26.10) + '@types/ember__polyfills': 4.0.6 + '@types/ember__routing': 4.0.22 + '@types/ember__runloop': 4.0.10 + '@types/ember__service': 4.0.9(@babel/core@7.26.10) + '@types/ember__string': 3.16.3 + '@types/ember__template': 4.0.7 + '@types/ember__test': 4.0.6(@babel/core@7.26.10) + '@types/ember__utils': 4.0.7 + '@types/rsvp': 4.0.9 + '@types/ember@4.0.11(@babel/core@7.26.10)': dependencies: '@types/ember__application': 4.0.11(@babel/core@7.26.10) @@ -11097,11 +11110,11 @@ snapshots: '@types/ember__application@4.0.11(@babel/core@7.26.10)': dependencies: '@glimmer/component': 1.1.2(@babel/core@7.26.10) - '@types/ember': 4.0.11(@babel/core@7.26.10) + '@types/ember': 4.0.11 '@types/ember__engine': 4.0.11(@babel/core@7.26.10) '@types/ember__object': 4.0.12(@babel/core@7.26.10) '@types/ember__owner': 4.0.9 - '@types/ember__routing': 4.0.22(@babel/core@7.26.10) + '@types/ember__routing': 4.0.22 transitivePeerDependencies: - '@babel/core' - supports-color @@ -11114,6 +11127,11 @@ snapshots: - '@babel/core' - supports-color + '@types/ember__component@4.0.22': + dependencies: + '@types/ember': 4.0.11 + '@types/ember__object': 4.0.12(@babel/core@7.26.10) + '@types/ember__component@4.0.22(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11159,6 +11177,13 @@ snapshots: '@types/ember__polyfills@4.0.6': {} + '@types/ember__routing@4.0.22': + dependencies: + '@types/ember': 4.0.11 + '@types/ember__controller': 4.0.12(@babel/core@7.26.10) + '@types/ember__object': 4.0.12(@babel/core@7.26.10) + '@types/ember__service': 4.0.9(@babel/core@7.26.10) + '@types/ember__routing@4.0.22(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11169,6 +11194,10 @@ snapshots: - '@babel/core' - supports-color + '@types/ember__runloop@4.0.10': + dependencies: + '@types/ember': 4.0.11 + '@types/ember__runloop@4.0.10(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11196,6 +11225,10 @@ snapshots: - '@babel/core' - supports-color + '@types/ember__utils@4.0.7': + dependencies: + '@types/ember': 4.0.11 + '@types/ember__utils@4.0.7(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11605,25 +11638,25 @@ snapshots: ajv-formats@2.1.1: dependencies: - ajv: 8.17.1 + ajv: 8.18.0 - ajv-keywords@3.5.2(ajv@6.12.6): + ajv-keywords@3.5.2(ajv@6.14.0): dependencies: - ajv: 6.12.6 + ajv: 6.14.0 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.0.6 @@ -14037,8 +14070,8 @@ snapshots: is-language-code: 3.1.0 isbinaryfile: 5.0.4 lodash: 4.17.23 - markdown-it: 13.0.2 - markdown-it-terminal: 0.4.0(markdown-it@13.0.2) + markdown-it: 14.1.1 + markdown-it-terminal: 0.4.0(markdown-it@14.1.1) minimatch: 7.4.6 morgan: 1.10.0 nopt: 3.0.6 @@ -14896,7 +14929,7 @@ snapshots: '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.1 @@ -16257,8 +16290,8 @@ snapshots: escape-string-regexp: 2.0.0 js2xmlparser: 4.0.2 klaw: 3.0.0 - markdown-it: 14.1.0 - markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0) + markdown-it: 14.1.1 + markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.1) marked: 4.3.0 mkdirp: 1.0.4 requizzle: 0.2.4 @@ -16369,10 +16402,6 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@4.0.1: - dependencies: - uc.micro: 1.0.6 - linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -16558,28 +16587,20 @@ snapshots: map-obj@4.3.0: {} - markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0): + markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.1): dependencies: '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 + markdown-it: 14.1.1 - markdown-it-terminal@0.4.0(markdown-it@13.0.2): + markdown-it-terminal@0.4.0(markdown-it@14.1.1): dependencies: ansi-styles: 3.2.1 cardinal: 1.0.0 cli-table: 0.3.11 lodash.merge: 4.6.2 - markdown-it: 13.0.2 + markdown-it: 14.1.1 - markdown-it@13.0.2: - dependencies: - argparse: 2.0.1 - entities: 3.0.1 - linkify-it: 4.0.1 - mdurl: 1.0.1 - uc.micro: 1.0.6 - - markdown-it@14.1.0: + markdown-it@14.1.1: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -17849,21 +17870,21 @@ snapshots: schema-utils@2.7.1: dependencies: '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) schema-utils@4.3.2: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats: 2.1.1 - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.18.0) semver@5.7.2: {} @@ -18376,7 +18397,7 @@ snapshots: table@6.9.0: dependencies: - ajv: 8.17.1 + ajv: 8.18.0 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 @@ -18734,8 +18755,6 @@ snapshots: typical@7.3.0: {} - uc.micro@1.0.6: {} - uc.micro@2.1.0: {} uglify-js@3.19.3: From bf5e0a33e6c3029bde80c4d3882210273e7eb7c0 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 09:22:02 -0700 Subject: [PATCH 018/468] VAULT-39639: SCIM custom User extension including metadata (#12382) (#12562) * add schemas * tests all passing * split out tests * set metadata to empty * remove unneeded functions * Fixes * tests passing * rename from user extension to metadata extension * update userSchemaID to be metadata * add additional test to check duplicate_of_canonical_id can't be overwritten Co-authored-by: miagilepner --- vault/identity_store_conflicts.go | 6 ++++-- vault/identity_store_entities_update.go | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/vault/identity_store_conflicts.go b/vault/identity_store_conflicts.go index 4541476aba..c0a593d59d 100644 --- a/vault/identity_store_conflicts.go +++ b/vault/identity_store_conflicts.go @@ -17,6 +17,8 @@ import ( var errDuplicateIdentityName = errors.New("duplicate identity name") +const duplicateCanonicalIDMetadataKey = "duplicate_of_canonical_id" + // ConflictResolver defines the interface for resolving conflicts between // entities, groups, and aliases. All methods should implement a check for // existing=nil. This is an intentional design choice to allow the caller to @@ -419,7 +421,7 @@ func (r *renameResolver) ResolveEntities(ctx context.Context, existing, duplicat if duplicate.Metadata == nil { duplicate.Metadata = make(map[string]string) } - duplicate.Metadata["duplicate_of_canonical_id"] = existing.ID + duplicate.Metadata[duplicateCanonicalIDMetadataKey] = existing.ID r.logger.Warn("renaming entity with duplicate name", "namespace_id", duplicate.NamespaceID, @@ -453,7 +455,7 @@ func (r *renameResolver) ResolveGroups(ctx context.Context, existing, duplicate if duplicate.Metadata == nil { duplicate.Metadata = make(map[string]string) } - duplicate.Metadata["duplicate_of_canonical_id"] = existing.ID + duplicate.Metadata[duplicateCanonicalIDMetadataKey] = existing.ID r.logger.Warn("renaming group with duplicate name", "namespace_id", duplicate.NamespaceID, "group_id", duplicate.ID, diff --git a/vault/identity_store_entities_update.go b/vault/identity_store_entities_update.go index 275f7f4ba0..a74d04274f 100644 --- a/vault/identity_store_entities_update.go +++ b/vault/identity_store_entities_update.go @@ -139,10 +139,14 @@ func (b *EntityBuilder) WithDisabled(disabled bool) *EntityBuilder { } // WithMetadata sets the metadata for the entity. +// The original entity's value for duplicate_of_canonical_id will be preserved. func (b *EntityBuilder) WithMetadata(metadata map[string]string) *EntityBuilder { if b.err != nil { return b } + if value, ok := b.entity.Metadata[duplicateCanonicalIDMetadataKey]; ok { + metadata[duplicateCanonicalIDMetadataKey] = value + } b.entity.Metadata = metadata return b } From 27339c235af35c88dc06be33d76310ed2451abf6 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 10:13:46 -0700 Subject: [PATCH 019/468] Tried to fix some race issues (#12558) (#12592) * Tried to fix some race issues * Revert "Tried to fix some race issues" This reverts commit c86318361ac3f7bdacd4d6f044bc6ec2cecb7648. * Added billing sub view * Another fix attempt * Another fix Co-authored-by: divyaac --- vault/consumption_billing.go | 5 +++++ vault/consumption_billing_util.go | 13 ++++++------- vault/core.go | 3 +++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/vault/consumption_billing.go b/vault/consumption_billing.go index 931dccb452..ff75d69d95 100644 --- a/vault/consumption_billing.go +++ b/vault/consumption_billing.go @@ -32,6 +32,11 @@ func (c *Core) setupConsumptionBilling(ctx context.Context) error { }, Logger: logger, } + if c.systemBarrierView != nil { + c.consumptionBillingSubView = c.systemBarrierView.SubView(billing.BillingSubPath) + } else { + c.consumptionBilling.Logger.Error("system barrier view is not initialized, consumption billing view is not initialized") + } c.consumptionBillingLock.Unlock() c.postUnsealFuncs = append(c.postUnsealFuncs, func() { diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 33ca3abd39..82c614d870 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -444,14 +444,13 @@ func (c *Core) getStoredTotpKeyCountsLocked(ctx context.Context, localPathPrefix } func (c *Core) GetBillingSubView() (*BarrierView, bool) { - c.mountsLock.RLock() - view := c.systemBarrierView - c.mountsLock.RUnlock() - - if view == nil { - return nil, false + c.consumptionBillingLock.RLock() + defer c.consumptionBillingLock.RUnlock() + if c.consumptionBillingSubView == nil { + // Initialize the consumption billing sub view + c.consumptionBillingSubView = c.systemBarrierView.SubView(billing.BillingSubPath) } - return view.SubView(billing.BillingSubPath), true + return c.consumptionBillingSubView, true } // storeTransitCallCountsLocked must be called with BillingStorageLock held diff --git a/vault/core.go b/vault/core.go index 5e71829661..1f75e4a3fd 100644 --- a/vault/core.go +++ b/vault/core.go @@ -452,6 +452,9 @@ type Core struct { // consumptionBillingLock protects the consumptionBilling struct consumptionBillingLock sync.RWMutex + // consumptionBillingSubView is the sub-view of the system barrier view that is used to store consumption billing metrics + consumptionBillingSubView *BarrierView + // metricsCh is used to stop the metrics streaming metricsCh chan struct{} From a3859d67e37296dc19d3466cb1a2844f527f79b6 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 11:19:47 -0700 Subject: [PATCH 020/468] Backport rework UI CI workflow to partition JS tests into ce/main (#12515) * rework UI CI workflow to partition JS tests (#11967) * add setup-pnpm action * remove reading vault keys from vault server output * update ci workflow to build app and go binary first, then run tests in partitions * fix errant tests * address PR feedback * Apply suggestions from code review Co-authored-by: Ryan Cragun * more feedback changes * restore test-helper.js * restore auth test helpers * check in ui/tests/helpers/vault-keys.js * use v7 of download-artifact action * make test-ui reusable workflow * add status job --------- Co-authored-by: Ryan Cragun * update new UI tests to run CE tests on the CE branch (#12537) --------- Co-authored-by: Matthew Irish <39469+meirish@users.noreply.github.com> Co-authored-by: Ryan Cragun --- .github/actions/setup-pnpm/action.yml | 32 +++ .github/workflows/ci.yml | 133 ++-------- .github/workflows/test-ui.yml | 230 ++++++++++++++++++ ui/package.json | 2 +- ui/scripts/start-vault.js | 54 +--- .../acceptance/reduced-disclosure-test.js | 5 +- ui/tests/acceptance/unseal-test.js | 5 +- ui/tests/helpers/vault-keys.js | 15 ++ ui/tests/unit/helpers/await-test.js | 6 +- 9 files changed, 309 insertions(+), 173 deletions(-) create mode 100644 .github/actions/setup-pnpm/action.yml create mode 100644 .github/workflows/test-ui.yml create mode 100644 ui/tests/helpers/vault-keys.js diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml new file mode 100644 index 0000000000..1a7c2fbdcb --- /dev/null +++ b/.github/actions/setup-pnpm/action.yml @@ -0,0 +1,32 @@ +# Copyright IBM Corp. 2016, 2025 +# SPDX-License-Identifier: BUSL-1.1 + + +# This action will set up Node and then install pnpm dependencies, which might be restored from cache if available. +# In case of a cache miss, pnpm dependencies will be installed and later be stored in the "post step" of actions/cache. +--- +name: Setup pnpm +description: Setup Node and install pnpm +runs: + using: composite + steps: + + - name: Install PNPM + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + with: + run_install: false + package_json_file: './ui/package.json' + + - name: Setup Node Caching + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version-file: './ui/package.json' + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Install UI dependencies + working-directory: ./ui + shell: bash + run: | + pnpm i + pnpm rebuild node-sass diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff3853aff1..2e8cba3b85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: outputs: changed-files: ${{ steps.changed-files.outputs.changed-files }} checkout-ref: ${{ steps.checkout.outputs.ref }} + compute-build: ${{ steps.metadata.outputs.compute-build }} compute-small: ${{ steps.metadata.outputs.compute-small }} compute-test-go: ${{ steps.metadata.outputs.compute-test-go }} compute-test-ui: ${{ steps.metadata.outputs.compute-test-ui }} @@ -38,6 +39,7 @@ jobs: is-fork: ${{ steps.metadata.outputs.is-fork }} labels: ${{ steps.metadata.outputs.labels }} workflow-trigger: ${{ steps.metadata.outputs.workflow-trigger }} + run-ui-tests: ${{ steps.ui-should-run.outputs.run-ui-tests }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Make sure we check out correct ref based on PR labels and such @@ -78,6 +80,12 @@ jobs: # Don't download them on a cache hit during setup, just make sure they're cached before # subsequent workflows are run. no-restore: true + # Run the UI tests if our UI has changed, or a 'ui' label is present, or our workflow trigger + # was triggered by a merge to main or releases/*. + - id: ui-should-run + name: Determine whether or not we should run UI tests + run: | + echo "run-ui-tests=${{ contains(fromJSON(steps.changed-files.outputs.changed-files).groups, 'ui') || steps.metadata.outputs.workflow-trigger == 'push' || contains(steps.metadata.outputs.labels, 'ui') }}" | tee -a "$GITHUB_OUTPUT" test-autopilot-upgrade: name: Run Autopilot upgrade tool @@ -252,120 +260,16 @@ jobs: test-ui: name: Test UI - # Run the UI tests if our UI has changed, or a 'ui' label is present, or our workflow trigger - # was triggered by a merge to main or releases/*. - if: | - contains(fromJSON(needs.setup.outputs.changed-files).groups, 'ui') || - needs.setup.outputs.workflow-trigger == 'push' || - contains(needs.setup.outputs.labels, 'ui') needs: setup - permissions: - id-token: write - contents: read - runs-on: ${{ fromJSON(needs.setup.outputs.compute-test-ui) }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: status - with: - ref: ${{ needs.setup.outputs.checkout-ref }} - - uses: ./.github/actions/set-up-go - with: - github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - - name: Install PNPM - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - with: - run_install: false - package_json_file: './ui/package.json' - - # Setup node.js with caching using the pnpm-lock.yaml file - - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version-file: './ui/package.json' - cache: pnpm - cache-dependency-path: ui/pnpm-lock.yaml - - - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 - - name: ui-dependencies - working-directory: ./ui - run: | - pnpm i - npm rebuild node-sass - - if: needs.setup.outputs.is-ent-repo != 'true' - name: Rebuild font cache on Github hosted runner - # Fix `Fontconfig error: No writable cache directories` error on Github hosted runners - # This seems to have been introduced with this runner image: https://github.com/actions/runner-images/releases/tag/ubuntu22%2F20240818.1 - # Hopefully this will resolve itself at some point with a newer image and we can remove it - run: fc-cache -f -v - - if: needs.setup.outputs.is-ent-repo == 'true' - id: vault-auth - name: Authenticate to Vault - run: vault-auth - - if: needs.setup.outputs.is-ent-repo == 'true' - id: secrets - name: Fetch secrets - uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 - with: - url: ${{ steps.vault-auth.outputs.addr }} - caCertificate: ${{ steps.vault-auth.outputs.ca_certificate }} - token: ${{ steps.vault-auth.outputs.token }} - secrets: | - kv/data/github/hashicorp/vault-enterprise/github-token username-and-token | PRIVATE_REPO_GITHUB_TOKEN; - kv/data/github/hashicorp/vault-enterprise/license license_1 | VAULT_LICENSE; - kv/data/github/${{ github.repository }}/datadog-ci DATADOG_API_KEY; - - if: needs.setup.outputs.is-ent-repo == 'true' - name: Set up Git - run: git config --global url."https://${{ steps.secrets.outputs.PRIVATE_REPO_GITHUB_TOKEN }}@github.com".insteadOf https://github.com - - uses: ./.github/actions/install-tools - - name: build-go-dev - run: | - rm -rf ./pkg - mkdir ./pkg - make prep dev - - name: test-ui - env: - VAULT_LICENSE: ${{ steps.secrets.outputs.VAULT_LICENSE }} - run: | - export PATH="${PWD}/bin:${PATH}" - # Run Ember tests - cd ui - mkdir -p test-results/qunit - pnpm ${{ needs.setup.outputs.is-ent-branch == 'true' && 'test' || 'test:oss' }} - - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: test-results-ui - path: ui/test-results - - name: Prepare datadog-ci - if: (github.repository == 'hashicorp/vault' || github.repository == 'hashicorp/vault-enterprise') && (success() || failure()) - continue-on-error: true - run: | - if type datadog-ci > /dev/null 2>&1; then - exit 0 - fi - # Curl does not always exit 1 if things go wrong. To determine if this is successful - # we'll silence all non-error output and check the results to determine success. - if ! out="$(curl -sSL --fail https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_linux-x64 --output /usr/local/bin/datadog-ci 2>&1)"; then - printf "failed to download datadog-ci: %s" "$out" - fi - if [[ -n "$out" ]]; then - printf "failed to download datadog-ci: %s" "$out" - fi - chmod +x /usr/local/bin/datadog-ci - - name: Upload test results to DataDog - if: success() || failure() - continue-on-error: true - env: - DD_ENV: ci - run: | - if [[ ${{ github.repository }} == 'hashicorp/vault' ]]; then - export DATADOG_API_KEY=${{ secrets.DATADOG_API_KEY }} - fi - datadog-ci junit upload --service "$GITHUB_REPOSITORY" 'ui/test-results/qunit/results.xml' - - if: always() - uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4 - with: - paths: "ui/test-results/qunit/results.xml" - show: "fail" + if: needs.setup.outputs.run-ui-tests == 'true' + uses: ./.github/workflows/test-ui.yml + with: + checkout-ref: ${{ needs.setup.outputs.checkout-ref }} + runs-on: ${{ needs.setup.outputs.compute-build }} + runs-on-small: ${{ needs.setup.outputs.compute-test-ui }} + is-ent-repo: ${{ needs.setup.outputs.is-ent-repo }} + is-ent-branch: ${{ needs.setup.outputs.is-ent-branch }} + secrets: inherit tests-completed: needs: @@ -430,8 +334,7 @@ jobs: secrets: | kv/data/github/${{ github.repository }}/slack feed-vault-ci-official-webhook-url | slackbot-webhook-url; - id: slackbot-webhook-url - run: - echo "slackbot-webhook-url=${{ needs.setup.outputs.is-ent-repo != 'true' && secrets.FEED_VAULT_CI_OFFICIAL_WEBHOOK_URL || steps.secrets.outputs.slackbot-webhook-url }}" >> "$GITHUB_OUTPUT" + run: echo "slackbot-webhook-url=${{ needs.setup.outputs.is-ent-repo != 'true' && secrets.FEED_VAULT_CI_OFFICIAL_WEBHOOK_URL || steps.secrets.outputs.slackbot-webhook-url }}" >> "$GITHUB_OUTPUT" - if: | always() && needs.setup.outputs.workflow-trigger == 'push' && diff --git a/.github/workflows/test-ui.yml b/.github/workflows/test-ui.yml new file mode 100644 index 0000000000..a69b85b4fd --- /dev/null +++ b/.github/workflows/test-ui.yml @@ -0,0 +1,230 @@ +on: + workflow_call: + inputs: + checkout-ref: + description: The ref to use for checkout. + required: false + default: ${{ github.ref }} + type: string + runs-on: + description: An expression indicating which kind of runners to use Go testing jobs. + required: false + type: string + default: '"ubuntu-latest"' + runs-on-small: + description: An expression indicating which kind of runners to use for small computing jobs. + required: false + type: string + default: '"ubuntu-latest"' + is-ent-repo: + description: A boolean indicating whether the repository is an enterprise repository. + required: false + type: string + default: 'false' + is-ent-branch: + description: A boolean indicating whether the repository is an enterprise branch. + required: false + type: string + default: 'false' + +jobs: + test-ui-build-go: + name: Build Vault Binary for UI Tests + permissions: + id-token: write + contents: read + runs-on: ${{ fromJSON(inputs.runs-on) }} + outputs: + ui-go-binary-artifact-id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: status + with: + ref: ${{ inputs.checkout-ref }} + - uses: ./.github/actions/set-up-go + with: + github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} + + - if: inputs.is-ent-repo == 'true' + id: vault-auth + name: Authenticate to Vault + run: vault-auth + - if: inputs.is-ent-repo == 'true' + id: secrets + name: Fetch secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: ${{ steps.vault-auth.outputs.addr }} + caCertificate: ${{ steps.vault-auth.outputs.ca_certificate }} + token: ${{ steps.vault-auth.outputs.token }} + secrets: | + kv/data/github/hashicorp/vault-enterprise/github-token username-and-token | PRIVATE_REPO_GITHUB_TOKEN; + - if: inputs.is-ent-repo == 'true' + name: Set up Git + run: git config --global url."https://${{ steps.secrets.outputs.PRIVATE_REPO_GITHUB_TOKEN }}@github.com".insteadOf https://github.com + - uses: ./.github/actions/install-tools + - name: build-go-dev + run: | + rm -rf ./pkg + mkdir ./pkg + make prep dev + - name: Upload Vault Binary for UI Tests + id: upload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + path: ./bin/vault + name: vault-ui-test-binary + retention-days: 1 + + test-ui-build-js: + name: Build JS for UI Tests + permissions: + id-token: write + contents: read + runs-on: ${{ fromJSON(inputs.runs-on-small) }} + outputs: + ui-js-bundle-artifact-id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + name: status + with: + ref: ${{ inputs.checkout-ref }} + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + - name: Build Ember Test Bundle + working-directory: ./ui + run: pnpm build:jsondiffpatch && pnpm exec ember build --environment=test --output-path=dist + - name: Upload Ember Test Bundle + id: upload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + path: ./ui/dist + name: vault-ui-test-bundle + retention-days: 1 + + test-ui: + name: Run UI Tests + needs: [test-ui-build-go, test-ui-build-js] + permissions: + id-token: write + contents: read + runs-on: ${{ fromJSON(inputs.runs-on-small) }} + strategy: + fail-fast: false + matrix: + ci-index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + name: status + with: + ref: ${{ inputs.checkout-ref }} + - if: inputs.is-ent-repo == 'true' + id: vault-auth + name: Authenticate to Vault + run: vault-auth + - if: inputs.is-ent-repo == 'true' + id: secrets + name: Fetch secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: ${{ steps.vault-auth.outputs.addr }} + caCertificate: ${{ steps.vault-auth.outputs.ca_certificate }} + token: ${{ steps.vault-auth.outputs.token }} + secrets: | + kv/data/github/hashicorp/vault-enterprise/github-token username-and-token | PRIVATE_REPO_GITHUB_TOKEN; + kv/data/github/hashicorp/vault-enterprise/license license_1 | VAULT_LICENSE; + kv/data/github/${{ github.repository }}/datadog-ci DATADOG_API_KEY; + - name: Install Chrome + uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 + with: + chrome-version: stable + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + - name: Download Ember Test Bundle + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + path: ./ui/dist + artifact-ids: ${{ needs.test-ui-build-js.outputs.ui-js-bundle-artifact-id }} + - name: Download Vault Binary + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + path: ./bin + artifact-ids: ${{ needs.test-ui-build-go.outputs.ui-go-binary-artifact-id }} + - name: Make Vault Binary Executable + run: chmod +x ./bin/vault + - name: Set Parallel Count + # hardcoding this to 1 for now because multiple parallelism in UI tests with a vault server casuses test failures due to the shared backend + run: echo "PARALLEL_COUNT=1" >> "$GITHUB_ENV" + - name: Create test-results directory + run: mkdir -p ui/test-results/qunit + - name: Run UI Lint Checks + if: strategy.job-index == 0 + working-directory: ./ui + run: pnpm lint + - name: Run UI Tests + if: strategy.job-index != 0 + env: + VAULT_LICENSE: ${{ steps.secrets.outputs.VAULT_LICENSE }} + working-directory: ./ui + # NOTE: We subtract 1 from the total number of jobs because job-index 0 is the lint job + run: | + pnpm test${{ inputs.is-ent-branch == 'false' && ':oss' || '' }} \ + --load-balance \ + --split=$((${{ strategy.job-total }} - 1)) \ + --partition=${{ strategy.job-index }} \ + --parallel="$PARALLEL_COUNT" \ + --path=dist + - if: always() && strategy.job-index != 0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: test-results-ui-${{ strategy.job-index }} + path: ui/test-results + - name: Prepare datadog-ci + if: always() && startsWith(github.repository, 'hashicorp/vault') && strategy.job-index != 0 + continue-on-error: true + run: | + if type datadog-ci > /dev/null 2>&1; then + exit 0 + fi + # Curl does not always exit 1 if things go wrong. To determine if this is successful + # we'll silence all non-error output and check the results to determine success. + if ! out="$(curl -sSL --fail https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_linux-x64 --output /usr/local/bin/datadog-ci 2>&1)"; then + printf "failed to download datadog-ci: %s" "$out" + fi + if [[ -n "$out" ]]; then + printf "failed to download datadog-ci: %s" "$out" + fi + chmod +x /usr/local/bin/datadog-ci + - name: Upload test results to DataDog + if: (success() || failure()) && strategy.job-index != 0 + continue-on-error: true + env: + DD_ENV: ci + run: | + if [[ ${{ github.repository }} == 'hashicorp/vault' ]]; then + export DATADOG_API_KEY=${{ secrets.DATADOG_API_KEY }} + fi + datadog-ci junit upload --service "$GITHUB_REPOSITORY" 'ui/test-results/qunit/results.xml' + - if: always() && strategy.job-index != 0 + uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4 + with: + paths: "ui/test-results/qunit/results.xml" + show: "fail" + + test-ui-complete: + runs-on: ${{ fromJSON(inputs.runs-on-small) }} + needs: [test-ui-build-go, test-ui-build-js, test-ui] + steps: + - id: status + name: Determine status + run: | + results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}') + if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then + result="failed" + else + result="success" + fi + { + echo "result=${result}" + echo "results=${results}" + } | tee -a "$GITHUB_OUTPUT" diff --git a/ui/package.json b/ui/package.json index 15ef309225..43b3061ddf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -34,7 +34,7 @@ "start2": "pnpm build:jsondiffpatch && ember server --proxy=http://127.0.0.1:8202 --port=4202", "start:chroot": "ember server --proxy=http://127.0.0.1:8300 --port=4300", "lint": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types \"pnpm:lint:js:quiet\" \"pnpm:lint:hbs:quiet\" \"pnpm:lint:types\"", - "test": "pnpm lint && node scripts/start-vault.js", + "test": "node scripts/start-vault.js", "test:enos": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types,enos \"pnpm:lint:js:quiet\" \"pnpm:lint:hbs:quiet\" \"pnpm:lint:types\" \"node scripts/enos-test-ember.js {@}\" --", "test:oss": "pnpm test -f='!enterprise'", "test:ent": "node scripts/start-vault.js -f='enterprise'", diff --git a/ui/scripts/start-vault.js b/ui/scripts/start-vault.js index 0687b9934c..122b630a91 100755 --- a/ui/scripts/start-vault.js +++ b/ui/scripts/start-vault.js @@ -8,26 +8,16 @@ /* eslint-disable no-process-exit */ /* eslint-disable n/no-extraneous-require */ -var readline = require('readline'); const testHelper = require('./test-helper'); -var output = ''; -var unseal, root, written, initError; - -async function processLines(input, eachLine = () => {}) { - const rl = readline.createInterface({ - input, - terminal: true, - }); - for await (const line of rl) { - eachLine(line); - } -} - (async function () { + // ignore first 2 args (node and path) and extract flags to pass to test/exam command + const args = process.argv.slice(2); + // in CI use local vault binary, otherwise assume vault is in PATH + const vaultCommand = process.env.CI ? '../bin/vault' : 'vault'; try { - const vault = testHelper.run( - 'vault', + testHelper.run( + vaultCommand, [ 'server', '-dev', @@ -38,39 +28,7 @@ async function processLines(input, eachLine = () => {}) { ], false ); - processLines(vault.stdout, function (line) { - if (written) { - output = null; - return; - } - output = output + line; - var unsealMatch = output.match(/Unseal Key: (.+)$/m); - if (unsealMatch && !unseal) { - unseal = [unsealMatch[1]]; - } - var rootMatch = output.match(/Root Token: (.+)$/m); - if (rootMatch && !root) { - root = rootMatch[1]; - } - var errorMatch = output.match(/Error initializing core: (.*)$/m); - if (errorMatch) { - initError = errorMatch[1]; - } - if (root && unseal && !written) { - testHelper.writeKeysFile(unseal, root); - written = true; - console.log('VAULT SERVER READY'); - } else if (initError) { - console.log('VAULT SERVER START FAILED'); - console.log( - 'If this is happening, run `export VAULT_LICENSE_PATH=/Users/username/license.hclic` to your valid local vault license filepath, or use OSS Vault' - ); - process.exit(1); - } - }); try { - // ignore first 2 args (node and path) and extract flags to pass to test/exam command - const args = process.argv.slice(2); const withServer = args.includes('--server') || args.includes('-s'); // current issue with headless Chrome where an event listener in Hds::Modal is not triggered resulting in a pending test waiter and timeout // the workaround for now is to run the tests in headless firefox for local runs diff --git a/ui/tests/acceptance/reduced-disclosure-test.js b/ui/tests/acceptance/reduced-disclosure-test.js index c8b6f59f19..8e4ab6396e 100644 --- a/ui/tests/acceptance/reduced-disclosure-test.js +++ b/ui/tests/acceptance/reduced-disclosure-test.js @@ -10,12 +10,11 @@ import { click, currentRouteName, currentURL, fillIn, settled, visit } from '@em import { login, loginNs, logout } from 'vault/tests/helpers/auth/auth-helpers'; import { createTokenCmd, deleteNS, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; import { pollCluster } from 'vault/tests/helpers/poll-cluster'; -import VAULT_KEYS from 'vault/tests/helpers/vault-keys'; import reducedDisclosureHandlers from 'vault/mirage/handlers/reduced-disclosure'; import { overrideResponse } from 'vault/tests/helpers/stubs'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -const { unsealKeys } = VAULT_KEYS; +const unsealKeys = ['unseal-key-1', 'unseal-key-2', 'unseal-key-3']; const SELECTORS = { footerVersion: `[data-test-footer-version]`, }; @@ -24,7 +23,7 @@ module('Acceptance | reduced disclosure test', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - hooks.beforeEach(function () { + hooks.beforeEach(async function () { reducedDisclosureHandlers(this.server); this.unsealCount = 0; this.sealed = false; diff --git a/ui/tests/acceptance/unseal-test.js b/ui/tests/acceptance/unseal-test.js index 32fae0e4cd..3685589766 100644 --- a/ui/tests/acceptance/unseal-test.js +++ b/ui/tests/acceptance/unseal-test.js @@ -8,19 +8,18 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import VAULT_KEYS from 'vault/tests/helpers/vault-keys'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { pollCluster } from 'vault/tests/helpers/poll-cluster'; import { overrideResponse } from 'vault/tests/helpers/stubs'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -const { unsealKeys } = VAULT_KEYS; +const unsealKeys = ['unseal-key-1', 'unseal-key-2', 'unseal-key-3']; module('Acceptance | unseal', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - hooks.beforeEach(function () { + hooks.beforeEach(async function () { this.unsealCount = 0; this.sealed = false; return login(); diff --git a/ui/tests/helpers/vault-keys.js b/ui/tests/helpers/vault-keys.js new file mode 100644 index 0000000000..2e3fb8fd9f --- /dev/null +++ b/ui/tests/helpers/vault-keys.js @@ -0,0 +1,15 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +// DO NOT EDIT +// This file can be automatically generated by the writeKeysFile function in test-helper.js, which is used by +// the enos tests to write the unseal keys and root token to a file that can be imported in the tests. +// +// In cases where `writeKeysFile` is used, it will overwrite this file. + +export default { + unsealKeys: [], + rootToken: 'root', +}; diff --git a/ui/tests/unit/helpers/await-test.js b/ui/tests/unit/helpers/await-test.js index fab8f3e654..f0d1161d52 100644 --- a/ui/tests/unit/helpers/await-test.js +++ b/ui/tests/unit/helpers/await-test.js @@ -6,7 +6,7 @@ import AwaitHelper from 'vault/helpers/await'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { waitUntil } from '@ember/test-helpers'; +import { waitUntil, settled } from '@ember/test-helpers'; import { Promise } from 'rsvp'; import { later } from '@ember/runloop'; import sinon from 'sinon'; @@ -67,12 +67,12 @@ module('Unit | Helpers | await', function (hooks) { }); test('it always returns value from latest promise', async function (assert) { - const promise1 = new Promise((resolve) => later(() => resolve('foo'), 500)); + const promise1 = new Promise((resolve) => later(() => resolve('foo'), 200)); const promise2 = new Promise((resolve) => resolve('bar')); this.helper.compute([promise1]); this.helper.compute([promise2]); // allow first promise time to resolve - await waitUntil(() => later(() => true, 500)); + await settled(); assert.strictEqual(this.spy.returnValues[2], 'bar', 'Latest promise value is returned'); }); }); From 17e72d890491e3bfed80f8091ade03d5875008dd Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 12:57:54 -0700 Subject: [PATCH 021/468] Ignore Basic Constraint Extentions in CSRs for sign-intermediate (#12603) (#12606) * Ignore Basic Constraint Extentions in CSRs for sign-intermediate * add cl * PR feedback Co-authored-by: Steven Clark --- builtin/logical/pki/backend_test.go | 76 +++++++++++++++++++++++++++++ changelog/_12603.txt | 3 ++ sdk/helper/certutil/helpers.go | 4 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 changelog/_12603.txt diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index bf0e9cb70e..ab11c7feb7 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -7899,3 +7899,79 @@ func TestIssuance_ValidityPeriodContainedByCA(t *testing.T) { }) } } + +// TestBackend_SignIntermediate_IgnoresCSR_BasicConstraint verifies that when signing an intermediate CSR, +// with use_csr_values set to true, we ignore the CSR's Basic constraint extension as we do +// not properly support max_path_length. +func TestBackend_SignIntermediate_IgnoresCSR_BasicConstraint(t *testing.T) { + t.Parallel() + b, s := CreateBackendWithStorage(t) + + // Generate root CA with max_path_length of 2 + resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ + "common_name": "Root CA", + "ttl": "180h", + "max_path_length": 2, + }) + requireSuccessNonNilResponse(t, resp, err) + + // Generate private key for CSR + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "failed to generate private key") + + bcExt, err := certutil.CreateBasicConstraintExtension(true, 5) + require.NoError(t, err, "failed to create basic constraint extension") + + // Create CSR template + csrTemplate := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "Intermediate CA", + }, + ExtraExtensions: []pkix.Extension{bcExt}, + } + + // Create the CSR + csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privateKey) + require.NoError(t, err, "failed to create CSR") + + csrPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrDER, + }) + + // Sign the intermediate CSR + signParams := map[string]interface{}{ + "csr": string(csrPEM), + "common_name": "Intermediate CA", + "use_csr_values": true, + "ttl": "87600h", + } + + resp, err = CBWrite(b, s, "root/sign-intermediate", signParams) + require.NoError(t, err, "failed to sign intermediate") + require.NotNil(t, resp, "expected response") + require.NotEmpty(t, resp.Data["certificate"], "expected certificate in response") + + // Parse the signed certificate + certPEM := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(certPEM)) + require.NotNil(t, block, "failed to decode certificate PEM") + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "failed to parse certificate") + + // Verify Basic Constraints extension exists and is critical + hasBasicConstraints := false + for _, ext := range cert.Extensions { + if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { + hasBasicConstraints = true + require.True(t, ext.Critical, "Basic Constraints should be marked as critical") + isCA, maxPathLen, err := certutil.ParseBasicConstraintExtension(ext) + require.NoError(t, err, "failed to parse Basic Constraints extension") + require.True(t, isCA, "Basic Constraints should be marked as CA") + require.Equal(t, 1, maxPathLen, "max_path_length should be set to 1, root of 2-1") + break + } + } + require.True(t, hasBasicConstraints, "certificate should have Basic Constraints extension") +} diff --git a/changelog/_12603.txt b/changelog/_12603.txt new file mode 100644 index 0000000000..07d8f56df4 --- /dev/null +++ b/changelog/_12603.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/pki: The root/sign-intermediate endpoint should not fail when provided a CSR with a basic constraint extension containing isCa set to true +``` diff --git a/sdk/helper/certutil/helpers.go b/sdk/helper/certutil/helpers.go index 0e34dca079..71c0fc5a14 100644 --- a/sdk/helper/certutil/helpers.go +++ b/sdk/helper/certutil/helpers.go @@ -1359,7 +1359,9 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun for _, ext := range data.CSR.Extensions { switch { case ext.Id.Equal(ExtensionBasicConstraintsOID): - if data.Params.UseCSRValues { + // For now only copy basic constraint extensions for non-ca use-cases, + // we don't properly handle max path length constraints otherwise + if data.Params.UseCSRValues && !data.Params.IsCA { isCa, _, err := ParseBasicConstraintExtension(ext) if err != nil { return nil, errutil.UserError{Err: fmt.Sprintf("refusing to accept CSR with invalid Basic Constraints extension: %s", err.Error())} From 12294781c4c9999276fd775c0b88eddd6bc83628 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 13:54:29 -0700 Subject: [PATCH 022/468] [VAULT-42867] Consumption billing use UTC (#12605) (#12610) * make all times utc * one more Co-authored-by: Jenny Deng --- vault/consumption_billing.go | 6 +++--- vault/logical_system_use_case_billing.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vault/consumption_billing.go b/vault/consumption_billing.go index ff75d69d95..f3a073473e 100644 --- a/vault/consumption_billing.go +++ b/vault/consumption_billing.go @@ -80,18 +80,18 @@ func (c *Core) consumptionBillingMetricsWorker(ctx context.Context) { } return d } - endOfMonth := clock.NewTimer(untilNextMonth(clock.Now())) + endOfMonth := clock.NewTimer(untilNextMonth(clock.Now().UTC())) for { select { case <-ticker.C: - if err := c.updateBillingMetrics(ctx, clock.Now()); err != nil { + if err := c.updateBillingMetrics(ctx, clock.Now().UTC()); err != nil { c.logger.Error("error updating billing metrics", "error", err) } case <-ctx.Done(): return case <-endOfMonth.C: // Reset the timer for the next month - currentMonth := clock.Now() + currentMonth := clock.Now().UTC() c.logger.Debug("reached end of month, resetting timer", "currentMonth", currentMonth) previousMonth := timeutil.StartOfPreviousMonth(currentMonth) // On month boundary, we need to flush the current in-memory counts to storage diff --git a/vault/logical_system_use_case_billing.go b/vault/logical_system_use_case_billing.go index 3a463f7821..a39d6a9bc5 100644 --- a/vault/logical_system_use_case_billing.go +++ b/vault/logical_system_use_case_billing.go @@ -65,7 +65,7 @@ func (b *SystemBackend) useCaseConsumptionBillingPaths() []*framework.Path { func (b *SystemBackend) handleUseCaseConsumption(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { refreshData := data.Get("refresh_data").(bool) - currentMonth := time.Now() + currentMonth := time.Now().UTC() previousMonth := timeutil.StartOfPreviousMonth(currentMonth) warnings := make([]string, 0) From d1de170cb6638dd5e90ce52ca8c9fcc7d776f928 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 27 Feb 2026 14:17:41 -0700 Subject: [PATCH 023/468] UI: Adding playwright test for Namespaces & updates (#12577) (#12611) * adding namespace test, removing excess policy and updating other tests for cleanup * test updates * code quality bot calling me out Co-authored-by: Dan Rivera --- ui/e2e/policies/superuser.hcl | 4 ---- ui/e2e/tests/superuser/namespace.spec.ts | 26 ++++++++++++++++++++++++ ui/e2e/tests/superuser/seal.spec.ts | 15 +++++++++++++- ui/e2e/tests/superuser/userpass.spec.ts | 9 +++----- 4 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 ui/e2e/tests/superuser/namespace.spec.ts diff --git a/ui/e2e/policies/superuser.hcl b/ui/e2e/policies/superuser.hcl index 589857650d..2c64d21233 100644 --- a/ui/e2e/policies/superuser.hcl +++ b/ui/e2e/policies/superuser.hcl @@ -3,8 +3,4 @@ path "*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] -} -// needed permissions to be able to seal vault -path "sys/seal" { - capabilities = ["sudo", "update"] } \ No newline at end of file diff --git a/ui/e2e/tests/superuser/namespace.spec.ts b/ui/e2e/tests/superuser/namespace.spec.ts new file mode 100644 index 0000000000..6fa043b476 --- /dev/null +++ b/ui/e2e/tests/superuser/namespace.spec.ts @@ -0,0 +1,26 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('namespace workflow', async ({ page }) => { + await page.goto('dashboard'); + // nav to namespaces and create a new namespace + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'Namespaces' }).click(); + // skip guided tour if it appears + await page.getByRole('button', { name: 'Skip' }).click(); + + await page.getByRole('link', { name: 'Create namespace' }).click(); + await page.getByRole('textbox', { name: 'Path' }).fill('testNamespace'); + await page.getByRole('button', { name: 'Save' }).click(); + + // click on the namespace picker in the top navbar and switch to the new namespace + await page.getByRole('button', { name: 'root' }).click(); + await page.getByRole('option', { name: 'testNamespace' }).click(); + + // verify that we are switched into the new namespace by checking for the namespace name in the header + await expect(page.locator('#app-main-content').getByText('testNamespace')).toBeVisible(); +}); diff --git a/ui/e2e/tests/superuser/seal.spec.ts b/ui/e2e/tests/superuser/seal.spec.ts index 142c511381..aa16ed86cd 100644 --- a/ui/e2e/tests/superuser/seal.spec.ts +++ b/ui/e2e/tests/superuser/seal.spec.ts @@ -4,12 +4,25 @@ */ import { test, expect } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; -test('sealing workflow', async ({ page }) => { +const keysPath = path.resolve(__dirname, '../../tmp/superuser-keys.json'); + +test('sealing/unsealing workflow', async ({ page }) => { await page.goto('dashboard'); await page.getByRole('link', { name: 'Resilience and recovery' }).click(); await page.getByRole('link', { name: 'Seal Vault' }).click(); await page.getByRole('button', { name: 'Seal' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await expect(page.getByText('Vault is sealed')).toBeVisible(); + + // unseal vault for sequential tests + const { keys, root_token } = JSON.parse(fs.readFileSync(keysPath, 'utf-8')); + await page.getByRole('textbox', { name: 'Unseal Key Portion' }).fill(keys[0]); + await page.getByRole('button', { name: 'Unseal' }).click(); + await page.getByRole('textbox', { name: 'Token' }).fill(root_token); + await page.getByRole('button', { name: 'Sign in' }).click(); + await expect(page.getByRole('button', { name: 'root' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible(); }); diff --git a/ui/e2e/tests/superuser/userpass.spec.ts b/ui/e2e/tests/superuser/userpass.spec.ts index a954a4d2ff..efefe5f639 100644 --- a/ui/e2e/tests/superuser/userpass.spec.ts +++ b/ui/e2e/tests/superuser/userpass.spec.ts @@ -11,12 +11,9 @@ test('userpass workflow', async ({ page }) => { await page.getByRole('link', { name: 'Access control' }).click(); await page.getByRole('link', { name: 'Authentication methods' }).click(); - // if intro page is visible, click enable method there otherwise click enable method in toolbar - if (await page.getByRole('link', { name: 'Enable a new method' }).isVisible()) { - await page.getByRole('link', { name: 'Enable a new method' }).click(); - } else { - await page.getByRole('link', { name: 'Enable new method' }).click(); - } + // dismiss intro page and click enable method in toolbar + await page.getByRole('button', { name: 'Skip' }).click(); + await page.getByRole('link', { name: 'Enable new method' }).click(); // enable userpass auth method await page.getByLabel('Userpass - enabled engine type').click(); From 36626c328d8ce14cf7caead301f6ac0befb6eee5 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 2 Mar 2026 06:56:48 -0700 Subject: [PATCH 024/468] =?UTF-8?q?Bugfix,=20updating=20certificates=20wit?= =?UTF-8?q?hout=20server=5Fflag=20from=20enrollment=20met=E2=80=A6=20(#126?= =?UTF-8?q?09)=20(#12612)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bugfix, updating certificates without server_flag from enrollment methods. * Add changelog. Co-authored-by: Kit Haines --- changelog/_12609.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/_12609.txt diff --git a/changelog/_12609.txt b/changelog/_12609.txt new file mode 100644 index 0000000000..3f544285b9 --- /dev/null +++ b/changelog/_12609.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets (pki): Allow issuance of certificates without the server_flag key usage from SCEP, EST and CMPV2 protocols. +``` \ No newline at end of file From 7933fc763eb56b325beab1aace98eb1ed1773933 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 2 Mar 2026 13:36:30 -0700 Subject: [PATCH 025/468] hooks(pre-push): add a better explanation when git fails to write pre-push commit info to STDIN (#12635) (#12638) Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .hooks/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hooks/pre-push b/.hooks/pre-push index 2c9faf51ab..3e49740464 100755 --- a/.hooks/pre-push +++ b/.hooks/pre-push @@ -131,7 +131,7 @@ main() { fi done - fail "git pre-push hook failed!" "git did not write expected commit information to STDIN! Please reach out to #team-vault-automation for help!" + fail "git pre-push hook failed!" "git did not write expected commit information to STDIN! This is likely because git could not push one-or-more refs to the remote repository. Did you change history with a rebase and need to force push? If that does not resolve the issue please reach out to #team-vault-automation for help!" } # Call the main function. We currently only care about the URL so we'll pass in From 3d420fec98178bd943e4cd38122b26d188fd9b85 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 2 Mar 2026 15:48:49 -0700 Subject: [PATCH 026/468] actions: bump actions to latest version (#12630) Bump our action version pins to the latest versions. - actions/checkout v6.0.1 => v6.0.2 Tag handling improvements - actions/download-artifact v7.0.0 => v8.0.0 Supports automatic detection of unzipping based on Content-Type Enforces digest checking Uses ES modules - actions/setup-go v6.2.0 => v6.3.0 Uses go.mod for default module caching (which we don't use) Fixes to download URL - actions/upload-artifact v6.0.0 => v7.0.0 Supports disabling automatic archiving Uses ES modules - aws-actions/configure-aws-credentials v5.1.1 => v6.0.0 Uses Node 24 - browser-actions/setup-chrome v2.1.0 => v2.1.1 Bug fix for Node runtime version - docker/build-push-action v6.18.0 => v6.19.2 Internal dep updates and auth support for different Github servers. - hashicorp/setup-terraform v3.1.2 => v4.0.0 Uses Node 24 Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .github/actions/build-vault/action.yml | 10 +++++----- .github/actions/set-up-go/action.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/enos-lint.yml | 2 +- .github/workflows/mend-pr-scan.yml | 2 +- .github/workflows/plugin-update-check.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/test-ci-bootstrap.yml | 4 ++-- .github/workflows/test-ci-cleanup.yml | 6 +++--- .github/workflows/test-enos-scenario-ui.yml | 6 +++--- .github/workflows/test-go.yml | 14 +++++++------- .../workflows/test-run-acc-tests-for-path.yml | 2 +- .../test-run-enos-scenario-containers.yml | 4 ++-- .../workflows/test-run-enos-scenario-matrix.yml | 8 ++++---- .github/workflows/test-run-enos-scenario.yml | 8 ++++---- .github/workflows/test-ui.yml | 16 ++++++++-------- 17 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/actions/build-vault/action.yml b/.github/actions/build-vault/action.yml index 616631b282..6639e7d635 100644 --- a/.github/actions/build-vault/action.yml +++ b/.github/actions/build-vault/action.yml @@ -143,7 +143,7 @@ runs: if: inputs.cgo-enabled == '1' id: build-push-action-attempt-1 continue-on-error: true # we will retry this if it fails - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false with: @@ -165,7 +165,7 @@ runs: id: build-push-action-attempt-2 continue-on-error: false if: inputs.cgo-enabled == '1' && steps.build-push-action-attempt-1.outcome != 'success' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false with: @@ -218,7 +218,7 @@ runs: BUNDLE_PATH: out/${{ steps.metadata.outputs.artifact-basename }}.zip shell: bash run: make ci-bundle - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.metadata.outputs.artifact-basename }}.zip path: out/${{ steps.metadata.outputs.artifact-basename }}.zip @@ -250,13 +250,13 @@ runs: echo "deb-files=$(basename out/*.deb)" } | tee -a "$GITHUB_OUTPUT" - if: inputs.create-packages == 'true' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.package-files.outputs.rpm-files }} path: out/${{ steps.package-files.outputs.rpm-files }} if-no-files-found: error - if: inputs.create-packages == 'true' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.package-files.outputs.deb-files }} path: out/${{ steps.package-files.outputs.deb-files }} diff --git a/.github/actions/set-up-go/action.yml b/.github/actions/set-up-go/action.yml index f30bc3ea7c..323ca16427 100644 --- a/.github/actions/set-up-go/action.yml +++ b/.github/actions/set-up-go/action.yml @@ -40,7 +40,7 @@ runs: else echo "go-version=${{ inputs.go-version }}" | tee -a "$GITHUB_OUTPUT" fi - - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ steps.go-version.outputs.go-version }} cache: false # We use our own caching strategy diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6808cbc484..898cbf2b25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -617,7 +617,7 @@ jobs: with: version: ${{ needs.setup.outputs.vault-version-metadata }} product: ${{ needs.setup.outputs.vault-binary-name }} - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: steps.generate-metadata-file.outcome == 'success' # upload our metadata if we created it with: name: metadata.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e8cba3b85..31f64c2432 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -390,7 +390,7 @@ jobs: # to secrets. - if: ${{ needs.setup.outputs.is-fork == 'false' }} name: Download failure summaries - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: failure-summary-*.md path: failure-summaries diff --git a/.github/workflows/enos-lint.yml b/.github/workflows/enos-lint.yml index 4dad11e45d..79176a8d70 100644 --- a/.github/workflows/enos-lint.yml +++ b/.github/workflows/enos-lint.yml @@ -42,7 +42,7 @@ jobs: no-restore: true no-save: true - uses: ./.github/actions/install-tools - - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: terraform_wrapper: false - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 diff --git a/.github/workflows/mend-pr-scan.yml b/.github/workflows/mend-pr-scan.yml index 7277ff06fe..21f8f55ff2 100644 --- a/.github/workflows/mend-pr-scan.yml +++ b/.github/workflows/mend-pr-scan.yml @@ -34,7 +34,7 @@ jobs: psirt-id: "PSIRT_PRD0014264" - name: Upload Scan Artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: always() with: name: mend-scan-results-pr-${{ github.event.number }} diff --git a/.github/workflows/plugin-update-check.yml b/.github/workflows/plugin-update-check.yml index 3ed737b900..ff48747371 100644 --- a/.github/workflows/plugin-update-check.yml +++ b/.github/workflows/plugin-update-check.yml @@ -29,7 +29,7 @@ jobs: # https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: cache: false # save cache space for vault builds: https://github.com/hashicorp/vault/pull/21764 go-version-file: .go-version diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index b4e2043f21..61cbfd50d6 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: cache: false # save cache space for vault builds: https://github.com/hashicorp/vault/pull/21764 go-version: 'stable' diff --git a/.github/workflows/test-ci-bootstrap.yml b/.github/workflows/test-ci-bootstrap.yml index 88822e2872..5133b728b2 100644 --- a/.github/workflows/test-ci-bootstrap.yml +++ b/.github/workflows/test-ci-bootstrap.yml @@ -32,11 +32,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Terraform - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: terraform_wrapper: false - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} diff --git a/.github/workflows/test-ci-cleanup.yml b/.github/workflows/test-ci-cleanup.yml index d60dd11543..15de73d871 100644 --- a/.github/workflows/test-ci-cleanup.yml +++ b/.github/workflows/test-ci-cleanup.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Configure AWS credentials id: aws-configure - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} @@ -43,7 +43,7 @@ jobs: steps: - name: Configure AWS credentials id: aws-configure - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} @@ -80,7 +80,7 @@ jobs: steps: - name: Configure AWS credentials id: aws-configure - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} diff --git a/.github/workflows/test-enos-scenario-ui.yml b/.github/workflows/test-enos-scenario-ui.yml index a43b20d1fb..6c0acc7185 100644 --- a/.github/workflows/test-enos-scenario-ui.yml +++ b/.github/workflows/test-enos-scenario-ui.yml @@ -98,7 +98,7 @@ jobs: with: package_json_file: './ui/package.json' - name: Set Up Terraform - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} terraform_wrapper: false @@ -120,12 +120,12 @@ jobs: sudo apt install -y libnss3-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxss-dev libasound2 - name: Install Chrome if: steps.chrome-check.outputs.chrome-version == 'not-installed' - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 + uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1 - name: Installed Chrome Version run: | echo "Installed Chrome Version = [$(chrome --version 2> /dev/null || google-chrome --version 2> /dev/null || google-chrome-stable --version 2> /dev/null)]" - name: Configure AWS credentials from Test account - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index e785a4b16d..f69b163562 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -473,14 +473,14 @@ jobs: tar -cvf '${{ steps.metadata.outputs.go-test-log-archive-name }}' -C "${{ steps.metadata.outputs.go-test-log-dir }}" . - if: success() || failure() name: Upload test logs archives - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.metadata.outputs.go-test-log-archive-name }} path: ${{ steps.metadata.outputs.go-test-log-archive-name }} retention-days: 7 - if: success() || failure() name: Upload test results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.metadata.outputs.go-test-results-upload-key }} path: | @@ -518,7 +518,7 @@ jobs: echo "data-race-result=${result}" | tee -a "$GITHUB_OUTPUT" - if: (success() || failure()) && steps.data-race-check.outputs.data-race-result == 'failure' name: Upload data race detector failure log - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.metadata.outputs.data-race-log-upload-key }} path: ${{ steps.metadata.outputs.go-test-dir }}/${{ steps.metadata.outputs.data-race-log-file }} @@ -530,7 +530,7 @@ jobs: # so we have to fetch it from the API - if: success() || failure() name: Fetch job logs URL - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 continue-on-error: true with: retries: 3 @@ -592,7 +592,7 @@ jobs: >> '${{ steps.metadata.outputs.failure-summary-file-name }}' - if: success() || failure() name: Upload failure summary - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.metadata.outputs.failure-summary-file-name }} path: ${{ steps.metadata.outputs.failure-summary-file-name }} @@ -609,7 +609,7 @@ jobs: data-race-output: ${{ steps.status.outputs.data-race-output }} data-race-result: ${{ steps.status.outputs.data-race-result }} steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: ${{ needs.test-go.outputs.data-race-log-download-pattern }} path: data-race-logs @@ -659,7 +659,7 @@ jobs: ${{ inputs.test-timing-cache-key }}- go-test-timing- - if: ${{ ! cancelled() && needs.test-go.result == 'success' && inputs.test-timing-cache-save }} - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ${{ needs.test-matrix.outputs.go-test-dir }} pattern: ${{ needs.test-go.outputs.go-test-results-download-pattern }} diff --git a/.github/workflows/test-run-acc-tests-for-path.yml b/.github/workflows/test-run-acc-tests-for-path.yml index 13f0295746..858b636fa8 100644 --- a/.github/workflows/test-run-acc-tests-for-path.yml +++ b/.github/workflows/test-run-acc-tests-for-path.yml @@ -25,7 +25,7 @@ jobs: with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - run: go test -v ./${{ inputs.path }}/... 2>&1 | tee ${{ inputs.name }}.txt - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ inputs.name }}-output path: ${{ inputs.name }}.txt diff --git a/.github/workflows/test-run-enos-scenario-containers.yml b/.github/workflows/test-run-enos-scenario-containers.yml index 4e8dd5fc21..04224e003c 100644 --- a/.github/workflows/test-run-enos-scenario-containers.yml +++ b/.github/workflows/test-run-enos-scenario-containers.yml @@ -82,7 +82,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: # the Terraform wrapper will break Terraform execution in Enos because # it changes the output to text when we expect it to be JSON. @@ -92,7 +92,7 @@ jobs: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Download Docker Image id: download - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: ${{ inputs.build-artifact-name }} path: ./enos/support/downloads diff --git a/.github/workflows/test-run-enos-scenario-matrix.yml b/.github/workflows/test-run-enos-scenario-matrix.yml index f3ebc3517b..5f6dd24d83 100644 --- a/.github/workflows/test-run-enos-scenario-matrix.yml +++ b/.github/workflows/test-run-enos-scenario-matrix.yml @@ -200,13 +200,13 @@ jobs: echo 'ENOS_VAR_verify_ldap_secrets_engine=false' echo 'ENOS_VAR_verify_log_secrets=true' } | tee -a "$GITHUB_ENV" - - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: # the Terraform wrapper will break Terraform execution in Enos because # it changes the output to text when we expect it to be JSON. terraform_wrapper: false - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ steps.secrets.outputs.aws-access-key-id }} aws-secret-access-key: ${{ steps.secrets.outputs.aws-secret-access-key }} @@ -232,7 +232,7 @@ jobs: du -h "./enos/support/private_key.pem" echo "debug_data_artifact_name=enos-debug-data_$(echo "${{ matrix.scenario }}" | sed -e 's/ /_/g' | sed -e 's/:/=/g')" >> "$GITHUB_OUTPUT" - if: contains(inputs.sample-name, 'build') - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: ${{ inputs.build-artifact-name }} path: ./enos/support/downloads @@ -257,7 +257,7 @@ jobs: run: enos scenario launch --timeout 45m0s --chdir ./enos ${{ matrix.scenario.id.filter }} - name: Upload Debug Data if: failure() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: # The name of the artifact is the same as the matrix scenario name with the spaces replaced with underscores and colons replaced by equals. name: ${{ steps.prepare_scenario.outputs.debug_data_artifact_name }} diff --git a/.github/workflows/test-run-enos-scenario.yml b/.github/workflows/test-run-enos-scenario.yml index 5d8949d5bc..b8bc1e66b5 100644 --- a/.github/workflows/test-run-enos-scenario.yml +++ b/.github/workflows/test-run-enos-scenario.yml @@ -77,13 +77,13 @@ jobs: uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: package_json_file: './ui/package.json' - - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: # the Terraform wrapper will break Terraform execution in Enos because # it changes the output to text when we expect it to be JSON. terraform_wrapper: false - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI_09042025 }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI_09042025 }} @@ -114,13 +114,13 @@ jobs: run: | bash -x ./scripts/gha_enos_logs.sh "${{ steps.scenario-deps.outputs.logsdir }}" "${{ inputs.scenario }}" "${{ inputs.distro }}" "${{ inputs.artifact-type }}" 2>/dev/null find "${{ steps.scenario-deps.outputs.logsdir }}" -maxdepth 0 -empty -exec rmdir {} \; - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ always() }} with: name: enos-scenario-logs path: ${{ steps.scenario-deps.outputs.logsdir }} retention-days: 1 - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ always() }} with: name: enos-debug-data-logs diff --git a/.github/workflows/test-ui.yml b/.github/workflows/test-ui.yml index a69b85b4fd..f68e647e28 100644 --- a/.github/workflows/test-ui.yml +++ b/.github/workflows/test-ui.yml @@ -70,7 +70,7 @@ jobs: make prep dev - name: Upload Vault Binary for UI Tests id: upload - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: path: ./bin/vault name: vault-ui-test-binary @@ -85,7 +85,7 @@ jobs: outputs: ui-js-bundle-artifact-id: ${{ steps.upload.outputs.artifact-id }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 name: status with: ref: ${{ inputs.checkout-ref }} @@ -96,7 +96,7 @@ jobs: run: pnpm build:jsondiffpatch && pnpm exec ember build --environment=test --output-path=dist - name: Upload Ember Test Bundle id: upload - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: path: ./ui/dist name: vault-ui-test-bundle @@ -114,7 +114,7 @@ jobs: matrix: ci-index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 name: status with: ref: ${{ inputs.checkout-ref }} @@ -135,18 +135,18 @@ jobs: kv/data/github/hashicorp/vault-enterprise/license license_1 | VAULT_LICENSE; kv/data/github/${{ github.repository }}/datadog-ci DATADOG_API_KEY; - name: Install Chrome - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 + uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1 with: chrome-version: stable - name: Setup pnpm uses: ./.github/actions/setup-pnpm - name: Download Ember Test Bundle - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ./ui/dist artifact-ids: ${{ needs.test-ui-build-js.outputs.ui-js-bundle-artifact-id }} - name: Download Vault Binary - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ./bin artifact-ids: ${{ needs.test-ui-build-go.outputs.ui-go-binary-artifact-id }} @@ -175,7 +175,7 @@ jobs: --parallel="$PARALLEL_COUNT" \ --path=dist - if: always() && strategy.job-index != 0 - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: test-results-ui-${{ strategy.job-index }} path: ui/test-results From ba786ab7594783207d1fb0634735244dfcef9db0 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 2 Mar 2026 17:20:36 -0700 Subject: [PATCH 027/468] Add schedule to hcp runs (#12636) (#12655) * Add schedule to hcp runs * formatting Co-authored-by: Luis (LT) Carbonell --- .github/workflows/build.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 898cbf2b25..15d10c4da3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -474,10 +474,16 @@ jobs: # been configured with the correct label. if: | needs.setup.outputs.is-ent-branch == 'true' && - needs.setup.outputs.workflow-trigger == 'pull_request' && - needs.artifacts-ent.result == 'success' && - needs.hcp-image.result == 'success' && - contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') + needs.setup.outputs.workflow-trigger == 'schedule' || + ( needs.setup.outputs.workflow-trigger == 'pull_request' && + needs.artifacts-ent.result == 'success' && + needs.hcp-image.result == 'success' && + contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') + ) || + ( needs.setup.outputs.workflow-trigger == 'schedule' && + needs.artifacts-ent.result == 'success' && + needs.hcp-image.result == 'success' + ) needs: - setup - artifacts-ent From 49b5cf988af5a800ab488cdd8e9d524ebfda0f5b Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 3 Mar 2026 08:46:40 -0700 Subject: [PATCH 028/468] Allow a caller to override the logger within the dnstest helper (#12647) (#12670) Co-authored-by: Steven Clark --- sdk/helper/testhelpers/dnstest/server.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/helper/testhelpers/dnstest/server.go b/sdk/helper/testhelpers/dnstest/server.go index 68472c3fe3..c1323ef687 100644 --- a/sdk/helper/testhelpers/dnstest/server.go +++ b/sdk/helper/testhelpers/dnstest/server.go @@ -41,13 +41,17 @@ func SetupResolver(t *testing.T, domain string) *TestServer { } func SetupResolverOnNetwork(t *testing.T, domain string, network string) *TestServer { + return SetupResolverOnNetworkWithLogger(t, domain, network, hclog.NewNullLogger()) +} + +func SetupResolverOnNetworkWithLogger(t *testing.T, domain string, network string, logger hclog.Logger) *TestServer { var ts TestServer ts.t = t ts.ctx = context.Background() ts.domains = []string{domain} ts.records = map[string]map[string][]string{} ts.network = network - ts.log = hclog.L() + ts.log = logger ts.setupRunner(domain, network) ts.startContainer(network) @@ -65,9 +69,9 @@ func (ts *TestServer) setupRunner(domain string, network string) { NetworkName: network, Ports: []string{"53/udp"}, // DNS container logging was disabled to reduce content within CI logs. - //LogConsumer: func(s string) { - // ts.log.Info(s) - //}, + LogConsumer: func(s string) { + ts.log.Trace(s) + }, }) require.NoError(ts.t, err) } From 0276dbb83e2daad5e6de23b0a4584d92338e4306 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 3 Mar 2026 10:21:38 -0700 Subject: [PATCH 029/468] Vault 42938 kv collection perf improvements (#12648) (#12662) * KV Collections Edited kv secrets Small changes Make sure tests don't fail Some more small edits Fix build errors * Further optimizations Comment fix * Temp * Revert "Temp" This reverts commit 874bbded2998271e7b78f78f952c5a55a1c17945. * Added a comment Added comment Fix Co-authored-by: divyaac --- vault/consumption_billing_util.go | 14 ++------ vault/core_metrics.go | 59 ++++++++++++++++++------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 82c614d870..64c05b32b7 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -191,22 +191,14 @@ func (c *Core) UpdateMaxKvCounts(ctx context.Context, localPathPrefix string, cu local := localPathPrefix == billing.LocalPrefix - // Get the current count of kv version 1 secrets - currentKvCounts, err := c.GetKvUsageMetricsByNamespace(ctx, "1", "", local, !local, false) + // Get the current count of all KV secrets + currentKvCounts, err := c.GetKvUsageMetricsByNamespace(ctx, "0", "", local, !local, false) if err != nil { - c.logger.Error("error getting count of kv version 1 secrets", "error", err) + c.logger.Error("error getting count of all KV secrets", "error", err) return 0, err } totalKvCounts := getTotalSecretsAcrossAllNamespaces(currentKvCounts) - // Get the current count of kv version 2 secrets - currentKvCounts, err = c.GetKvUsageMetricsByNamespace(ctx, "2", "", local, !local, false) - if err != nil { - c.logger.Error("error getting current count of kv version 2 secrets", "error", err) - return 0, err - } - totalKvCounts += getTotalSecretsAcrossAllNamespaces(currentKvCounts) - // Get the stored max kv counts maxKvCounts, err := c.getStoredMaxKvCountsLocked(ctx, localPathPrefix, currentMonth) if err != nil { diff --git a/vault/core_metrics.go b/vault/core_metrics.go index 4a3c0f468d..048791cf8b 100644 --- a/vault/core_metrics.go +++ b/vault/core_metrics.go @@ -424,7 +424,7 @@ type kvMount struct { // or externally compiled KV mounts that are still of type KV. // It's a simple function that's slightly reimplemented to prevent needing a context // in findKvMounts. -func (c *Core) findOfficialKvMounts(ctx context.Context) []*kvMount { +func (c *Core) findOfficialKvMounts(ctx context.Context, includeLocal, includeReplicated bool, kvVersion string) []*kvMount { mounts := make([]*kvMount, 0) c.mountsLock.RLock() @@ -438,12 +438,23 @@ func (c *Core) findOfficialKvMounts(ctx context.Context) []*kvMount { } for _, entry := range c.mounts.Entries { + if !includeLocal && entry.Local { + continue + } + if !includeReplicated && !entry.Local { + continue + } + if entry.Type == pluginconsts.SecretEngineKV || entry.Type == pluginconsts.SecretEngineGeneric { version, ok := entry.Options["version"] if !ok || version == "" { version = "1" } + if kvVersion != "0" && version != kvVersion { + continue + } + pluginName := getAdjustedPluginType(entry) if pluginName == "" { continue @@ -473,7 +484,8 @@ func (c *Core) findOfficialKvMounts(ctx context.Context) []*kvMount { return mounts } -func (c *Core) findKvMounts() []*kvMount { +// findKvMounts finds all KV mounts. If kvVersion is "0", all versions of KV are included. +func (c *Core) findKvMounts(includeLocal, includeReplicated bool, kvVersion string) []*kvMount { mounts := make([]*kvMount, 0) c.mountsLock.RLock() @@ -487,11 +499,24 @@ func (c *Core) findKvMounts() []*kvMount { } for _, entry := range c.mounts.Entries { + if !includeLocal && entry.Local { + continue + } + if !includeReplicated && !entry.Local { + continue + } + if entry.Type == pluginconsts.SecretEngineKV || entry.Type == pluginconsts.SecretEngineGeneric { version, ok := entry.Options["version"] if !ok || version == "" { version = "1" } + + // If kvVersion is "0", all versions of KV are included. + if kvVersion != "0" && version != kvVersion { + continue + } + mounts = append(mounts, &kvMount{ Namespace: entry.namespace, MountPoint: entry.Path, @@ -658,7 +683,7 @@ func (c *Core) walkKvMountSecrets(ctx context.Context, m *kvMount) { func (c *Core) kvSecretGaugeCollector(ctx context.Context) ([]metricsutil.GaugeLabelValues, error) { // Find all KV mounts - mounts := c.findKvMounts() + mounts := c.findKvMounts(true, true, "0") results := make([]metricsutil.GaugeLabelValues, len(mounts)) // Use a root namespace, so include namespace path @@ -1007,32 +1032,18 @@ func (c *Core) GetKvUsageMetrics(ctx context.Context, kvVersion string) (map[str // GetKvUsageMetricsByNamespace returns a map of namespace paths to KV secret counts within a specific namespace. func (c *Core) GetKvUsageMetricsByNamespace(ctx context.Context, kvVersion string, nsPath string, includeLocal bool, includeReplicated bool, includeUnofficial bool) (map[string]int, error) { - mounts := c.findKvMounts() + if kvVersion != "0" && kvVersion != "1" && kvVersion != "2" { + return nil, fmt.Errorf("kv version %s not supported, must be 0, 1, or 2", kvVersion) + } + var mounts []*kvMount if !includeUnofficial { - mounts = c.findOfficialKvMounts(ctx) + mounts = c.findOfficialKvMounts(ctx, includeLocal, includeReplicated, kvVersion) + } else { + mounts = c.findKvMounts(includeLocal, includeReplicated, kvVersion) } results := make(map[string]int) - if kvVersion == "1" || kvVersion == "2" { - var newMounts []*kvMount - for _, mount := range mounts { - if mount.Version == kvVersion { - newMounts = append(newMounts, mount) - } - } - mounts = newMounts - } else if kvVersion != "0" { - return results, fmt.Errorf("kv version %s not supported, must be 0, 1, or 2", kvVersion) - } - for _, m := range mounts { - if !includeLocal && m.Local { - continue - } - if !includeReplicated && !m.Local { - continue - } - if nsPath != "" && !strings.HasPrefix(m.Namespace.Path, nsPath) { continue } From d160737ced45db8e6a7acd02292160d69985236a Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 3 Mar 2026 10:31:03 -0700 Subject: [PATCH 030/468] [VAULT-42862] upgrade cloudflare/circl => v1.6.3 to partially resolve CVE-2026-1229 (#12567) (#12651) Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229. We had several transient dependencies that depend on various versions of `circl` that also needed to be updated in order to resolve the latest version everywhere. - github.com/ProtonMail/go-crypto v1.2.0 => v1.3.0 - github.com/google/go-github v17 => v83/v83.0.0 - github.com/google/go-github/v81 => v83/v83.0.0 Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- builtin/credential/github/backend.go | 2 +- builtin/credential/github/path_config.go | 2 +- builtin/credential/github/path_login.go | 2 +- changelog/_12567.txt | 6 ++ go.mod | 8 +- go.sum | 17 +++-- tools/pipeline/go.mod | 4 +- tools/pipeline/go.sum | 10 +-- tools/pipeline/internal/cmd/github.go | 2 +- .../internal/pkg/changed/config_test.go | 2 +- tools/pipeline/internal/pkg/changed/file.go | 2 +- .../internal/pkg/config/config_test.go | 2 +- .../internal/pkg/github/add_assignees.go | 2 +- .../pkg/github/check_commit_status.go | 2 +- .../pkg/github/check_go_mod_diff_request.go | 2 +- .../close_copied_origin_pull_request.go | 2 +- tools/pipeline/internal/pkg/github/commit.go | 2 +- .../internal/pkg/github/copy_pull_request.go | 2 +- .../pkg/github/copy_pull_request_test.go | 2 +- .../internal/pkg/github/create_backport.go | 17 ++++- .../pkg/github/create_backport_test.go | 75 ++++++++++++++++++- .../pkg/github/find_workflow_artifact.go | 2 +- tools/pipeline/internal/pkg/github/issue.go | 2 +- tools/pipeline/internal/pkg/github/labels.go | 54 +++++++++++++ .../internal/pkg/github/list_changed_files.go | 2 +- .../pkg/github/list_commit_statuses.go | 2 +- .../internal/pkg/github/list_workflow_runs.go | 2 +- .../internal/pkg/github/pull_request.go | 2 +- .../pkg/github/sync_branch_request.go | 2 +- .../internal/pkg/github/templates_test.go | 2 +- .../pipeline/internal/pkg/github/workflows.go | 2 +- 31 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 changelog/_12567.txt create mode 100644 tools/pipeline/internal/pkg/github/labels.go diff --git a/builtin/credential/github/backend.go b/builtin/credential/github/backend.go index 7f8026e680..e584e9f858 100644 --- a/builtin/credential/github/backend.go +++ b/builtin/credential/github/backend.go @@ -7,7 +7,7 @@ import ( "context" "net/url" - "github.com/google/go-github/github" + "github.com/google/go-github/v83/github" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" diff --git a/builtin/credential/github/path_config.go b/builtin/credential/github/path_config.go index f871a2ff8f..e9c6da9538 100644 --- a/builtin/credential/github/path_config.go +++ b/builtin/credential/github/path_config.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/google/go-github/github" + "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/tokenutil" "github.com/hashicorp/vault/sdk/logical" diff --git a/builtin/credential/github/path_login.go b/builtin/credential/github/path_login.go index b01967bd26..9e343f322e 100644 --- a/builtin/credential/github/path_login.go +++ b/builtin/credential/github/path_login.go @@ -9,7 +9,7 @@ import ( "fmt" "net/url" - "github.com/google/go-github/github" + "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/cidrutil" "github.com/hashicorp/vault/sdk/helper/policyutil" diff --git a/changelog/_12567.txt b/changelog/_12567.txt new file mode 100644 index 0000000000..debb1f87ff --- /dev/null +++ b/changelog/_12567.txt @@ -0,0 +1,6 @@ +```release-note:security +vault/sdk: Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +``` +```release-note:security +Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +``` diff --git a/go.mod b/go.mod index 5f4bb33724..839b7ad268 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/Azure/azure-storage-blob-go v0.15.0 github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/adal v0.9.24 - github.com/ProtonMail/go-crypto v1.2.0 + github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/gopenpgp/v3 v3.2.1 github.com/SAP/go-hdb v1.10.1 github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a @@ -79,7 +79,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/google/certificate-transparency-go v1.3.2 github.com/google/go-cmp v0.7.0 - github.com/google/go-github v17.0.0+incompatible + github.com/google/go-github/v83 v83.0.0 github.com/google/go-metrics-stackdriver v0.2.0 github.com/hashicorp/cap v0.11.0 github.com/hashicorp/cap/ldap v0.0.0-20250911140431-44d01434c285 @@ -339,7 +339,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect github.com/circonus-labs/circonusllhist v0.1.3 // indirect - github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudfoundry-community/go-cfclient v0.0.0-20220930021109-9c4e6c59ccf1 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/continuity v0.4.5 // indirect @@ -405,7 +405,7 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index d1cb64dd4d..3ed0af0d1d 100644 --- a/go.sum +++ b/go.sum @@ -750,8 +750,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= -github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/gopenpgp/v3 v3.2.1 h1:ohRlKL5YwyIkN5kk7uBvijiMsyA57mK0yBEJg9xButU= github.com/ProtonMail/gopenpgp/v3 v3.2.1/go.mod h1:x7RduTo/0n/2PjTFRoEHApaxye/8PFbhoCquwfYBUGM= github.com/SAP/go-hdb v1.10.1 h1:c9dGT5xHZNDwPL3NQcRpnNISn3MchwYaGoMZpCAllUs= @@ -905,8 +905,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d h1:IiIprFGH6SqstblP0Y9NIo3eaUJGkI/YDOFVSL64Uq4= -github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudfoundry-community/go-cfclient v0.0.0-20220930021109-9c4e6c59ccf1 h1:ef0OsiQjSQggHrLFAMDRiu6DfkVSElA5jfG1/Nkyu6c= github.com/cloudfoundry-community/go-cfclient v0.0.0-20220930021109-9c4e6c59ccf1/go.mod h1:sgaEj3tRn0hwe7GPdEUwxrdOqjBzyjyvyOCGf1OQyZY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -1270,14 +1270,15 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v83 v83.0.0 h1:Ydy4gAfqxrnFUwXAuKl/OMhhGa0KtMtnJ3EozIIuHT0= +github.com/google/go-github/v83 v83.0.0/go.mod h1:gbqarhK37mpSu8Xy7sz21ITtznvzouyHSAajSaYCHe8= github.com/google/go-metrics-stackdriver v0.2.0 h1:rbs2sxHAPn2OtUj9JdR/Gij1YKGl0BTVD0augB+HEjE= github.com/google/go-metrics-stackdriver v0.2.0/go.mod h1:KLcPyp3dWJAFD+yHisGlJSZktIsTjb50eB72U2YZ9K0= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/tools/pipeline/go.mod b/tools/pipeline/go.mod index 30bc9b0bbc..88a91dd1b8 100644 --- a/tools/pipeline/go.mod +++ b/tools/pipeline/go.mod @@ -9,7 +9,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/avast/retry-go/v4 v4.6.1 - github.com/google/go-github/v81 v81.0.0 + github.com/google/go-github/v83 v83.0.0 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/releases-api v0.2.3 github.com/jedib0t/go-pretty/v6 v6.6.8 @@ -45,7 +45,7 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect diff --git a/tools/pipeline/go.sum b/tools/pipeline/go.sum index a86daca15a..b732a7a64c 100644 --- a/tools/pipeline/go.sum +++ b/tools/pipeline/go.sum @@ -77,14 +77,13 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE= github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v81 v81.0.0 h1:hTLugQRxSLD1Yei18fk4A5eYjOGLUBKAl/VCqOfFkZc= -github.com/google/go-github/v81 v81.0.0/go.mod h1:upyjaybucIbBIuxgJS7YLOZGziyvvJ92WX6WEBNE3sM= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-github/v83 v83.0.0 h1:Ydy4gAfqxrnFUwXAuKl/OMhhGa0KtMtnJ3EozIIuHT0= +github.com/google/go-github/v83 v83.0.0/go.mod h1:gbqarhK37mpSu8Xy7sz21ITtznvzouyHSAajSaYCHe8= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -300,7 +299,6 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= diff --git a/tools/pipeline/internal/cmd/github.go b/tools/pipeline/internal/cmd/github.go index 8af77d1f6a..e3b10a8446 100644 --- a/tools/pipeline/internal/cmd/github.go +++ b/tools/pipeline/internal/cmd/github.go @@ -10,7 +10,7 @@ import ( "os" "path/filepath" - "github.com/google/go-github/v81/github" + "github.com/google/go-github/v83/github" "github.com/shurcooL/githubv4" "github.com/spf13/cobra" "golang.org/x/oauth2" diff --git a/tools/pipeline/internal/pkg/changed/config_test.go b/tools/pipeline/internal/pkg/changed/config_test.go index c197d73113..67c7786f6a 100644 --- a/tools/pipeline/internal/pkg/changed/config_test.go +++ b/tools/pipeline/internal/pkg/changed/config_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" "github.com/stretchr/testify/require" ) diff --git a/tools/pipeline/internal/pkg/changed/file.go b/tools/pipeline/internal/pkg/changed/file.go index b5ac87603b..2d3dd02459 100644 --- a/tools/pipeline/internal/pkg/changed/file.go +++ b/tools/pipeline/internal/pkg/changed/file.go @@ -8,7 +8,7 @@ import ( "slices" "strings" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" ) // File represents a changed file in a PR or commit. diff --git a/tools/pipeline/internal/pkg/config/config_test.go b/tools/pipeline/internal/pkg/config/config_test.go index bef5de46a1..571e98fb44 100644 --- a/tools/pipeline/internal/pkg/config/config_test.go +++ b/tools/pipeline/internal/pkg/config/config_test.go @@ -10,7 +10,7 @@ import ( "path/filepath" "testing" - "github.com/google/go-github/v81/github" + "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed" "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty" diff --git a/tools/pipeline/internal/pkg/github/add_assignees.go b/tools/pipeline/internal/pkg/github/add_assignees.go index 580d2a52bb..1b7bcb8d0f 100644 --- a/tools/pipeline/internal/pkg/github/add_assignees.go +++ b/tools/pipeline/internal/pkg/github/add_assignees.go @@ -8,7 +8,7 @@ import ( "log/slog" "slices" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" slogctx "github.com/veqryn/slog-context" ) diff --git a/tools/pipeline/internal/pkg/github/check_commit_status.go b/tools/pipeline/internal/pkg/github/check_commit_status.go index 7496e704c5..2bab6654be 100644 --- a/tools/pipeline/internal/pkg/github/check_commit_status.go +++ b/tools/pipeline/internal/pkg/github/check_commit_status.go @@ -10,7 +10,7 @@ import ( "slices" "strings" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/jedib0t/go-pretty/v6/table" ) diff --git a/tools/pipeline/internal/pkg/github/check_go_mod_diff_request.go b/tools/pipeline/internal/pkg/github/check_go_mod_diff_request.go index 0009a4b56c..33c1124ec7 100644 --- a/tools/pipeline/internal/pkg/github/check_go_mod_diff_request.go +++ b/tools/pipeline/internal/pkg/github/check_go_mod_diff_request.go @@ -11,7 +11,7 @@ import ( "log/slog" "os" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git/client" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/golang" "github.com/jedib0t/go-pretty/v6/table" diff --git a/tools/pipeline/internal/pkg/github/close_copied_origin_pull_request.go b/tools/pipeline/internal/pkg/github/close_copied_origin_pull_request.go index 4cf9683b9b..dcdb4d5bad 100644 --- a/tools/pipeline/internal/pkg/github/close_copied_origin_pull_request.go +++ b/tools/pipeline/internal/pkg/github/close_copied_origin_pull_request.go @@ -12,7 +12,7 @@ import ( "slices" "strings" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/jedib0t/go-pretty/v6/table" "github.com/shurcooL/githubv4" slogctx "github.com/veqryn/slog-context" diff --git a/tools/pipeline/internal/pkg/github/commit.go b/tools/pipeline/internal/pkg/github/commit.go index da852d810d..5e5c0c392f 100644 --- a/tools/pipeline/internal/pkg/github/commit.go +++ b/tools/pipeline/internal/pkg/github/commit.go @@ -7,7 +7,7 @@ import ( "context" "log/slog" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" slogctx "github.com/veqryn/slog-context" ) diff --git a/tools/pipeline/internal/pkg/github/copy_pull_request.go b/tools/pipeline/internal/pkg/github/copy_pull_request.go index 5f9c2effe3..5214194bcd 100644 --- a/tools/pipeline/internal/pkg/github/copy_pull_request.go +++ b/tools/pipeline/internal/pkg/github/copy_pull_request.go @@ -15,7 +15,7 @@ import ( "slices" "strings" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git/client" "github.com/jedib0t/go-pretty/v6/table" slogctx "github.com/veqryn/slog-context" diff --git a/tools/pipeline/internal/pkg/github/copy_pull_request_test.go b/tools/pipeline/internal/pkg/github/copy_pull_request_test.go index a042ad2141..6c3b585b36 100644 --- a/tools/pipeline/internal/pkg/github/copy_pull_request_test.go +++ b/tools/pipeline/internal/pkg/github/copy_pull_request_test.go @@ -6,7 +6,7 @@ package github import ( "testing" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/stretchr/testify/require" ) diff --git a/tools/pipeline/internal/pkg/github/create_backport.go b/tools/pipeline/internal/pkg/github/create_backport.go index e3ea4b78be..c42aecb4bb 100644 --- a/tools/pipeline/internal/pkg/github/create_backport.go +++ b/tools/pipeline/internal/pkg/github/create_backport.go @@ -15,7 +15,7 @@ import ( "slices" "strings" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/config" libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git/client" @@ -760,6 +760,21 @@ func (r *CreateBackportReq) backportRef( return res } + // Copy non-backport labels from the original PR to the backport PR + labelsToAdd := filterNonBackportLabels(pr.Labels, r.BackportLabelPrefix) + err = addLabelsToIssue( + ctx, + github, + r.Owner, + r.Repo, + int(res.PullRequest.GetNumber()), + labelsToAdd, + ) + if err != nil { + res.Error = fmt.Errorf("adding labels to backport PR: %w", err) + return res + } + return res } diff --git a/tools/pipeline/internal/pkg/github/create_backport_test.go b/tools/pipeline/internal/pkg/github/create_backport_test.go index 6c627fa6ec..c6f43d705f 100644 --- a/tools/pipeline/internal/pkg/github/create_backport_test.go +++ b/tools/pipeline/internal/pkg/github/create_backport_test.go @@ -8,7 +8,7 @@ import ( "errors" "testing" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/config" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases" @@ -746,3 +746,76 @@ func TestCreateBackportRes_Err(t *testing.T) { }) } } + +// Test_filterNonBackportLabels tests the label filtering functionality +func Test_filterNonBackportLabels(t *testing.T) { + t.Parallel() + + for name, test := range map[string]struct { + backportPrefix string + sourceLabels Labels + expectedLabels []string + }{ + "no labels": { + backportPrefix: "backport", + sourceLabels: Labels{}, + expectedLabels: nil, + }, + "only backport labels": { + backportPrefix: "backport", + sourceLabels: Labels{ + &libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")}, + &libgithub.Label{Name: libgithub.Ptr("backport/1.19.x")}, + }, + expectedLabels: nil, + }, + "mixed labels": { + backportPrefix: "backport", + sourceLabels: Labels{ + &libgithub.Label{Name: libgithub.Ptr("bug")}, + &libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")}, + &libgithub.Label{Name: libgithub.Ptr("enhancement")}, + &libgithub.Label{Name: libgithub.Ptr("backport/ce/main")}, + &libgithub.Label{Name: libgithub.Ptr("docs")}, + }, + expectedLabels: []string{"bug", "enhancement", "docs"}, + }, + "no backport labels": { + backportPrefix: "backport", + sourceLabels: Labels{ + &libgithub.Label{Name: libgithub.Ptr("bug")}, + &libgithub.Label{Name: libgithub.Ptr("enhancement")}, + &libgithub.Label{Name: libgithub.Ptr("docs")}, + &libgithub.Label{Name: libgithub.Ptr("priority/high")}, + }, + expectedLabels: []string{"bug", "enhancement", "docs", "priority/high"}, + }, + "custom backport prefix": { + backportPrefix: "cherry-pick", + sourceLabels: Labels{ + &libgithub.Label{Name: libgithub.Ptr("bug")}, + &libgithub.Label{Name: libgithub.Ptr("cherry-pick/1.18.x")}, + &libgithub.Label{Name: libgithub.Ptr("enhancement")}, + }, + expectedLabels: []string{"bug", "enhancement"}, + }, + "backport-like but different prefix": { + backportPrefix: "backport", + sourceLabels: Labels{ + &libgithub.Label{Name: libgithub.Ptr("backup/daily")}, + &libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")}, + &libgithub.Label{Name: libgithub.Ptr("enhancement")}, + }, + expectedLabels: []string{"backup/daily", "enhancement"}, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + filteredLabels := filterNonBackportLabels(test.sourceLabels, test.backportPrefix) + + require.Equal(t, test.expectedLabels, filteredLabels, + "filtered labels should match expected labels") + }) + } +} diff --git a/tools/pipeline/internal/pkg/github/find_workflow_artifact.go b/tools/pipeline/internal/pkg/github/find_workflow_artifact.go index 3bdccb90d3..636ddfe945 100644 --- a/tools/pipeline/internal/pkg/github/find_workflow_artifact.go +++ b/tools/pipeline/internal/pkg/github/find_workflow_artifact.go @@ -13,7 +13,7 @@ import ( "regexp" "slices" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" "github.com/jedib0t/go-pretty/v6/table" slogctx "github.com/veqryn/slog-context" ) diff --git a/tools/pipeline/internal/pkg/github/issue.go b/tools/pipeline/internal/pkg/github/issue.go index e817c82c00..ab65cddc5c 100644 --- a/tools/pipeline/internal/pkg/github/issue.go +++ b/tools/pipeline/internal/pkg/github/issue.go @@ -7,7 +7,7 @@ import ( "context" "log/slog" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" slogctx "github.com/veqryn/slog-context" ) diff --git a/tools/pipeline/internal/pkg/github/labels.go b/tools/pipeline/internal/pkg/github/labels.go new file mode 100644 index 0000000000..4f5b60e53c --- /dev/null +++ b/tools/pipeline/internal/pkg/github/labels.go @@ -0,0 +1,54 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package github + +import ( + "context" + "log/slog" + "strings" + + libgithub "github.com/google/go-github/v83/github" + slogctx "github.com/veqryn/slog-context" +) + +// filterNonBackportLabels returns a slice of label names that do not have the +// specified backport prefix, filtering out backport labels from the input labels +func filterNonBackportLabels(labels Labels, backportPrefix string) []string { + var labelsToAdd []string + for _, label := range labels { + if label.GetName() != "" && !strings.HasPrefix(label.GetName(), backportPrefix+"/") { + labelsToAdd = append(labelsToAdd, label.GetName()) + } + } + return labelsToAdd +} + +// addLabelsToIssue adds the given labels to the issue or pull request +func addLabelsToIssue( + ctx context.Context, + github *libgithub.Client, + owner string, + repo string, + number int, + labels []string, +) error { + if len(labels) < 1 { + slog.Default().DebugContext(ctx, "skipping label assignment because no labels were provided") + return nil + } + + ctx = slogctx.Append(ctx, + slog.String("labels", strings.Join(labels, ", ")), + slog.Int("issue-number", number), + ) + + slog.Default().DebugContext(ctx, "adding labels to issue or pull request") + _, _, err := github.Issues.AddLabelsToIssue(ctx, owner, repo, number, labels) + if err != nil { + return err + } + + slog.Default().DebugContext(ctx, "successfully added labels to issue or pull request") + return nil +} diff --git a/tools/pipeline/internal/pkg/github/list_changed_files.go b/tools/pipeline/internal/pkg/github/list_changed_files.go index 23b1279c98..994b3c1251 100644 --- a/tools/pipeline/internal/pkg/github/list_changed_files.go +++ b/tools/pipeline/internal/pkg/github/list_changed_files.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/config" "github.com/jedib0t/go-pretty/v6/table" diff --git a/tools/pipeline/internal/pkg/github/list_commit_statuses.go b/tools/pipeline/internal/pkg/github/list_commit_statuses.go index 6e91399e0f..1efa061b54 100644 --- a/tools/pipeline/internal/pkg/github/list_commit_statuses.go +++ b/tools/pipeline/internal/pkg/github/list_commit_statuses.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/jedib0t/go-pretty/v6/table" ) diff --git a/tools/pipeline/internal/pkg/github/list_workflow_runs.go b/tools/pipeline/internal/pkg/github/list_workflow_runs.go index e4af16a646..9cb45f6cf2 100644 --- a/tools/pipeline/internal/pkg/github/list_workflow_runs.go +++ b/tools/pipeline/internal/pkg/github/list_workflow_runs.go @@ -10,7 +10,7 @@ import ( "net/http" "sync" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" ) // PerPageMax is the maximum number of entities to request for enpoints that diff --git a/tools/pipeline/internal/pkg/github/pull_request.go b/tools/pipeline/internal/pkg/github/pull_request.go index df1e274c41..bb5300b629 100644 --- a/tools/pipeline/internal/pkg/github/pull_request.go +++ b/tools/pipeline/internal/pkg/github/pull_request.go @@ -8,7 +8,7 @@ import ( "fmt" "log/slog" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/shurcooL/githubv4" slogctx "github.com/veqryn/slog-context" ) diff --git a/tools/pipeline/internal/pkg/github/sync_branch_request.go b/tools/pipeline/internal/pkg/github/sync_branch_request.go index 93e5464711..69b7510245 100644 --- a/tools/pipeline/internal/pkg/github/sync_branch_request.go +++ b/tools/pipeline/internal/pkg/github/sync_branch_request.go @@ -13,7 +13,7 @@ import ( "path/filepath" "strings" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/config" gitpkg "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git" gitclient "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git/client" diff --git a/tools/pipeline/internal/pkg/github/templates_test.go b/tools/pipeline/internal/pkg/github/templates_test.go index 83d61bf674..38e963d17c 100644 --- a/tools/pipeline/internal/pkg/github/templates_test.go +++ b/tools/pipeline/internal/pkg/github/templates_test.go @@ -8,7 +8,7 @@ import ( "io" "testing" - libgithub "github.com/google/go-github/v81/github" + libgithub "github.com/google/go-github/v83/github" "github.com/stretchr/testify/require" ) diff --git a/tools/pipeline/internal/pkg/github/workflows.go b/tools/pipeline/internal/pkg/github/workflows.go index 0bc86120ea..029718a182 100644 --- a/tools/pipeline/internal/pkg/github/workflows.go +++ b/tools/pipeline/internal/pkg/github/workflows.go @@ -8,7 +8,7 @@ import ( "fmt" "log/slog" - gh "github.com/google/go-github/v81/github" + gh "github.com/google/go-github/v83/github" slogctx "github.com/veqryn/slog-context" ) From 9f986e80de433d592b6e3af838817e8381199d78 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 3 Mar 2026 14:28:04 -0700 Subject: [PATCH 031/468] Backport Import PKI External CA plugin into vault-enterprise into ce/main (#12671) * no-op commit * Backport ce: Import PKI External CA plugin * Rename from pki_external_ca to pki-external-ca --------- Co-authored-by: Steven Clark --- changelog/_12644.txt | 3 ++ command/base_predict_test.go | 53 ++++---------------------- helper/builtinplugins/registry_test.go | 6 +-- helper/pluginconsts/plugin_consts.go | 1 + scripts/gen_openapi.sh | 1 + 5 files changed, 16 insertions(+), 48 deletions(-) create mode 100644 changelog/_12644.txt diff --git a/changelog/_12644.txt b/changelog/_12644.txt new file mode 100644 index 0000000000..187f6c36e5 --- /dev/null +++ b/changelog/_12644.txt @@ -0,0 +1,3 @@ +```release-note:feature +*** PKI External CA (Enterprise) ***: A new plugin that provides the ability to acquire PKI certificates from Public CA providers through the ACME protocol +``` diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 8da9e3e1ca..18c55eb6d9 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -383,6 +383,7 @@ func TestPredict_Plugins(t *testing.T) { "openldap", "pcf", // Deprecated. "pki", + "pki-external-ca", "postgresql-database-plugin", "rabbitmq", "radius", @@ -414,51 +415,13 @@ func TestPredict_Plugins(t *testing.T) { act := p.plugins() - if !strutil.StrListContains(act, "keymgmt") { - for i, v := range tc.exp { - if v == "keymgmt" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "kmip") { - for i, v := range tc.exp { - if v == "kmip" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "transform") { - for i, v := range tc.exp { - if v == "transform" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "saml") { - for i, v := range tc.exp { - if v == "saml" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "scep") { - for i, v := range tc.exp { - if v == "scep" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break - } - } - } - if !strutil.StrListContains(act, "spiffe") { - for i, v := range tc.exp { - if v == "spiffe" { - tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) - break + for _, pluginName := range []string{"keymgmt", "kmip", "transform", "saml", "scep", "spiffe", "pki-external-ca"} { + if !strutil.StrListContains(act, pluginName) { + for i, v := range tc.exp { + if v == pluginName { + tc.exp = append(tc.exp[:i], tc.exp[i+1:]...) + break + } } } } diff --git a/helper/builtinplugins/registry_test.go b/helper/builtinplugins/registry_test.go index 6e135f53ac..62d3832209 100644 --- a/helper/builtinplugins/registry_test.go +++ b/helper/builtinplugins/registry_test.go @@ -110,7 +110,7 @@ func Test_RegistryKeyCounts(t *testing.T) { name: "number of secrets plugins", pluginType: consts.PluginTypeSecrets, want: 19, - entWant: 4, + entWant: 5, }, } for _, tt := range tests { @@ -257,10 +257,10 @@ func Test_RegistryMatchesGenOpenapi(t *testing.T) { var ( credentialBackends []string - credentialBackendsRe = regexp.MustCompile(leading + `vault auth enable (?:-.+ )*(?:"([a-zA-Z]+)"|([a-zA-Z]+))$`) + credentialBackendsRe = regexp.MustCompile(leading + `vault auth enable (?:-.+ )*(?:"([a-zA-Z-]+)"|([a-zA-Z-]+))$`) secretsBackends []string - secretsBackendsRe = regexp.MustCompile(leading + `vault secrets enable (?:-.+ )*(?:"([a-zA-Z]+)"|([a-zA-Z]+))$`) + secretsBackendsRe = regexp.MustCompile(leading + `vault secrets enable (?:-.+ )*(?:"([a-zA-Z-]+)"|([a-zA-Z-]+))$`) ) scanner := bufio.NewScanner(f) diff --git a/helper/pluginconsts/plugin_consts.go b/helper/pluginconsts/plugin_consts.go index 285a51ce39..be534fafa7 100644 --- a/helper/pluginconsts/plugin_consts.go +++ b/helper/pluginconsts/plugin_consts.go @@ -42,6 +42,7 @@ const ( SecretEngineNomad = "nomad" SecretEngineOpenLDAP = "openldap" SecretEngineLDAP = "ldap" + SecretEnginePkiExternalCa = "pki-external-ca" SecretEnginePostgresql = "postgresql" SecretEngineRabbitMQ = "rabbitmq" SecretEngineSpiffe = "spiffe" diff --git a/scripts/gen_openapi.sh b/scripts/gen_openapi.sh index fc91149009..8901db0be1 100755 --- a/scripts/gen_openapi.sh +++ b/scripts/gen_openapi.sh @@ -93,6 +93,7 @@ vault secrets enable "transit" if vault version | grep -q "+ent"; then vault secrets enable "keymgmt" vault secrets enable "kmip" + vault secrets enable "pki-external-ca" vault secrets enable "transform" vault secrets enable "spiffe" vault auth enable "saml" From b7ee1a6e79ee628a29ab22cd10d6b1d41c5f5ee8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 3 Mar 2026 15:13:02 -0700 Subject: [PATCH 032/468] UI: Adding playwright test for Secrets engines filtering (#12677) (#12691) * test run * adding test for secrets engines filtering * fix * fix assert * updating filter to use regex Co-authored-by: Dan Rivera --- .../tests/superuser/filtering-engines.spec.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 ui/e2e/tests/superuser/filtering-engines.spec.ts diff --git a/ui/e2e/tests/superuser/filtering-engines.spec.ts b/ui/e2e/tests/superuser/filtering-engines.spec.ts new file mode 100644 index 0000000000..04c906c3ea --- /dev/null +++ b/ui/e2e/tests/superuser/filtering-engines.spec.ts @@ -0,0 +1,81 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; +import { BasePage } from '../../pages/base'; + +test('filtering secrets engines workflow', async ({ page }) => { + const basePage = new BasePage(page); + // nav to secrets engines page and enable a couple of engines to test filtering + await page.goto('dashboard'); + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + + // skip intro page if it appears + if (await page.getByRole('button', { name: 'Skip' }).isVisible()) { + await page.getByRole('button', { name: 'Skip' }).click(); + } + + // enable transit + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('Transit - enabled engine type').click(); + await page.getByRole('button', { name: 'Enable engine' }).click(); + await basePage.dismissFlashMessages(); + await page.getByLabel('breadcrumbs').getByRole('link', { name: 'Secrets engines' }).click(); + + // enable kv + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByRole('heading', { name: 'KV' }).click(); + await page.getByRole('button', { name: 'Enable engine' }).click(); + await basePage.dismissFlashMessages(); + await page.getByLabel('breadcrumbs').getByRole('link', { name: 'Secrets engines' }).click(); + + // search for transit engine + await page.getByRole('searchbox', { name: 'Search' }).fill('transit'); + + // assert theres only 1 result and its the transit engine + await expect(page.getByText('1–1 of 1 page 1 Items per')).toBeVisible(); + await expect(page.getByRole('link', { name: 'transit/' })).toBeVisible(); + + // assert user can click into transit engine from search results and nav back + await page.getByRole('link', { name: 'transit/' }).click(); + await expect(page.getByRole('heading', { name: 'transit', exact: true })).toBeVisible(); + await page.getByLabel('breadcrumbs').getByRole('link', { name: 'Secrets engines' }).click(); + + // filter for cubbyhole engine type + await page.getByRole('button', { name: 'Engine type' }).click(); + await page.getByRole('checkbox', { name: 'cubbyhole', exact: true }).check(); + await page.getByRole('button', { name: 'Engine type' }).click(); + await expect(page.getByRole('link', { name: 'cubbyhole/' })).toBeVisible(); + + // clear filter + await page.getByRole('button', { name: 'Clear all' }).click(); + + // filter by engine type and version + await page.getByRole('button', { name: 'Engine type' }).click(); + await page.getByRole('checkbox', { name: 'cubbyhole', exact: true }).check(); + await page.getByRole('button', { name: 'Version', exact: true }).click(); + // clicking the label here so it closes the dropdown, the checkbox is inside the label so it will still be checked + // Note: /\+builtin\.vault/ is regex to partial match the builtin label since the version will change + await page + .locator('label') + .filter({ hasText: /\+builtin\.vault/ }) + .click(); + + // assert filter chips are visible and applied correctly + await expect( + page + .locator('span') + .filter({ hasText: /\+builtin\.vault/ }) + .nth(1) + ).toBeVisible(); + await expect(page.locator('span').filter({ hasText: 'cubbyhole' }).nth(1)).toBeVisible(); + await expect(page.getByRole('link', { name: 'cubbyhole/' })).toBeVisible(); + await expect(page.getByRole('gridcell', { name: /\+builtin\.vault/ })).toBeVisible(); + + // clear filters and assert all engines are visible again + await page.getByRole('button', { name: 'Clear all' }).click(); + await expect(page.getByRole('link', { name: 'kv/' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'transit/' })).toBeVisible(); +}); From b86668fad6129c6ddf9c044edebb4ec7580c2012 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 4 Mar 2026 08:43:03 -0700 Subject: [PATCH 033/468] Allow the Location header in identity store responses (#12663) (#12703) --- http/handler_test.go | 2 ++ http/sys_mount_test.go | 12 ++++++++++++ vault/logical_system_test.go | 3 +++ vault/mount.go | 1 + 4 files changed, 18 insertions(+) diff --git a/http/handler_test.go b/http/handler_test.go index e91fd40c83..0a8d6426a8 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -491,6 +491,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -592,6 +593,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 8c15bb54ca..0d19fb2912 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -95,6 +95,7 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -179,6 +180,7 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -327,6 +329,7 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -427,6 +430,7 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -676,6 +680,7 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -776,6 +781,7 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -908,6 +914,7 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -992,6 +999,7 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -1233,6 +1241,7 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -1333,6 +1342,7 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -1507,6 +1517,7 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, @@ -1607,6 +1618,7 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, + "allowed_response_headers": []interface{}{"Location"}, }, "local": false, "seal_wrap": false, diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 0e658d51b1..60b3a0bd32 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -242,6 +242,7 @@ func TestSystemBackend_mounts(t *testing.T) { "max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, + "allowed_response_headers": []string{"Location"}, }, "local": false, "seal_wrap": false, @@ -399,6 +400,7 @@ func TestSystemBackend_mount(t *testing.T) { "max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, + "allowed_response_headers": []string{"Location"}, }, "local": false, "seal_wrap": false, @@ -4607,6 +4609,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, + "allowed_response_headers": []string{"Location"}, }, "local": false, "seal_wrap": false, diff --git a/vault/mount.go b/vault/mount.go index fb6a64c905..1bb1e95089 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -2001,6 +2001,7 @@ func (c *Core) requiredMountTable() *MountTable { BackendAwareUUID: identityBackendUUID, Config: MountConfig{ PassthroughRequestHeaders: []string{"Authorization"}, + AllowedResponseHeaders: []string{"Location"}, }, RunningVersion: versions.DefaultBuiltinVersion, } From 45c2c94f902afdcc2e9eab0a361c717c8ca2d90a Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 4 Mar 2026 08:48:48 -0700 Subject: [PATCH 034/468] Allow "glob" style wildcards in DNS names when issuing PKI certs (#12674) (#12704) * Allow "glob" style wildcards in DNS names when issuing PKI certs. Change the regex we use to validate hostnames to allow wildcards in the first "label" of the hostname to be not just "*" by itself, but to be in any position. Remove unused duplicated regexes from cert_util.go. Add unit tests. Co-authored-by: Victor Rodriguez Rizo --- builtin/logical/pki/backend_test.go | 356 ++++++++++++++++++ builtin/logical/pki/cert_util.go | 28 -- builtin/logical/pki/issuing/issue_common.go | 51 +-- .../logical/pki/issuing/issue_common_test.go | 330 ++++++++++++++++ changelog/_12674.txt | 3 + 5 files changed, 718 insertions(+), 50 deletions(-) create mode 100644 builtin/logical/pki/issuing/issue_common_test.go create mode 100644 changelog/_12674.txt diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index ab11c7feb7..9f9668c0da 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -7975,3 +7975,359 @@ func TestBackend_SignIntermediate_IgnoresCSR_BasicConstraint(t *testing.T) { } require.True(t, hasBasicConstraints, "certificate should have Basic Constraints extension") } + +// TestBackend_IDNWithWildcards_CommonName tests IDNA conversion and wildcard validation +// in the Common Name (CN) field using both /issue and /sign endpoints. +func TestBackend_IDNWithWildcards_CommonName(t *testing.T) { + t.Parallel() + b, s := CreateBackendWithStorage(t) + + // Generate root CA + resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ + "common_name": "Root CA", + "ttl": "40h", + }) + require.NoError(t, err) + require.NotNil(t, resp) + + // Create a role that allows wildcards and any name + _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ + "allow_any_name": true, + "allow_wildcard_certificates": true, + "enforce_hostnames": true, + "max_ttl": "2h", + "key_type": "ec", + }) + require.NoError(t, err) + + testCases := []struct { + name string + commonName string + expectDNSNames []string + expectError bool + }{ + { + name: "ASCII wildcard in CN", + commonName: "*.example.com", + expectDNSNames: []string{"*.example.com"}, + }, + { + name: "IDN with wildcard in CN - German umlaut", + commonName: "*.müller.com", + expectDNSNames: []string{"*.xn--mller-kva.com"}, + }, + { + name: "IDN with wildcard in CN - Japanese", + commonName: "*.日本.com", + expectDNSNames: []string{"*.xn--wgv71a.com"}, + }, + { + name: "IDN with wildcard in CN - Chinese", + commonName: "*.中国.com", + expectDNSNames: []string{"*.xn--fiqs8s.com"}, + }, + { + name: "IDN with wildcard in CN - Arabic", + commonName: "*.مثال.com", + expectDNSNames: []string{"*.xn--mgbh0fb.com"}, + }, + { + name: "IDN with multiple labels and wildcard", + commonName: "*.subdomain.müller.com", + expectDNSNames: []string{"*.subdomain.xn--mller-kva.com"}, + }, + // Invalid hostname test cases + { + name: "Invalid - wildcard not in leftmost position", + commonName: "sub.*.example.com", + expectError: true, + }, + { + name: "Invalid - multiple wildcards", + commonName: "*.*.example.com", + expectError: true, + }, + { + name: "Invalid - wildcard in IDN not in leftmost position", + commonName: "sub.*.müller.com", + expectError: true, + }, + { + name: "Invalid - empty label in hostname", + commonName: "example..com", + expectError: true, + }, + { + name: "Invalid - label starting with hyphen", + commonName: "-example.com", + expectError: true, + }, + { + name: "Invalid - label ending with hyphen", + commonName: "example-.com", + expectError: true, + }, + { + name: "Invalid - label too long (>63 chars)", + commonName: "*.verylonglabelverylonglabelverylonglabelverylonglabelverylonglabel.com", + expectError: true, + }, + { + name: "Invalid - hostname starting with dot", + commonName: ".example.com", + expectError: true, + }, + } + + for _, useCSR := range []bool{false, true} { + testType := "issue" + if useCSR { + testType = "sign" + } + t.Run(testType, func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var resp *logical.Response + var err error + + if useCSR { + // Generate a CSR with the test common name + csrTemplate := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: tc.commonName, + }, + } + _, _, csrPem := generateCSR(t, csrTemplate, "ec", 256) + + resp, err = CBWrite(b, s, "sign/test", map[string]interface{}{ + "csr": csrPem, + }) + } else { + // Use direct issue + resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ + "common_name": tc.commonName, + }) + } + + if tc.expectError { + require.Error(t, err, "expected error for test case: %s", tc.name) + return + } + + require.NoError(t, err, "unexpected error for test case: %s", tc.name) + require.NotNil(t, resp, "response should not be nil for test case: %s", tc.name) + require.NotNil(t, resp.Data["certificate"], "certificate should be present for test case: %s", tc.name) + + // Parse the certificate to verify DNS names + certPEM := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(certPEM)) + require.NotNil(t, block, "failed to decode PEM for test case: %s", tc.name) + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "failed to parse certificate for test case: %s", tc.name) + + // Verify DNS names match expected (order may vary, so use ElementsMatch) + require.ElementsMatch(t, tc.expectDNSNames, cert.DNSNames, + "DNS names mismatch for test case: %s\nExpected: %v\nGot: %v", + tc.name, tc.expectDNSNames, cert.DNSNames) + }) + } + }) + } +} + +// TestBackend_IDNWithWildcards_AltNames tests IDNA conversion and wildcard validation +// in the alternative names field using both /issue and /sign endpoints. +func TestBackend_IDNWithWildcards_AltNames(t *testing.T) { + t.Parallel() + b, s := CreateBackendWithStorage(t) + + // Generate root CA + resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ + "common_name": "Root CA", + "ttl": "40h", + }) + require.NoError(t, err) + require.NotNil(t, resp) + + // Create a role that allows wildcards and any name + _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ + "allow_any_name": true, + "allow_subdomains": true, + "allow_glob_domains": true, + "allow_wildcard_certificates": true, + "enforce_hostnames": true, + "max_ttl": "2h", + "key_type": "ec", + }) + require.NoError(t, err) + + testCases := []struct { + name string + commonName string + altNames string + expectDNSNames []string + expectError bool + }{ + { + name: "ASCII wildcard in alt_names", + commonName: "example.com", + altNames: "*.test.com", + expectDNSNames: []string{"example.com", "*.test.com"}, + }, + { + name: "Mixed ASCII and IDN with wildcards in alt_names", + commonName: "example.com", + altNames: "*.example.com,*.müller.de", + expectDNSNames: []string{"example.com", "*.example.com", "*.xn--mller-kva.de"}, + }, + { + name: "Multiple IDN domains with wildcards in alt_names", + commonName: "example.com", + altNames: "*.日本.com,*.中国.cn", + expectDNSNames: []string{"example.com", "*.xn--wgv71a.com", "*.xn--fiqs8s.cn"}, + }, + { + name: "Complex IDN with multiple subdomains and wildcard", + commonName: "example.com", + altNames: "*.api.v2.müller.com", + expectDNSNames: []string{"example.com", "*.api.v2.xn--mller-kva.com"}, + }, + { + name: "IDN with trailing dot and wildcard", + commonName: "example.com", + altNames: "*.müller.com.", + expectDNSNames: []string{"example.com", "*.xn--mller-kva.com."}, + }, + { + name: "Invalid - wildcard in alt_names not in leftmost position", + commonName: "example.com", + altNames: "sub*.test.com", + expectDNSNames: []string{"example.com", "sub*.test.com"}, + }, + // Invalid hostname test cases + { + name: "Invalid - wildcard in alt_names not in leftmost position", + commonName: "example.com", + altNames: "sub*.test.com,sub.*.test.com", + expectError: true, + }, + { + name: "Invalid - multiple wildcards in alt_names", + commonName: "example.com", + altNames: "*.*.müller.com", + expectError: true, + }, + { + name: "Invalid - wildcard in IDN not in leftmost position", + commonName: "example.com", + altNames: "sub.*.müller.com", + expectError: true, + }, + { + name: "Invalid - empty label in hostname", + commonName: "example.com", + altNames: "example..com", + expectError: true, + }, + { + name: "Invalid - label starting with hyphen", + commonName: "example.com", + altNames: "-example.com", + expectError: true, + }, + { + name: "Invalid - label ending with hyphen", + commonName: "example.com", + altNames: "example-.com", + expectError: true, + }, + { + name: "Invalid - label too long (>63 chars)", + commonName: "example.com", + altNames: "*.verylonglabelverylonglabelverylonglabelverylonglabelverylonglabel.com", + expectError: true, + }, + { + name: "Invalid - hostname starting with dot", + commonName: "example.com", + altNames: ".example.com", + expectError: true, + }, + } + + for _, useCSR := range []bool{false, true} { + testType := "issue" + if useCSR { + testType = "sign" + } + t.Run(testType, func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var resp *logical.Response + var err error + + if useCSR { + // Generate a CSR with the test common name and alt names + // Note: CSRs include DNS SANs in the request, and they must be in Punycode (ASCII) + var dnsNames []string + if tc.altNames != "" { + // Split alt names by comma and convert to Punycode + altNamesList := strings.Split(tc.altNames, ",") + for _, name := range altNamesList { + // Convert IDN to Punycode for CSR (CSRs only support ASCII) + punycoded, err := idna.ToASCII(name) + if err != nil { + // If conversion fails, use original (will fail validation as expected) + dnsNames = append(dnsNames, name) + } else { + dnsNames = append(dnsNames, punycoded) + } + } + } + + csrTemplate := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: tc.commonName, + }, + DNSNames: dnsNames, + } + _, _, csrPem := generateCSR(t, csrTemplate, "ec", 256) + + resp, err = CBWrite(b, s, "sign/test", map[string]interface{}{ + "csr": csrPem, + }) + } else { + // Use direct issue + resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ + "common_name": tc.commonName, + "alt_names": tc.altNames, + }) + } + + if tc.expectError { + require.Error(t, err, "expected error for test case: %s", tc.name) + return + } + + require.NoError(t, err, "unexpected error for test case: %s", tc.name) + require.NotNil(t, resp, "response should not be nil for test case: %s", tc.name) + require.NotNil(t, resp.Data["certificate"], "certificate should be present for test case: %s", tc.name) + + // Parse the certificate to verify DNS names + certPEM := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(certPEM)) + require.NotNil(t, block, "failed to decode PEM for test case: %s", tc.name) + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "failed to parse certificate for test case: %s", tc.name) + + // Verify DNS names match expected (order may vary, so use ElementsMatch) + require.ElementsMatch(t, tc.expectDNSNames, cert.DNSNames, + "DNS names mismatch for test case: %s\nExpected: %v\nGot: %v", + tc.name, tc.expectDNSNames, cert.DNSNames) + }) + } + }) + } +} diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 7f039711ed..ebe10fd184 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -15,7 +15,6 @@ import ( "math/big" "net" "net/url" - "regexp" "strconv" "strings" "time" @@ -37,33 +36,6 @@ type inputBundle struct { apiData *framework.FieldData } -var ( - // labelRegex is a single label from a valid domain name and was extracted - // from hostnameRegex below for use in leftWildLabelRegex, without any - // label separators (`.`). - labelRegex = `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])` - - // A note on hostnameRegex: although we set the StrictDomainName option - // when doing the idna conversion, this appears to only affect output, not - // input, so it will allow e.g. host^123.example.com straight through. So - // we still need to use this to check the output. - hostnameRegex = regexp.MustCompile(`^(\*\.)?(` + labelRegex + `\.)*` + labelRegex + `\.?$`) - - // Left Wildcard Label Regex is equivalent to a single domain label - // component from hostnameRegex above, but with additional wildcard - // characters added. There are four possibilities here: - // - // 1. Entire label is a wildcard, - // 2. Wildcard exists at the start, - // 3. Wildcard exists at the end, - // 4. Wildcard exists in the middle. - allWildRegex = `\*` - startWildRegex = `\*` + labelRegex - endWildRegex = labelRegex + `\*` - middleWildRegex = labelRegex + `\*` + labelRegex - leftWildLabelRegex = regexp.MustCompile(`^(` + allWildRegex + `|` + startWildRegex + `|` + endWildRegex + `|` + middleWildRegex + `)$`) -) - func doesPublicKeyAlgoMatchSignatureAlgo(pubKey x509.PublicKeyAlgorithm, algo x509.SignatureAlgorithm) bool { return issuing.DoesPublicKeyAlgoMatchSignatureAlgo(pubKey, algo) } diff --git a/builtin/logical/pki/issuing/issue_common.go b/builtin/logical/pki/issuing/issue_common.go index dade2bf598..370168716a 100644 --- a/builtin/logical/pki/issuing/issue_common.go +++ b/builtin/logical/pki/issuing/issue_common.go @@ -32,16 +32,10 @@ const ( ) var ( - // labelRegex is a single label from a valid domain name and was extracted + // labelPattern is a single label from a valid domain name and was extracted // from hostnameRegex below for use in leftWildLabelRegex, without any // label separators (`.`). - labelRegex = `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])` - - // A note on hostnameRegex: although we set the StrictDomainName option - // when doing the idna conversion, this appears to only affect output, not - // input, so it will allow e.g. host^123.example.com straight through. So - // we still need to use this to check the output. - hostnameRegex = regexp.MustCompile(`^(\*\.)?(` + labelRegex + `\.)*` + labelRegex + `\.?$`) + labelPattern = `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])` // Left Wildcard Label Regex is equivalent to a single domain label // component from hostnameRegex above, but with additional wildcard @@ -51,11 +45,20 @@ var ( // 2. Wildcard exists at the start, // 3. Wildcard exists at the end, // 4. Wildcard exists in the middle. - allWildRegex = `\*` - startWildRegex = `\*` + labelRegex - endWildRegex = labelRegex + `\*` - middleWildRegex = labelRegex + `\*` + labelRegex - leftWildLabelRegex = regexp.MustCompile(`^(` + allWildRegex + `|` + startWildRegex + `|` + endWildRegex + `|` + middleWildRegex + `)$`) + allWildPattern = `\*` + startWildPattern = `\*` + labelPattern + endWildPattern = labelPattern + `\*` + middleWildPattern = labelPattern + `\*` + labelPattern + + leftWildLabelPattern = fmt.Sprintf(`(%s|%s|%s|%s)`, allWildPattern, startWildPattern, endWildPattern, middleWildPattern) + hostnamePattern = fmt.Sprintf(`(%s\.)*%s\.?$`, labelPattern, labelPattern) + wildHostnamePattern = fmt.Sprintf(`^(%s\.)?%s`, leftWildLabelPattern, hostnamePattern) + + // A note on hostnameRegex: although we set the StrictDomainName option + // when doing the idna conversion, this appears to only affect output, not + // input, so it will allow e.g. host^123.example.com straight through. So + // we still need to use this to check the output. + wildHostnameRegex = regexp.MustCompile(wildHostnamePattern) ) type EntityInfo struct { @@ -153,7 +156,7 @@ func GenerateCreationBundle(b logical.SystemView, role *RoleEntry, entityInfo En if err != nil { return nil, nil, errutil.UserError{Err: err.Error()} } - if hostnameRegex.MatchString(converted) { + if wildHostnameRegex.MatchString(converted) { dnsNames = append(dnsNames, converted) } } @@ -177,8 +180,10 @@ func GenerateCreationBundle(b logical.SystemView, role *RoleEntry, entityInfo En if err != nil { return nil, nil, errutil.UserError{Err: err.Error()} } - if hostnameRegex.MatchString(converted) { + if wildHostnameRegex.MatchString(converted) { dnsNames = append(dnsNames, converted) + } else { + return nil, nil, errutil.UserError{Err: fmt.Sprintf("subject alternate name %s is not a valid DNS name and cannot be included as a SAN", v)} } } } @@ -544,6 +549,9 @@ func ValidateNames(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, return name } + // At this point, we know reducedName does not have an '*'. If isWildcard is true, + // the '*' is in wildcardLabel + // AllowAnyName is checked after this because EnforceHostnames still // applies when allowing any name. Also, we check the reduced name to // ensure that we are not either checking a full email address or a @@ -560,16 +568,15 @@ func ValidateNames(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, if err != nil { return name } - if !hostnameRegex.MatchString(converted) { + if isWildcard { + // When a wildcard is specified, we additionally need to validate + // the label with the wildcard is correctly formed. + converted = wildcardLabel + "." + converted + } + if !wildHostnameRegex.MatchString(converted) { return name } } - - // When a wildcard is specified, we additionally need to validate - // the label with the wildcard is correctly formed. - if isWildcard && !leftWildLabelRegex.MatchString(wildcardLabel) { - return name - } } // Self-explanatory, but validations from EnforceHostnames and diff --git a/builtin/logical/pki/issuing/issue_common_test.go b/builtin/logical/pki/issuing/issue_common_test.go new file mode 100644 index 0000000000..1c1c4f2d63 --- /dev/null +++ b/builtin/logical/pki/issuing/issue_common_test.go @@ -0,0 +1,330 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package issuing + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Test_wildHostnameRegex tests the wildHostnameRegex regular expression against a variety of +// valid and invalid hostnames. +func Test_wildHostnameRegex(t *testing.T) { + tests := []struct { + name string + hostname string + want bool + }{ + // Valid hostnames without wildcards - with trailing dot + { + name: "simple domain with dot", + hostname: "example.com.", + want: true, + }, + { + name: "subdomain with dot", + hostname: "www.example.com.", + want: true, + }, + { + name: "multi-level subdomain with dot", + hostname: "api.v1.example.com.", + want: true, + }, + { + name: "single label with dot", + hostname: "localhost.", + want: true, + }, + + // Valid hostnames without wildcards - without trailing dot + { + name: "simple domain without dot", + hostname: "example.com", + want: true, + }, + { + name: "subdomain without dot", + hostname: "www.example.com", + want: true, + }, + { + name: "multi-level subdomain without dot", + hostname: "api.v1.example.com", + want: true, + }, + { + name: "single label without dot", + hostname: "localhost", + want: true, + }, + { + name: "hyphenated domain", + hostname: "my-domain.example.com", + want: true, + }, + { + name: "numeric in domain", + hostname: "server123.example.com", + want: true, + }, + + // Valid wildcards - entire label + { + name: "wildcard entire first label", + hostname: "*.example.com", + want: true, + }, + { + name: "wildcard entire first label with dot", + hostname: "*.example.com.", + want: true, + }, + { + name: "wildcard entire label multi-level", + hostname: "*.api.example.com", + want: true, + }, + + // Valid wildcards - start of label + { + name: "wildcard at start of label", + hostname: "*test.example.com", + want: true, + }, + { + name: "wildcard at start with numbers", + hostname: "*123.example.com", + want: true, + }, + { + name: "wildcard at start alphanumeric", + hostname: "*abc123.example.com", + want: true, + }, + + // Valid wildcards - end of label + { + name: "wildcard at end of label", + hostname: "test*.example.com", + want: true, + }, + { + name: "wildcard at end with numbers", + hostname: "123*.example.com", + want: true, + }, + { + name: "wildcard at end alphanumeric", + hostname: "abc123*.example.com", + want: true, + }, + + // Valid wildcards - middle of label + { + name: "wildcard in middle of label", + hostname: "test*server.example.com", + want: true, + }, + { + name: "wildcard in middle with numbers", + hostname: "api*v1.example.com", + want: true, + }, + { + name: "wildcard in middle complex", + hostname: "my*server.example.com", + want: true, + }, + + // Invalid cases - hyphens adjacent to wildcards (labels can't start/end with hyphen) + { + name: "wildcard at start with hyphen after", + hostname: "*-server.example.com", + want: false, + }, + { + name: "wildcard at end with hyphen before", + hostname: "server-*.example.com", + want: false, + }, + { + name: "wildcard in middle with hyphens", + hostname: "my-*-server.example.com", + want: false, + }, + + // Invalid cases - wildcard in wrong position + { + name: "wildcard in second label", + hostname: "example.*.com", + want: false, + }, + { + name: "wildcard in last label", + hostname: "example.com*", + want: false, + }, + { + name: "wildcard in TLD", + hostname: "example.*", + want: false, + }, + + // Invalid cases - multiple wildcards + { + name: "multiple wildcards in same label", + hostname: "*test*", + want: false, + }, + { + name: "multiple wildcard labels", + hostname: "*.*.example.com", + want: false, + }, + + // Invalid cases - empty or malformed + { + name: "empty string", + hostname: "", + want: false, + }, + { + name: "just dot", + hostname: ".", + want: false, + }, + { + name: "double dots", + hostname: "example..com", + want: false, + }, + + // Invalid cases - invalid characters + { + name: "underscore in domain", + hostname: "test_server.example.com", + want: false, + }, + { + name: "space in domain", + hostname: "test server.example.com", + want: false, + }, + { + name: "special char in domain", + hostname: "test@server.example.com", + want: false, + }, + + // Edge cases - label boundaries + { + name: "label starting with hyphen", + hostname: "-test.example.com", + want: false, + }, + { + name: "label ending with hyphen", + hostname: "test-.example.com", + want: false, + }, + { + name: "single character label", + hostname: "a.example.com", + want: true, + }, + { + name: "single digit label", + hostname: "1.example.com", + want: true, + }, + + // Edge cases - wildcard combinations + { + name: "wildcard with single char after", + hostname: "*a.example.com", + want: true, + }, + { + name: "wildcard with single char before", + hostname: "a*.example.com", + want: true, + }, + { + name: "wildcard between single chars", + hostname: "a*b.example.com", + want: true, + }, + + // Complex valid cases + { + name: "long subdomain chain", + hostname: "service.api.v2.prod.example.com", + want: true, + }, + { + name: "wildcard in long chain", + hostname: "*service.api.v2.prod.example.com", + want: true, + }, + { + name: "alphanumeric with hyphens", + hostname: "my-api-v2-test.example.com", + want: true, + }, + + // Invalid - wildcard not in leftmost label + { + name: "wildcard in middle of hostname", + hostname: "api.*.example.com", + want: false, + }, + { + name: "wildcard at end of hostname", + hostname: "api.example.*.com", + want: false, + }, + + // Edge cases - only wildcard + { + name: "only wildcard", + hostname: "*", + want: false, + }, + { + name: "wildcard with dot", + hostname: "*.", + want: false, + }, + + // Edge cases - wildcard without domain + { + name: "wildcard label only", + hostname: "*..", + want: false, + }, + + // Valid - wildcard with minimal domain + { + name: "wildcard with single label domain", + hostname: "*.com", + want: true, + }, + { + name: "wildcard pattern with single label domain", + hostname: "*test.com", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := wildHostnameRegex.MatchString(tt.hostname) + require.Equal(t, tt.want, got, "wildHostnameRegex.MatchString(%q) = %v, want %v", tt.hostname, got, tt.want) + }) + } +} + +// Made with Bob diff --git a/changelog/_12674.txt b/changelog/_12674.txt new file mode 100644 index 0000000000..892db22041 --- /dev/null +++ b/changelog/_12674.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/pki: allow glob-style DNS names in alt_names. +``` From 54e61295135cc8df74978cd4c22ef93c5e1856c8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 4 Mar 2026 10:11:40 -0700 Subject: [PATCH 035/468] Copy UI/fix renew token button into main (#12637) (#12722) * Copy https://github.com/hashicorp/vault/pull/31807 into main * ui: hide Renew token button when renew-self capability is denied * Changed test title by mistake * Changed test title by mistake #2 * Moved and updated renew capability fetching * Undo additional removal * final tweaks and removed redundant test * Changelog entry --------- Co-authored-by: lklivingstone Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- changelog/31807.txt | 3 + ui/app/components/token-expire-warning.hbs | 20 ++-- ui/app/components/token-expire-warning.js | 16 ++++ .../components/token-expire-warning-test.js | 94 +++++++++++++++++++ 4 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 changelog/31807.txt diff --git a/changelog/31807.txt b/changelog/31807.txt new file mode 100644 index 0000000000..eaaa7aa67b --- /dev/null +++ b/changelog/31807.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix renew token button rendering for denied renew-self. +``` diff --git a/ui/app/components/token-expire-warning.hbs b/ui/app/components/token-expire-warning.hbs index f70f2a4a2b..5017f31740 100644 --- a/ui/app/components/token-expire-warning.hbs +++ b/ui/app/components/token-expire-warning.hbs @@ -37,15 +37,17 @@ on {{date-format @expirationDate "MMMM do yyyy, h:mm:ss a O"}}. - + {{#if this.canShowRenew}} + + {{/if}} { @@ -36,6 +48,10 @@ export default class TokenExpireWarning extends Component { yield this.handleRenew(); } + get canShowRenew() { + return this.auth?.authData?.renewable && this.canRenewSelf; + } + get queryParams() { // Bring user back to current page after login return { redirect_to: this.router.currentURL }; diff --git a/ui/tests/integration/components/token-expire-warning-test.js b/ui/tests/integration/components/token-expire-warning-test.js index e0fdcc16b7..5780328ad1 100644 --- a/ui/tests/integration/components/token-expire-warning-test.js +++ b/ui/tests/integration/components/token-expire-warning-test.js @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; +import Service from '@ember/service'; import hbs from 'htmlbars-inline-precompile'; import { addMinutes, subMinutes } from 'date-fns'; @@ -70,4 +71,97 @@ module('Integration | Component | token-expire-warning', function (hooks) { assert.dom('[data-test-token-expiring-banner]').doesNotExist('Does not show token expiring banner'); assert.dom('[data-test-content]').hasText('This is the content'); }); + + test('it shows Renew button when token is renewable and capability allowed', async function (assert) { + this.owner.register( + 'service:auth', + class extends Service { + authData = { renewable: true }; + } + ); + + this.owner.register( + 'service:capabilities', + class extends Service { + async fetchPathCapabilities() { + return { canUpdate: true }; + } + } + ); + + const expirationDate = addMinutes(Date.now(), 3); + this.set('expirationDate', expirationDate); + this.set('allowingExpiration', true); + + await render(hbs` + + `); + + assert.dom('[data-test-renew-token-button]').exists(); + }); + + test('it hides Renew button when renew-self capability is denied', async function (assert) { + this.owner.register( + 'service:auth', + class extends Service { + authData = { renewable: true }; + } + ); + + this.owner.register( + 'service:capabilities', + class extends Service { + async fetchPathCapabilities() { + return { canUpdate: false }; + } + } + ); + + const expirationDate = addMinutes(Date.now(), 3); + this.set('expirationDate', expirationDate); + this.set('allowingExpiration', true); + + await render(hbs` + + `); + + assert.dom('[data-test-renew-token-button]').doesNotExist(); + }); + + test('it hides Renew button when token is not renewable', async function (assert) { + this.owner.register( + 'service:auth', + class extends Service { + authData = { renewable: false }; + } + ); + + this.owner.register( + 'service:capabilities', + class extends Service { + async fetchPathCapabilities() { + return { canUpdate: true }; + } + } + ); + + const expirationDate = addMinutes(Date.now(), 3); + this.set('expirationDate', expirationDate); + this.set('allowingExpiration', true); + + await render(hbs` + + `); + + assert.dom('[data-test-renew-token-button]').doesNotExist(); + }); }); From a0b32a5b972d340316d5c5e25cdc0db4faff114e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 4 Mar 2026 10:43:06 -0700 Subject: [PATCH 036/468] add playwright test for PKI engine (#12591) (#12710) * add playwright test for PKI engine * remove disable flow, test download and revoke actions, test overview cards * same intro skip condition Co-authored-by: lane-wetmore --- ui/e2e/tests/superuser/kv.spec.ts | 5 + ui/e2e/tests/superuser/pki.spec.ts | 160 ++++++++++++++++++ ui/e2e/tests/superuser/tools.spec.ts | 2 +- ui/lib/core/addon/components/ttl-picker.hbs | 2 + .../addon/templates/certificates/index.hbs | 2 +- 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 ui/e2e/tests/superuser/pki.spec.ts diff --git a/ui/e2e/tests/superuser/kv.spec.ts b/ui/e2e/tests/superuser/kv.spec.ts index f9c8cd0d63..7249ca4b46 100644 --- a/ui/e2e/tests/superuser/kv.spec.ts +++ b/ui/e2e/tests/superuser/kv.spec.ts @@ -11,6 +11,11 @@ test('kvv2 workflow', async ({ page }) => { await page.goto('dashboard'); // enable kv secrets engine await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + // skip if intro page is shown + const skipButton = page.getByRole('button', { name: 'Skip' }); + if (await skipButton.isVisible()) { + await skipButton.click(); + } await page.getByRole('link', { name: 'Enable new engine' }).click(); await page.locator('div').filter({ hasText: 'KV' }).nth(4).click(); await page.getByRole('textbox', { name: 'Path' }).click(); diff --git a/ui/e2e/tests/superuser/pki.spec.ts b/ui/e2e/tests/superuser/pki.spec.ts new file mode 100644 index 0000000000..075728b7aa --- /dev/null +++ b/ui/e2e/tests/superuser/pki.spec.ts @@ -0,0 +1,160 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('pki workflow', async ({ page }) => { + await page.goto('dashboard'); + // enable PKI Engine + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + // skip if intro page is shown + const skipButton = page.getByRole('button', { name: 'Skip' }); + if (await skipButton.isVisible()) { + await skipButton.click(); + } + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('PKI Certificates - enabled').click(); + await page.getByRole('textbox', { name: 'Path' }).fill('pki-engine'); + await page.locator('label').filter({ hasText: 'Default Lease TTL Vault will' }).click(); + await page.getByLabel('TTL unit for Default Lease TTL').selectOption('m'); + await page + .getByRole('group', { name: 'Default Lease TTL Lease will' }) + .getByLabel('Number of units') + .fill('5'); + await page.getByLabel('TTL unit for Max Lease TTL').selectOption('m'); + await page + .getByRole('group', { name: 'Max Lease TTL Lease will' }) + .getByLabel('Number of units') + .fill('10'); + await page.getByRole('button', { name: 'Enable engine' }).click(); + + // configure PKI Engine + await expect(page.getByRole('heading', { name: 'pki-engine' })).toContainText('pki-engine'); + await expect(page.locator('section')).toContainText('PKI not configured'); + await page.getByRole('link', { name: 'Configure PKI' }).click(); + await expect(page.locator('section')).toContainText('Import a CA'); + await expect(page.locator('section')).toContainText('Generate root'); + await expect(page.locator('section')).toContainText('Generate intermediate CSR'); + await page.locator('label').filter({ hasText: 'Generate root Generates a new' }).click(); + await page.getByLabel('Type').selectOption('internal'); + await page.getByRole('textbox', { name: 'Common name' }).fill('pki-common-name'); + await page.getByRole('textbox', { name: 'Issuer name' }).fill('pki-issuer'); + await page.getByRole('textbox', { name: 'Not valid after' }).fill('36000'); + await page.getByLabel('TTL unit for Not before').selectOption('m'); + await page.getByRole('textbox', { name: 'Number of units' }).fill('10'); + await page.getByLabel('Format', { exact: true }).selectOption('der'); + await page.getByRole('textbox', { name: 'Max path length' }).fill('16'); + await page.getByRole('button', { name: 'Key parameters' }).click(); + await page.getByRole('textbox', { name: 'Key name' }).fill('pki-key'); + await page.getByLabel('Key type').selectOption('ed25519'); + await page.getByRole('button', { name: 'Done' }).click(); + await expect(page.getByRole('heading', { name: 'pki-engine configuration' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'PKI Certificates settings' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Generate policy' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Exit configuration' })).toBeVisible(); + await page.getByRole('link', { name: 'General settings' }).click(); + await expect(page.getByRole('button', { name: 'Generate policy' })).not.toBeVisible(); + await page.getByRole('link', { name: 'pki-engine', exact: true }).click(); + + // create role + await page.getByRole('link', { name: 'Roles', exact: true }).click(); + await page.getByRole('link', { name: 'Create role' }).click(); + await page.getByRole('textbox', { name: 'Role Name' }).fill('pki-role'); + await expect(page.getByLabel('issuer_ref')).toContainText('pki-issuer'); + await page.locator('label').filter({ hasText: 'Use default issuer' }).click(); + await page.getByLabel('TTL unit for TTL').selectOption('m'); + await page + .getByText('TTL Set relative certificate') + .getByRole('textbox', { name: 'Number of units' }) + .fill('30'); + await page.getByLabel('TTL unit for Backdate validity').selectOption('m'); + await page + .getByRole('group', { name: 'Backdate validity Lease will' }) + .getByLabel('Number of units') + .fill('10'); + await page.locator('label').filter({ hasText: 'Max TTL Vault will use the' }).click(); + await page.getByRole('button', { name: 'Domain handling' }).click(); + await page.getByLabel('TTL unit for Max TTL').selectOption('m'); + await page + .getByRole('group', { name: 'Max TTL Lease will expire' }) + .getByLabel('Number of units') + .fill('50'); + await page.getByRole('checkbox', { name: 'Allow any name' }).check(); + await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('link', { name: 'Generate Certificate' }).click(); + await page.getByRole('textbox', { name: 'Common name' }).fill('role-cert'); + await page.getByRole('textbox', { name: 'Number of units' }).fill('1'); + await page.getByLabel('TTL unit for TTL').selectOption('m'); + await page.getByRole('button', { name: 'Generate' }).click(); + await expect(page.getByLabel('Next steps')).toBeVisible(); + // used for testing role certificate revocation as issuer certificates cannot be revoked via the UI currently + const roleSerialNumber = await page.getByText(/^[0-9a-f]{2}(?::[0-9a-f]{2}){9,}$/i).textContent(); + await page.getByLabel('breadcrumbs').getByText('Roles').click(); + await expect(page.locator('section')).toContainText('pki-role'); + + // view certificates + await page.getByRole('link', { name: 'Certificates' }).click(); + await expect(page.getByLabel('certificate serial number')).toHaveCount(2); + await page.getByLabel('certificate serial number').filter({ hasText: roleSerialNumber }).click(); + await expect(page.getByRole('heading', { name: 'View Certificate' })).toContainText('View Certificate'); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Download' }).click(); + await expect(page.getByText('Your download has started')).toBeVisible(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toMatch(/\.pem$/); + await expect(page.getByText('Revocation time')).not.toBeVisible(); + await page.getByRole('button', { name: 'Revoke certificate' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.getByText('Revocation time')).toBeVisible(); + + // generate issuer + await page.getByRole('link', { name: 'pki-engine' }).click(); + await page.getByRole('link', { name: 'Issuers', exact: true }).click(); + await expect(page.getByRole('button', { name: 'Generate policy' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Manage', exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Generate', exact: true }).click(); + await page.getByRole('link', { name: 'Root', exact: true }).click(); + await page.getByLabel('Type').selectOption('exported'); + await page.getByRole('textbox', { name: 'Common name' }).fill('pki-common-name-exported'); + await page.getByRole('textbox', { name: 'Issuer name' }).fill('pki-issuer-exported'); + await page.getByRole('button', { name: 'Done' }).click(); + await expect(page.getByRole('heading', { name: 'View generated root' })).toBeVisible(); + await expect(page.getByLabel('Next steps')).toBeVisible(); + await page.getByRole('button', { name: 'Done' }).click(); + + // generate keys + await page.getByRole('link', { name: 'Keys' }).click(); + await expect(page.locator('section')).toContainText( + 'Below is information about the private keys used by the issuers to sign certificates. While certificates represent a public assertion of an identity, private keys represent the private part of that identity, a secret used to prove who they are and who they trust.' + ); + await expect(page.locator('section')).toContainText('pki-key'); + await expect(page.getByRole('link', { name: 'pki-key' })).toBeVisible(); + await page.getByRole('link', { name: 'Generate' }).click(); + await page.getByRole('textbox', { name: 'Key name' }).fill('pki-generated-key'); + await page.getByLabel('Type', { exact: true }).selectOption('internal'); + await page.getByLabel('Key type').selectOption('rsa'); + await page.getByLabel('Key bits').selectOption('3072'); + await page.getByRole('button', { name: 'Generate key' }).click(); + await expect(page.getByRole('heading', { name: 'View Key' })).toBeVisible(); + await expect(page.locator('section')).toContainText('pki-generated-key'); + + // overview + await page.getByRole('link', { name: 'pki-engine' }).click(); + await expect(page.locator('section')).toContainText( + 'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2' + ); + await expect(page.locator('section')).toContainText( + 'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 1' + ); + await expect(page.getByText('Issue certificate Begin')).toBeVisible(); + await page.getByText('Type to find a role...').click(); + await expect(page.getByRole('option', { name: 'pki-role' })).toBeVisible(); + await expect(page.getByText('View certificate Quickly view')).toBeVisible(); + await page.getByText('33:a3:').click(); + await expect(page.getByRole('option', { name: roleSerialNumber })).toBeVisible(); + await expect(page.getByText('View issuer Choose or type an')).toBeVisible(); + await page.getByText('Type to find an issuer...').click(); + await expect(page.getByRole('option').first()).toBeVisible(); +}); diff --git a/ui/e2e/tests/superuser/tools.spec.ts b/ui/e2e/tests/superuser/tools.spec.ts index 781a57b7c6..f9e25068b3 100644 --- a/ui/e2e/tests/superuser/tools.spec.ts +++ b/ui/e2e/tests/superuser/tools.spec.ts @@ -16,7 +16,7 @@ test('tools workflow', async ({ page }) => { await page.getByRole('textbox', { name: 'Value' }).fill('bar'); await page.locator('label').filter({ hasText: 'Wrap TTL Vault will use the' }).click(); await page.getByRole('textbox', { name: 'Number of units' }).fill('20'); - await page.getByLabel('ttl-unit').selectOption('h'); + await page.getByLabel('TTL unit for Wrap TTL').selectOption('h'); await page.getByRole('button', { name: 'Wrap data' }).click(); await page.getByRole('button', { name: 'copy hvs.' }).click(); // access the clipboard to get the copied token value diff --git a/ui/lib/core/addon/components/ttl-picker.hbs b/ui/lib/core/addon/components/ttl-picker.hbs index 312cde52be..e0f8068c93 100644 --- a/ui/lib/core/addon/components/ttl-picker.hbs +++ b/ui/lib/core/addon/components/ttl-picker.hbs @@ -46,6 +46,7 @@ {{! Select automatically gets id based on name }}
    - + {{cert}}
    From bc2aa7e8ec3e000e3d8904fa85b7a195a3712f01 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 4 Mar 2026 21:20:12 -0500 Subject: [PATCH 037/468] Update minimax (#12742) (#12746) Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/package.json | 5 ++ ui/pnpm-lock.yaml | 140 +++++++++++++++++++--------------------------- 2 files changed, 61 insertions(+), 84 deletions(-) diff --git a/ui/package.json b/ui/package.json index 43b3061ddf..407fb3a84c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -191,6 +191,11 @@ "kind-of": "6.0.3", "markdown-it": "14.1.1", "micromatch": "4.0.8", + "minimatch@<3.1.3": "3.1.5", + "minimatch@>=5.0.0 <5.1.7": "5.1.9", + "minimatch@>=7.0.0 <7.4.7": "7.4.9", + "minimatch@>=8.0.0 <8.0.5": "8.0.7", + "minimatch@>=9.0.0 <9.0.6": "9.0.9", "prismjs": "1.30.0", "qs": "6.14.1", "rollup": "2.79.2", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 80ca695754..e204c4c5c7 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -21,6 +21,11 @@ overrides: kind-of: 6.0.3 markdown-it: 14.1.1 micromatch: 4.0.8 + minimatch@<3.1.3: 3.1.5 + minimatch@>=5.0.0 <5.1.7: 5.1.9 + minimatch@>=7.0.0 <7.4.7: 7.4.9 + minimatch@>=8.0.0 <8.0.5: 8.0.7 + minimatch@>=9.0.0 <9.0.6: 9.0.9 prismjs: 1.30.0 qs: 6.14.1 rollup: 2.79.2 @@ -2693,6 +2698,9 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -6136,23 +6144,23 @@ packages: peerDependencies: webpack: ^5.0.0 - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} - minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + minimatch@7.4.9: + resolution: {integrity: sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==} engines: {node: '>=10'} - minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -10199,7 +10207,7 @@ snapshots: is-subdir: 1.2.0 js-string-escape: 1.0.1 lodash: 4.17.23 - minimatch: 3.1.2 + minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 semver: 7.7.2 @@ -10216,7 +10224,7 @@ snapshots: is-subdir: 1.2.0 js-string-escape: 1.0.1 lodash: 4.17.23 - minimatch: 3.1.2 + minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 resolve.exports: 2.0.3 @@ -10252,7 +10260,7 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -10581,7 +10589,7 @@ snapshots: dependencies: '@humanwhocodes/object-schema': 2.0.3 debug: 4.4.1 - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -11064,26 +11072,6 @@ snapshots: - '@babel/core' - supports-color - '@types/ember@4.0.11': - dependencies: - '@types/ember__application': 4.0.11(@babel/core@7.26.10) - '@types/ember__array': 4.0.10(@babel/core@7.26.10) - '@types/ember__component': 4.0.22 - '@types/ember__controller': 4.0.12(@babel/core@7.26.10) - '@types/ember__debug': 4.0.8(@babel/core@7.26.10) - '@types/ember__engine': 4.0.11(@babel/core@7.26.10) - '@types/ember__error': 4.0.6 - '@types/ember__object': 4.0.12(@babel/core@7.26.10) - '@types/ember__polyfills': 4.0.6 - '@types/ember__routing': 4.0.22 - '@types/ember__runloop': 4.0.10 - '@types/ember__service': 4.0.9(@babel/core@7.26.10) - '@types/ember__string': 3.16.3 - '@types/ember__template': 4.0.7 - '@types/ember__test': 4.0.6(@babel/core@7.26.10) - '@types/ember__utils': 4.0.7 - '@types/rsvp': 4.0.9 - '@types/ember@4.0.11(@babel/core@7.26.10)': dependencies: '@types/ember__application': 4.0.11(@babel/core@7.26.10) @@ -11110,11 +11098,11 @@ snapshots: '@types/ember__application@4.0.11(@babel/core@7.26.10)': dependencies: '@glimmer/component': 1.1.2(@babel/core@7.26.10) - '@types/ember': 4.0.11 + '@types/ember': 4.0.11(@babel/core@7.26.10) '@types/ember__engine': 4.0.11(@babel/core@7.26.10) '@types/ember__object': 4.0.12(@babel/core@7.26.10) '@types/ember__owner': 4.0.9 - '@types/ember__routing': 4.0.22 + '@types/ember__routing': 4.0.22(@babel/core@7.26.10) transitivePeerDependencies: - '@babel/core' - supports-color @@ -11127,11 +11115,6 @@ snapshots: - '@babel/core' - supports-color - '@types/ember__component@4.0.22': - dependencies: - '@types/ember': 4.0.11 - '@types/ember__object': 4.0.12(@babel/core@7.26.10) - '@types/ember__component@4.0.22(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11177,13 +11160,6 @@ snapshots: '@types/ember__polyfills@4.0.6': {} - '@types/ember__routing@4.0.22': - dependencies: - '@types/ember': 4.0.11 - '@types/ember__controller': 4.0.12(@babel/core@7.26.10) - '@types/ember__object': 4.0.12(@babel/core@7.26.10) - '@types/ember__service': 4.0.9(@babel/core@7.26.10) - '@types/ember__routing@4.0.22(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11194,10 +11170,6 @@ snapshots: - '@babel/core' - supports-color - '@types/ember__runloop@4.0.10': - dependencies: - '@types/ember': 4.0.11 - '@types/ember__runloop@4.0.10(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -11225,10 +11197,6 @@ snapshots: - '@babel/core' - supports-color - '@types/ember__utils@4.0.7': - dependencies: - '@types/ember': 4.0.11 - '@types/ember__utils@4.0.7(@babel/core@7.26.10)': dependencies: '@types/ember': 4.0.11(@babel/core@7.26.10) @@ -12070,6 +12038,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -12080,7 +12052,7 @@ snapshots: broccoli-filter: 1.3.0 broccoli-persistent-filter: 1.4.6 json-stable-stringify: 1.3.0 - minimatch: 3.1.2 + minimatch: 3.1.5 rsvp: 3.6.2 transitivePeerDependencies: - supports-color @@ -12271,7 +12243,7 @@ snapshots: fast-ordered-set: 1.0.3 fs-tree-diff: 0.5.9 heimdalljs: 0.2.6 - minimatch: 3.1.2 + minimatch: 3.1.5 mkdirp: 0.5.6 path-posix: 1.0.0 rimraf: 2.7.1 @@ -12289,7 +12261,7 @@ snapshots: fast-ordered-set: 1.0.3 fs-tree-diff: 0.5.9 heimdalljs: 0.2.6 - minimatch: 3.1.2 + minimatch: 3.1.5 mkdirp: 0.5.6 path-posix: 1.0.0 rimraf: 2.7.1 @@ -12305,7 +12277,7 @@ snapshots: debug: 4.4.1 fs-tree-diff: 2.0.1 heimdalljs: 0.2.6 - minimatch: 3.1.2 + minimatch: 3.1.5 walk-sync: 2.2.0 transitivePeerDependencies: - supports-color @@ -12507,7 +12479,7 @@ snapshots: debug: 4.4.1 ensure-posix-path: 1.1.1 fs-extra: 8.1.0 - minimatch: 3.1.2 + minimatch: 3.1.5 resolve: 1.22.10 rsvp: 4.8.5 symlink-or-copy: 1.3.1 @@ -13572,7 +13544,7 @@ snapshots: js-string-escape: 1.0.1 lodash: 4.17.23 mini-css-extract-plugin: 2.9.2(webpack@5.94.0) - minimatch: 3.1.2 + minimatch: 3.1.5 parse5: 6.0.1 pkg-entry-points: 1.1.1 resolve: 1.22.10 @@ -14072,7 +14044,7 @@ snapshots: lodash: 4.17.23 markdown-it: 14.1.1 markdown-it-terminal: 0.4.0(markdown-it@14.1.1) - minimatch: 7.4.6 + minimatch: 7.4.9 morgan: 1.10.0 nopt: 3.0.6 npm-package-arg: 10.1.0 @@ -14880,7 +14852,7 @@ snapshots: ignore: 5.3.2 is-builtin-module: 3.2.1 is-core-module: 2.16.1 - minimatch: 3.1.2 + minimatch: 3.1.5 resolve: 1.22.10 semver: 7.7.2 @@ -14954,7 +14926,7 @@ snapshots: json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 strip-ansi: 6.0.1 @@ -15296,7 +15268,7 @@ snapshots: is-type: 0.0.1 lodash.debounce: 3.1.1 lodash.flatten: 3.0.2 - minimatch: 3.1.2 + minimatch: 3.1.5 fixturify-project@1.10.0: dependencies: @@ -15592,7 +15564,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.9 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -15601,7 +15573,7 @@ snapshots: dependencies: inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -15610,7 +15582,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -15619,13 +15591,13 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.6 + minimatch: 5.1.9 once: 1.4.0 glob@9.3.5: dependencies: fs.realpath: 1.0.0 - minimatch: 8.0.4 + minimatch: 8.0.7 minipass: 4.2.8 path-scurry: 1.11.1 @@ -16617,12 +16589,12 @@ snapshots: matcher-collection@1.1.2: dependencies: - minimatch: 3.1.2 + minimatch: 3.1.5 matcher-collection@2.0.1: dependencies: '@types/minimatch': 3.0.5 - minimatch: 3.1.2 + minimatch: 3.1.5 math-intrinsics@1.1.0: {} @@ -16848,25 +16820,25 @@ snapshots: tapable: 2.2.2 webpack: 5.94.0(webpack-cli@6.0.1) - minimatch@3.1.2: + minimatch@3.1.5: dependencies: brace-expansion: 1.1.11 - minimatch@5.1.6: + minimatch@5.1.9: dependencies: brace-expansion: 2.0.1 - minimatch@7.4.6: + minimatch@7.4.9: + dependencies: + brace-expansion: 2.0.2 + + minimatch@8.0.7: dependencies: brace-expansion: 2.0.1 - minimatch@8.0.4: + minimatch@9.0.9: dependencies: - brace-expansion: 2.0.1 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -18931,14 +18903,14 @@ snapshots: '@types/minimatch': 3.0.5 ensure-posix-path: 1.1.1 matcher-collection: 2.0.1 - minimatch: 3.1.2 + minimatch: 3.1.5 walk-sync@3.0.0: dependencies: '@types/minimatch': 3.0.5 ensure-posix-path: 1.1.1 matcher-collection: 2.0.1 - minimatch: 3.1.2 + minimatch: 3.1.5 walker@1.0.8: dependencies: From b25410c747cd3b8da00c8302d9136b588b7b8295 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 05:02:20 -0500 Subject: [PATCH 038/468] VAULT-42603: SCIM guardrails for identity resources (#12626) (#12748) * base * unit tests * group tests * groups test * entity test * alias test and fix error code * fix error message * lint --------- Co-authored-by: miagilepner Co-authored-by: Kuba Wieczorek --- helper/identity/identity.go | 12 +++++ vault/identity_store_aliases.go | 11 +++++ vault/identity_store_entities.go | 5 +- vault/identity_store_entities_update.go | 16 ++++-- vault/identity_store_groups.go | 14 ++++-- vault/identity_store_scim_schema.go | 66 ++++++++++++++++++++++++- 6 files changed, 115 insertions(+), 9 deletions(-) diff --git a/helper/identity/identity.go b/helper/identity/identity.go index 8c04401536..9f9f84f2ad 100644 --- a/helper/identity/identity.go +++ b/helper/identity/identity.go @@ -192,3 +192,15 @@ func ToSDKGroups(groups []*Group) []*logical.Group { } return ret } + +func (g *Group) SCIMClientID() string { + return g.ScimClientID +} + +func (e *Entity) SCIMClientID() string { + return e.ScimClientID +} + +func (a *Alias) SCIMClientID() string { + return a.ScimClientID +} diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index 4bce45ac50..7697b441aa 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -279,6 +279,9 @@ func (i *IdentityStore) handleAliasCreate(ctx context.Context, canonicalID, name return nil, err } + if err := i.scimResourceCheck(ctx, &identity.Alias{ScimClientID: scimClientID}, "", true); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } var entity *identity.Entity if canonicalID != "" { entity, err = i.MemDBEntityByID(canonicalID, true) @@ -370,6 +373,9 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name return nil, nil } + if err := i.scimResourceCheck(ctx, alias, alias.ScimClientID, false); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } alias.LastUpdateTime = timestamppb.Now() // Get our current entity, which may be the same as the new one if the @@ -604,6 +610,11 @@ func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc { return logical.ErrorResponse("request and alias are in different namespaces"), logical.ErrPermissionDenied } + scimClientID := scimClientIDFromContext(ctx) + if alias.ScimClientID != scimClientID { + return logical.ErrorResponse("SCIM-managed resources must be modified through SCIM"), logical.ErrPermissionDenied + } + // Fetch the associated entity entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true) if err != nil { diff --git a/vault/identity_store_entities.go b/vault/identity_store_entities.go index b95e909c7f..50e2884be7 100644 --- a/vault/identity_store_entities.go +++ b/vault/identity_store_entities.go @@ -597,7 +597,10 @@ func (i *IdentityStore) handleEntityDeleteCommon(ctx context.Context, txn *memdb if entity.NamespaceID != ns.ID { return nil } - + scimClientID := scimClientIDFromContext(ctx) + if entity.ScimClientID != scimClientID { + return errors.New("SCIM-managed resources must be modified through SCIM") + } // Remove entity ID as a member from all the groups it belongs, both // internal and external groups, err := i.MemDBGroupsByMemberEntityIDInTxn(txn, entity.ID, true, false) diff --git a/vault/identity_store_entities_update.go b/vault/identity_store_entities_update.go index a74d04274f..bee8cbdcc8 100644 --- a/vault/identity_store_entities_update.go +++ b/vault/identity_store_entities_update.go @@ -15,10 +15,11 @@ import ( // EntityBuilder is used to construct or update an identity.Entity. type EntityBuilder struct { - store *IdentityStore - entity *identity.Entity - isNew bool - err error + store *IdentityStore + entity *identity.Entity + isNew bool + originalSCIMID string + err error } // NewEntityBuilder creates a new builder instance. @@ -57,6 +58,7 @@ func (b *EntityBuilder) WithID(id string) *EntityBuilder { } b.entity = entity + b.originalSCIMID = b.entity.ScimClientID b.isNew = false return b } @@ -76,6 +78,7 @@ func (b *EntityBuilder) WithExternalID(ctx context.Context, externalID string) * if entityByExternalID != nil { // An entity with this external ID already exists, so we'll update it. b.entity = entityByExternalID + b.originalSCIMID = b.entity.ScimClientID b.isNew = false } else { // No entity found, so we're just setting the external ID on the current one. @@ -103,6 +106,7 @@ func (b *EntityBuilder) WithName(ctx context.Context, name string) *EntityBuilde case b.isNew: // We haven't loaded an entity yet, but one with this name exists. Let's update it. b.entity = entityByName + b.originalSCIMID = b.entity.ScimClientID b.isNew = false case b.entity.ID == entityByName.ID: // The loaded entity and the one found by name are the same. No-op. @@ -167,6 +171,10 @@ func (b *EntityBuilder) Build(ctx context.Context) (*logical.Response, error) { return logical.ErrorResponse(b.err.Error()), nil } + if err := b.store.scimResourceCheck(ctx, b.entity, b.originalSCIMID, b.isNew); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } + // Sanitize and persist the entity if err := b.store.sanitizeEntity(ctx, b.entity); err != nil { return nil, err diff --git a/vault/identity_store_groups.go b/vault/identity_store_groups.go index c475c566c6..c51da8a5e2 100644 --- a/vault/identity_store_groups.go +++ b/vault/identity_store_groups.go @@ -313,12 +313,15 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica group.Name = groupName } - _, ok = d.Schema["scim_client_id"] + originalSCIMID := group.ScimClientID + scimClientID, ok := d.GetOk("scim_client_id") if ok { - entitSCIMClientID := d.Get("scim_client_id").(string) - group.ScimClientID = entitSCIMClientID + group.ScimClientID = scimClientID.(string) } + if err := i.scimResourceCheck(ctx, group, originalSCIMID, newGroup); err != nil { + return logical.ErrorResponse(err.Error()), nil + } metadata, ok, err := d.GetOkErr("metadata") if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to parse metadata: %v", err)), nil @@ -525,6 +528,11 @@ func (i *IdentityStore) handleGroupDeleteCommon(ctx context.Context, key string, return logical.ErrorResponse("request namespace is not the same as the group namespace"), logical.ErrPermissionDenied } + scimID := scimClientIDFromContext(ctx) + if scimID != group.ScimClientID { + return logical.ErrorResponse("SCIM-managed resources must be modified through SCIM"), logical.ErrPermissionDenied + } + // Delete group alias from memdb if group.Type == groupTypeExternal && group.Alias != nil { err = i.MemDBDeleteAliasByIDInTxn(txn, group.Alias.ID, true) diff --git a/vault/identity_store_scim_schema.go b/vault/identity_store_scim_schema.go index 9a9393f66a..8c672dc7a4 100644 --- a/vault/identity_store_scim_schema.go +++ b/vault/identity_store_scim_schema.go @@ -3,7 +3,13 @@ package vault -import "github.com/hashicorp/go-memdb" +import ( + "context" + "errors" + + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/vault/helper/identity" +) // SCIM client storage prefix const scimClientStoragePrefix = "scim/client/" @@ -69,3 +75,61 @@ func scimClientSchema(_ bool) *memdb.TableSchema { }, } } + +type scimClientRequest struct{} + +func addSCIMClientIDToContext(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, scimClientRequest{}, id) +} + +func scimClientIDFromContext(ctx context.Context) string { + val := ctx.Value(scimClientRequest{}) + if val == nil { + return "" + } + return val.(string) +} + +func (i *IdentityStore) scimResourceCheck(ctx context.Context, resource scimManaged, originalSCIMID string, isCreate bool) error { + reqSCIMClientID := scimClientIDFromContext(ctx) + resourceSCIMClientID := resource.SCIMClientID() + + switch isCreate { + case true: + // The request must have come via a SCIM API in order to set + // the SCIM client ID + if reqSCIMClientID == "" && resourceSCIMClientID != "" { + return errors.New("cannot set scim_client_id") + } + if reqSCIMClientID != "" && resourceSCIMClientID == "" { + // this shouldn't ever happen + return errors.New("cannot create a resource via SCIM without a SCIM client ID") + } + if reqSCIMClientID != resourceSCIMClientID { + // this also shouldn't ever happen + return errors.New("cannot create resource via SCIM with a different SCIM client ID") + } + + default: + // if the resource is being updated, the SCIM client ID + // cannot be modified + if originalSCIMID != resourceSCIMClientID { + return errors.New("cannot update scim_client_id") + } + // if the resource is being updated, this must be via SCIM + if originalSCIMID != reqSCIMClientID { + return errors.New("SCIM-managed resources must be modified through SCIM") + } + } + return nil +} + +type scimManaged interface { + SCIMClientID() string +} + +var ( + _ scimManaged = (*identity.Entity)(nil) + _ scimManaged = (*identity.Group)(nil) + _ scimManaged = (*identity.Alias)(nil) +) From f5dbe55f55433876e4cee1aa66cba2d02617f433 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 10:25:59 -0500 Subject: [PATCH 039/468] VAULT-42657: Merge feature branch for OAuth and Agent Registry into main (#12587) (#12754) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * [POC] Accept and Validate External OAuth Token for Agent Identity (#10991) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * Add JWT token tidy cleanup functionality * Refactor JWT tidy tests to use retryUntil instead of time.Sleep * feat(identity)(Accept OAuth JWT tokens): fix leaky lease * feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE * Add TestLogicalWithJwtAndSCIM e2e test * feat(identity)(Accept OAuth JWT tokens): add more tests * feat(identity)(Accept OAuth JWT tokens): change email * feat(identity)(Accept OAuth JWT tokens): e2e test for scim * Accept and validate RAR (#11005) * fix(identity)(Accept OAuth JWT tokens): add missing headers --------- * wip * wip * wip * feat(identity): auth with delegation jwts * feat(identity): optimize jwks fetching * wip * add lookup by entity-id * feat(identity): optimize jwks fetching * feat(identity): optimize jwks fetching and refactoring * feat(identity): fix breaking tests * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * [POC] Accept and Validate External OAuth Token for Agent Identity (#10991) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * Add JWT token tidy cleanup functionality * Refactor JWT tidy tests to use retryUntil instead of time.Sleep * feat(identity)(Accept OAuth JWT tokens): fix leaky lease * feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE * Add TestLogicalWithJwtAndSCIM e2e test * feat(identity)(Accept OAuth JWT tokens): add more tests * feat(identity)(Accept OAuth JWT tokens): change email * feat(identity)(Accept OAuth JWT tokens): e2e test for scim * Accept and validate RAR (#11005) * fix(identity)(Accept OAuth JWT tokens): add missing headers --------- * feat(identity): auth with delegation jwts * feat(identity): optimize jwks fetching * wip * wip * wip * wip * add lookup by entity-id * feat(identity): optimize jwks fetching * feat(identity): optimize jwks fetching and refactoring * feat(identity): fix breaking tests * VAULT-42642 Fix feature branch tests, some CE -> Ent moving (#12417) * VAULT-42642 Fix feathre branch tests, some CE -> Ent moving * VAULT-42642 fix last test? * skip test * proto * more test fixes * buf format * Fix createVaultEntityForUser signature (#12439) * VAULT-42533 Agent Registry: enforce entity invariants, miscellaneous improvements (#12399) * VAULT-42533 enforce entity invariants, miscellaneous improvements * typos * Improve path handling for rar + add tests (#11934) * Moving stuff around for CE * fmt * backend * more CE changes * more fixes * further fixes * rework clone * VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files (#12460) * VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files * logical * skip failing tests * more skips * missed tests * one more * VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise (#12473) * VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise * test fixes * feedback * VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring (#12509) * VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring * godocs * VAULT-42779 refactor JWT parts of acl.go to enterprise files (#12512) * VAULT-42630 move token tidy for JWT to ent files (#12534) * VAULT-42818 rename jwtjti in request struct (#12539) * VAULT-42633 CE-ify request handling and flow around validateJwtAndFetchEntity (#12541) * VAULT-42633 CE-ify request handling * Copyright headers * VAULT-42633 Move IsJWT back to CE code, add explanations (#12576) * VAULT-42633 CE-ify request handling * Copyright headers * VAULT-42633 Move IsJWT back to CE code, add explanations * VAULT-42870 Move jwtAuthManager to enterprise (#12586) * VAULT-42796 Move config to enterprise files (#12604) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAULT42796 - fix missing return (#12617) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAUlt-42796 fix test * Fix linting for rar_ent (#12618) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAUlt-42796 fix test * Fix linter for rar code * fix authresults * typo * return error * fix method signature --------- Signed-off-by: Arnab Chatterjee Co-authored-by: Violet Hynes Co-authored-by: Arnab Chatterjee Co-authored-by: Arnab Chatterjee Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: davidadeleon Co-authored-by: Bianca Moreira --- sdk/helper/consts/token_consts_ce.go | 10 +++++ sdk/logical/request.go | 3 ++ sdk/logical/token.go | 8 +++- sdk/logical/token_ce.go | 10 +++++ sdk/logical/tokentype_enumer.go | 7 +-- vault/acl.go | 8 ++++ vault/acl_ce.go | 21 +++++++++ vault/request_handling.go | 65 +++++++++++++++++++++++----- vault/request_handling_ce.go | 30 +++++++++++++ vault/router.go | 4 +- vault/token_store.go | 41 +++++++++++++++--- vault/token_store_ce.go | 21 +++++++++ vault/version_store.go | 3 ++ vault/version_store_ce.go | 10 +++++ vault/wrapping.go | 10 +---- 15 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 sdk/helper/consts/token_consts_ce.go create mode 100644 sdk/logical/token_ce.go create mode 100644 vault/acl_ce.go create mode 100644 vault/request_handling_ce.go create mode 100644 vault/token_store_ce.go create mode 100644 vault/version_store_ce.go diff --git a/sdk/helper/consts/token_consts_ce.go b/sdk/helper/consts/token_consts_ce.go new file mode 100644 index 0000000000..ff9902fe0c --- /dev/null +++ b/sdk/helper/consts/token_consts_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: MPL-2.0 + +//go:build !enterprise + +package consts + +func GetEnterpriseTokenPrefix() string { + return "unimplemented" +} diff --git a/sdk/logical/request.go b/sdk/logical/request.go index 4ba4bd4043..596d97c5db 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -137,6 +137,9 @@ type Request struct { // hashed. ClientToken string `json:"client_token" structs:"client_token" mapstructure:"client_token" sentinel:""` + // EnterpriseTokenMetadata is used to store metadata related to enterprise token based requests. + EnterpriseTokenMetadata string `json:"enterprise_token_metadata" structs:"enterprise_token_metadata" mapstructure:"enterprise_token_metadata" sentinel:""` + // ClientTokenAccessor is provided to the core so that the it can get // logged as part of request audit logging. ClientTokenAccessor string `json:"client_token_accessor" structs:"client_token_accessor" mapstructure:"client_token_accessor" sentinel:""` diff --git a/sdk/logical/token.go b/sdk/logical/token.go index f2caca9c9d..f1aad686f4 100644 --- a/sdk/logical/token.go +++ b/sdk/logical/token.go @@ -36,6 +36,8 @@ const ( // TokenTypeDefault is sent back by the mount, create Batch tokens TokenTypeDefaultBatch + TokenTypeEnt + // ClientIDTWEDelimiter Delimiter between the string fields used to generate a client // ID for tokens without entities. This is the 0 character, which // is a non-printable string. Please see unicode.IsPrint for details. @@ -67,6 +69,8 @@ func (t *TokenType) UnmarshalJSON(b []byte) error { *t = TokenTypeDefaultService case `"default-batch"`: *t = TokenTypeDefaultBatch + case `"ent"`: + *t = TokenTypeEnt default: return fmt.Errorf("unknown token type %q", s) } @@ -75,6 +79,8 @@ func (t *TokenType) UnmarshalJSON(b []byte) error { // TokenEntry is used to represent a given token type TokenEntry struct { + EntToken + Type TokenType `json:"type" mapstructure:"type" structs:"type" sentinel:""` // ID of this entry, generally a random UUID @@ -253,7 +259,7 @@ func (te *TokenEntry) SentinelGet(key string) (interface{}, error) { case "type": teType := te.Type switch teType { - case TokenTypeBatch, TokenTypeService: + case TokenTypeBatch, TokenTypeService, TokenTypeEnt: case TokenTypeDefault: teType = TokenTypeService default: diff --git a/sdk/logical/token_ce.go b/sdk/logical/token_ce.go new file mode 100644 index 0000000000..b5fff2a7dd --- /dev/null +++ b/sdk/logical/token_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: MPL-2.0 + +//go:build !enterprise + +package logical + +type ( + EntToken struct{} +) diff --git a/sdk/logical/tokentype_enumer.go b/sdk/logical/tokentype_enumer.go index 9b350a74d3..39156d7cb3 100644 --- a/sdk/logical/tokentype_enumer.go +++ b/sdk/logical/tokentype_enumer.go @@ -6,9 +6,9 @@ import ( "fmt" ) -const _TokenTypeName = "defaultservicebatchdefault-servicedefault-batch" +const _TokenTypeName = "defaultservicebatchdefault-servicedefault-batchent" -var _TokenTypeIndex = [...]uint8{0, 7, 14, 19, 34, 47} +var _TokenTypeIndex = [...]uint8{0, 7, 14, 19, 34, 47, 50} func (i TokenType) String() string { if i >= TokenType(len(_TokenTypeIndex)-1) { @@ -17,7 +17,7 @@ func (i TokenType) String() string { return _TokenTypeName[_TokenTypeIndex[i]:_TokenTypeIndex[i+1]] } -var _TokenTypeValues = []TokenType{0, 1, 2, 3, 4} +var _TokenTypeValues = []TokenType{0, 1, 2, 3, 4, 5} var _TokenTypeNameToValueMap = map[string]TokenType{ _TokenTypeName[0:7]: 0, @@ -25,6 +25,7 @@ var _TokenTypeNameToValueMap = map[string]TokenType{ _TokenTypeName[14:19]: 2, _TokenTypeName[19:34]: 3, _TokenTypeName[34:47]: 4, + _TokenTypeName[47:50]: 5, } // TokenTypeString retrieves an enum value from the enum constants string name. diff --git a/vault/acl.go b/vault/acl.go index 84c6de6649..561abcd735 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -26,6 +26,8 @@ import ( // ACL is used to wrap a set of policies to provide // an efficient interface for access control. type ACL struct { + entAcl + // exactRules contains the path policies that are exact exactRules *radix.Tree @@ -49,6 +51,7 @@ type PolicyCheckOpts struct { } type AuthResults struct { + entAuthResults ACLResults *ACLResults SentinelResults *SentinelResults Allowed bool @@ -358,6 +361,11 @@ func (a *ACL) Capabilities(ctx context.Context, path string) []string { // AllowOperation is used to check if the given operation is permitted. func (a *ACL) AllowOperation(ctx context.Context, req *logical.Request, capCheckOnly bool) (ret *ACLResults) { + ret = a.performEnterpriseAclChecks(ctx, req, capCheckOnly) + if ret != nil { + return ret + } + ret = new(ACLResults) // Fast-path root diff --git a/vault/acl_ce.go b/vault/acl_ce.go new file mode 100644 index 0000000000..c352b1288a --- /dev/null +++ b/vault/acl_ce.go @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + + "github.com/hashicorp/vault/sdk/logical" +) + +type ( + entAcl struct{} + entAuthResults struct{} +) + +func (a *ACL) performEnterpriseAclChecks(_ context.Context, _ *logical.Request, _ bool) (ret *ACLResults) { + return nil +} diff --git a/vault/request_handling.go b/vault/request_handling.go index 0ec4cc8a33..1195119f5c 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -101,8 +101,6 @@ func (c *Core) fetchEntityAndDerivedPolicies(ctx context.Context, tokenNS *names policies := make(map[string][]string) if !skipDeriveEntityPolicies { - // c.logger.Debug("entity successfully fetched; adding entity policies to token's policies to create ACL") - // Attach the policies on the entity if len(entity.Policies) != 0 { policies[entity.NamespaceID] = append(policies[entity.NamespaceID], entity.Policies...) @@ -239,12 +237,33 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, ErrInternalError } + var secondEntity *identity.Entity + if IsEnterpriseToken(req.ClientToken) { + isValidEnterpriseToken, tokenMetadataContainer, entity, entity2, err := c.validateEnterpriseTokenAndFetchEntity(ctx, req.ClientToken) + if err != nil { + c.logger.Error("failed to validate enterprise token", "error", err) + } + if !isValidEnterpriseToken { + return nil, nil, nil, nil, logical.ErrPermissionDenied + } + req.EnterpriseTokenMetadata = getEnterpriseTokenMetadata(tokenMetadataContainer) + secondEntity = entity2 + err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity) + if err != nil { + return nil, nil, nil, nil, multierror.Append(err, errors.New("failed in processing enterprise token")) + } + } + // Resolve the token policy var te *logical.TokenEntry switch req.TokenEntry() { case nil: var err error - te, err = c.tokenStore.Lookup(ctx, req.ClientToken) + if IsEnterpriseToken(req.ClientToken) { + te, err = c.tokenStore.Lookup(ctx, getEnterpriseTokenId(req.EnterpriseTokenMetadata)) + } else { + te, err = c.tokenStore.Lookup(ctx, req.ClientToken) + } if err != nil { c.logger.Error("failed to lookup acl token", "error", err) return nil, nil, nil, nil, ErrInternalError @@ -304,6 +323,21 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req policyNames[nsID] = policyutil.SanitizePolicies(append(policyNames[nsID], nsPolicies...), false) } + var secondEntityPolicyNames map[string][]string + if secondEntity != nil { + c.logger.Debug("building separate ACL for second entity", "entity_id", secondEntity.ID) + secondEntityPolicyNames = make(map[string][]string) + _, secondEntityIdentityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, tokenNS, secondEntity.ID, false) + if err != nil { + c.logger.Error("failed to fetch second entity policies", "error", err) + return nil, nil, nil, nil, ErrInternalError + } + // Store second entity policies separately - do NOT merge with primary entity's policies + for nsID, nsPolicies := range secondEntityIdentityPolicies { + secondEntityPolicyNames[nsID] = policyutil.SanitizePolicies(nsPolicies, false) + } + } + // Attach token's namespace information to the context. Wrapping tokens by // should be able to be used anywhere, so we also special case behavior. var tokenCtx context.Context @@ -344,6 +378,14 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, ErrInternalError } + if secondEntity != nil && len(secondEntityPolicyNames) > 0 { + newAcl, err := c.performSecondaryEntityTokenChecks(tokenCtx, acl, secondEntity, secondEntityPolicyNames) + if err != nil { + return nil, nil, nil, nil, err + } + acl = newAcl + } + return acl, te, entity, identityPolicies, nil } @@ -804,7 +846,7 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request // We don't care if the token is a server side consistent token or not. Either way, we're going // to be returning it for these paths instead of the short token stored in vault. requestBodyToken = token.(string) - if IsSSCToken(token.(string)) { + if IsSSCToken(token.(string)) && !IsEnterpriseToken(token.(string)) { token, err = c.CheckSSCToken(ctx, token.(string), c.isLoginRequest(ctx, req), c.perfStandby) // If we receive an error from CheckSSCToken, we can assume the token is bad somehow, and the client // should receive a 403 bad token error like they do for all other invalid tokens, unless the error @@ -1096,12 +1138,15 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp return } + var auth *logical.Auth + var te *logical.TokenEntry + var ctErr error // Validate the token - auth, te, ctErr := c.CheckToken(ctx, req, false) - if ctErr == logical.ErrRelativePath { + auth, te, ctErr = c.CheckToken(ctx, req, false) + if errors.Is(ctErr, logical.ErrRelativePath) { return logical.ErrorResponse(ctErr.Error()), nil, ctErr } - if ctErr == logical.ErrPerfStandbyPleaseForward { + if errors.Is(ctErr, logical.ErrPerfStandbyPleaseForward) { return nil, nil, ctErr } @@ -2693,7 +2738,7 @@ func (c *Core) LocalUpdateUserFailedLoginInfo(ctx context.Context, userKey Faile // PopulateTokenEntry looks up req.ClientToken in the token store and uses // it to set other fields in req. Does nothing if ClientToken is empty -// or a JWT token, or for service tokens that don't exist in the token store. +// or an Enterprise token, or for service tokens that don't exist in the token store. // Should be called with read stateLock held. func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) error { if req.ClientToken == "" { @@ -2704,7 +2749,7 @@ func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) err // doesn't exist because the request may be to an unauthenticated // endpoint/login endpoint where a bad current token doesn't matter, or // a token from a Vault version pre-accessors. We ignore errors for - // JWTs. + // Enterprise tokens. token := req.ClientToken var err error req.InboundSSCToken = token @@ -2754,8 +2799,6 @@ func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) err if errors.Is(err, logical.ErrPerfStandbyPleaseForward) || errors.Is(err, logical.ErrMissingRequiredState) { return err } - // If we have two dots but the second char is a dot it's a vault - // token of the form s.SOMETHING.nsid, not a JWT if !IsJWT(token) { return fmt.Errorf("error performing token check: %w", err) } diff --git a/vault/request_handling_ce.go b/vault/request_handling_ce.go new file mode 100644 index 0000000000..cbf26bae73 --- /dev/null +++ b/vault/request_handling_ce.go @@ -0,0 +1,30 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + "errors" + + "github.com/hashicorp/vault/helper/identity" + "github.com/hashicorp/vault/sdk/logical" +) + +func (c *Core) validateEnterpriseTokenAndFetchEntity(ctx context.Context, tokenString string) (bool, map[string]interface{}, *identity.Entity, *identity.Entity, error) { + return false, nil, nil, nil, errors.New("not implemented") +} + +func (c *Core) createAndStoreEnterpriseTokenEntry(ctx context.Context, req *logical.Request, allClaims map[string]interface{}, entity *identity.Entity) error { + return nil +} + +func getEnterpriseTokenMetadata(_ map[string]interface{}) string { + return "" +} + +func (c *Core) performSecondaryEntityTokenChecks(_ context.Context, _ *ACL, _ *identity.Entity, _ map[string][]string) (*ACL, error) { + return nil, errors.New("not implemented") +} diff --git a/vault/router.go b/vault/router.go index d017cdbd8a..3db7fb6caa 100644 --- a/vault/router.go +++ b/vault/router.go @@ -677,8 +677,8 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc return nil, false, false, fmt.Errorf("nil token entry") } - if te.Type != logical.TokenTypeService { - return logical.ErrorResponse(`cubbyhole operations are only supported by "service" type tokens`), false, false, nil + if te.Type != logical.TokenTypeService && te.Type != logical.TokenTypeEnt { + return logical.ErrorResponse(`cubbyhole operations are only supported by "service" or enterprise type tokens`), false, false, nil } switch { diff --git a/vault/token_store.go b/vault/token_store.go index c70f585003..eb0aa81852 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -1107,9 +1107,11 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err } switch entry.Type { - case logical.TokenTypeDefault, logical.TokenTypeService: + case logical.TokenTypeDefault, logical.TokenTypeService, logical.TokenTypeEnt: // In case it was default, force to service - entry.Type = logical.TokenTypeService + if entry.Type == logical.TokenTypeDefault { + entry.Type = logical.TokenTypeService + } // Generate an ID if necessary userSelectedID := true @@ -1126,7 +1128,7 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err } } - if userSelectedID { + if userSelectedID && entry.Type != logical.TokenTypeEnt { switch { case strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix): return fmt.Errorf("custom token ID cannot have the 'hvs.' prefix") @@ -1151,7 +1153,10 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err entry.ID = fmt.Sprintf("%s.%s", entry.ID, tokenNS.ID) } - if tokenNS.ID != namespace.RootNamespaceID || strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix) || strings.HasPrefix(entry.ID, consts.LegacyServiceTokenPrefix) { + if tokenNS.ID != namespace.RootNamespaceID || + strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix) || + strings.HasPrefix(entry.ID, consts.LegacyServiceTokenPrefix) || + strings.HasPrefix(entry.ID, consts.GetEnterpriseTokenPrefix()) { if entry.CubbyholeID == "" { cubbyholeID, err := base62.Random(TokenLength) if err != nil { @@ -1163,7 +1168,7 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err // If the user didn't specifically pick the ID, e.g. because they were // sudo/root, check for collision; otherwise trust the process - if userSelectedID { + if userSelectedID || entry.Type == logical.TokenTypeEnt { exist, _ := ts.lookupInternal(ctx, entry.ID, false, true) if exist != nil { return fmt.Errorf("cannot create a token with a duplicate ID") @@ -1636,7 +1641,7 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai // If possible, always use the token's namespace. If it doesn't match // the request namespace, ensure the request namespace is a child _, nsID := namespace.SplitIDFromString(id) - if nsID != "" { + if nsID != "" || strings.HasPrefix(id, consts.GetEnterpriseTokenPrefix()) { tokenNS, err := NamespaceByID(ctx, nsID, ts.core) if err != nil { return nil, fmt.Errorf("failed to look up namespace from the token: %w", err) @@ -1753,6 +1758,11 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai } } + // don't check for lease + if entry.Type == logical.TokenTypeEnt { + return entry, nil + } + le, err := ts.expiration.FetchLeaseTimesByToken(ctx, entry) if err != nil { return nil, fmt.Errorf("failed to fetch lease times: %w", err) @@ -2266,6 +2276,11 @@ func (ts *TokenStore) handleTidy(ctx context.Context, req *logical.Request, data return fmt.Errorf("failed to fetch cubbyhole storage keys: %w", err) } + err = ts.handleTidyEnterpriseTokens(quitCtx, ns, tidyErrors) + if err != nil { + return err + } + var countParentEntries, deletedCountParentEntries, countParentList, deletedCountParentList int64 // Scan through the secondary index entries; if there is an entry @@ -3321,6 +3336,9 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request } func (ts *TokenStore) revokeCommon(ctx context.Context, req *logical.Request, data *framework.FieldData, id string) (*logical.Response, error) { + if IsEnterpriseToken(id) { + return logical.ErrorResponse("cannot revoke ent token"), nil + } te, err := ts.Lookup(ctx, id) if err != nil { return nil, err @@ -3365,6 +3383,10 @@ func (ts *TokenStore) handleRevokeOrphan(ctx context.Context, req *logical.Reque return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } + if IsEnterpriseToken(id) { + return logical.ErrorResponse("enterprise token cannot be revoked"), nil + } + // Do a lookup. Among other things, that will ensure that this is either // running in the same namespace or a parent. te, err := ts.Lookup(ctx, id) @@ -3402,7 +3424,9 @@ func (ts *TokenStore) handleLookup(ctx context.Context, req *logical.Request, da if id == "" { return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } - + if IsEnterpriseToken(id) { + id = getEnterpriseTokenId(req.EnterpriseTokenMetadata) + } lock := locksutil.LockForKey(ts.tokenLocks, id) lock.RLock() defer lock.RUnlock() @@ -3514,6 +3538,9 @@ func (ts *TokenStore) handleRenew(ctx context.Context, req *logical.Request, dat if id == "" { return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } + if IsEnterpriseToken(id) { + return logical.ErrorResponse("enterprise tokens cannot be renewed"), nil + } incrementRaw := data.Get("increment").(int) // Convert the increment diff --git a/vault/token_store_ce.go b/vault/token_store_ce.go new file mode 100644 index 0000000000..8a60254e70 --- /dev/null +++ b/vault/token_store_ce.go @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/helper/namespace" +) + +func getEnterpriseTokenId(_ string) string { + return "" +} + +func (ts *TokenStore) handleTidyEnterpriseTokens(_ context.Context, _ *namespace.Namespace, _ *multierror.Error) error { + return nil +} diff --git a/vault/version_store.go b/vault/version_store.go index 46fbac98ad..4121abaa1b 100644 --- a/vault/version_store.go +++ b/vault/version_store.go @@ -199,6 +199,9 @@ func (c *Core) IsNewInstall(ctx context.Context) bool { return oldestVersion == "" && newestVersion == "" } +// IsJWT validates if a token is of JWT format, which was historically +// a possibility for wrapping tokens, though not something we +// encourage today. func IsJWT(token string) bool { return len(token) > 3 && strings.Count(token, ".") == 2 && (token[3] != '.' && token[1] != '.') diff --git a/vault/version_store_ce.go b/vault/version_store_ce.go new file mode 100644 index 0000000000..04baeb05d8 --- /dev/null +++ b/vault/version_store_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +func IsEnterpriseToken(token string) bool { + return false +} diff --git a/vault/wrapping.go b/vault/wrapping.go index cedba608f1..36b8abfaf3 100644 --- a/vault/wrapping.go +++ b/vault/wrapping.go @@ -348,8 +348,7 @@ DONELISTHANDLING: } // validateWrappingToken checks whether a token is a wrapping token. The passed -// in logical request will be updated if the wrapping token was provided within -// a JWT token. +// in logical request may be updated. func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) { if req == nil { return false, fmt.Errorf("invalid request") @@ -409,13 +408,8 @@ func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) } // Check for it being a JWT. If it is, and it is valid, we extract the - // internal client token from it and use that during lookup. The second - // check is a quick check to verify that we don't consider a namespaced - // token to be a JWT -- namespaced tokens have two dots too, but Vault - // token types (for now at least) begin with a letter representing a type - // and then a dot. + // internal client token from it and use that during lookup. if IsJWT(token) { - // Implement the jose library way parsedJWT, err := jwt.ParseSigned(token) if err != nil { return false, fmt.Errorf("wrapping token could not be parsed: %w", err) From 561a757ade06091b47060deab789c1d5af976318 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 12:52:36 -0500 Subject: [PATCH 040/468] UI: Intro Page Style Updates + Fixes (#12488) (#12619) * update styles and casing * fix check for default secret engines * show content behind intro modal * revert header changes * update alert and test * add test coverage and update flash message * update doc link style class and update test * update tests * update test to avoid state mismatches * update test Co-authored-by: lane-wetmore --- ui/app/components/page/methods.hbs | 6 +- ui/app/components/page/methods.ts | 8 ++- ui/app/components/page/namespaces.hbs | 9 ++- ui/app/components/page/namespaces.ts | 8 ++- ui/app/components/page/policies.hbs | 6 +- ui/app/components/page/policies.ts | 32 ++++++----- ui/app/components/secret-engine/list.hbs | 26 ++++----- ui/app/components/secret-engine/list.ts | 13 ++++- .../wizard/acl-policies/acl-wizard.hbs | 2 +- ui/app/components/wizard/guided-start.hbs | 2 +- ui/app/components/wizard/index.hbs | 3 +- .../wizard/intro-content/feature.hbs | 2 +- ui/app/components/wizard/intro.hbs | 6 +- .../wizard/methods/methods-wizard.hbs | 9 ++- .../wizard/namespaces/namespace-wizard.hbs | 4 +- .../wizard/namespaces/namespace-wizard.ts | 22 +++++--- .../secret-engines/secret-engines-wizard.hbs | 6 +- .../secret-engines/secret-engines-wizard.ts | 5 -- ui/app/styles/components/wizard.scss | 56 +++++++++++-------- .../secret-engine-list-view-test.js | 24 ++++++++ ui/tests/integration/components/list-test.js | 21 +++++-- .../components/page/namespaces-wizard-test.js | 4 ++ 22 files changed, 185 insertions(+), 89 deletions(-) diff --git a/ui/app/components/page/methods.hbs b/ui/app/components/page/methods.hbs index 8222418435..bc1c68830d 100644 --- a/ui/app/components/page/methods.hbs +++ b/ui/app/components/page/methods.hbs @@ -14,7 +14,7 @@ class="has-right-margin-4" @color="secondary" @icon="bulb" - @text="New to Auth Methods?" + @text="New to Auth methods?" {{on "click" this.showIntroPage}} data-test-button="intro" /> @@ -25,7 +25,9 @@ {{#if this.showWizard}} -{{else}} +{{/if}} + +{{#if this.showContent}} { return this.args.model.methods.length === 1; } + get showContent() { + // Show when the 1) wizard is not shown OR 2) wizard intro modal is shown + // This ensures the wizard intro modal is shown on top of the list view and the background content is not blank behind the modal + return !this.showWizard || (this.shouldRenderIntroModal && this.wizard.isIntroVisible(WIZARD_ID)); + } + get showIntroButton() { - return !this.showWizard && this.hasOnlyDefaultMethods; + return this.showContent && this.hasOnlyDefaultMethods; } get showWizard() { diff --git a/ui/app/components/page/namespaces.hbs b/ui/app/components/page/namespaces.hbs index f903eddf67..a0a83667a5 100644 --- a/ui/app/components/page/namespaces.hbs +++ b/ui/app/components/page/namespaces.hbs @@ -26,7 +26,7 @@ {{/if}} {{#unless this.hasNamespaces}} - {{else}} + {{/if}} + + {{#if this.showContent}} {{! Show namespace list }} {{#if this.hasNamespaces}} @@ -135,7 +138,7 @@ {{/if}} - + { return !this.showWizard || this.wizard.isIntroVisible(WIZARD_ID); } + get showContent() { + // Show when the 1) wizard is not shown OR 2) wizard intro modal is shown + // This ensures the wizard intro modal is shown on top of the list view and the background content is not blank behind the modal + return !this.showWizard || (this.shouldRenderIntroModal && this.wizard.isIntroVisible(WIZARD_ID)); + } + get showIntroButton() { - return !this.showWizard && !this.hasNamespaces; + return this.showContent && !this.hasNamespaces; } get showWizard() { diff --git a/ui/app/components/page/policies.hbs b/ui/app/components/page/policies.hbs index c5a138f9a6..04cac5dbcb 100644 --- a/ui/app/components/page/policies.hbs +++ b/ui/app/components/page/policies.hbs @@ -23,7 +23,7 @@ @@ -33,7 +33,9 @@ {{#if this.showWizard}} - {{else}} + {{/if}} + + {{#if this.showContent}} {{#if @model.meta.total}} diff --git a/ui/app/components/page/policies.ts b/ui/app/components/page/policies.ts index df48c04871..22af985125 100644 --- a/ui/app/components/page/policies.ts +++ b/ui/app/components/page/policies.ts @@ -43,15 +43,6 @@ export default class PagePoliciesComponent extends Component { this.filter = this.args.filter || ''; } - // callback from HDS pagination to set the queryParams page - get paginationQueryParams() { - return (page: number) => { - return { - page, - }; - }; - } - // Check if the filter exactly matches a policy ID get filterMatchesKey(): boolean { const filter = this.filter; @@ -81,6 +72,25 @@ export default class PagePoliciesComponent extends Component { return this.args.model.meta?.total <= expectedLength; } + // callback from HDS pagination to set the queryParams page + get paginationQueryParams() { + return (page: number) => { + return { + page, + }; + }; + } + + get showContent() { + // Show when the 1) wizard is not shown OR 2) wizard intro modal is shown + // This ensures the wizard intro modal is shown on top of the list view and the background content is not blank behind the modal + return !this.showWizard || (this.shouldRenderIntroModal && this.wizard.isIntroVisible(WIZARD_ID)); + } + + get showIntroButton() { + return this.showContent && this.hasOnlyDefaultPolicies; + } + // Show when it is not in a dismissed state and there are no non-default policies and get showWizard() { if (this.args.policyType !== 'acl') return false; @@ -88,10 +98,6 @@ export default class PagePoliciesComponent extends Component { return !this.wizard.isDismissed(WIZARD_ID) && this.hasOnlyDefaultPolicies; } - get showIntroButton() { - return !this.showWizard && this.hasOnlyDefaultPolicies; - } - @action async deletePolicy(policyToDelete: PolicyModel) { try { diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 201f70e9cb..3498c85352 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -12,18 +12,16 @@ <:actions> - {{#unless this.showWizard}} - {{#if this.hasOnlyDefaultEngines}} - - {{/if}} - - {{/unless}} + {{#if this.showIntroButton}} + + {{/if}} + @@ -32,7 +30,9 @@ @isIntroModal={{this.shouldRenderIntroModal}} @onRefresh={{this.refreshSecretEngineList}} /> -{{else}} +{{/if}} + +{{#if this.showContent}} {{! Filters section }} { // While not ideal, we can check whether there are other engines than the default cubbyhole/ engine // to determine whether we should show the intro page get hasOnlyDefaultEngines() { - const listedEngines = this.sortedDisplayableBackends; + // use displayableBackends to check against unfiltered results to avoid flashing intro page when a filter has no results + const listedEngines = this.displayableBackends; return !listedEngines.length || (listedEngines.length === 1 && listedEngines[0]?.path === 'cubbyhole/'); } + get showContent() { + // Show when the 1) wizard is not shown OR 2) wizard intro modal is shown + // This ensures the wizard intro modal is shown on top of the list view and the background content is not blank behind the modal + return !this.showWizard || (this.shouldRenderIntroModal && this.wizard.isIntroVisible(WIZARD_ID)); + } + + get showIntroButton() { + return this.showContent && this.hasOnlyDefaultEngines; + } + get showWizard() { return !this.wizard.isDismissed(this.wizardId) && this.hasOnlyDefaultEngines; } diff --git a/ui/app/components/wizard/acl-policies/acl-wizard.hbs b/ui/app/components/wizard/acl-policies/acl-wizard.hbs index e57a92de97..86e0235727 100644 --- a/ui/app/components/wizard/acl-policies/acl-wizard.hbs +++ b/ui/app/components/wizard/acl-policies/acl-wizard.hbs @@ -26,7 +26,7 @@ @iconPosition="trailing" @text="View documentation" @href={{doc-link "/vault/docs/concepts/policies"}} - class="has-left-margin-m" + class="margin-left-auto" /> diff --git a/ui/app/components/wizard/guided-start.hbs b/ui/app/components/wizard/guided-start.hbs index bf1e10c0aa..e3ef27c476 100644 --- a/ui/app/components/wizard/guided-start.hbs +++ b/ui/app/components/wizard/guided-start.hbs @@ -5,7 +5,7 @@ -
    +
    + <:body> {{yield to="intro"}} @@ -23,6 +23,7 @@ @updateWizardState={{@updateWizardState}} @onStepChange={{@onStepChange}} @onDismiss={{@onDismiss}} + class="wizard" data-test-guided-start > <:exit> diff --git a/ui/app/components/wizard/intro-content/feature.hbs b/ui/app/components/wizard/intro-content/feature.hbs index 65cd338f9b..db2c7e637b 100644 --- a/ui/app/components/wizard/intro-content/feature.hbs +++ b/ui/app/components/wizard/intro-content/feature.hbs @@ -4,7 +4,7 @@ }} - + {{yield}} diff --git a/ui/app/components/wizard/intro.hbs b/ui/app/components/wizard/intro.hbs index 147fe911f2..a0b94664a5 100644 --- a/ui/app/components/wizard/intro.hbs +++ b/ui/app/components/wizard/intro.hbs @@ -3,10 +3,10 @@ SPDX-License-Identifier: BUSL-1.1 }} {{#if @isModal}} - + Welcome to {{@title}} - + {{yield to="body"}} @@ -15,7 +15,7 @@ {{else}} - + Welcome to {{@title}} diff --git a/ui/app/components/wizard/methods/methods-wizard.hbs b/ui/app/components/wizard/methods/methods-wizard.hbs index e05d9c6fd6..a3ed2ae591 100644 --- a/ui/app/components/wizard/methods/methods-wizard.hbs +++ b/ui/app/components/wizard/methods/methods-wizard.hbs @@ -15,13 +15,18 @@ @route="vault.cluster.settings.auth.enable" data-test-button="Enable a new method" /> - + diff --git a/ui/app/components/wizard/namespaces/namespace-wizard.hbs b/ui/app/components/wizard/namespaces/namespace-wizard.hbs index 52ea7bc2bc..8400e2af4c 100644 --- a/ui/app/components/wizard/namespaces/namespace-wizard.hbs +++ b/ui/app/components/wizard/namespaces/namespace-wizard.hbs @@ -37,7 +37,7 @@ @iconPosition="trailing" @text="View documentation" @href={{doc-link "/vault/docs/enterprise/namespaces"}} - class="has-left-margin-m" + class="margin-left-auto" /> @@ -49,7 +49,7 @@ <:submit> {{#if (eq this.wizardState.securityPolicyChoice this.policy.FLEXIBLE)}} - + {{else if (eq this.wizardState.creationMethod this.methods.UI)}} {{else}} diff --git a/ui/app/components/wizard/namespaces/namespace-wizard.ts b/ui/app/components/wizard/namespaces/namespace-wizard.ts index a3fb7ddb9d..576bf0f54f 100644 --- a/ui/app/components/wizard/namespaces/namespace-wizard.ts +++ b/ui/app/components/wizard/namespaces/namespace-wizard.ts @@ -26,6 +26,7 @@ const DEFAULT_STEPS = [ interface Args { isIntroModal: boolean; onRefresh: CallableFunction; + onFlexiblePolicyComplete: CallableFunction; } interface WizardState { @@ -116,6 +117,19 @@ export default class WizardNamespacesWizardComponent extends Component { }; } + @action + async onDone() { + await this.onDismiss(); + this.args.onFlexiblePolicyComplete(); + this.flashMessages.success(`Your current setup is 1 namespace.`, { title: 'Guided start complete' }); + } + + @action + async onDismiss() { + this.wizard.dismiss(this.wizardId); + await this.args.onRefresh(); + } + @action async onSubmit() { switch (this.wizardState.creationMethod) { @@ -129,12 +143,6 @@ export default class WizardNamespacesWizardComponent extends Component { } } - @action - async onDismiss() { - this.wizard.dismiss(this.wizardId); - await this.args.onRefresh(); - } - @action onIntroChange(visible: boolean) { this.wizard.setIntroVisible(this.wizardId, visible); @@ -155,7 +163,7 @@ export default class WizardNamespacesWizardComponent extends Component { await this.createNamespace(namespaceName, fullPath); } - this.flashMessages.success(`The namespaces have been successfully created.`); + this.flashMessages.success('Your new configuration has been applied.', { title: 'Namespaces created' }); } catch (error) { const { message } = await this.api.parseError(error); this.flashMessages.danger(`Error creating namespaces: ${message}`); diff --git a/ui/app/components/wizard/secret-engines/secret-engines-wizard.hbs b/ui/app/components/wizard/secret-engines/secret-engines-wizard.hbs index c4388c655c..3d5c43501b 100644 --- a/ui/app/components/wizard/secret-engines/secret-engines-wizard.hbs +++ b/ui/app/components/wizard/secret-engines/secret-engines-wizard.hbs @@ -12,8 +12,8 @@ diff --git a/ui/app/components/wizard/secret-engines/secret-engines-wizard.ts b/ui/app/components/wizard/secret-engines/secret-engines-wizard.ts index 725cc2a3fc..58db7c3600 100644 --- a/ui/app/components/wizard/secret-engines/secret-engines-wizard.ts +++ b/ui/app/components/wizard/secret-engines/secret-engines-wizard.ts @@ -32,9 +32,4 @@ export default class WizardSecretEnginesWizardComponent extends Component this.wizard.dismiss(this.wizardId); this.args.onRefresh(); } - - @action - onIntroChange(visible: boolean) { - this.wizard.setIntroVisible(this.wizardId, visible); - } } diff --git a/ui/app/styles/components/wizard.scss b/ui/app/styles/components/wizard.scss index 996340a7ea..8706f1e11c 100644 --- a/ui/app/styles/components/wizard.scss +++ b/ui/app/styles/components/wizard.scss @@ -7,40 +7,52 @@ */ .wizard { - &.guided-start { - @extend .is-flex-column; - @extend .is-flex-grow-1; - } - &.intro { - @extend .top-margin-32; + &.full { + @extend .top-margin-32; + @extend .has-padding-l; + .margin-left-auto { + @extend .has-left-margin-m; + } + } + + &.modal { + .margin-left-auto { + margin-left: auto; + } + } svg { flex-shrink: 0; } } - .content { - padding-top: 40px; - overflow: visible; + &.guided-start { + @extend .is-flex-column; + @extend .is-flex-grow-1; - // Non-active panels are hidden and prevented from taking up space - &[hidden] { - display: none; + .content { + padding-top: 40px; + overflow: visible; + + // Non-active panels are hidden and prevented from taking up space + &[hidden] { + display: none; + } } - } - .button-bar { - @extend .has-padding-m; - @extend .is-flex-between; + .button-bar { + @extend .has-padding-m; + @extend .is-flex-between; - .hds-button-set { - margin-left: auto; + .hds-button-set { + margin-left: auto; + } } - } - .tree { - border: 1px solid var(--token-color-border-primary); - border-radius: var(--token-border-radius-medium); + .tree { + border: 1px solid var(--token-color-border-primary); + border-radius: var(--token-border-radius-medium); + } } } diff --git a/ui/tests/acceptance/secret-engine-list-view-test.js b/ui/tests/acceptance/secret-engine-list-view-test.js index 6e14e8a295..eb0b7693f7 100644 --- a/ui/tests/acceptance/secret-engine-list-view-test.js +++ b/ui/tests/acceptance/secret-engine-list-view-test.js @@ -14,9 +14,11 @@ import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/com import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers'; import page from 'vault/tests/pages/settings/mount-secret-backend'; import localStorage from 'vault/lib/local-storage'; +import { setupMirage } from 'ember-cli-mirage/test-support'; module('Acceptance | secret-engine list view', function (hooks) { setupApplicationTest(hooks); + setupMirage(hooks); const createSecret = async (path, key, value, enginePath) => { await click(SES.createSecretLink); @@ -175,6 +177,28 @@ module('Acceptance | secret-engine list view', function (hooks) { await runCmd(deleteEngineCmd(enginePath1)); }); + test('it can navigate to the enablement page from the intro', async function (assert) { + // stub the mount response to meet the empty state criteria for showing the intro page + this.server.get('/sys/internal/ui/mounts', () => ({ + data: { + secret: { + 'cubbyhole/': { + type: 'cubbyhole', + local: true, + path: 'cubbyhole/', + }, + }, + }, + })); + await visit(`/vault/secrets-engines`); + await click(GENERAL.button('intro')); + await click(GENERAL.button('enable')); + + assert.strictEqual(currentURL(), `/vault/secrets-engines/enable`, 'It navigates to the enablement page'); + assert.dom('[data-test-guided-setup]').doesNotExist('The guided setup is not shown'); + assert.dom('[data-test-intro]').doesNotExist('The intro is not shown'); + }); + module('enterprise | namespaces', function (hooks) { hooks.beforeEach(async function () { await login(); diff --git a/ui/tests/integration/components/list-test.js b/ui/tests/integration/components/list-test.js index 41bece1504..f3c7d299d4 100644 --- a/ui/tests/integration/components/list-test.js +++ b/ui/tests/integration/components/list-test.js @@ -15,6 +15,10 @@ import { createSecretsEngine } from 'vault/tests/helpers/secret-engine/secret-en import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +const SELECTORS = { + intro: '[data-test-intro]', +}; + module('Integration | Component | secret-engine/list', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); @@ -180,8 +184,8 @@ module('Integration | Component | secret-engine/list', function (hooks) { await render(hbs``); - assert.dom('[data-test-intro]').exists('Intro page is shown'); - assert.dom(GENERAL.button('intro')).exists('Shows intro button'); + assert.dom(SELECTORS.intro).exists('Intro page is shown'); + assert.dom(GENERAL.button('enable')).exists('Shows enable button'); assert.dom(GENERAL.button('Skip')).exists('Shows skip button'); }); @@ -189,7 +193,14 @@ module('Integration | Component | secret-engine/list', function (hooks) { // Has engines beyond the default cubbyhole await render(hbs``); - assert.dom('[data-test-intro]').doesNotExist('Intro modal is not shown when engines exist'); + assert.dom(SELECTORS.intro).doesNotExist('Intro modal is not shown when engines exist'); + assert.dom(GENERAL.button('intro')).doesNotExist('Intro button is not shown'); + }); + + test('it does not show the intro page when a filter has no results', async function (assert) { + await render(hbs``); + await fillIn(GENERAL.inputSearch('secret-engine-path'), `foobar`); + assert.dom(SELECTORS.intro).doesNotExist('Intro modal is not shown when engines exist'); assert.dom(GENERAL.button('intro')).doesNotExist('Intro button is not shown'); }); @@ -199,9 +210,9 @@ module('Integration | Component | secret-engine/list', function (hooks) { await render(hbs``); await click(GENERAL.button('Skip')); - assert.dom('[data-test-intro]').doesNotExist('Intro is dismissed'); + assert.dom(SELECTORS.intro).doesNotExist('Intro is dismissed'); await click(GENERAL.button('intro')); - assert.dom('[data-test-intro]').exists('Intro can be shown again after reset'); + assert.dom(SELECTORS.intro).exists('Intro can be shown again after reset'); }); }); diff --git a/ui/tests/integration/components/page/namespaces-wizard-test.js b/ui/tests/integration/components/page/namespaces-wizard-test.js index fe89a412c0..221781e25c 100644 --- a/ui/tests/integration/components/page/namespaces-wizard-test.js +++ b/ui/tests/integration/components/page/namespaces-wizard-test.js @@ -26,11 +26,14 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function hooks.beforeEach(function () { this.refreshSpy = sinon.spy(); + this.flexiblePolicyCompleteSpy = sinon.spy(); this.wizardService = this.owner.lookup('service:wizard'); this.renderComponent = () => { return render(hbs` `); @@ -182,6 +185,7 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function assert.true(this.wizardService.isDismissed('namespace'), 'Wizard was marked as dismissed after Done'); assert.true(this.refreshSpy.calledOnce, 'onRefresh callback was called'); + assert.true(this.flexiblePolicyCompleteSpy.calledOnce, 'flexiblePolicyComplete callback was called'); }); test('it shows tree chart only when there are multiple globals, orgs, or projects', async function (assert) { From 46e6a418a867a616d96f1674879c402ae0a68fe0 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 13:05:47 -0500 Subject: [PATCH 041/468] UI: adding playwright test for Transit secrets engine (#12730) (#12757) * adding test for transit * specify path Co-authored-by: Dan Rivera --- ui/e2e/tests/superuser/transit.spec.ts | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 ui/e2e/tests/superuser/transit.spec.ts diff --git a/ui/e2e/tests/superuser/transit.spec.ts b/ui/e2e/tests/superuser/transit.spec.ts new file mode 100644 index 0000000000..aba90dfd08 --- /dev/null +++ b/ui/e2e/tests/superuser/transit.spec.ts @@ -0,0 +1,89 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; +import { BasePage } from '../../pages/base'; + +test('transit workflow', async ({ page }) => { + const basePage = new BasePage(page); + + await page.goto('dashboard'); + // enable Transit Engine + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByRole('heading', { name: 'Transit' }).click(); + await page.getByRole('textbox', { name: 'Path' }).fill('transit-workflow'); + await page.getByRole('button', { name: 'Enable engine' }).click(); + + // create key + await page.getByRole('link', { name: 'Create key' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('testKey'); + await page.getByRole('button', { name: 'Create key' }).click(); + await expect(page.getByLabel('breadcrumbs').getByText('testKey')).toBeVisible(); + await basePage.dismissFlashMessages(); + + // rotate key + await page.getByRole('link', { name: 'Versions' }).click(); + await page.getByRole('button', { name: 'Rotate key' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + //verify a new version is created after rotation + await expect(page.getByText('Version 2')).toBeVisible(); + + // encrypt text + await page.getByRole('link', { name: 'Key Actions' }).click(); + await page.getByRole('link', { name: 'Encrypt Looks up wrapping' }).click(); + await page.getByRole('textbox', { name: 'Plaintext' }).fill('testString'); + await page.getByRole('button', { name: 'Encrypt' }).click(); + + // grab the generated ciphertext and copy it to clipboard + await page.getByRole('button', { name: 'copy vault:v2:' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + // decrypt text + await page.getByRole('link', { name: 'Decrypt' }).click(); + await page.getByRole('textbox', { name: 'Ciphertext' }).press('ControlOrMeta+v'); + await page.getByRole('button', { name: 'Decrypt' }).click(); + await expect(page.getByText('Copy your unwrapped data')).toBeVisible(); + await page.getByRole('button', { name: 'copy dGVzdFN0cmluZw==' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + // generate datakey + await page.getByRole('link', { name: 'Datakey' }).click(); + await page.getByRole('button', { name: 'Create datakey' }).click(); + await page.locator('html').click(); + await expect(page.getByText('Copy your generated key')).toBeVisible(); + await page.getByRole('button', { name: 'copy vault:v2:' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + // rewrap key + await page.getByRole('link', { name: 'Rewrap' }).click(); + await page.getByRole('textbox', { name: 'Ciphertext' }).press('ControlOrMeta+v'); + await page.getByRole('button', { name: 'Rewrap' }).click(); + await expect(page.getByText('Copy your token')).toBeVisible(); + await page.getByRole('button', { name: 'Close' }).click(); + + // HMAC generate + await page.getByRole('link', { name: 'HMAC' }).click(); + await page.getByRole('textbox', { name: 'Input' }).fill('test'); + await page.getByRole('button', { name: 'HMAC' }).click(); + await expect(page.getByText('Copy your unwrapped data')).toBeVisible(); + await page.getByRole('button', { name: 'copy vault:v2:' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + // HMAC verify + await page.getByRole('link', { name: 'Verify' }).click(); + //verify test and hmac is prefilled from previous step + await expect( + page + .getByLabel('Input') + .locator('div') + .filter({ hasText: /^test$/ }) + ).toBeVisible(); + await expect(page.getByText(/vault:v2:.*/)).toBeVisible(); + await page.getByRole('button', { name: 'Verify' }).click(); + + await expect(page.getByText('Results Valid')).toBeVisible(); + await page.getByRole('button', { name: 'Close' }).click(); +}); From e5c23d42d101e8434fc1649d775ebfd881617649 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 13:21:21 -0500 Subject: [PATCH 042/468] [UI][VAULT-42864][Bugfix] Update container margins and padding (#12687) (#12709) * Update container margins and padding * Use 64px padding * Add centered-container * This wraps all the page content in vault Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/styles/core/containers.scss | 9 ++++++++- .../vault/cluster/replication-dr-promote/details.hbs | 2 +- .../vault/cluster/replication-dr-promote/index.hbs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ui/app/styles/core/containers.scss b/ui/app/styles/core/containers.scss index e49ff90dd2..06a53e3079 100644 --- a/ui/app/styles/core/containers.scss +++ b/ui/app/styles/core/containers.scss @@ -17,6 +17,7 @@ display: flex; flex-direction: column; justify-content: flex-end; + padding: 0 size_variables.$spacing-64; } .section { @@ -38,8 +39,14 @@ .container { flex-grow: 1; - margin: size_variables.$spacing-24 size_variables.$spacing-64; + margin: size_variables.$spacing-24 0; max-width: 1032px; position: relative; width: auto; } + +.centered-container { + margin: 0 auto; + max-width: 1032px; + padding: size_variables.$spacing-24; +} diff --git a/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs b/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs index 5dce459196..f6668e7fce 100644 --- a/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs +++ b/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs @@ -4,7 +4,7 @@ }}
    -
    +
    {{#if Page.isDisabled}} diff --git a/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs b/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs index 85abad8d6c..a2055cf26c 100644 --- a/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs +++ b/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs @@ -4,7 +4,7 @@ }}
    -
    +
    {{#if Page.isDisabled}} From ce5dd467f2adf6fbfa5997760eb382f33474e54e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 13:45:38 -0500 Subject: [PATCH 043/468] set is_ent_branch=false when on the CE branch (#12698) (#12717) Co-authored-by: Matthew Irish <39469+meirish@users.noreply.github.com> --- .github/actions/metadata/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/metadata/action.yml b/.github/actions/metadata/action.yml index 8c7efd1ff5..8556a866c9 100644 --- a/.github/actions/metadata/action.yml +++ b/.github/actions/metadata/action.yml @@ -159,6 +159,7 @@ runs: is_ce_in_enterprise=$([[ $base_ref == ce/* ]] && echo "true" || echo "false") if [ "$is_ce_in_enterprise" = 'true' ]; then is_enterprise="false" + is_ent_branch="false" go_tags='' version_metadata='${{ inputs.vault-version }}' else From 0278b1f9c49491a0826425c2ff71f402f7748a02 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 15:22:39 -0500 Subject: [PATCH 044/468] hide revoke button for certificates that have already been revoked (#12743) (#12766) Co-authored-by: lane-wetmore --- ui/e2e/tests/superuser/pki.spec.ts | 1 + .../pki/addon/components/page/pki-certificate-details.hbs | 2 +- .../pki/addon/components/page/pki-certificate-details.ts | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/e2e/tests/superuser/pki.spec.ts b/ui/e2e/tests/superuser/pki.spec.ts index 075728b7aa..7601c5ebf0 100644 --- a/ui/e2e/tests/superuser/pki.spec.ts +++ b/ui/e2e/tests/superuser/pki.spec.ts @@ -108,6 +108,7 @@ test('pki workflow', async ({ page }) => { await page.getByRole('button', { name: 'Revoke certificate' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); await expect(page.getByText('Revocation time')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Revoke certificate' })).not.toBeVisible(); // generate issuer await page.getByRole('link', { name: 'pki-engine' }).click(); diff --git a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs index c47a040f44..4084b7eda1 100644 --- a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs @@ -13,7 +13,7 @@ {{on "click" this.downloadCert}} data-test-pki-cert-download-button /> - {{#if @canRevoke}} + {{#if this.showRevoke}} { constructor(owner: Owner, args: Args) { super(owner, args); this.parsedCertificate = parseCertificate(this.args.certData.certificate || ''); + this.didRevoke = Boolean(this.args.certData.revocation_time); } get displayFields() { @@ -63,12 +64,16 @@ export default class PkiCertificateDetailsComponent extends Component { 'private_key_type', ]; // insert revocation_time after common_name if revoked - if (this.args.certData.revocation_time || this.didRevoke) { + if (this.didRevoke) { fields.splice(2, 0, 'revocation_time'); } return fields; } + get showRevoke() { + return this.args.canRevoke && !this.didRevoke; + } + isCertificate = (field: string) => ['certificate', 'issuing_ca', 'ca_chain', 'private_key'].includes(field); label = (field: string) => { From 13a485be05021d1ef965a41f3cd5e7cefb9044b2 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 17:46:04 -0500 Subject: [PATCH 045/468] Update CHANGELOG with version 1.21.4, 1.20.9, 1.19.15, 1.16.31 notes (#12765) (#12767) Added release notes for versions 1.21.4, 1.20.9, 1.19.15, and 1.16.31 Enterprise, including security upgrades, changes, improvements, and bug fixes. Co-authored-by: Tony Wittinger --- CHANGELOG.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27472494ff..4cb261b812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,41 @@ - [v1.0.0 - v1.9.10](CHANGELOG-pre-v1.10.md) - [v0.11.6 and earlier](CHANGELOG-v0.md) +## 1.21.4 +### March 05, 2026 + +SECURITY: + +* Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +* Upgrade `filippo.io/edwards25519` to v1.1.1 to resolve GO-2026-4503 +* vault/sdk: Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +* vault/sdk: Upgrade `go.opentelemetry.io/otel/sdk` to v1.40.0 to resolve GO-2026-4394 + +CHANGES: + +* core: Bump Go version to 1.25.7 +* mfa/duo: Upgrade duo_api_golang client to 0.2.0 to include the new Duo certificate authorities +* ui: Remove ability to bulk delete secrets engines from the list view. + +IMPROVEMENTS: + +* core/seal: Enhance sys/seal-backend-status to provide more information about seal backends. +* secrets/kmip (Enterprise): Obey configured best_effort_wal_wait_duration when forwarding kmip requests. +* secrets/pki (enterprise): Return the POSTPKIOperation capability within SCEP GetCACaps endpoint for better legacy client support. + +BUG FIXES: + +* core (enterprise): Buffer the POST body on binary paths to allow re-reading on non-logical forwarding attempts. Addresses an issue for SCEP, EST and CMPv2 certificate issuances with slow replication of entities +* core/identity (enterprise): Fix excessive logging when updating existing aliases +* core/managed-keys (enterprise): client credentials should not be required when using Azure Managed Identities in managed keys. +* plugins (enterprise): Fix bug where requests to external plugins that modify storage weren't populating the X-Vault-Index response header. +* secrets (pki): Allow issuance of certificates without the server_flag key usage from SCEP, EST and CMPV2 protocols. +* secrets/pki (enterprise): Address cache invalidation issues with CMPv2 on performance standby nodes. +* secrets/pki (enterprise): Address issues using SCEP on performance standby nodes failing due to configuration invalidation issues along with errors writing to storage +* secrets/pki (enterprise): Modify the SCEP GetCACaps endpoint to dynamically reflect the configured encryption and digest algorithms. +* secrets/pki: The root/sign-intermediate endpoint should not fail when provided a CSR with a basic constraint extension containing isCa set to true +* secrets/pki: allow glob-style DNS names in alt_names. + ## 1.21.3 ### February 05, 2026 @@ -327,6 +362,40 @@ BUG FIXES: * ui: Revert camelizing of parameters returned from `sys/internal/ui/mounts` so mount paths match serve value * ui: Fixes permissions for hiding and showing sidebar navigation items for policies that include special characters: `+`, `*` +## 1.20.9 Enterprise +### March 05, 2026 + +SECURITY: + +* Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +* Upgrade `filippo.io/edwards25519` to v1.1.1 to resolve GO-2026-4503 +* vault/sdk: Upgrade `cloudflare/circl` to v1.6.3 to resolve CVE-2026-1229 +* vault/sdk: Upgrade `go.opentelemetry.io/otel/sdk` to v1.40.0 to resolve GO-2026-4394 + +CHANGES: + +* core: Bump Go version to 1.25.7 +* mfa/duo: Upgrade duo_api_golang client to 0.2.0 to include the new Duo certificate authorities + +IMPROVEMENTS: + +* core/seal: Enhance sys/seal-backend-status to provide more information about seal backends. +* secrets/kmip (Enterprise): Obey configured best_effort_wal_wait_duration when forwarding kmip requests. +* secrets/pki (enterprise): Return the POSTPKIOperation capability within SCEP GetCACaps endpoint for better legacy client support. + +BUG FIXES: + +* core (enterprise): Buffer the POST body on binary paths to allow re-reading on non-logical forwarding attempts. Addresses an issue for SCEP, EST and CMPv2 certificate issuances with slow replication of entities +* core/identity (enterprise): Fix excessive logging when updating existing aliases +* core/managed-keys (enterprise): client credentials should not be required when using Azure Managed Identities in managed keys. +* plugins (enterprise): Fix bug where requests to external plugins that modify storage weren't populating the X-Vault-Index response header. +* secrets (pki): Allow issuance of certificates without the server_flag key usage from SCEP, EST and CMPV2 protocols. +* secrets/pki (enterprise): Address cache invalidation issues with CMPv2 on performance standby nodes. +* secrets/pki (enterprise): Address issues using SCEP on performance standby nodes failing due to configuration invalidation issues along with errors writing to storage +* secrets/pki (enterprise): Modify the SCEP GetCACaps endpoint to dynamically reflect the configured encryption and digest algorithms. +* secrets/pki: allow glob-style DNS names in alt_names. +* ui: Fixes login form so `?with=` query param correctly displays only the specified mount when multiple mounts of the same auth type are configured with `listing_visibility="unauth"` + ## 1.20.8 Enterprise ### February 05, 2026 @@ -750,6 +819,33 @@ intermediate certificates. [[GH-30034](https://github.com/hashicorp/vault/pull/3 * ui: MFA methods now display the namespace path instead of the namespace id. [[GH-29588](https://github.com/hashicorp/vault/pull/29588)] * ui: Redirect users authenticating with Vault as an OIDC provider to log in again when token expires. [[GH-30838](https://github.com/hashicorp/vault/pull/30838)] +## 1.19.15 Enterprise +### March 05, 2026 + +SECURITY: + +* Upgrade `filippo.io/edwards25519` to v1.1.1 to resolve GO-2026-4503 +* vault/sdk: Upgrade `go.opentelemetry.io/otel/sdk` to v1.40.0 to resolve GO-2026-4394 + +CHANGES: + +* core: Bump Go version to 1.25.7 +* mfa/duo: Upgrade duo_api_golang client to 0.2.0 to include the new Duo certificate authorities + +IMPROVEMENTS: + +* core/seal: Enhance sys/seal-backend-status to provide more information about seal backends. +* secrets/kmip (Enterprise): Obey configured best_effort_wal_wait_duration when forwarding kmip requests. + +BUG FIXES: + +* core (enterprise): Buffer the POST body on binary paths to allow re-reading on non-logical forwarding attempts. Addresses an issue for SCEP, EST and CMPv2 certificate issuances with slow replication of entities +* core/identity (enterprise): Fix excessive logging when updating existing aliases +* core/managed-keys (enterprise): client credentials should not be required when using Azure Managed Identities in managed keys. +* secrets (pki): Allow issuance of certificates without the server_flag key usage from SCEP, EST and CMPV2 protocols. +* secrets/pki (enterprise): Address cache invalidation issues with CMPv2 on performance standby nodes. +* secrets/pki: allow glob-style DNS names in alt_names. + ## 1.19.14 Enterprise ### February 05, 2026 @@ -2586,7 +2682,26 @@ autopilot to fail to discover new server versions and so not trigger an upgrade. * ui: fixed a bug where the replication pages did not update display when navigating between DR and performance [[GH-26325](https://github.com/hashicorp/vault/pull/26325)] * ui: fixes undefined start time in filename for downloaded client count attribution csv [[GH-26485](https://github.com/hashicorp/vault/pull/26485)] -## 1.16.30 +## 1.16.31 Enterprise +### March 05, 2026 + +**Enterprise LTS:** Vault Enterprise 1.16 is a [Long-Term Support (LTS)](https://developer.hashicorp.com/vault/docs/enterprise/lts) release. + +SECURITY: + +* Upgrade `filippo.io/edwards25519` to v1.1.1 to resolve GO-2026-4503 +* vault/sdk: Upgrade `go.opentelemetry.io/otel/sdk` to v1.40.0 to resolve GO-2026-4394 + +CHANGES: + +* core: Bump Go version to 1.24.13 +* mfa/duo: Upgrade duo_api_golang client to 0.2.0 to include the new Duo certificate authorities + +IMPROVEMENTS: + +* core/seal: Enhance sys/seal-backend-status to provide more information about seal backends. + +## 1.16.30 Enterprise ### February 05, 2026 **Enterprise LTS:** Vault Enterprise 1.16 is a [Long-Term Support (LTS)](https://developer.hashicorp.com/vault/docs/enterprise/lts) release. From 617b5e8571c74c86098d59c478a7d46d9b04c87e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 19:13:05 -0500 Subject: [PATCH 046/468] Fix nightly hcp build error (#12731) (#12732) Co-authored-by: Luis (LT) Carbonell --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15d10c4da3..28294eaff4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -407,8 +407,8 @@ jobs: - artifacts-ent uses: ./.github/workflows/build-hcp-image.yml with: - pull-request: ${{ needs.setup.outputs.workflow-trigger == 'pull_request' && github.event.pull_request.number || '' }} - branch: ${{ needs.setup.outputs.workflow-trigger == 'schedule' && 'main' || '' }} + pull-request: ${{ needs.setup.outputs.workflow-trigger == 'pull_request' && github.event.pull_request.number || null }} + branch: ${{ needs.setup.outputs.workflow-trigger == 'schedule' && 'main' || null }} create-aws-image: true create-azure-image: false hcp-environment: int From 23ebe05f59c3fd630aa54cfe03ca1ad777a2b321 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 6 Mar 2026 04:50:49 -0500 Subject: [PATCH 047/468] VAULT-42460 : fix for secret sync api failure (#12336) (#12527) * code change to fix secret sync api failure * adding unit test and changelog * updating test cases Co-authored-by: Stuti Srivastava --- changelog/_12336.txt | 3 +++ vault/request_handling.go | 11 ++++++++++- vault/request_handling_test.go | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelog/_12336.txt diff --git a/changelog/_12336.txt b/changelog/_12336.txt new file mode 100644 index 0000000000..6c183bcd18 --- /dev/null +++ b/changelog/_12336.txt @@ -0,0 +1,3 @@ +```release-note:bug +secret sync (enterprise): fix panic in set-association API when using Vault Proxy with token-bound CIDRs. The panic occurred due to missing connection information during CIDR validation. +``` \ No newline at end of file diff --git a/vault/request_handling.go b/vault/request_handling.go index 1195119f5c..fe307f33e0 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -226,7 +226,10 @@ func (c *Core) getApplicableGroupPolicies(ctx context.Context, tokenNS *namespac func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Request) (*ACL, *logical.TokenEntry, *identity.Entity, map[string][]string, error) { defer metrics.MeasureSince([]string{"core", "fetch_acl_and_token"}, time.Now()) - + if req == nil { + c.logger.Error("fetchACLTokenEntryAndEntity called with nil request") + return nil, nil, nil, nil, ErrInternalError + } // Ensure there is a client token if req.ClientToken == "" { return nil, nil, nil, nil, logical.ErrPermissionDenied @@ -282,6 +285,12 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req // CIDR checks bind all tokens except non-expiring root tokens if te.TTL != 0 && len(te.BoundCIDRs) > 0 { var valid bool + + // Validate req for connection on CIDR + if req.Connection == nil || req.Connection.RemoteAddr == "" { + c.logger.Warn("token bound CIDRs found but no connection information available for validation") + return nil, nil, nil, nil, logical.ErrPermissionDenied + } remoteSockAddr, err := sockaddr.NewSockAddr(req.Connection.RemoteAddr) if err != nil { if c.Logger().IsDebug() { diff --git a/vault/request_handling_test.go b/vault/request_handling_test.go index 1bff029633..7fe5ebe1d2 100644 --- a/vault/request_handling_test.go +++ b/vault/request_handling_test.go @@ -628,3 +628,16 @@ func TestRequestHandling_TokenRenewal(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +// TestRequestHandling_fetchACLTokenEntryAndEntity_NilRequest tests that +// fetchACLTokenEntryAndEntity returns an error when called with a nil request +func TestRequestHandling_fetchACLTokenEntryAndEntity_NilRequest(t *testing.T) { + core, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + // Call with nil request - should return ErrInternalError + _, _, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, nil) + + require.Error(t, err) + require.Equal(t, ErrInternalError, err) +} From 6890ec6e9844aeddefa398a3faf4600cd1309b0c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 6 Mar 2026 10:30:50 -0500 Subject: [PATCH 048/468] Update max_parallel along with other PKCS#11 managed key fields (#12761) (#12780) * Update max_parallel along with other PKCS#11 managed key fields. * Add changelog entry. Co-authored-by: Victor Rodriguez Rizo --- changelog/_12761.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/_12761.txt diff --git a/changelog/_12761.txt b/changelog/_12761.txt new file mode 100644 index 0000000000..6fbad55e12 --- /dev/null +++ b/changelog/_12761.txt @@ -0,0 +1,3 @@ +```release-note:bug +core/managed-keys (enterprise): Fix a bug that prevented the max_parallel field of PKCS#11 managed keys from being updated. +``` From f9c0ea544c15c000fdc495d9460c71d78637bcab Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 6 Mar 2026 12:33:55 -0500 Subject: [PATCH 049/468] Enable mac and generate_random key usages for managed keys (#12769) (#12788) * Enable mac and generate_random key usages for managed keys. Change response of reading manage key configuration to return the string representation of key usages rather than the numerical values. * Add changelog entry. * Update changelog/_12769.txt --------- Co-authored-by: Victor Rodriguez Rizo Co-authored-by: Steven Clark --- changelog/_12769.txt | 9 +++++++++ sdk/logical/keyusage_enumer.go | 7 ++++--- sdk/logical/managed_key.go | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 changelog/_12769.txt diff --git a/changelog/_12769.txt b/changelog/_12769.txt new file mode 100644 index 0000000000..8bea6ab9ed --- /dev/null +++ b/changelog/_12769.txt @@ -0,0 +1,9 @@ +```release-note:bug +core/managed-keys (enterprise): Fix a problem that prevented 'mac' and 'generate_random' key usages from being set. +``` +```release-note:change +core/managed-keys (enterprise): The response to API endpoint GET sys/managed-keys/:type/:name now +returns an array of string values for key usages, rather than an array of integer values. The +strings used are 'encrypt' (1), 'decrypt' (2), 'sign' (3), 'verify' (4), 'wrap' (5), 'unwrap' (6), +'generate_random' (7), and 'mac' (8). +``` diff --git a/sdk/logical/keyusage_enumer.go b/sdk/logical/keyusage_enumer.go index 83998c4a2a..fa813655e5 100644 --- a/sdk/logical/keyusage_enumer.go +++ b/sdk/logical/keyusage_enumer.go @@ -6,9 +6,9 @@ import ( "fmt" ) -const _KeyUsageName = "encryptdecryptsignverifywrapunwrapgenerate_random" +const _KeyUsageName = "encryptdecryptsignverifywrapunwrapgenerate_randommac" -var _KeyUsageIndex = [...]uint8{0, 7, 14, 18, 24, 28, 34, 49} +var _KeyUsageIndex = [...]uint8{0, 7, 14, 18, 24, 28, 34, 49, 52} func (i KeyUsage) String() string { i -= 1 @@ -18,7 +18,7 @@ func (i KeyUsage) String() string { return _KeyUsageName[_KeyUsageIndex[i]:_KeyUsageIndex[i+1]] } -var _KeyUsageValues = []KeyUsage{1, 2, 3, 4, 5, 6, 7} +var _KeyUsageValues = []KeyUsage{1, 2, 3, 4, 5, 6, 7, 8} var _KeyUsageNameToValueMap = map[string]KeyUsage{ _KeyUsageName[0:7]: 1, @@ -28,6 +28,7 @@ var _KeyUsageNameToValueMap = map[string]KeyUsage{ _KeyUsageName[24:28]: 5, _KeyUsageName[28:34]: 6, _KeyUsageName[34:49]: 7, + _KeyUsageName[49:52]: 8, } // KeyUsageString retrieves an enum value from the enum constants string name. diff --git a/sdk/logical/managed_key.go b/sdk/logical/managed_key.go index 37540f5567..fe543888db 100644 --- a/sdk/logical/managed_key.go +++ b/sdk/logical/managed_key.go @@ -22,6 +22,7 @@ const ( KeyUsageWrap KeyUsageUnwrap KeyUsageGenerateRandom + KeyUsageMAC ) type ManagedKey interface { From 3f201ff376ba26f4db86e20ae6c6391f33f7eb9b Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 6 Mar 2026 13:48:06 -0500 Subject: [PATCH 050/468] Verify that hahsicorp licenses work in HVD (#12516) (#12789) * test hvd tag * test license status * fix build date * write hcp specific test for vault license status * removing log line Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> --- vault/external_tests/blackbox/system_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vault/external_tests/blackbox/system_test.go b/vault/external_tests/blackbox/system_test.go index 70433696c7..ffc790a047 100644 --- a/vault/external_tests/blackbox/system_test.go +++ b/vault/external_tests/blackbox/system_test.go @@ -32,6 +32,21 @@ func TestVaultVersion(t *testing.T) { t.Logf("Vault version: %v", sealStatus.Data["version"]) } +// TestVaultLicenseStatus verifies Vault license status response +func TestVaultLicenseStatus(t *testing.T) { + v := blackbox.New(t) + + // Read the sys/license/status endpoint which should contain license info + licenseStatus := v.MustRead("sys/license/status") + if licenseStatus.Data["autoloaded"] == nil { + t.Fatal("Could not get license details from sys/license/status") + } + autoloaded := licenseStatus.Data["autoloaded"].(map[string]interface{}) + if autoloaded["license_id"].(string) == "" { + t.Fatal("Could not retrieve license_id from sys/license/status") + } +} + // TestRaftVoters verifies that all nodes in the raft cluster are voters func TestRaftVoters(t *testing.T) { v := blackbox.New(t) From 72d40c13438859dd580462790a0260c6217791c4 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Sat, 7 Mar 2026 17:30:12 -0500 Subject: [PATCH 051/468] Rotation Manager sdk helper tweaks (#12441) (#12810) * Clarify SetLastVaultRotation comment, only set some of the PopulateAutomatedRotationData fields when they are set * Revert "Clarify SetLastVaultRotation comment, only set some of the PopulateAutomatedRotationData fields when they are set" This reverts commit f8353cd1c86bfd705dc47d059b8f4f4da2d93677. * Add deprecation remark and new function * Force PR to recognize changes, hopefully * Create a TestRotationSystemView in the sdk for ent testing * ent directive * Remove LastVaultRotation from test systemview * Add MigratedLegacyNextRotationTime for rotation manager migration * comment * Set the MountPoint on a rotation job based on the entry regardless of what the plugin claims * Update fields.go * Add test coverage Co-authored-by: Robert <17119716+robmonte@users.noreply.github.com> --- sdk/helper/automatedrotationutil/fields.go | 22 + .../automatedrotationutil/fields_test.go | 64 +++ sdk/plugin/grpc_system.go | 21 +- sdk/plugin/pb/backend.pb.go | 435 +++++++++--------- sdk/plugin/pb/backend.proto | 1 + sdk/rotation/rotation_job.go | 35 +- sdk/rotation/rotation_job_test.go | 34 +- vault/dynamic_system_view.go | 3 + 8 files changed, 364 insertions(+), 251 deletions(-) diff --git a/sdk/helper/automatedrotationutil/fields.go b/sdk/helper/automatedrotationutil/fields.go index 04f292f814..267482be92 100644 --- a/sdk/helper/automatedrotationutil/fields.go +++ b/sdk/helper/automatedrotationutil/fields.go @@ -97,6 +97,8 @@ func (p *AutomatedRotationParams) ParseAutomatedRotationFields(d *framework.Fiel return nil } +// Use PopulateSetAutomatedRotationData instead, *unless* all these +// fields are necessary to maintain backwards compatibility with the plugin's pre-existing response API. // PopulateAutomatedRotationData adds PluginIdentityTokenParams info into the given map. func (p *AutomatedRotationParams) PopulateAutomatedRotationData(m map[string]interface{}) { m["rotation_schedule"] = p.RotationSchedule @@ -106,6 +108,26 @@ func (p *AutomatedRotationParams) PopulateAutomatedRotationData(m map[string]int m["rotation_policy"] = p.RotationPolicy } +// PopulateSetAutomatedRotationData adds PluginIdentityTokenParams info into the given map, based +// on which fields were set for rotation. Setting a rotation schedule will not return a rotation +// period, and setting a rotation period will not return a rotation schedule or rotation window. +func (p *AutomatedRotationParams) PopulateSetAutomatedRotationData(m map[string]interface{}) { + // Always set these even if they are zero values, to avoid confusion. + m["disable_automated_rotation"] = p.DisableAutomatedRotation + m["rotation_policy"] = p.RotationPolicy + + // Set both of these if a schedule is set. + if p.RotationSchedule != "" { + m["rotation_schedule"] = p.RotationSchedule + m["rotation_window"] = p.RotationWindow.Seconds() + } + + // Set this if a period is set. + if p.RotationPeriod != 0 { + m["rotation_period"] = p.RotationPeriod.Seconds() + } +} + // PopulateRotationInfo adds RotationInfoResponseParams info into the given map. func (p *RotationInfoResponseParams) PopulateRotationInfo(m map[string]interface{}) { // Only set last_vault_rotation and next_vault_rotation if they are non-zero diff --git a/sdk/helper/automatedrotationutil/fields_test.go b/sdk/helper/automatedrotationutil/fields_test.go index e5542983ce..45ddd19e90 100644 --- a/sdk/helper/automatedrotationutil/fields_test.go +++ b/sdk/helper/automatedrotationutil/fields_test.go @@ -284,6 +284,70 @@ func TestPopulateAutomatedRotationData(t *testing.T) { } } +func TestPopulateSetAutomatedRotationData(t *testing.T) { + tests := []struct { + name string + inputParams *AutomatedRotationParams + expected map[string]interface{} + unexpected map[string]interface{} + }{ + { + name: "only schedule and window set", + expected: map[string]interface{}{ + "rotation_schedule": "*/15 * * * *", + "rotation_window": time.Duration(60).Seconds(), + "rotation_policy": "", + "disable_automated_rotation": false, + }, + unexpected: map[string]interface{}{ + "rotation_period": "", + }, + inputParams: &AutomatedRotationParams{ + RotationSchedule: "*/15 * * * *", + RotationWindow: 60, + RotationPeriod: 0, + DisableAutomatedRotation: false, + }, + }, + { + name: "only period set", + expected: map[string]interface{}{ + "rotation_period": time.Duration(120).Seconds(), + "rotation_policy": "", + "disable_automated_rotation": false, + }, + unexpected: map[string]interface{}{ + "rotation_schedule": "", + "rotation_window": "", + }, + inputParams: &AutomatedRotationParams{ + RotationSchedule: "", + RotationWindow: 0, + RotationPeriod: 120, + DisableAutomatedRotation: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := make(map[string]interface{}) + + tt.inputParams.PopulateSetAutomatedRotationData(m) + + if !reflect.DeepEqual(m, tt.expected) { + t.Errorf("PopulateSetAutomatedRotationData() error comparing values; got %v, expected %v", m, tt.expected) + } + + for k, v := range tt.unexpected { + if m[k] == v { + t.Errorf("PopulateSetAutomatedRotationData() unexpected value for key %s; got %v", k, m[k]) + } + } + }) + } +} + func TestShouldRegisterRotationJob(t *testing.T) { tests := []struct { name string diff --git a/sdk/plugin/grpc_system.go b/sdk/plugin/grpc_system.go index df7d35c4f2..a05c14e1d0 100644 --- a/sdk/plugin/grpc_system.go +++ b/sdk/plugin/grpc_system.go @@ -250,11 +250,12 @@ func (s *gRPCSystemViewClient) GetRotationInformation(ctx context.Context, req * func (s *gRPCSystemViewClient) RegisterRotationJobWithResponse(ctx context.Context, req *rotation.RotationJobConfigureRequest) (*rotation.RotationInfo, error) { cfgReq := &pb.RegisterRotationJobRequest{ Job: &pb.RotationJobInput{ - Name: req.Name, - MountPoint: req.MountPoint, - Path: req.ReqPath, - RotationSchedule: req.RotationSchedule, - RotationPolicy: req.RotationPolicy, + Name: req.Name, + MountPoint: req.MountPoint, + Path: req.ReqPath, + RotationSchedule: req.RotationSchedule, + RotationPolicy: req.RotationPolicy, + MigratedLegacyNextRotationTime: req.MigratedLegacyNextRotationTime.Unix(), // on the side outbound from the plugin, we convert duration to seconds, so seconds get sent over the wire RotationWindow: int64(req.RotationWindow.Seconds()), @@ -534,8 +535,9 @@ func (s *gRPCSystemViewServer) RegisterRotationJobWithResponse(ctx context.Conte // on the side inbound to vault, we convert seconds back to time.Duration // Note: this value is seconds (as per the outbound client call, despite being int64) // The field is int64 because of gRPC reasons, not time.Duration reasons - RotationWindow: time.Duration(req.Job.RotationWindow) * time.Second, - RotationPeriod: time.Duration(req.Job.RotationPeriod) * time.Second, + RotationWindow: time.Duration(req.Job.RotationWindow) * time.Second, + RotationPeriod: time.Duration(req.Job.RotationPeriod) * time.Second, + MigratedLegacyNextRotationTime: time.Unix(req.Job.MigratedLegacyNextRotationTime, 0).UTC(), } resp, err := s.impl.RegisterRotationJobWithResponse(ctx, cfgReq) @@ -566,8 +568,9 @@ func (s *gRPCSystemViewServer) RegisterRotationJob(ctx context.Context, req *pb. // on the side inbound to vault, we convert seconds back to time.Duration // Note: this value is seconds (as per the outbound client call, despite being int64) // The field is int64 because of gRPC reasons, not time.Duration reasons - RotationWindow: time.Duration(req.Job.RotationWindow) * time.Second, - RotationPeriod: time.Duration(req.Job.RotationPeriod) * time.Second, + RotationWindow: time.Duration(req.Job.RotationWindow) * time.Second, + RotationPeriod: time.Duration(req.Job.RotationPeriod) * time.Second, + MigratedLegacyNextRotationTime: time.Unix(req.Job.MigratedLegacyNextRotationTime, 0).UTC(), } rotationID, err := s.impl.RegisterRotationJob(ctx, cfgReq) diff --git a/sdk/plugin/pb/backend.pb.go b/sdk/plugin/pb/backend.pb.go index 5b04176889..caa05db35d 100644 --- a/sdk/plugin/pb/backend.pb.go +++ b/sdk/plugin/pb/backend.pb.go @@ -3644,16 +3644,17 @@ func (x *RegisterRotationJobResponse) GetErr() string { } type RotationJobInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - MountPoint string `protobuf:"bytes,2,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - RotationSchedule string `protobuf:"bytes,4,opt,name=rotation_schedule,json=rotationSchedule,proto3" json:"rotation_schedule,omitempty"` - RotationWindow int64 `protobuf:"varint,5,opt,name=rotation_window,json=rotationWindow,proto3" json:"rotation_window,omitempty"` - RotationPeriod int64 `protobuf:"varint,6,opt,name=rotation_period,json=rotationPeriod,proto3" json:"rotation_period,omitempty"` - RotationPolicy string `protobuf:"bytes,7,opt,name=rotation_policy,json=rotationPolicy,proto3" json:"rotation_policy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + MountPoint string `protobuf:"bytes,2,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + RotationSchedule string `protobuf:"bytes,4,opt,name=rotation_schedule,json=rotationSchedule,proto3" json:"rotation_schedule,omitempty"` + RotationWindow int64 `protobuf:"varint,5,opt,name=rotation_window,json=rotationWindow,proto3" json:"rotation_window,omitempty"` + RotationPeriod int64 `protobuf:"varint,6,opt,name=rotation_period,json=rotationPeriod,proto3" json:"rotation_period,omitempty"` + RotationPolicy string `protobuf:"bytes,7,opt,name=rotation_policy,json=rotationPolicy,proto3" json:"rotation_policy,omitempty"` + MigratedLegacyNextRotationTime int64 `protobuf:"varint,8,opt,name=migrated_legacy_next_rotation_time,json=migratedLegacyNextRotationTime,proto3" json:"migrated_legacy_next_rotation_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RotationJobInput) Reset() { @@ -3735,6 +3736,13 @@ func (x *RotationJobInput) GetRotationPolicy() string { return "" } +func (x *RotationJobInput) GetMigratedLegacyNextRotationTime() int64 { + if x != nil { + return x.MigratedLegacyNextRotationTime + } + return 0 +} + type DeregisterRotationRequestInput struct { state protoimpl.MessageState `protogen:"open.v1"` MountPoint string `protobuf:"bytes,1,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` @@ -4689,7 +4697,7 @@ var file_sdk_plugin_pb_backend_proto_rawDesc = string([]byte{ 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, - 0x65, 0x72, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x72, 0x72, 0x22, 0x83, + 0x65, 0x72, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x72, 0x72, 0x22, 0xcf, 0x02, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x75, 0x6e, 0x74, @@ -4706,208 +4714,213 @@ var file_sdk_plugin_pb_backend_proto_rawDesc = string([]byte{ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x22, 0x5c, 0x0a, 0x1e, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x5f, 0x70, - 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x71, 0x50, 0x61, - 0x74, 0x68, 0x22, 0x54, 0x0a, 0x1c, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x22, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, - 0x70, 0x75, 0x74, 0x52, 0x03, 0x72, 0x65, 0x71, 0x22, 0x8e, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x72, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x10, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xbb, 0x04, 0x0a, 0x0f, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x68, 0x61, 0x6e, 0x64, 0x73, - 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x11, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x69, 0x64, 0x5f, 0x72, 0x65, - 0x73, 0x75, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x64, 0x69, 0x64, 0x52, - 0x65, 0x73, 0x75, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, - 0x73, 0x75, 0x69, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x69, 0x70, - 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x13, 0x6e, 0x65, 0x67, 0x6f, - 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, - 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x41, 0x0a, 0x1d, 0x6e, 0x65, 0x67, - 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x5f, 0x69, 0x73, 0x5f, 0x6d, 0x75, 0x74, 0x75, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x1a, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x49, 0x73, 0x4d, 0x75, 0x74, 0x75, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x41, 0x0a, - 0x11, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x10, - 0x70, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x3d, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, - 0x0e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, - 0x42, 0x0a, 0x1d, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x1b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x63, 0x73, 0x70, 0x5f, 0x72, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6f, 0x63, 0x73, 0x70, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6c, 0x73, 0x5f, - 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6c, - 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x22, 0x2a, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x6e, 0x31, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x61, 0x73, 0x6e, 0x31, 0x44, - 0x61, 0x74, 0x61, 0x22, 0x47, 0x0a, 0x10, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x33, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, - 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0c, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x10, - 0x53, 0x65, 0x6e, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x28, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x61, - 0x74, 0x61, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x72, 0x0a, 0x18, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xa5, 0x03, - 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x3e, 0x0a, 0x0d, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, - 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x72, 0x67, - 0x73, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x30, 0x0a, 0x0c, 0x53, 0x70, 0x65, - 0x63, 0x69, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x70, 0x65, 0x63, 0x69, 0x61, - 0x6c, 0x50, 0x61, 0x74, 0x68, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x53, 0x0a, 0x14, 0x48, - 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x12, 0x1c, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x45, - 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x41, 0x72, 0x67, - 0x73, 0x1a, 0x1d, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x45, 0x78, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x12, 0x1f, 0x0a, 0x07, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x12, 0x09, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x31, 0x0a, 0x0d, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x26, 0x0a, 0x05, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x0d, 0x2e, - 0x70, 0x62, 0x2e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x0e, 0x2e, 0x70, - 0x62, 0x2e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x0a, - 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, - 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x13, - 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x52, 0x65, - 0x70, 0x6c, 0x79, 0x12, 0x20, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x65, 0x70, 0x6c, 0x79, 0x32, 0xd5, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x12, 0x31, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x53, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x14, - 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2e, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x47, 0x65, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, - 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2e, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x75, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, - 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x75, 0x74, 0x52, - 0x65, 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, - 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x32, 0x94, 0x09, - 0x0a, 0x0a, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x69, 0x65, 0x77, 0x12, 0x2a, 0x0a, 0x0f, - 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x54, 0x4c, 0x12, - 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x62, 0x2e, - 0x54, 0x54, 0x4c, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x26, 0x0a, 0x0b, 0x4d, 0x61, 0x78, 0x4c, - 0x65, 0x61, 0x73, 0x65, 0x54, 0x54, 0x4c, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x54, 0x4c, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x12, 0x26, 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x09, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x69, 0x6e, - 0x74, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x36, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x09, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x12, 0x38, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x19, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, - 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, - 0x44, 0x61, 0x74, 0x61, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x19, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, - 0x70, 0x6c, 0x79, 0x12, 0x30, 0x0a, 0x0c, 0x4d, 0x6c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x15, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x6c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2c, 0x0a, 0x0a, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4d, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x13, - 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, - 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x0a, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, - 0x6f, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x09, 0x50, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x45, 0x6e, 0x76, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x45, 0x6e, - 0x76, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3f, 0x0a, 0x0f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x46, 0x6f, 0x72, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x18, 0x2e, - 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x68, 0x0a, 0x1a, 0x47, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x70, - 0x62, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6c, - 0x79, 0x12, 0x2e, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x70, 0x62, - 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x70, 0x6c, - 0x79, 0x12, 0x5c, 0x0a, 0x15, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x62, 0x2e, - 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, - 0x62, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x48, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x56, 0x0a, 0x13, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, + 0x6c, 0x69, 0x63, 0x79, 0x12, 0x4a, 0x0a, 0x22, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x1e, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x64, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, + 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x22, 0x5c, 0x0a, 0x1e, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x6f, + 0x69, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x71, 0x50, 0x61, 0x74, 0x68, 0x22, 0x54, + 0x0a, 0x1c, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, + 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x62, + 0x2e, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, + 0x03, 0x72, 0x65, 0x71, 0x22, 0x8e, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xbb, 0x04, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, + 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x11, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x64, 0x69, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, 0x73, 0x75, 0x69, 0x74, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, + 0x75, 0x69, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x13, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x12, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x41, 0x0a, 0x1d, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x69, 0x73, 0x5f, + 0x6d, 0x75, 0x74, 0x75, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x6e, 0x65, + 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x49, 0x73, 0x4d, 0x75, 0x74, 0x75, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x41, 0x0a, 0x11, 0x70, 0x65, 0x65, + 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x10, 0x70, 0x65, 0x65, 0x72, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x0f, + 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x0e, 0x76, 0x65, 0x72, + 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x1d, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x1b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x6f, 0x63, 0x73, 0x70, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6f, 0x63, 0x73, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6c, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x71, + 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6c, 0x73, 0x55, 0x6e, 0x69, + 0x71, 0x75, 0x65, 0x22, 0x2a, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x6e, 0x31, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x61, 0x73, 0x6e, 0x31, 0x44, 0x61, 0x74, 0x61, 0x22, + 0x47, 0x0a, 0x10, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, + 0x61, 0x69, 0x6e, 0x12, 0x33, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x67, + 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x05, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x72, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x62, 0x73, + 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xa5, 0x03, 0x0a, 0x07, 0x42, 0x61, + 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x3e, 0x0a, 0x0d, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x16, 0x2e, + 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x30, 0x0a, 0x0c, 0x53, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, + 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, 0x50, 0x61, 0x74, + 0x68, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x53, 0x0a, 0x14, 0x48, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, + 0x1c, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x1d, 0x2e, + 0x70, 0x62, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1f, 0x0a, 0x07, + 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x31, 0x0a, + 0x0d, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x15, + 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x26, 0x0a, 0x05, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x53, + 0x65, 0x74, 0x75, 0x70, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, + 0x74, 0x75, 0x70, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x0a, 0x49, 0x6e, 0x69, 0x74, + 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x69, 0x74, + 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, + 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, + 0x20, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x70, 0x6c, + 0x79, 0x32, 0xd5, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x31, 0x0a, + 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x14, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x2e, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x47, 0x65, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x13, 0x2e, 0x70, 0x62, + 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x2e, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x50, 0x75, 0x74, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x13, 0x2e, 0x70, 0x62, + 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x75, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x67, + 0x73, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x32, 0x94, 0x09, 0x0a, 0x0a, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x56, 0x69, 0x65, 0x77, 0x12, 0x2a, 0x0a, 0x0f, 0x44, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x54, 0x4c, 0x12, 0x09, 0x2e, 0x70, 0x62, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x54, 0x4c, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x26, 0x0a, 0x0b, 0x4d, 0x61, 0x78, 0x4c, 0x65, 0x61, 0x73, 0x65, + 0x54, 0x54, 0x4c, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, + 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x54, 0x4c, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x26, 0x0a, 0x07, + 0x54, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x64, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x36, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x38, 0x0a, 0x10, + 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x62, + 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, 0x2e, 0x70, 0x62, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, + 0x41, 0x72, 0x67, 0x73, 0x1a, 0x19, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x57, 0x72, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, + 0x30, 0x0a, 0x0c, 0x4d, 0x6c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, + 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x15, 0x2e, 0x70, 0x62, 0x2e, + 0x4d, 0x6c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, + 0x79, 0x12, 0x2c, 0x0a, 0x0a, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, + 0x35, 0x0a, 0x0a, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x2e, + 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x41, 0x72, 0x67, + 0x73, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x09, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x45, 0x6e, 0x76, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, + 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x45, 0x6e, 0x76, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x12, 0x3f, 0x0a, 0x0f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x12, 0x68, 0x0a, 0x1a, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x12, 0x25, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x46, 0x72, + 0x6f, 0x6d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2e, 0x0a, + 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x09, 0x2e, 0x70, + 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6c, 0x75, + 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x5c, 0x0a, + 0x15, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x6e, 0x65, + 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x16, 0x47, + 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, + 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x56, 0x0a, 0x13, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, + 0x1f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x6b, 0x0a, 0x1f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x44, - 0x0a, 0x15, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x12, 0x20, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, - 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x32, 0x36, 0x0a, 0x06, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2c, - 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x4c, 0x0a, 0x0c, - 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x11, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x1c, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4f, 0x62, 0x73, - 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x1a, 0x28, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, 0x68, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x44, 0x0a, 0x15, 0x44, 0x65, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4a, 0x6f, 0x62, 0x12, 0x20, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x32, 0x36, 0x0a, 0x06, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x09, 0x53, 0x65, + 0x6e, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, + 0x70, 0x62, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x4c, 0x0a, 0x0c, 0x4f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x11, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, + 0x70, 0x62, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x70, 0x62, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, + 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/sdk/plugin/pb/backend.proto b/sdk/plugin/pb/backend.proto index 32f9ad78e3..af4bf5f89c 100644 --- a/sdk/plugin/pb/backend.proto +++ b/sdk/plugin/pb/backend.proto @@ -672,6 +672,7 @@ message RotationJobInput { int64 rotation_window = 5; int64 rotation_period = 6; string rotation_policy = 7; + int64 migrated_legacy_next_rotation_time = 8; } message DeregisterRotationRequestInput { diff --git a/sdk/rotation/rotation_job.go b/sdk/rotation/rotation_job.go index 92b786c6f1..6377cad35a 100644 --- a/sdk/rotation/rotation_job.go +++ b/sdk/rotation/rotation_job.go @@ -29,21 +29,23 @@ type RotationJob struct { // RotationID is the ID returned to the user to manage this secret. // This is generated by Vault core. Any set value will be ignored. // For requests, this will always be blank. - RotationID string `sentinel:""` - Path string - MountPoint string - Name string - RotationPolicy string + RotationID string `sentinel:""` + Path string + MountPoint string + Name string + RotationPolicy string + MigratedLegacyNextRotationTime time.Time } type RotationJobConfigureRequest struct { - Name string - MountPoint string - ReqPath string - RotationSchedule string - RotationWindow time.Duration - RotationPeriod time.Duration - RotationPolicy string + Name string + MountPoint string + ReqPath string + RotationSchedule string + RotationWindow time.Duration + RotationPeriod time.Duration + RotationPolicy string + MigratedLegacyNextRotationTime time.Time } type RotationJobDeregisterRequest struct { @@ -121,10 +123,11 @@ func newRotationJob(configRequest *RotationJobConfigureRequest) (*RotationJob, e RotationOptions: RotationOptions{ Schedule: rs, }, - MountPoint: configRequest.MountPoint, - Path: configRequest.ReqPath, - Name: configRequest.Name, - RotationPolicy: configRequest.RotationPolicy, + MountPoint: configRequest.MountPoint, + Path: configRequest.ReqPath, + Name: configRequest.Name, + RotationPolicy: configRequest.RotationPolicy, + MigratedLegacyNextRotationTime: configRequest.MigratedLegacyNextRotationTime, }, nil } diff --git a/sdk/rotation/rotation_job_test.go b/sdk/rotation/rotation_job_test.go index 6b4f79f045..4c342d3c99 100644 --- a/sdk/rotation/rotation_job_test.go +++ b/sdk/rotation/rotation_job_test.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" "testing" + "time" ) func TestConfigureRotationJob(t *testing.T) { @@ -19,33 +20,36 @@ func TestConfigureRotationJob(t *testing.T) { { name: "no rotation params", req: &RotationJobConfigureRequest{ - MountPoint: "aws", - ReqPath: "config/root", - RotationSchedule: "", - RotationWindow: 60, - RotationPeriod: 0, + MountPoint: "aws", + ReqPath: "config/root", + RotationSchedule: "", + RotationWindow: 60, + RotationPeriod: 0, + MigratedLegacyNextRotationTime: time.Time{}, }, expectedError: "RotationSchedule or RotationPeriod is required to set up rotation job", }, { name: "no mount point", req: &RotationJobConfigureRequest{ - MountPoint: "", - ReqPath: "config/root", - RotationSchedule: "", - RotationWindow: 60, - RotationPeriod: 5, + MountPoint: "", + ReqPath: "config/root", + RotationSchedule: "", + RotationWindow: 60, + RotationPeriod: 5, + MigratedLegacyNextRotationTime: time.Time{}, }, expectedError: "MountPoint is required", }, { name: "no req path", req: &RotationJobConfigureRequest{ - MountPoint: "aws", - ReqPath: "", - RotationSchedule: "", - RotationWindow: 60, - RotationPeriod: 5, + MountPoint: "aws", + ReqPath: "", + RotationSchedule: "", + RotationWindow: 60, + RotationPeriod: 5, + MigratedLegacyNextRotationTime: time.Time{}, }, expectedError: "ReqPath is required", }, diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 012368452b..157522cc7a 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -383,6 +383,9 @@ func (d dynamicSystemView) RegisterRotationJobWithResponse(ctx context.Context, return nil, fmt.Errorf("error configuring rotation job: %s", err) } + // Set the MountPoint here from the mountEntry so plugins cannot force-set a mount point other than its own + job.MountPoint = mountEntry.APIPath() + resp, err := d.core.RegisterRotationJob(nsCtx, job) if err != nil { return nil, fmt.Errorf("error registering rotation job: %s", err) From f43fdf54ab6b754d08267dbd730a276526faa1b4 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 00:51:29 -0400 Subject: [PATCH 052/468] Vault 42257 root rotation in LDAP auth method for AD schema (#12223) (#12595) * adding root rotation for ldap auth method for schema AD * adding test cases for root rotation * code fix and adding TestRotateRoot_EncodeUTF16LEBytes * adding constants * schema validation + unit test * updated unit test * removed duplicate enum * adding acceptance test, unit test, changelog and updating schemaType to schema * adding logs and comments for debugging * added validation for config params * adding validation and test cases to enforce encrypted connection requirements for AD password rotation * adding fix to data race error in CI pipeline * addressing PR comments * fix for backward compatibility for schema and test * adding validation and tests for multiple URLs for AD root rotation --------- Co-authored-by: Stuti Srivastava Co-authored-by: Prajna Nayak --- builtin/credential/ldap/backend_test.go | 1 + builtin/credential/ldap/path_config.go | 6 + .../ldap/path_config_rotate_root.go | 107 ++++- .../ldap/path_config_rotate_root_test.go | 389 +++++++++++++++++- builtin/credential/ldap/path_config_test.go | 113 +++++ changelog/_12223.txt | 4 + helper/testhelpers/ldap/ldaphelper.go | 21 +- sdk/helper/ldaputil/config.go | 40 ++ sdk/helper/ldaputil/config_test.go | 294 ++++++++++++- 9 files changed, 962 insertions(+), 13 deletions(-) create mode 100644 builtin/credential/ldap/path_config_test.go create mode 100644 changelog/_12223.txt diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go index adcfb7922e..e8425fa187 100644 --- a/builtin/credential/ldap/backend_test.go +++ b/builtin/credential/ldap/backend_test.go @@ -1543,6 +1543,7 @@ func TestLdapAuthBackend_ConfigUpgrade(t *testing.T) { UsernameAsAlias: false, DerefAliases: "never", MaximumPageSize: 1000, + Schema: ldaputil.SchemaOpenLDAP, // Default schema when not specified }, } diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go index f990ce8db6..975fc5ecf5 100644 --- a/builtin/credential/ldap/path_config.go +++ b/builtin/credential/ldap/path_config.go @@ -128,6 +128,12 @@ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ldapConfig persistNeeded = true } + // Upgrade path: Set default schema for configs created before schema field was added + if result.Schema == "" { + result.Schema = ldaputil.SchemaOpenLDAP + persistNeeded = true + } + if persistNeeded && (b.System().LocalMount() || !b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary|consts.ReplicationPerformanceStandby)) { entry, err := logical.StorageEntryJSON("config", result) if err != nil { diff --git a/builtin/credential/ldap/path_config_rotate_root.go b/builtin/credential/ldap/path_config_rotate_root.go index a683a30240..580d0e4344 100644 --- a/builtin/credential/ldap/path_config_rotate_root.go +++ b/builtin/credential/ldap/path_config_rotate_root.go @@ -5,7 +5,11 @@ package ldap import ( "context" + "encoding/binary" "errors" + "fmt" + "strings" + "unicode/utf16" "github.com/go-ldap/ldap/v3" "github.com/hashicorp/vault/sdk/framework" @@ -83,6 +87,19 @@ func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request return responseError{errors.New("auth is not using authenticated search, no root to rotate")} } + // Validate TLS requirements for AD password rotation + schema := ldaputil.NormalizedSchema(cfg.Schema) + if schema == ldaputil.SchemaAD { + // Validate URL(s) which will actually be used for rotation + urlToValidate := cfg.Url + if cfg.RotationUrl != "" { + urlToValidate = cfg.RotationUrl + } + if err := validateADRotationURLs(urlToValidate, cfg.StartTLS); err != nil { + return responseError{err} + } + } + // grab our ldap client client := ldaputil.Client{ Logger: b.Logger(), @@ -98,16 +115,14 @@ func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request if err != nil { return err } + // Close the connection when done to avoid leaking connections, especially during repeated rotation attempts.defer conn.Close() + defer conn.Close() err = conn.Bind(u, p) if err != nil { return err } - lreq := &ldap.ModifyRequest{ - DN: cfg.BindDN, - } - var newPassword string if cfg.PasswordPolicy != "" { newPassword, err = b.System().GeneratePasswordFromPolicy(ctx, cfg.PasswordPolicy) @@ -118,12 +133,38 @@ func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request return err } - lreq.Replace("userPassword", []string{newPassword}) + switch schema { + case ldaputil.SchemaAD: + // AD root rotation requires: + // 1) Quoted password + // 2) UTF-16LE encoding + // 3) Encrypted connection (LDAPS/StartTLS) + // Without these, AD rejects password updates. + b.Logger().Debug("rotating root password using AD schema") + quotedPwd := fmt.Sprintf("\"%s\"", newPassword) + utf16Bytes := encodeUTF16LEBytes(quotedPwd) - err = conn.Modify(lreq) - if err != nil { - return err + modReq := ldap.NewModifyRequest(cfg.BindDN, nil) + modReq.Replace("unicodePwd", []string{string(utf16Bytes)}) + + if err := conn.Modify(modReq); err != nil { + return fmt.Errorf("failed to modify AD password for %q: %w", cfg.BindDN, err) + } + + case ldaputil.SchemaOpenLDAP: + b.Logger().Debug("rotating root password using openldap schema") + lreq := &ldap.ModifyRequest{ + DN: cfg.BindDN, + } + lreq.Replace("userPassword", []string{newPassword}) + + if err := conn.Modify(lreq); err != nil { + return fmt.Errorf("failed to modify OpenLDAP password: %w", err) + } + default: + return responseError{fmt.Errorf("unsupported schema type for password rotation: %s", schema)} } + // update config with new password cfg.BindPassword = newPassword entry, err := logical.StorageEntryJSON("config", cfg) @@ -138,6 +179,56 @@ func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request return nil } +// encodeUTF16LEBytes encodes a string as UTF-16LE bytes for AD password changes. +// This encoding is required for Active Directory password changes via the unicodePwd attribute. +func encodeUTF16LEBytes(s string) []byte { + utf16Runes := utf16.Encode([]rune(s)) + buf := make([]byte, len(utf16Runes)*2) + for i, r := range utf16Runes { + binary.LittleEndian.PutUint16(buf[i*2:], r) + } + return buf +} + +// validateADRotationURLs validates that all URLs in the provided URL string +// meet the security requirements for AD password rotation. +func validateADRotationURLs(urlString string, startTLS bool) error { + // AD password rotation requires encrypted connections (LDAPS or StartTLS) + // Supported configurations: + // 1. ldaps:// with proper certificate validation (recommended) + // 2. ldaps:// with insecure_tls=true (skips certificate validation) + // 3. ldap:// with starttls=true (with or without explicit certificates) + if strings.TrimSpace(urlString) == "" { + return errors.New("AD password rotation requires a configured URL") + } + // Split on commas to handle multiple URLs + rawURLs := strings.Split(urlString, ",") + hasNonTLSURL := false + for _, rawURL := range rawURLs { + rawURL := strings.TrimSpace(rawURL) + if rawURL == "" { + continue + } + urlLower := strings.ToLower(rawURL) + isLDAPS := strings.HasPrefix(urlLower, "ldaps://") + isLDAP := strings.HasPrefix(urlLower, "ldap://") + + // Validate that URL uses a supported protocol + if !isLDAPS && !isLDAP { + return fmt.Errorf("AD password rotation requires ldap:// or ldaps:// protocol, got: %s", rawURL) + } + // Track if any URL uses non-TLS ldap:// + if isLDAP { + hasNonTLSURL = true + } + } + // If any URL uses ldap:// (non-TLS), require StartTLS to ensure encryption + if hasNonTLSURL && !startTLS { + return errors.New("AD password rotation with ldap:// requires starttls=true for encrypted connection") + } + return nil +} + const pathConfigRotateRootHelpSyn = ` Request to rotate the LDAP credentials used by Vault ` diff --git a/builtin/credential/ldap/path_config_rotate_root_test.go b/builtin/credential/ldap/path_config_rotate_root_test.go index c4790eca87..bc48882ed9 100644 --- a/builtin/credential/ldap/path_config_rotate_root_test.go +++ b/builtin/credential/ldap/path_config_rotate_root_test.go @@ -4,18 +4,24 @@ package ldap import ( + "bytes" "context" + "encoding/binary" "os" + "strings" "testing" + "unicode/utf16" "github.com/hashicorp/vault/helper/testhelpers/ldap" logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" + "github.com/hashicorp/vault/sdk/helper/ldaputil" "github.com/hashicorp/vault/sdk/logical" ) +// TestRotateRoot_DefaultSchema tests that the default schema type is used when not explicitly set and that rotation works in that case. // This test relies on a docker ldap server with a suitable person object (cn=admin,dc=planetexpress,dc=com) -// with bindpassword "admin". `PrepareTestContainer` does this for us. - see the backend_test for more details -func TestRotateRoot(t *testing.T) { +// with bindpassword "admin". `PrepareTestContainer` does this for us - see the backend_test for more details. +func TestRotateRoot_DefaultSchema(t *testing.T) { if os.Getenv(logicaltest.TestEnvVar) == "" { t.Skip("skipping rotate root tests because VAULT_ACC is unset") } @@ -57,6 +63,12 @@ func TestRotateRoot(t *testing.T) { } newCFG, err := b.Config(ctx, req) + if err != nil { + t.Fatalf("failed to get config after rotation: %s", err) + } + if newCFG == nil { + t.Fatal("config is nil after rotation") + } if newCFG.BindDN != cfg.BindDN { t.Fatalf("a value in config that should have stayed the same changed: %s", cfg.BindDN) } @@ -113,6 +125,12 @@ func TestRotateRootWithRotationUrl(t *testing.T) { } newCFG, err := b.Config(ctx, req) + if err != nil { + t.Fatalf("failed to get config after rotation: %s", err) + } + if newCFG == nil { + t.Fatal("config is nil after rotation") + } if newCFG.BindDN != cfg.BindDN { t.Fatalf("BindDN %q changed unexpectedly, found new value %q", cfg.BindDN, newCFG.BindDN) } @@ -124,3 +142,370 @@ func TestRotateRootWithRotationUrl(t *testing.T) { t.Fatalf("URL %q changed unexpectedly, found new value %q", mainDummyUrl, newCFG.Url) } } + +// TestRotateRoot_Schema_OpenLDAP tests that rotation for OpenLDAP schema +func TestRotateRoot_Schema_OpenLDAP(t *testing.T) { + if os.Getenv(logicaltest.TestEnvVar) == "" { + t.Skip("skipping rotate root tests because VAULT_ACC is unset") + } + ctx := context.Background() + b, store := createBackendWithStorage(t) + cleanup, cfg := ldap.PrepareTestContainer(t, ldap.DefaultVersion) + defer cleanup() + // set up auth config + req := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: store, + Data: map[string]interface{}{ + "url": cfg.Url, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, + "userdn": cfg.UserDN, + "schema": ldaputil.SchemaOpenLDAP, + }, + } + resp, err := b.HandleRequest(ctx, req) + if err != nil { + t.Fatalf("failed to initialize ldap auth config: %s", err) + } + if resp != nil && resp.IsError() { + t.Fatalf("failed to initialize ldap auth config: %s", resp.Data["error"]) + } + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: store, + } + _, err = b.HandleRequest(ctx, req) + if err != nil { + t.Fatalf("failed to rotate password: %s", err) + } + newCFG, err := b.Config(ctx, req) + if err != nil { + t.Fatalf("failed to get config after rotation: %s", err) + } + if newCFG == nil { + t.Fatal("config is nil after rotation") + } + if newCFG.BindDN != cfg.BindDN { + t.Fatalf("a value in config that should have stayed the same changed: %s", cfg.BindDN) + } + if newCFG.BindPassword == cfg.BindPassword { + t.Fatalf("the password should have changed, but it didn't") + } +} + +// TestRotateRoot_UnsupportedSchema tests unsupported schema handling +func TestRotateRoot_UnsupportedSchema(t *testing.T) { + if os.Getenv(logicaltest.TestEnvVar) == "" { + t.Skip("skipping rotate root tests because VAULT_ACC is unset") + } + ctx := context.Background() + b, store := createBackendWithStorage(t) + cleanup, cfg := ldap.PrepareTestContainer(t, ldap.DefaultVersion) + defer cleanup() + // set up auth config + req := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: store, + Data: map[string]interface{}{ + "url": cfg.Url, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, + "userdn": cfg.UserDN, + "schema": "unsupported_schema", // unsupported schema type + }, + } + + resp, err := b.HandleRequest(ctx, req) + if err != nil { + t.Fatalf("failed to initialize ldap auth config: %s", err) + } + // Config creation should fail with logical error + if resp == nil || !resp.IsError() { + t.Fatal("expected config creation to fail with unsupported schema type, but it succeeded") + } + // Verify error message contains expected text + errMsg := resp.Error().Error() + if !strings.Contains(errMsg, "unsupported schema type") { + t.Fatalf("expected error containing 'unsupported schema type', got: %s", errMsg) + } +} + +// TestRotateRoot_EncodeUTF16LEBytes tests the encoding of UTF-16LE bytes for AD password modification. +func TestRotateRoot_EncodeUTF16LEBytes(t *testing.T) { + tests := []struct { + name string + input string + }{ + {name: "empty_string", input: ""}, + {name: "single_char", input: "A"}, + {name: "quoted_password", input: "\"password\""}, + {name: "alphanumeric_with_special", input: "Pass123!"}, + {name: "unicode_chars", input: "Pāsswörd✓"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := encodeUTF16LEBytes(tt.input) + r := utf16.Encode([]rune(tt.input)) + expected := make([]byte, len(r)*2) + for i, v := range r { + binary.LittleEndian.PutUint16(expected[i*2:], v) + } + + if !bytes.Equal(got, expected) { + t.Fatalf("encodeUTF16LEBytes(%q) = %v, want %v", tt.input, got, expected) + } + }) + } +} + +// adTLSTestCase defines a test case for AD TLS validation +type adTLSTestCase struct { + name string + url string + insecureTLS bool + startTLS bool + certificate string + passwordPolicy string + expectError bool + errorContains string +} + +// TestRotateRoot_AD_TLS_Validation tests TLS validation for AD schema +func TestRotateRoot_AD_TLS_Validation(t *testing.T) { + tests := []adTLSTestCase{ + // Valid: ldaps - TLS validation passes, connection refused immediately (no real server). + { + name: "ldaps_valid", + url: "ldaps://127.0.0.1:1", + insecureTLS: true, + }, + // Valid: ldap + starttls - with optional cert and password policy + { + name: "ldap_with_starttls_valid", + url: "ldap://127.0.0.1:1", + startTLS: true, + certificate: testCert, + passwordPolicy: "test-policy", + }, + // Invalid: ldap without starttls - must surface as LogicalErrorResponse + { + name: "ldap_without_starttls_invalid", + url: "ldap://example.com", + expectError: true, + errorContains: "AD password rotation with ldap:// requires starttls=true for encrypted connection", + }, + // Invalid: unsupported protocol - must surface as LogicalErrorResponse + { + name: "invalid_protocol", + url: "http://example.com", + expectError: true, + errorContains: "AD password rotation requires ldap:// or ldaps:// protocol, got: http://example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testADTLSValidation(t, tt) + }) + } +} + +// Valid self-signed certificate for testing (shared across tests) +const testCert = `-----BEGIN CERTIFICATE----- +MIIB1jCCAUGgAwIBAgIFAMv4K9YwCwYJKoZIhvcNAQELMCkxEDAOBgNVBAoTB0Fj +bWUgQ28xFTATBgNVBAMTDEVkZGFyZCBTdGFyazAeFw0xNTA1MDYwMzU2NDBaFw0x +NjA1MDYwMzU2NDBaMCUxEDAOBgNVBAoTB0FjbWUgQ28xETAPBgNVBAMTCEpvbiBT +bm93MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK6NU0R0eiCYVquU4RcjKc +LzGfx0aa1lMr2TnLQUSeLFZHFxsyyMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg +8+Zg2r8xnnney7abxcuv0uATWSIeKlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP ++Zxp2ni5qHNraf3wE2VPIQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAKAwCwYJKoZI +hvcNAQELA4GBAIr2F7wsqmEU/J/kLyrCgEVXgaV/sKZq4pPNnzS0tBYk8fkV3V18 +sBJyHKRLL/wFZASvzDcVGCplXyMdAOCyfd8jO3F9Ac/xdlz10RrHJT75hNu3a7/n +9KNwKhfN4A1CQv2x372oGjRhCW5bHNCWx4PIVeNzCyq/KZhyY9sxHE1g +-----END CERTIFICATE-----` + +// testADTLSValidation is a helper function that runs AD TLS validation tests +func testADTLSValidation(t *testing.T, tt adTLSTestCase) { + t.Helper() + ctx := context.Background() + // Create fresh backend and storage for each subtest to avoid state bleeding + b, store := createBackendWithStorage(t) + + data := map[string]interface{}{ + "url": tt.url, + "binddn": "cn=admin,dc=example,dc=com", + "bindpass": "password", + "userdn": "ou=users,dc=example,dc=com", + "schema": ldaputil.SchemaAD, + "insecure_tls": tt.insecureTLS, + "starttls": tt.startTLS, + "certificate": tt.certificate, + "password_policy": tt.passwordPolicy, + } + + req := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: store, + Data: data, + } + + resp, err := b.HandleRequest(ctx, req) + if err != nil { + t.Fatalf("unexpected error during config: %v", err) + } + if resp != nil && resp.IsError() { + t.Fatalf("unexpected response error during config: %v", resp.Error()) + } + + // Try to rotate - this is where TLS validation happens + rotateReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: store, + } + + rotateResp, rotateErr := b.HandleRequest(ctx, rotateReq) + + if tt.expectError { + // responseError is returned as logical.ErrorResponse with nil error + if rotateResp == nil || !rotateResp.IsError() { + t.Fatalf("expected error response, got resp=%v", rotateResp) + } + errMsg := rotateResp.Error().Error() + if !strings.Contains(errMsg, tt.errorContains) { + t.Fatalf("expected error containing %q, got: %q", tt.errorContains, errMsg) + } + } else { + // For valid configs, TLS validation passes and we expect a connection error + // (no real LDAP server). The backend logs these at ERROR level, + // which is expected and does not indicate a test failure. + // Assert neither the Go error nor the response error is a TLS validation failure, + // proving execution got past the TLS checks. + if rotateErr != nil { + if isTLSValidationError(rotateErr.Error()) { + t.Fatalf("unexpected TLS validation error: %s", rotateErr) + } + // connection/bind error is expected — TLS validation passed + } + if rotateResp != nil && rotateResp.IsError() { + errMsg := rotateResp.Error().Error() + if isTLSValidationError(errMsg) { + t.Fatalf("unexpected TLS validation error in response: %s", errMsg) + } + // connection/bind error in response is expected — TLS validation passed + } + } +} + +// isTLSValidationError checks if an error message is a TLS validation error +func isTLSValidationError(msg string) bool { + return strings.Contains(msg, "AD password rotation with ldap:// requires starttls=true for encrypted connection") || + strings.Contains(msg, "AD password rotation requires ldap:// or ldaps:// protocol, got:") +} + +// TestValidateADRotationURLs tests the URL validation logic for AD password rotation +func TestValidateADRotationURLs(t *testing.T) { + tests := []struct { + name string + urlString string + startTLS bool + wantErr bool + errMsg string + }{ + { + name: "single ldaps URL - valid", + urlString: "ldaps://secure.example.com", + startTLS: false, + wantErr: false, + }, + { + name: "single ldap URL with StartTLS - valid", + urlString: "ldap://example.com", + startTLS: true, + wantErr: false, + }, + { + name: "single ldap URL without StartTLS - invalid", + urlString: "ldap://example.com", + startTLS: false, + wantErr: true, + errMsg: "starttls=true", + }, + { + name: "multiple ldaps URLs - valid", + urlString: "ldaps://server1.example.com,ldaps://server2.example.com", + startTLS: false, + wantErr: false, + }, + { + name: "multiple ldap URLs with StartTLS - valid", + urlString: "ldap://server1.example.com,ldap://server2.example.com", + startTLS: true, + wantErr: false, + }, + { + name: "mixed ldaps and ldap with StartTLS - valid", + urlString: "ldaps://server1.example.com,ldap://server2.example.com", + startTLS: true, + wantErr: false, + }, + { + name: "mixed ldaps and ldap without StartTLS - invalid", + urlString: "ldaps://server1.example.com,ldap://server2.example.com", + startTLS: false, + wantErr: true, + errMsg: "starttls=true", + }, + { + name: "multiple ldap URLs without StartTLS - invalid", + urlString: "ldap://server1.example.com,ldap://server2.example.com", + startTLS: false, + wantErr: true, + errMsg: "starttls=true", + }, + { + name: "invalid protocol - invalid", + urlString: "http://example.com", + startTLS: false, + wantErr: true, + errMsg: "ldap:// or ldaps://", + }, + { + name: "empty URL - invalid", + urlString: "", + startTLS: false, + wantErr: true, + errMsg: "requires a configured URL", + }, + { + name: "URL with spaces - valid", + urlString: "ldaps://server1.example.com, ldaps://server2.example.com", + startTLS: false, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateADRotationURLs(tt.urlString, tt.startTLS) + if tt.wantErr { + if err == nil { + t.Errorf("validateADRotationURLs() expected error but got none") + return + } + if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("validateADRotationURLs() error = %v, want error containing %q", err, tt.errMsg) + } + } else { + if err != nil { + t.Errorf("validateADRotationURLs() unexpected error = %v", err) + } + } + }) + } +} diff --git a/builtin/credential/ldap/path_config_test.go b/builtin/credential/ldap/path_config_test.go new file mode 100644 index 0000000000..4b2e377d2c --- /dev/null +++ b/builtin/credential/ldap/path_config_test.go @@ -0,0 +1,113 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package ldap + +import ( + "context" + "encoding/json" + "testing" + + "github.com/hashicorp/vault/sdk/helper/ldaputil" + "github.com/hashicorp/vault/sdk/logical" +) + +// TestConfig_UpgradePath_Schema verifies that a ConfigEntry written to storage +// before the "schema" field existed (i.e. Schema == "") is transparently +// upgraded to the default value (SchemaOpenLDAP) when read back via Config(). +// This mirrors the upgrade handling already in place for CaseSensitiveNames +// and UsePre111GroupCNBehavior. +func TestConfig_UpgradePath_Schema(t *testing.T) { + ctx := context.Background() + b, store := createBackendWithStorage(t) + + // Simulate a pre-existing stored config that has no "schema" key — + // i.e. data written before the field was introduced. We do this by + // marshalling a map that deliberately omits the field. + oldEntry := map[string]interface{}{ + "url": "ldap://127.0.0.1", + "userdn": "ou=People,dc=example,dc=org", + "binddn": "cn=admin,dc=example,dc=org", + "bindpass": "secret", + // "schema" intentionally absent + "CaseSensitiveNames": false, + "use_pre111_group_cn_behavior": true, + } + raw, err := json.Marshal(oldEntry) + if err != nil { + t.Fatalf("failed to marshal old config entry: %s", err) + } + entry := &logical.StorageEntry{ + Key: "config", + Value: raw, + } + if err := store.Put(ctx, entry); err != nil { + t.Fatalf("failed to write legacy config to storage: %s", err) + } + + // Read back via Config() — this should trigger the upgrade path. + req := &logical.Request{Storage: store} + cfg, err := b.Config(ctx, req) + if err != nil { + t.Fatalf("Config() returned unexpected error: %s", err) + } + if cfg == nil { + t.Fatal("Config() returned nil; expected a valid ConfigEntry") + } + + // Schema must be defaulted to SchemaOpenLDAP, not left as "". + if cfg.Schema != ldaputil.SchemaOpenLDAP { + t.Errorf("expected Schema=%q after upgrade, got %q", ldaputil.SchemaOpenLDAP, cfg.Schema) + } + + // Verify the migrated value was persisted to storage. + persisted, err := store.Get(ctx, "config") + if err != nil { + t.Fatalf("failed to read persisted config: %s", err) + } + if persisted == nil { + t.Fatal("expected config to be re-persisted after upgrade, but nothing found in storage") + } + var persistedMap map[string]interface{} + if err := json.Unmarshal(persisted.Value, &persistedMap); err != nil { + t.Fatalf("failed to decode persisted config: %s", err) + } + if persistedMap["schema"] != ldaputil.SchemaOpenLDAP { + t.Errorf("persisted schema=%q; expected %q", persistedMap["schema"], ldaputil.SchemaOpenLDAP) + } +} + +// TestConfig_UpgradePath_Schema_NoOverwrite ensures that an already-set schema +// value is not overwritten by the upgrade logic. +func TestConfig_UpgradePath_Schema_NoOverwrite(t *testing.T) { + ctx := context.Background() + b, store := createBackendWithStorage(t) + + oldEntry := map[string]interface{}{ + "url": "ldap://127.0.0.1", + "userdn": "ou=People,dc=example,dc=org", + "binddn": "cn=admin,dc=example,dc=org", + "bindpass": "secret", + "schema": ldaputil.SchemaAD, // explicitly set to AD + "CaseSensitiveNames": false, + "use_pre111_group_cn_behavior": true, + } + raw, err := json.Marshal(oldEntry) + if err != nil { + t.Fatalf("failed to marshal config entry: %s", err) + } + entry := &logical.StorageEntry{Key: "config", Value: raw} + if err := store.Put(ctx, entry); err != nil { + t.Fatalf("failed to write config to storage: %s", err) + } + + req := &logical.Request{Storage: store} + cfg, err := b.Config(ctx, req) + if err != nil { + t.Fatalf("Config() returned unexpected error: %s", err) + } + + if cfg.Schema != ldaputil.SchemaAD { + t.Errorf("expected Schema=%q to be preserved, got %q", ldaputil.SchemaAD, cfg.Schema) + } +} diff --git a/changelog/_12223.txt b/changelog/_12223.txt new file mode 100644 index 0000000000..2f5a406c3f --- /dev/null +++ b/changelog/_12223.txt @@ -0,0 +1,4 @@ +```release-note:bug +ldap auth (enterprise): Fix root password rotation for Active Directory by implementing UTF-16LE encoding and schema-specific handling. Adds new 'schema' config field (defaults to 'openldap' for backward compatibility). +``` + diff --git a/helper/testhelpers/ldap/ldaphelper.go b/helper/testhelpers/ldap/ldaphelper.go index 4de3e40db0..2b022ac170 100644 --- a/helper/testhelpers/ldap/ldaphelper.go +++ b/helper/testhelpers/ldap/ldaphelper.go @@ -9,6 +9,7 @@ import ( "fmt" "runtime" "strings" + "sync" "testing" "time" @@ -17,6 +18,24 @@ import ( "github.com/hashicorp/vault/sdk/helper/ldaputil" ) +// safeBuffer is a thread-safe bytes.Buffer wrapper +type safeBuffer struct { + buf bytes.Buffer + mu sync.Mutex +} + +func (s *safeBuffer) Write(p []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.buf.Write(p) +} + +func (s *safeBuffer) String() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.buf.String() +} + // DefaultVersion is the default version of the container to pull. // NOTE: This is currently pinned to a sha instead of "master", see: https://github.com/rroemhild/docker-test-openldap/issues/62 const DefaultVersion = "sha256:f4d9c5ba97f9662e9aea082b4aa89233994ca6e232abc1952d5d90da7e16b0eb" @@ -28,7 +47,7 @@ func PrepareTestContainer(t *testing.T, version string) (cleanup func(), cfg *ld t.Skip("Skipping, as this image is not supported on ARM architectures") } - logsWriter := bytes.NewBuffer([]byte{}) + logsWriter := &safeBuffer{} runner, err := docker.NewServiceRunner(docker.RunOptions{ ImageRepo: "ghcr.io/rroemhild/docker-test-openldap", diff --git a/sdk/helper/ldaputil/config.go b/sdk/helper/ldaputil/config.go index 0ed2eb3845..a4ac61647b 100644 --- a/sdk/helper/ldaputil/config.go +++ b/sdk/helper/ldaputil/config.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/go-secure-stdlib/tlsutil" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/strutil" ) var ldapDerefAliasMap = map[string]int{ @@ -26,6 +27,18 @@ var ldapDerefAliasMap = map[string]int{ "always": ldap.DerefAlways, } +const ( + SchemaAD = "ad" + SchemaOpenLDAP = "openldap" +) + +// SupportedSchemas returns a slice of different LDAP schemas supported +// by the plugin. This is used to change behavior when modifying +// user passwords (for example, during root password rotation). +func SupportedSchemas() []string { + return []string{SchemaOpenLDAP, SchemaAD} +} + // ConfigFields returns all the config fields that can potentially be used by the LDAP client. // Not all fields will be used by every integration. func ConfigFields() map[string]*framework.FieldSchema { @@ -287,6 +300,11 @@ Default: ({{.UserAttr}}={{.Username}})`, Description: "If true, matching sAMAccountName attribute values will be allowed to login when upndomain is defined.", Default: false, }, + "schema": { + Type: framework.TypeString, + Description: "LDAP schema type: 'ad' for Active Directory, 'openldap' for OpenLDAP. Determines root password rotation behavior.", + Default: SchemaOpenLDAP, + }, } } @@ -468,6 +486,14 @@ func NewConfigEntry(existing *ConfigEntry, d *framework.FieldData) (*ConfigEntry if _, ok := d.Raw["enable_samaccountname_login"]; ok || !hadExisting { cfg.EnableSamaccountnameLogin = d.Get("enable_samaccountname_login").(bool) } + if _, ok := d.Raw["schema"]; ok || !hadExisting { + rawSchema := d.Get("schema").(string) + cfg.Schema = NormalizedSchema(rawSchema) + // Validate the normalized schema, not the raw input string, to allow for case-insensitive input while still enforcing valid schema types. + if !strutil.StrListContains(SupportedSchemas(), cfg.Schema) { + return nil, fmt.Errorf("unsupported schema type %q: must be one of %v", rawSchema, SupportedSchemas()) + } + } return cfg, nil } @@ -498,6 +524,7 @@ type ConfigEntry struct { ConnectionTimeout int `json:"connection_timeout"` // deprecated: use RequestTimeout DerefAliases string `json:"dereference_aliases"` MaximumPageSize int `json:"max_page_size"` + Schema string `json:"schema"` // These json tags deviate from snake case because there was a past issue // where the tag was being ignored, causing it to be jsonified as "CaseSensitiveNames", etc. @@ -541,6 +568,7 @@ func (c *ConfigEntry) PasswordlessMap() map[string]interface{} { "dereference_aliases": c.DerefAliases, "max_page_size": c.MaximumPageSize, "enable_samaccountname_login": c.EnableSamaccountnameLogin, + "schema": NormalizedSchema(c.Schema), // LDAP schema type for password operations } if c.CaseSensitiveNames != nil { m["case_sensitive_names"] = *c.CaseSensitiveNames @@ -593,6 +621,10 @@ func (c *ConfigEntry) Validate() error { return errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err) } } + normalizedSchema := NormalizedSchema(c.Schema) + if !strutil.StrListContains(SupportedSchemas(), normalizedSchema) { + return fmt.Errorf("unsupported schema type %q: must be one of %v", c.Schema, SupportedSchemas()) + } return nil } @@ -648,3 +680,11 @@ func min(a, b int) int { } return b } + +func NormalizedSchema(schema string) string { + normalizedSchema := strings.ToLower(strings.TrimSpace(schema)) + if normalizedSchema == "" { + return SchemaOpenLDAP + } + return normalizedSchema +} diff --git a/sdk/helper/ldaputil/config_test.go b/sdk/helper/ldaputil/config_test.go index 31bd42359e..ba6b8fff32 100644 --- a/sdk/helper/ldaputil/config_test.go +++ b/sdk/helper/ldaputil/config_test.go @@ -5,6 +5,7 @@ package ldaputil import ( "encoding/json" + "strings" "testing" "github.com/go-test/deep" @@ -68,6 +69,292 @@ func TestConfig(t *testing.T) { t.Errorf("expected false UseTokenGroups from JSON but got %t", configFromJSON.UseTokenGroups) } }) + + t.Run("default_schema_type", func(t *testing.T) { + if config.Schema != SchemaOpenLDAP { + t.Errorf("expected default Schema %s but got %s", SchemaOpenLDAP, config.Schema) + } + }) +} + +func TestNormalizedSchema(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty_string_defaults_to_openldap", + input: "", + expected: SchemaOpenLDAP, + }, + { + name: "lowercase_ad", + input: "ad", + expected: SchemaAD, + }, + { + name: "uppercase_AD", + input: "AD", + expected: SchemaAD, + }, + { + name: "mixed_case_Ad", + input: "Ad", + expected: SchemaAD, + }, + { + name: "lowercase_openldap", + input: "openldap", + expected: SchemaOpenLDAP, + }, + { + name: "uppercase_OPENLDAP", + input: "OPENLDAP", + expected: SchemaOpenLDAP, + }, + { + name: "mixed_case_OpenLDAP", + input: "OpenLDAP", + expected: SchemaOpenLDAP, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizedSchema(tt.input) + if result != tt.expected { + t.Errorf("NormalizedSchema(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestSchemaInConfigEntry(t *testing.T) { + t.Run("new_config_defaults_to_openldap", func(t *testing.T) { + s := &framework.FieldData{Schema: ConfigFields()} + config, err := NewConfigEntry(nil, s) + if err != nil { + t.Fatal("error getting default config") + } + + if config.Schema != SchemaOpenLDAP { + t.Errorf("expected default Schema %s but got %s", SchemaOpenLDAP, config.Schema) + } + }) + + t.Run("schema_normalized_on_create", func(t *testing.T) { + schema := ConfigFields() + data := &framework.FieldData{ + Raw: map[string]interface{}{ + "schema": "AD", + }, + Schema: schema, + } + + config, err := NewConfigEntry(nil, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if config.Schema != SchemaAD { + t.Errorf("expected normalized Schema 'ad' but got %s", config.Schema) + } + }) + + t.Run("schema_in_passwordless_map", func(t *testing.T) { + config := &ConfigEntry{ + Schema: SchemaAD, + } + + m := config.PasswordlessMap() + schema, ok := m["schema"] + if !ok { + t.Error("schema not found in PasswordlessMap") + } + + if schema != SchemaAD { + t.Errorf("expected schema %s in map but got %v", SchemaAD, schema) + } + }) + + t.Run("schema_update_existing_config", func(t *testing.T) { + existingConfig := &ConfigEntry{ + Url: "ldap://127.0.0.1", + Schema: SchemaOpenLDAP, + } + + schema := ConfigFields() + data := &framework.FieldData{ + Raw: map[string]interface{}{ + "schema": "AD", + }, + Schema: schema, + } + + updatedConfig, err := NewConfigEntry(existingConfig, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if updatedConfig.Schema != SchemaAD { + t.Errorf("expected updated Schema 'ad' but got %s", updatedConfig.Schema) + } + }) +} + +func TestSupportedSchemas(t *testing.T) { + schemas := SupportedSchemas() + + expectedSchemas := []string{SchemaOpenLDAP, SchemaAD} + if len(schemas) != len(expectedSchemas) { + t.Errorf("expected %d schemas but got %d", len(expectedSchemas), len(schemas)) + } + + for _, expected := range expectedSchemas { + found := false + for _, schema := range schemas { + if schema == expected { + found = true + break + } + } + if !found { + t.Errorf("expected schema %q not found in SupportedSchemas()", expected) + } + } +} + +func TestSchemaValidation(t *testing.T) { + t.Run("valid_openldap_schema", func(t *testing.T) { + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: SchemaOpenLDAP, + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + if err := config.Validate(); err != nil { + t.Errorf("expected no error for valid openldap schema, got: %v", err) + } + }) + + t.Run("valid_ad_schema", func(t *testing.T) { + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: SchemaAD, + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + if err := config.Validate(); err != nil { + t.Errorf("expected no error for valid ad schema, got: %v", err) + } + }) + + t.Run("empty_schema_passes_validation", func(t *testing.T) { + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: "", + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + if err := config.Validate(); err != nil { + t.Errorf("expected no error for empty schema (defaults to openldap), got: %v", err) + } + }) + + t.Run("generic_unsupported_schema_fails_validation", func(t *testing.T) { + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: "unsupported_schema", + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + err := config.Validate() + if err == nil { + t.Error("expected error for unsupported schema, got nil") + } + + if err != nil && !strings.Contains(err.Error(), "unsupported schema") { + t.Errorf("expected error message to contain 'unsupported schema', got: %v", err) + } + }) + + t.Run("freeipa_schema_fails_validation", func(t *testing.T) { + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: "freeipa", + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + err := config.Validate() + if err == nil { + t.Error("expected error for unsupported schema 'freeipa', got nil") + } + + if err != nil && !strings.Contains(err.Error(), "freeipa") { + t.Errorf("expected error message to include 'freeipa', got: %v", err) + } + }) + + t.Run("typo_in_schema_fails_validation", func(t *testing.T) { + // Test common typos like "adc" instead of "ad" + config := &ConfigEntry{ + Url: "ldap://127.0.0.1", + UserDN: "ou=users,dc=example,dc=com", + Schema: "adc", + TLSMinVersion: "tls12", + TLSMaxVersion: "tls12", + } + + err := config.Validate() + if err == nil { + t.Error("expected error for typo schema 'adc', got nil") + } + + // Verify error message is helpful + if err != nil && !strings.Contains(err.Error(), "unsupported schema") { + t.Errorf("expected error message to contain 'unsupported schema', got: %v", err) + } + if err != nil && !strings.Contains(err.Error(), "adc") { + t.Errorf("expected error message to include the unsupported value 'adc', got: %v", err) + } + }) + + t.Run("normalized_unsupported_schema_fails_validation", func(t *testing.T) { + // Test that even after normalization (lowercase), unsupported schemas fail + schema := ConfigFields() + data := &framework.FieldData{ + Raw: map[string]interface{}{ + "url": "ldap://127.0.0.1", + "userdn": "ou=users,dc=example,dc=com", + "schema": "UNSUPPORTED_SCHEMAS", + }, + Schema: schema, + } + + // NewConfigEntry should fail because schema validation happens during creation + _, err := NewConfigEntry(nil, data) + if err == nil { + t.Error("expected error from NewConfigEntry for unsupported schema, got nil") + } + + // Verify the error message is helpful + if err != nil { + if !strings.Contains(err.Error(), "unsupported schema") { + t.Errorf("expected error message to contain %q, got: %v", "unsupported schema", err) + } + } + }) } func testConfig(t *testing.T) *ConfigEntry { @@ -84,6 +371,7 @@ func testConfig(t *testing.T) *ConfigEntry { ConnectionTimeout: 15, ClientTLSCert: "", ClientTLSKey: "", + Schema: SchemaOpenLDAP, } } @@ -144,7 +432,8 @@ var jsonConfig = []byte(`{ "request_timeout": 30, "connection_timeout": 15, "ClientTLSCert": "", - "ClientTLSKey": "" + "ClientTLSKey": "", + "schema": "openldap" }`) var jsonConfigDefault = []byte(` @@ -179,6 +468,7 @@ var jsonConfigDefault = []byte(` "CaseSensitiveNames": false, "ClientTLSCert": "", "ClientTLSKey": "", - "enable_samaccountname_login": false + "enable_samaccountname_login": false, + "schema": "openldap" } `) From b72907dcdfc6175b276717e1ff344313a6926e35 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 09:38:13 -0400 Subject: [PATCH 053/468] PKI: Properly limit the max_path_length argument on sign-intermediate to a parent's max_path_length (#12623) (#12819) * Properly limit the max_path_length argument to a parent's max_path_length * add cl Co-authored-by: Steven Clark --- .../logical/pki/path_generate_root_test.go | 171 ++++++++++++++ builtin/logical/pki/path_root.go | 31 ++- .../pki/path_sign_intermediate_test.go | 221 ++++++++++++++++++ changelog/_12623.txt | 4 + 4 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 builtin/logical/pki/path_generate_root_test.go create mode 100644 builtin/logical/pki/path_sign_intermediate_test.go create mode 100644 changelog/_12623.txt diff --git a/builtin/logical/pki/path_generate_root_test.go b/builtin/logical/pki/path_generate_root_test.go new file mode 100644 index 0000000000..c9ba56d250 --- /dev/null +++ b/builtin/logical/pki/path_generate_root_test.go @@ -0,0 +1,171 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package pki + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestGenerateRoot_MaxPathLengthValidation validates the behavior of the max_path_length +// parameter on the POST /root/generate/:exported API handler. +// +// The handler logic (pathCAGenerateRoot in path_root.go) is: +// - If max_path_length is not provided in the request, the field is left +// unset on the role and the certificate generation code picks a default +// (no BasicConstraints pathLenConstraint, i.e. MaxPathLen == -1 in Go's +// x509 representation). +// - If max_path_length is provided and is < -1, the request is rejected with +// an error. +// - If max_path_length is provided and is >= -1, the generated certificate +// will carry that pathLenConstraint. +// - When the generated certificate has MaxPathLen == 0 (pathLenConstraint=0), +// Vault adds a warning that the certificate cannot be used to issue +// intermediate CAs. +func TestGenerateRoot_MaxPathLengthValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + maxPathLength int + omitParam bool + + expectError bool + errorContains string + + // Expected fields on the issued certificate (only checked when + // expectError == false). + expectedMaxPathLen int // cert.MaxPathLen; -1 means no constraint + }{ + // ---------------------------------------------------------------- + // Parameter omitted: Vault generates a root with no pathLenConstraint. + // ---------------------------------------------------------------- + { + name: "param_omitted_no_constraint", + omitParam: true, + expectError: false, + expectedMaxPathLen: -1, + }, + + // ---------------------------------------------------------------- + // Explicit -1: "no constraint" — identical outcome to omitting the + // parameter; the generated certificate carries no pathLenConstraint. + // ---------------------------------------------------------------- + { + name: "explicit_neg1_no_constraint", + maxPathLength: -1, + expectError: false, + expectedMaxPathLen: -1, + }, + + // ---------------------------------------------------------------- + // Explicit 0: pathLenConstraint=0 — the CA may not issue further + // intermediate CAs. Vault should succeed but add a warning. + // ---------------------------------------------------------------- + { + name: "explicit_0_zero_constraint_with_warning", + maxPathLength: 0, + expectError: false, + expectedMaxPathLen: 0, + }, + + // ---------------------------------------------------------------- + // Explicit positive values: pathLenConstraint is set as requested. + // ---------------------------------------------------------------- + { + name: "explicit_1", + maxPathLength: 1, + expectError: false, + expectedMaxPathLen: 1, + }, + { + name: "explicit_2", + maxPathLength: 2, + expectError: false, + expectedMaxPathLen: 2, + }, + { + name: "explicit_5", + maxPathLength: 5, + expectError: false, + expectedMaxPathLen: 5, + }, + + // ---------------------------------------------------------------- + // Invalid values (< -1): the handler must reject these immediately. + // ---------------------------------------------------------------- + { + name: "invalid_neg2", + maxPathLength: -2, + expectError: true, + errorContains: "max_path_length -2 is invalid", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b, s := CreateBackendWithStorage(t) + + params := map[string]interface{}{ + "common_name": "root.example.com", + "ttl": "87600h", + "key_type": "ec", + } + if !tc.omitParam { + params["max_path_length"] = tc.maxPathLength + } + + resp, err := CBWrite(b, s, "root/generate/internal", params) + + if tc.expectError { + require.Error(t, err, "expected root/generate/internal to fail but it succeeded") + if tc.errorContains != "" { + require.Contains(t, err.Error(), tc.errorContains, + "error message did not contain expected text") + } + return + } + + // Success path. + require.NoError(t, err, "expected root/generate/internal to succeed but it failed") + require.NotNil(t, resp, "expected non-nil response from root/generate/internal") + require.False(t, resp.IsError(), "root/generate/internal returned error response: %v", resp.Error()) + + // Parse the returned PEM certificate and verify BasicConstraints. + certPEM, ok := resp.Data["certificate"].(string) + require.True(t, ok, "response missing 'certificate' field") + + cert := parseCert(t, certPEM) + require.True(t, cert.BasicConstraintsValid, "expected BasicConstraints to be set on root CA") + require.True(t, cert.IsCA, "expected IsCA to be true on root CA") + + require.Equal(t, tc.expectedMaxPathLen, cert.MaxPathLen, + "certificate has unexpected MaxPathLen") + require.Equal(t, tc.expectedMaxPathLen == 0, cert.MaxPathLenZero, + "certificate has unexpected MaxPathLenZero") + + // Check for the zero-path-length warning. + if tc.expectedMaxPathLen == 0 { + requireMaxPathLengthZeroWarning(t, resp.Warnings) + } + }) + } +} + +func requireMaxPathLengthZeroWarning(t testing.TB, warnings []string) { + t.Helper() + require.NotEmpty(t, warnings, "expected at least one warning for zero max_path_length") + foundWarning := false + for _, w := range warnings { + if strings.Contains(w, "Max path length of the generated certificate is zero") { + foundWarning = true + break + } + } + require.True(t, foundWarning, "expected warning about zero max path length, got warnings: %v", warnings) +} diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index 62c051be05..de88690186 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -150,6 +150,9 @@ func (b *backend) pathCAGenerateRoot(ctx context.Context, req *logical.Request, maxPathLengthIface, ok := data.GetOk("max_path_length") if ok { maxPathLength := maxPathLengthIface.(int) + if maxPathLength < -1 { + return logical.ErrorResponse("requested max_path_length %d is invalid: must be a non-negative integer or -1 for no constraint", maxPathLength), nil + } role.MaxPathLength = &maxPathLength } @@ -426,9 +429,27 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R useCSRValues := data.Get("use_csr_values").(bool) - maxPathLengthIface, ok := data.GetOk("max_path_length") - if ok { + if maxPathLengthIface, ok := data.GetOk("max_path_length"); ok { maxPathLength := maxPathLengthIface.(int) + if maxPathLength < -1 { + return logical.ErrorResponse("requested max_path_length %d is invalid: must be a non-negative integer or -1 for no constraint", maxPathLength), nil + } + // Validate the requested max_path_length against the signing CA's + // BasicConstraints path length constraint (RFC 5280 4.2.1.9). + // If the signing CA has a pathLenConstraint of N, any intermediate + // it signs may have a pathLenConstraint strictly less than N. + // An explicit -1 means "no constraint on the intermediate", which + // is also invalid when the CA already has a pathLenConstraint. + caMaxPathLen := signingBundle.Certificate.MaxPathLen + caHasConstraint := caMaxPathLen >= 0 || signingBundle.Certificate.MaxPathLenZero + if caHasConstraint { + if maxPathLength < 0 || maxPathLength >= caMaxPathLen { + return logical.ErrorResponse( + fmt.Sprintf("requested max_path_length %d is not allowed: the signing CA has a pathLenConstraint of %[2]d, so the intermediate's pathLenConstraint must be a non-negative value less than %[2]d", + maxPathLength, caMaxPathLen), + ), nil + } + } role.MaxPathLength = &maxPathLength } @@ -469,6 +490,10 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R resp.AddWarning(intCaTruncatationWarning) } + if parsedBundle.Certificate.MaxPathLen == 0 { + resp.AddWarning("Max path length of the generated certificate is zero. This CA certificate cannot be used to issue further intermediate CA certificates.") + } + if keyUsages, ok := data.GetOk("key_usage"); ok { err = validateCaKeyUsages(keyUsages.([]string)) if err != nil { @@ -571,7 +596,7 @@ func signIntermediateResponse(signingBundle *certutil.CAInfoBundle, parsedBundle } if parsedBundle.Certificate.MaxPathLen == 0 { - resp.AddWarning("Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") + resp.AddWarning("Max path length of the signed certificate is zero. This CA certificate cannot be used to issue intermediate CA certificates.") } resp = addWarnings(resp, warnings) diff --git a/builtin/logical/pki/path_sign_intermediate_test.go b/builtin/logical/pki/path_sign_intermediate_test.go new file mode 100644 index 0000000000..50ce2aafe0 --- /dev/null +++ b/builtin/logical/pki/path_sign_intermediate_test.go @@ -0,0 +1,221 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package pki + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestSignIntermediate_MaxPathLengthValidation verifies that when signing an +// intermediate CA, the requested max_path_length is validated against the +// signing CA's BasicConstraints pathLenConstraint per RFC 5280 4.2.1.9. +// +// If the signing CA has a pathLenConstraint of N, any intermediate it signs +// must have a pathLenConstraint strictly less than N. +// +// When max_path_length is not provided at all (omitted from the request), the +// validation block is skipped entirely and the request always succeeds. +// +// When max_path_length is explicitly set to -1 (meaning "no constraint on the +// intermediate"), and the signing CA already has a pathLenConstraint, the +// request is rejected because an unconstrained intermediate would violate the +// CA's constraint. +// +// When the generated certificate has MaxPathLen == 0 (pathLenConstraint=0), +// Vault adds a warning that the certificate cannot be used to issue +// intermediate CAs. +func TestSignIntermediate_MaxPathLengthValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + caMaxPathLength int // max_path_length set on the root CA (-1 means not set / unconstrained) + intMaxPathLength int // max_path_length requested when signing the intermediate (-1 = "no constraint") + omitParam bool // if true, do not include max_path_length in the sign request at all + expectError bool + errorContains string + // Expected values on the issued certificate (only checked when expectError==false). + expectedMaxPathLen int // expected cert.MaxPathLen; -1 means "no constraint" + }{ + // --- Explicit positive values: validation is active --- + { + name: "ca_path_len_2_int_path_len_1_allowed", + caMaxPathLength: 2, + intMaxPathLength: 1, + expectError: false, + expectedMaxPathLen: 1, + }, + { + name: "ca_path_len_2_int_path_len_2_rejected", + caMaxPathLength: 2, + intMaxPathLength: 2, + expectError: true, + errorContains: "requested max_path_length 2 is not allowed", + }, + { + name: "ca_path_len_2_int_path_len_3_rejected", + caMaxPathLength: 2, + intMaxPathLength: 3, + expectError: true, + errorContains: "requested max_path_length 3 is not allowed", + }, + { + // pathLenConstraint=0 on the issued cert; Go represents this with + // MaxPathLen==0 and MaxPathLenZero==true. + name: "ca_path_len_1_int_path_len_0_allowed", + caMaxPathLength: 1, + intMaxPathLength: 0, + expectError: false, + expectedMaxPathLen: 0, + }, + { + name: "ca_path_len_1_int_path_len_1_rejected", + caMaxPathLength: 1, + intMaxPathLength: 1, + expectError: true, + errorContains: "requested max_path_length 1 is not allowed", + }, + { + // pathLenConstraint=0 on the CA means it may not issue further CAs + // with any pathLenConstraint (including 0). + name: "ca_path_len_0_int_path_len_0_rejected", + caMaxPathLength: 0, + intMaxPathLength: 0, + expectError: true, + errorContains: "requested max_path_length 0 is not allowed", + }, + { + // CA has no pathLenConstraint; any explicit positive value is allowed. + name: "ca_no_path_len_constraint_int_path_len_5_allowed", + caMaxPathLength: -1, // no constraint on CA + intMaxPathLength: 5, + expectError: false, + expectedMaxPathLen: 5, + }, + + // --- Explicit -1 ("no constraint on intermediate"): rejected only when CA is constrained --- + { + // CA has no constraint; explicit -1 is allowed and results in no + // pathLenConstraint on the issued certificate. + name: "ca_no_constraint_int_explicit_neg1_allowed", + caMaxPathLength: -1, + intMaxPathLength: -1, + expectError: false, + expectedMaxPathLen: -1, + }, + { + // CA has a constraint; explicit -1 means "no constraint on the + // intermediate", which violates the CA's pathLenConstraint. + name: "ca_path_len_5_int_explicit_neg1_rejected", + caMaxPathLength: 5, + intMaxPathLength: -1, + expectError: true, + errorContains: "requested max_path_length -1 is not allowed", + }, + { + // CA has pathLenConstraint=0; explicit -1 is also rejected. + name: "ca_path_len_0_int_explicit_neg1_rejected", + caMaxPathLength: 0, + intMaxPathLength: -1, + expectError: true, + errorContains: "requested max_path_length -1 is not allowed", + }, + + // --- Parameter omitted entirely: validation is skipped --- + // When omitted, Vault auto-derives the pathLenConstraint as (CA pathLen - 1). + // When the CA has no constraint, the issued cert also has no constraint (-1). + { + // CA has no constraint; omitting the parameter results in no + // pathLenConstraint on the issued certificate. + name: "ca_no_constraint_int_param_omitted_allowed", + caMaxPathLength: -1, + omitParam: true, + expectError: false, + expectedMaxPathLen: -1, + }, + { + // CA has pathLenConstraint=5; omitting the parameter causes Vault to + // auto-derive pathLenConstraint = 5 - 1 = 4 on the issued certificate. + name: "ca_path_len_5_int_param_omitted_allowed", + caMaxPathLength: 5, + omitParam: true, + expectError: false, + expectedMaxPathLen: 4, + }, + { + // CA has pathLenConstraint=1; omitting the parameter causes Vault to + // auto-derive pathLenConstraint = 1 - 1 = 0 on the issued certificate. + name: "ca_path_len_1_int_param_omitted_allowed", + caMaxPathLength: 1, + omitParam: true, + expectError: false, + expectedMaxPathLen: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b, s := CreateBackendWithStorage(t) + + // Generate root CA with the specified max_path_length. + rootParams := map[string]interface{}{ + "common_name": "root.example.com", + "ttl": "87600h", + } + if tc.caMaxPathLength >= 0 { + rootParams["max_path_length"] = tc.caMaxPathLength + } + + resp, err := CBWrite(b, s, "root/generate/internal", rootParams) + requireSuccessNonNilResponse(t, resp, err, "failed to generate root CA") + + // Generate an intermediate CSR. + resp, err = CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{ + "common_name": "int.example.com", + }) + requireSuccessNonNilResponse(t, resp, err, "failed to generate intermediate CSR") + csr := resp.Data["csr"].(string) + + // Build the sign-intermediate request. + signParams := map[string]interface{}{ + "common_name": "int.example.com", + "csr": csr, + "ttl": "43800h", + } + if !tc.omitParam { + signParams["max_path_length"] = tc.intMaxPathLength + } + + resp, err = CBWrite(b, s, "root/sign-intermediate", signParams) + + if tc.expectError { + require.Error(t, err, "expected sign-intermediate to fail but it succeeded") + if tc.errorContains != "" { + require.Contains(t, err.Error(), tc.errorContains, + "error message did not contain expected text") + } + } else { + require.NoError(t, err, "expected sign-intermediate to succeed but it failed") + require.NotNil(t, resp, "expected non-nil response from sign-intermediate") + require.False(t, resp.IsError(), "sign-intermediate returned error: %v", resp.Error()) + + cert := parseCert(t, resp.Data["certificate"].(string)) + require.True(t, cert.BasicConstraintsValid, "expected BasicConstraints to be set") + require.True(t, cert.IsCA, "expected IsCA to be true") + + require.Equal(t, tc.expectedMaxPathLen == 0, cert.MaxPathLenZero, + "issued certificate has unexpected MaxPathLenZero") + require.Equal(t, tc.expectedMaxPathLen, cert.MaxPathLen, + "issued certificate has unexpected MaxPathLen") + if tc.expectedMaxPathLen == 0 { + requireMaxPathLengthZeroWarning(t, resp.Warnings) + } + } + }) + } +} diff --git a/changelog/_12623.txt b/changelog/_12623.txt new file mode 100644 index 0000000000..c818d0596d --- /dev/null +++ b/changelog/_12623.txt @@ -0,0 +1,4 @@ + +```release-note:bug +secrets/pki: The root/sign-intermediate endpoint max_path_length parameter is now restricted by the signing CA's max_path_length if set. +``` From ef97ba7518d827449bb2cca80b8701c3127f5fd9 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 10:44:43 -0400 Subject: [PATCH 054/468] Backport Eliminate need for defer cluster.Cleanup into ce/main (#12770) --- helper/testhelpers/minimal/minimal.go | 1 - sdk/helper/testcluster/docker/environment.go | 7 ++- vault/testing.go | 57 +++++++++++--------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/helper/testhelpers/minimal/minimal.go b/helper/testhelpers/minimal/minimal.go index 5903db136c..a468b9bb7b 100644 --- a/helper/testhelpers/minimal/minimal.go +++ b/helper/testhelpers/minimal/minimal.go @@ -76,6 +76,5 @@ func NewTestSoloCluster(t testing.TB, config *vault.CoreConfig) *vault.TestClust HandlerFunc: http.Handler, Logger: logger, }) - t.Cleanup(cluster.Cleanup) return cluster } diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index 0c85c0a617..2adc9f30a8 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -82,6 +82,7 @@ type DockerCluster struct { storage testcluster.ClusterStorage disableMlock bool disableTLS bool + cleanupOnce sync.Once } func (dc *DockerCluster) NamedLogger(s string) log.Logger { @@ -144,7 +145,9 @@ func (dc *DockerCluster) GetCACertPEMFile() string { } func (dc *DockerCluster) Cleanup() { - dc.cleanup() + dc.cleanupOnce.Do(func() { + dc.cleanup() + }) } func (dc *DockerCluster) cleanup() error { @@ -432,6 +435,8 @@ func NewTestDockerClusterWithErr(t *testing.T, opts *DockerClusterOptions) (*Doc dc, err := NewDockerCluster(ctx, opts) if err == nil { dc.Logger.Trace("cluster started", "helpful_env", fmt.Sprintf("VAULT_TOKEN=%s VAULT_CACERT=/vault/config/ca.pem", dc.GetRootToken())) + // Register cleanup with t.Cleanup so it's automatically called when the test ends + t.Cleanup(dc.Cleanup) } return dc, err } diff --git a/vault/testing.go b/vault/testing.go index 8d7d1fb9d2..d2a0fc9625 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -739,6 +739,7 @@ type TestCluster struct { LicensePublicKey ed25519.PublicKey LicensePrivateKey ed25519.PrivateKey opts *TestClusterOptions + cleanupOnce sync.Once } func (c *TestCluster) SetRootToken(token string) { @@ -992,36 +993,38 @@ func (c *TestClusterCore) NetworkLayer() cluster.NetworkLayer { } func (c *TestCluster) Cleanup() { - c.Logger.Info("cleaning up vault cluster") - if tl, ok := c.Logger.(*corehelpers.TestLogger); ok { - tl.StopLogging() - } + c.cleanupOnce.Do(func() { + c.Logger.Info("cleaning up vault cluster") + if tl, ok := c.Logger.(*corehelpers.TestLogger); ok { + tl.StopLogging() + } - wg := &sync.WaitGroup{} - for _, core := range c.Cores { - wg.Add(1) - lc := core + wg := &sync.WaitGroup{} + for _, core := range c.Cores { + wg.Add(1) + lc := core - go func() { - defer wg.Done() - if err := lc.stop(); err != nil { - // Note that this log won't be seen if using TestLogger, due to - // the above call to StopLogging. - lc.Logger().Error("error during cleanup", "error", err) - } - }() - } + go func() { + defer wg.Done() + if err := lc.stop(); err != nil { + // Note that this log won't be seen if using TestLogger, due to + // the above call to StopLogging. + lc.Logger().Error("error during cleanup", "error", err) + } + }() + } - wg.Wait() + wg.Wait() - // Remove any temp dir that exists - if c.TempDir != "" { - os.RemoveAll(c.TempDir) - } + // Remove any temp dir that exists + if c.TempDir != "" { + os.RemoveAll(c.TempDir) + } - if c.CleanupFunc != nil { - c.CleanupFunc() - } + if c.CleanupFunc != nil { + c.CleanupFunc() + } + }) } func (c *TestCluster) ensureCoresSealed() error { @@ -1733,6 +1736,10 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T // once, otherwise when they re-initialize themselves they can yield 500s. time.Sleep(coreConfig.PeriodicLeaderRefreshInterval) } + + // Register cleanup with t.Cleanup so it's automatically called when the test ends + t.Cleanup(testCluster.Cleanup) + return &testCluster } From a2978a63f2f2158c03c2c9bfb6b8e2bf8c2d6b16 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 11:49:32 -0400 Subject: [PATCH 055/468] Fix HCP workflow expression evaluation and add test option (#12759) (#12833) Co-authored-by: Luis (LT) Carbonell --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28294eaff4..6e311e17fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -394,7 +394,7 @@ jobs: hcp-image: if: | - needs.setup.outputs.is-ent-branch == 'true' && + needs.setup.outputs.is-ent-branch == 'true' && ( needs.setup.outputs.workflow-trigger == 'schedule' || ( needs.setup.outputs.workflow-trigger == 'pull_request' && ( @@ -402,13 +402,14 @@ jobs: contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') ) ) + ) needs: - setup - artifacts-ent uses: ./.github/workflows/build-hcp-image.yml with: - pull-request: ${{ needs.setup.outputs.workflow-trigger == 'pull_request' && github.event.pull_request.number || null }} - branch: ${{ needs.setup.outputs.workflow-trigger == 'schedule' && 'main' || null }} + pull-request: ${{ needs.setup.outputs.workflow-trigger == 'pull_request' && github.event.pull_request && github.event.pull_request.number || '' }} + branch: ${{ needs.setup.outputs.workflow-trigger == 'schedule' && 'main' || '' }} create-aws-image: true create-azure-image: false hcp-environment: int From d657370cade4b81e03ed86a420702242c0c5f467 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 14:20:17 -0400 Subject: [PATCH 056/468] VAULT-42770 Fix tests using deprecated SCIM API, JWT-related panic and get them ready to be unskipped (#12763) (#12842) * WIP * debugging * VAULT-42770 Fix tests/panic and unskip them* * Extra belt-and-braces * fix SCIM, add skips Co-authored-by: Violet Hynes --- vault/expiration.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/vault/expiration.go b/vault/expiration.go index c00c73dbed..9843962b66 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -2300,7 +2300,7 @@ func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEnt tokenNS := namespace.RootNamespace saltCtx := namespace.ContextWithNamespace(ctx, namespace.RootNamespace) _, nsID := namespace.SplitIDFromString(token) - if nsID != "" { + if nsID != "" && !IsEnterpriseToken(token) { var err error tokenNS, err = NamespaceByID(ctx, nsID, m.core) if err != nil { @@ -2311,6 +2311,16 @@ func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEnt } } + // If it's an enterprise token, we cannot get the ID from the token, + // so let's get it from the lease. + if IsEnterpriseToken(token) { + ns, err := m.getNamespaceFromLeaseID(ctx, le.LeaseID) + if err != nil { + return err + } + tokenNS = ns + } + saltedID, err := m.tokenStore.SaltID(saltCtx, token) if err != nil { return err From ab5b314c9537f3c7820aff56b493477dd9b9c2ed Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 14:36:53 -0400 Subject: [PATCH 057/468] actions: pin actions to the latest versions (#12772) (#12793) - docker/setup-buildx-action v3.12.0 => v4.0.0 Node 24 upgrade, switch to ESM, some deprecated inputs have been removed. - docker/build-push-action v6.19.2 => v7.0.0 Node 24 upgrade, switch to ESM, some deprecated envs have been removed. - actions/setup-node v6.2.0 => v6.3.0 Bug fixes, internal dep updates, support for parsing `devEngines`. - action-setup-enos v1.50 => v1.51 Use enos 0.0.36 Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .github/actions/build-vault/action.yml | 6 +++--- .github/actions/setup-pnpm/action.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/enos-lint.yml | 2 +- .github/workflows/test-enos-scenario-ui.yml | 4 ++-- .github/workflows/test-run-enos-scenario-containers.yml | 4 ++-- .github/workflows/test-run-enos-scenario-matrix.yml | 4 ++-- .github/workflows/test-run-enos-scenario.yml | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/actions/build-vault/action.yml b/.github/actions/build-vault/action.yml index 6639e7d635..ee4acfa4e9 100644 --- a/.github/actions/build-vault/action.yml +++ b/.github/actions/build-vault/action.yml @@ -133,7 +133,7 @@ runs: shell: bash run: make ci-build - if: inputs.cgo-enabled == '1' - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: driver-opts: network=host # So we can run our own little registry - if: inputs.cgo-enabled == '1' @@ -143,7 +143,7 @@ runs: if: inputs.cgo-enabled == '1' id: build-push-action-attempt-1 continue-on-error: true # we will retry this if it fails - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false with: @@ -165,7 +165,7 @@ runs: id: build-push-action-attempt-2 continue-on-error: false if: inputs.cgo-enabled == '1' && steps.build-push-action-attempt-1.outcome != 'success' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false with: diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 1a7c2fbdcb..6e9ef8fe73 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -18,7 +18,7 @@ runs: package_json_file: './ui/package.json' - name: Setup Node Caching - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: './ui/package.json' cache: pnpm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e311e17fd..e210b430d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -319,7 +319,7 @@ jobs: package_json_file: './ui/package.json' - if: steps.cache-ui-assets.outputs.cache-hit != 'true' name: Set up node and pnpm - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ui/package.json cache: pnpm diff --git a/.github/workflows/enos-lint.yml b/.github/workflows/enos-lint.yml index 79176a8d70..e0aff92a41 100644 --- a/.github/workflows/enos-lint.yml +++ b/.github/workflows/enos-lint.yml @@ -45,7 +45,7 @@ jobs: - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: terraform_wrapper: false - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 - name: Ensure shellcheck is available for linting run: which shellcheck || (sudo apt update && sudo apt install -y shellcheck) - name: lint diff --git a/.github/workflows/test-enos-scenario-ui.yml b/.github/workflows/test-enos-scenario-ui.yml index 6c0acc7185..effe98f58b 100644 --- a/.github/workflows/test-enos-scenario-ui.yml +++ b/.github/workflows/test-enos-scenario-ui.yml @@ -82,13 +82,13 @@ jobs: - uses: ./.github/actions/set-up-go with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Set Up Git run: git config --global url."https://${{ secrets.elevated_github_token }}:@github.com".insteadOf "https://github.com" - name: Set Up Node - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: './ui/package.json' cache: pnpm diff --git a/.github/workflows/test-run-enos-scenario-containers.yml b/.github/workflows/test-run-enos-scenario-containers.yml index 04224e003c..bfef3bde0f 100644 --- a/.github/workflows/test-run-enos-scenario-containers.yml +++ b/.github/workflows/test-run-enos-scenario-containers.yml @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.vault-revision }} - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - uses: ./.github/actions/metadata @@ -87,7 +87,7 @@ jobs: # the Terraform wrapper will break Terraform execution in Enos because # it changes the output to text when we expect it to be JSON. terraform_wrapper: false - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Download Docker Image diff --git a/.github/workflows/test-run-enos-scenario-matrix.yml b/.github/workflows/test-run-enos-scenario-matrix.yml index 5f6dd24d83..971e526da7 100644 --- a/.github/workflows/test-run-enos-scenario-matrix.yml +++ b/.github/workflows/test-run-enos-scenario-matrix.yml @@ -70,7 +70,7 @@ jobs: token: ${{ steps.vault-auth.outputs.token }} secrets: | kv/data/github/${{ github.repository }}/github-token token | ELEVATED_GITHUB_TOKEN; - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ github.repository == 'hashicorp/vault' && secrets.ELEVATED_GITHUB_TOKEN || steps.vault-secrets.outputs.ELEVATED_GITHUB_TOKEN }} - uses: ./.github/actions/create-dynamic-config @@ -214,7 +214,7 @@ jobs: role-to-assume: ${{ steps.secrets.outputs.aws-role-arn }} role-skip-session-tagging: true role-duration-seconds: 3600 - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ steps.secrets.outputs.github-token }} - uses: ./.github/actions/create-dynamic-config diff --git a/.github/workflows/test-run-enos-scenario.yml b/.github/workflows/test-run-enos-scenario.yml index b8bc1e66b5..50fe1a3b2f 100644 --- a/.github/workflows/test-run-enos-scenario.yml +++ b/.github/workflows/test-run-enos-scenario.yml @@ -67,7 +67,7 @@ jobs: - name: Configure Git run: git config --global url."https://${{ secrets.ELEVATED_GITHUB_TOKEN }}:@github.com".insteadOf "https://github.com" - name: Set up node - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: './ui/package.json' cache: pnpm @@ -91,7 +91,7 @@ jobs: role-to-assume: ${{ secrets.AWS_ROLE_ARN_CI }} role-skip-session-tagging: true role-duration-seconds: 3600 - - uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a # v1.50 + - uses: hashicorp/action-setup-enos@3a9f736b68564c957cefbfcfb3d16b68e581a5b2 # v1.51 with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Prepare scenario dependencies From c596f130f55aef50e2193ac79ac9d1902a44709c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 14:55:10 -0400 Subject: [PATCH 058/468] go: upgrade Go to 1.26.1 (#12839) (#12849) * go: upgrade Go to 1.26.1 Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .go-version | 2 +- changelog/_go-ver-1220.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.go-version b/.go-version index 5ff8c4f5d2..dd43a143f0 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.26.0 +1.26.1 diff --git a/changelog/_go-ver-1220.txt b/changelog/_go-ver-1220.txt index 256caa4bb6..2b5163dc20 100644 --- a/changelog/_go-ver-1220.txt +++ b/changelog/_go-ver-1220.txt @@ -1,3 +1,3 @@ ```release-note:change -core: Bump Go version to 1.26.0 +core: Bump Go version to 1.26.1 ``` From 0b74c2b98bb97840b70b85b5350774b074a3453e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 15:23:48 -0400 Subject: [PATCH 059/468] Add test case and add some comments (#12811) (#12850) * Add test case and add some comments * Fix comment, remove duplicate Co-authored-by: Robert <17119716+robmonte@users.noreply.github.com> --- sdk/helper/automatedrotationutil/fields_test.go | 17 +++++++++++++++++ sdk/rotation/rotation_job.go | 17 ++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/sdk/helper/automatedrotationutil/fields_test.go b/sdk/helper/automatedrotationutil/fields_test.go index 45ddd19e90..ae2fec8cbf 100644 --- a/sdk/helper/automatedrotationutil/fields_test.go +++ b/sdk/helper/automatedrotationutil/fields_test.go @@ -309,6 +309,23 @@ func TestPopulateSetAutomatedRotationData(t *testing.T) { DisableAutomatedRotation: false, }, }, + { + name: "only schedule set", + expected: map[string]interface{}{ + "rotation_schedule": "*/15 * * * *", + "rotation_window": time.Duration(0).Seconds(), + "rotation_policy": "", + "disable_automated_rotation": false, + }, + unexpected: map[string]interface{}{ + "rotation_period": "", + }, + inputParams: &AutomatedRotationParams{ + RotationSchedule: "*/15 * * * *", + RotationPeriod: 0, + DisableAutomatedRotation: false, + }, + }, { name: "only period set", expected: map[string]interface{}{ diff --git a/sdk/rotation/rotation_job.go b/sdk/rotation/rotation_job.go index 6377cad35a..a5cf2f8df5 100644 --- a/sdk/rotation/rotation_job.go +++ b/sdk/rotation/rotation_job.go @@ -38,13 +38,16 @@ type RotationJob struct { } type RotationJobConfigureRequest struct { - Name string - MountPoint string - ReqPath string - RotationSchedule string - RotationWindow time.Duration - RotationPeriod time.Duration - RotationPolicy string + Name string + MountPoint string + ReqPath string + RotationSchedule string + RotationWindow time.Duration + RotationPeriod time.Duration + RotationPolicy string + // MigratedLegacyNextRotationTime is an optional field that will override the calculated NextVaultRotation time for + // only the *first* rotation of the job. This is intended for migrating any existing scheduled rotations from a + // plugin-managed rotation queue onto the Rotation Manager, without skipping its next scheduled rotation. MigratedLegacyNextRotationTime time.Time } From 4b9654f0963be1efc205927fac1255d1227276d2 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 18:43:30 -0400 Subject: [PATCH 060/468] release: remove 1.16.x from versions.hcl (#12852) (#12862) Remove 1.16.x+ent from .release/versions.hcl because 1.16.x+ent is now EOL and will receive no further updates. Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .release/versions.hcl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.release/versions.hcl b/.release/versions.hcl index ee3c4cb59d..f3637a1ce8 100644 --- a/.release/versions.hcl +++ b/.release/versions.hcl @@ -21,9 +21,4 @@ active_versions { ce_active = false lts = true } - - version "1.16.x" { - ce_active = false - lts = true - } } From aa10cc0e4ab529042bc6c5ac0eee194e4b69999d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 18:46:35 -0400 Subject: [PATCH 061/468] cloud: automatically trigger custom image test when changing the `hcp` testing toolchain (#12654) (#12664) * actions: pull in gotestsum when executing the cloud scenario * cloud: add 'hcp' changed-file group and trigger cloud scenario when the files change * slightly simplify expression --------- Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .github/workflows/build.yml | 21 +++++++++++---------- .release/pipeline.hcl | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e210b430d4..da67c28018 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -398,6 +398,7 @@ jobs: needs.setup.outputs.workflow-trigger == 'schedule' || ( needs.setup.outputs.workflow-trigger == 'pull_request' && ( + contains(fromJSON(needs.setup.outputs.changed-files).groups, 'hcp') || contains(fromJSON(needs.setup.outputs.labels), 'hcp/build-image') || contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') ) @@ -474,16 +475,16 @@ jobs: # Test our custom HCP image if our image build was successful and we've # been configured with the correct label. if: | - needs.setup.outputs.is-ent-branch == 'true' && - needs.setup.outputs.workflow-trigger == 'schedule' || - ( needs.setup.outputs.workflow-trigger == 'pull_request' && - needs.artifacts-ent.result == 'success' && - needs.hcp-image.result == 'success' && - contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') - ) || - ( needs.setup.outputs.workflow-trigger == 'schedule' && - needs.artifacts-ent.result == 'success' && - needs.hcp-image.result == 'success' + needs.setup.outputs.is-ent-branch == 'true' && ( + ( needs.setup.outputs.workflow-trigger == 'pull_request' && + needs.artifacts-ent.result == 'success' && + needs.hcp-image.result == 'success' && + ( contains(fromJSON(needs.setup.outputs.changed-files).groups, 'hcp') || contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') ) + ) || ( + needs.setup.outputs.workflow-trigger == 'schedule' && + needs.artifacts-ent.result == 'success' && + needs.hcp-image.result == 'success' + ) ) needs: - setup diff --git a/.release/pipeline.hcl b/.release/pipeline.hcl index 2f58f30b5c..42ab4ba393 100644 --- a/.release/pipeline.hcl +++ b/.release/pipeline.hcl @@ -217,6 +217,35 @@ changed_files { } } + // The "hcp" group is for files that are unique to testing Vault in the + // HashiCorp Cloud Platform, i.e. HVD or Vault Cloud. + group "hcp" { + match { + file = [ + joinpath(".github", "workflows", "build-hcp-image.yml"), + joinpath(".github", "workflows", "test-run-enos-scenario-cloud.yml"), + joinpath("enos", "enos-scenario-cloud-ent.hcl"), + ] + } + + match { + base_dir = [ + joinpath("enos", "modules", "cloud_docker_vault_cluster"), + joinpath("enos", "modules", "hcp"), + joinpath("tools", "pipeline", "internal", "pkg", "hcp"), + ] + } + + match { + base_dir = [ + joinpath("tools", "pipeline", "internal", "cmd"), + ] + contains = [ + "hcp" + ] + } + } + // The "pipeline" group matches directories where we house code and // configuration used in the CI/CD pipeline group "pipeline" { From 749e0dbbecd99e5337114ad5f6c5fc354ca31377 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 9 Mar 2026 19:08:51 -0400 Subject: [PATCH 062/468] Set sed command flags based on operation system (#12179) (#12871) * Check uname for sed flag * Account for gsed Co-authored-by: Robert <17119716+robmonte@users.noreply.github.com> --- Makefile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e986fdd73b..a7e2e98d79 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,13 @@ INTEG_TEST_TIMEOUT=120m VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr GOFMT_FILES?=$$(find . -name '*.go' | grep -v pb.go | grep -v vendor) SED?=$(shell command -v gsed || command -v sed) +SED_CMD := $(SED) -i +# MacOS without gsed requires an empty string argument for -i. +ifeq ($(shell uname -s),Darwin) + ifneq ($(findstring gsed,$(SED)),gsed) + SED_CMD := $(SED) -i '' + endif +endif GO_VERSION_MIN=$$(cat $(CURDIR)/.go-version) GO_CMD?=go @@ -229,8 +236,11 @@ proto: check-tools-external # No additional sed expressions should be added to this list. Going forward # we should just use the variable names chosen by protobuf. These are left # here for backwards compatibility, namely for SDK compilation. - $(SED) -i -e 's/Id/ID/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' vault/request_forwarding_service.pb.go - $(SED) -i -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/BoundCidrs/BoundCIDRs/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' helper/identity/types.pb.go helper/identity/mfa/types.pb.go helper/storagepacker/types.pb.go sdk/plugin/pb/backend.pb.go sdk/logical/identity.pb.go vault/activity/activity_log.pb.go + $(SED_CMD) -e 's/Id/ID/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' vault/request_forwarding_service.pb.go + $(SED_CMD) -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/BoundCidrs/BoundCIDRs/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' helper/identity/types.pb.go helper/identity/mfa/types.pb.go helper/storagepacker/types.pb.go sdk/plugin/pb/backend.pb.go sdk/logical/identity.pb.go vault/activity/activity_log.pb.go + + # Enterprise files + $(SED_CMD) -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/protobuf:"/sentinel:"" protobuf:"/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' vault/replication_services_ent.pb.go # This will inject the sentinel struct tags as decorated in the proto files. protoc-go-inject-tag -input=./helper/identity/types.pb.go From 9b35f7dae7fc7b4a369f9013c7abad2ed8ae23e6 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 10:52:50 -0400 Subject: [PATCH 063/468] VAULT-42598 add resource cleanup to SCIM client delete (#12489) (#12826) * add resource orphaning to SCIM client delete * add background orphaning handling * delete instead of orphan, add retry and startup tests * revert: undo accidental changes to Makefile and golang instructions * fix tests * stop log flood (try again) * fix linter findings * try to silence spam again * try to silence spam once more * dont allow running outside of active primary * go docs * fix active check and pass client id via context * remove unnecessary change * Remove Test_SCIM_ClientDeletion_Cascading this test was added in another PR but mine already has a bunch of deleting test that work with the new behavior Co-authored-by: Bruno Oliveira de Souza --- api/logical.go | 9 ++ helper/identity/types.pb.go | 167 +++++++++++++++++-------------- helper/identity/types.proto | 5 + sdk/physical/path_error.go | 146 +++++++++++++++++++++++++++ vault/core.go | 4 + vault/identity_store.go | 2 +- vault/identity_store_aliases.go | 4 + vault/identity_store_entities.go | 8 ++ vault/identity_store_groups.go | 4 + vault/identity_store_schema.go | 10 ++ vault/identity_store_scim_oss.go | 9 ++ vault/identity_store_structs.go | 12 ++- vault/identity_store_util.go | 62 +++++++++++- 13 files changed, 360 insertions(+), 82 deletions(-) create mode 100644 sdk/physical/path_error.go diff --git a/api/logical.go b/api/logical.go index 4f1d7e7789..ebcb9ea941 100644 --- a/api/logical.go +++ b/api/logical.go @@ -393,6 +393,15 @@ func (c *Logical) DeleteWithContext(ctx context.Context, path string) (*Secret, return c.DeleteWithDataWithContext(ctx, path, nil) } +func (c *Logical) DeleteRaw(path string) (*Response, error) { + return c.DeleteRawWithContext(context.Background(), path) +} + +func (c *Logical) DeleteRawWithContext(ctx context.Context, path string) (*Response, error) { + r := c.c.NewRequest(http.MethodDelete, "/v1/"+path) + return c.c.RawRequestWithContext(ctx, r) +} + func (c *Logical) DeleteWithData(path string, data map[string][]string) (*Secret, error) { return c.DeleteWithDataWithContext(context.Background(), path, data) } diff --git a/helper/identity/types.pb.go b/helper/identity/types.pb.go index 060965fccf..fad851ea48 100644 --- a/helper/identity/types.pb.go +++ b/helper/identity/types.pb.go @@ -731,7 +731,11 @@ type ScimClient struct { // belongs to. Do not return this value over the API when reading the // entity. // @inject_tag: sentinel:"-" - NamespaceID string `protobuf:"bytes,6,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty" sentinel:"-"` + NamespaceID string `protobuf:"bytes,6,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty" sentinel:"-"` + // Deleting indicates that the SCIM client is in the process of being deleted. + // This allows cascading cleanup to be eventually handled even if there are failures during the deletion process. + // @inject_tag: sentinel:"-" + Deleting bool `protobuf:"varint,7,opt,name=deleting,proto3" json:"deleting,omitempty" sentinel:"-"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -808,6 +812,13 @@ func (x *ScimClient) GetNamespaceID() string { return "" } +func (x *ScimClient) GetDeleting() bool { + if x != nil { + return x.Deleting + } + return false +} + // Deprecated. Retained for backwards compatibility. type EntityStorageEntry struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1191,7 +1202,7 @@ var file_helper_identity_types_proto_rawDesc = string([]byte{ 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x0a, 0x53, 0x63, 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x38, 0x01, 0x22, 0x92, 0x02, 0x0a, 0x0a, 0x53, 0x63, 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, @@ -1206,83 +1217,85 @@ var file_helper_identity_types_proto_rawDesc = string([]byte{ 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x22, 0x88, 0x05, 0x0a, 0x12, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x37, 0x0a, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x46, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x2a, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x88, 0x05, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, + 0x0a, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, + 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, + 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, - 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, - 0x0a, 0x11, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, - 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, - 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, - 0x0a, 0x0b, 0x6d, 0x66, 0x61, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x0a, 0x6d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, - 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, - 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x21, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, - 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf9, 0x03, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, - 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, - 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x29, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, - 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x65, + 0x72, 0x67, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, + 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x62, 0x75, 0x63, + 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0b, 0x6d, 0x66, + 0x61, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x66, + 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6d, + 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x6d, 0x66, 0x61, + 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xf9, 0x03, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x33, 0x0a, 0x16, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x13, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x49, 0x64, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, - 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, + 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x16, + 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x6d, 0x65, + 0x72, 0x67, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, + 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2c, + 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, + 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/helper/identity/types.proto b/helper/identity/types.proto index 0903ce4a75..3a7bed0943 100644 --- a/helper/identity/types.proto +++ b/helper/identity/types.proto @@ -308,6 +308,11 @@ message ScimClient { // entity. // @inject_tag: sentinel:"-" string namespace_id = 6; + + // Deleting indicates that the SCIM client is in the process of being deleted. + // This allows cascading cleanup to be eventually handled even if there are failures during the deletion process. + // @inject_tag: sentinel:"-" + bool deleting = 7; } // Deprecated. Retained for backwards compatibility. diff --git a/sdk/physical/path_error.go b/sdk/physical/path_error.go new file mode 100644 index 0000000000..37b05aefaf --- /dev/null +++ b/sdk/physical/path_error.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package physical + +import ( + "context" + "errors" + "math/rand" + "strings" + "sync" + "time" + + log "github.com/hashicorp/go-hclog" +) + +// PathErrorInjector wraps a physical backend and injects errors on storage +// operations whose keys match configured path prefixes. Unlike ErrorInjector, +// it does not support a global error rate — errors are only injected for +// paths that have been explicitly configured via SetErrorPercentageForPath. +// +// This is useful for tests that need surgical failure injection on specific +// storage paths (e.g. packer buckets) without affecting unrelated operations. +type PathErrorInjector struct { + backend Backend + pathErrors map[string]int + mu sync.RWMutex + randMu sync.Mutex + random *rand.Rand +} + +// TransactionalPathErrorInjector is the transactional version of +// PathErrorInjector. +type TransactionalPathErrorInjector struct { + *PathErrorInjector + Transactional +} + +var ( + _ Backend = (*PathErrorInjector)(nil) + _ Transactional = (*TransactionalPathErrorInjector)(nil) +) + +// NewPathErrorInjector creates a new PathErrorInjector wrapping the given +// backend. No errors are injected until SetErrorPercentageForPath is called. +func NewPathErrorInjector(b Backend, logger log.Logger) *PathErrorInjector { + logger.Info("creating path error injector") + return &PathErrorInjector{ + backend: b, + pathErrors: make(map[string]int), + random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))), + } +} + +// NewTransactionalPathErrorInjector creates a new transactional +// PathErrorInjector wrapping the given backend. +func NewTransactionalPathErrorInjector(b Backend, logger log.Logger) *TransactionalPathErrorInjector { + return &TransactionalPathErrorInjector{ + PathErrorInjector: NewPathErrorInjector(b, logger), + Transactional: b.(Transactional), + } +} + +// SetErrorPercentageForPath sets an error injection percentage for a specific +// path prefix. Any storage operation whose key starts with the given prefix +// will fail with the configured probability (0-100). The longest matching +// prefix wins when multiple prefixes match a key. This method is safe for +// concurrent use. +func (e *PathErrorInjector) SetErrorPercentageForPath(path string, p int) { + e.mu.Lock() + e.pathErrors[path] = p + e.mu.Unlock() +} + +func (e *PathErrorInjector) addError(key string) error { + e.mu.RLock() + percent := 0 + longestMatch := 0 + for prefix, p := range e.pathErrors { + if strings.HasPrefix(key, prefix) && len(prefix) > longestMatch { + longestMatch = len(prefix) + percent = p + } + } + e.mu.RUnlock() + + if percent == 0 { + return nil + } + + e.randMu.Lock() + roll := e.random.Intn(100) + e.randMu.Unlock() + + if roll < percent { + return errors.New("random error") + } + return nil +} + +func (e *PathErrorInjector) Put(ctx context.Context, entry *Entry) error { + if err := e.addError(entry.Key); err != nil { + return err + } + return e.backend.Put(ctx, entry) +} + +func (e *PathErrorInjector) Get(ctx context.Context, key string) (*Entry, error) { + if err := e.addError(key); err != nil { + return nil, err + } + return e.backend.Get(ctx, key) +} + +func (e *PathErrorInjector) Delete(ctx context.Context, key string) error { + if err := e.addError(key); err != nil { + return err + } + return e.backend.Delete(ctx, key) +} + +func (e *PathErrorInjector) List(ctx context.Context, prefix string) ([]string, error) { + if err := e.addError(prefix); err != nil { + return nil, err + } + return e.backend.List(ctx, prefix) +} + +func (e *TransactionalPathErrorInjector) Transaction(ctx context.Context, txns []*TxnEntry) error { + for _, txn := range txns { + if txn != nil { + if err := e.addError(txn.Entry.Key); err != nil { + return err + } + } + } + return e.Transactional.Transaction(ctx, txns) +} + +// TransactionLimits implements physical.TransactionalLimits +func (e *TransactionalPathErrorInjector) TransactionLimits() (int, int) { + if tl, ok := e.Transactional.(TransactionalLimits); ok { + return tl.TransactionLimits() + } + return 0, 0 +} diff --git a/vault/core.go b/vault/core.go index 1f75e4a3fd..5c79682c7c 100644 --- a/vault/core.go +++ b/vault/core.go @@ -3115,6 +3115,10 @@ func (c *Core) preSeal() error { result = multierror.Append(result, err) } + if c.identityStore != nil { + c.identityStore.stopSCIMDeletingClientCleanup() + } + if c.autoRotateCancel != nil { c.autoRotateCancel() c.autoRotateCancel = nil diff --git a/vault/identity_store.go b/vault/identity_store.go index f8e6b00bf0..64f56e5da0 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -78,7 +78,7 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo mountLister: core, mfaBackend: core.loginMFABackend, aliasLocks: locksutil.CreateLocks(), - renameDuplicates: core.FeatureActivationFlags, + activationManager: core.FeatureActivationFlags, activationErrorHandler: core, } diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index 7697b441aa..38380a4861 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -562,6 +562,10 @@ func (i *IdentityStore) handleAliasReadCommon(ctx context.Context, alias *identi respData["namespace_id"] = alias.NamespaceID respData["local"] = alias.Local + if i.scimEnabled { + respData["scim_client_id"] = alias.ScimClientID + } + if mountValidationResp := i.router.ValidateMountByAccessor(alias.MountAccessor); mountValidationResp != nil { respData["mount_path"] = mountValidationResp.MountPath respData["mount_type"] = mountValidationResp.MountType diff --git a/vault/identity_store_entities.go b/vault/identity_store_entities.go index 50e2884be7..62e11d4bd4 100644 --- a/vault/identity_store_entities.go +++ b/vault/identity_store_entities.go @@ -393,6 +393,10 @@ func (i *IdentityStore) handleEntityReadCommon(ctx context.Context, entity *iden aliasMap["local"] = alias.Local aliasMap["custom_metadata"] = alias.CustomMetadata + if i.scimEnabled { + aliasMap["scim_client_id"] = alias.ScimClientID + } + if mountValidationResp := i.router.ValidateMountByAccessor(alias.MountAccessor); mountValidationResp != nil { aliasMap["mount_type"] = mountValidationResp.MountType aliasMap["mount_path"] = mountValidationResp.MountPath @@ -405,6 +409,10 @@ func (i *IdentityStore) handleEntityReadCommon(ctx context.Context, entity *iden // formats respData["aliases"] = aliasesToReturn + if i.scimEnabled { + respData["scim_client_id"] = entity.ScimClientID + } + addExtraEntityDataToResponse(entity, respData) // Fetch the groups this entity belongs to and return their identifiers diff --git a/vault/identity_store_groups.go b/vault/identity_store_groups.go index c51da8a5e2..563449fe26 100644 --- a/vault/identity_store_groups.go +++ b/vault/identity_store_groups.go @@ -433,6 +433,10 @@ func (i *IdentityStore) handleGroupReadCommon(ctx context.Context, group *identi respData["type"] = group.Type respData["namespace_id"] = group.NamespaceID + if i.scimEnabled { + respData["scim_client_id"] = group.ScimClientID + } + aliasMap := map[string]interface{}{} if group.Alias != nil { aliasMap["id"] = group.Alias.ID diff --git a/vault/identity_store_schema.go b/vault/identity_store_schema.go index 5fd2fe032c..31a2942497 100644 --- a/vault/identity_store_schema.go +++ b/vault/identity_store_schema.go @@ -82,6 +82,16 @@ func aliasesTableSchema(lowerCaseName bool) *memdb.TableSchema { Field: "LocalBucketKey", }, }, + "scim_client_id": { + Name: "scim_client_id", + AllowMissing: true, + Indexer: &memdb.CompoundIndex{ + Indexes: []memdb.Indexer{ + &memdb.StringFieldIndex{Field: "NamespaceID"}, + &memdb.StringFieldIndex{Field: "ScimClientID"}, + }, + }, + }, }, } } diff --git a/vault/identity_store_scim_oss.go b/vault/identity_store_scim_oss.go index 5ddcd5344a..f54a2c3707 100644 --- a/vault/identity_store_scim_oss.go +++ b/vault/identity_store_scim_oss.go @@ -18,6 +18,15 @@ func (i *IdentityStore) loadSCIMClients(ctx context.Context) error { func (i *IdentityStore) invalidateSCIMClient(ctx context.Context, key string) { } +func (i *IdentityStore) startSCIMDeletingClientCleanup(ctx context.Context, isActive bool) { +} + +func (i *IdentityStore) stopSCIMDeletingClientCleanup() { +} + +func (i *IdentityStore) enqueueSCIMCleanup(clientID string, namespaceID string) { +} + func scimPaths(_ *IdentityStore) []*framework.Path { return []*framework.Path{} } diff --git a/vault/identity_store_structs.go b/vault/identity_store_structs.go index bf4c1abb92..55ea947594 100644 --- a/vault/identity_store_structs.go +++ b/vault/identity_store_structs.go @@ -122,9 +122,9 @@ type IdentityStore struct { conflictResolver ConflictResolver - // renameDuplicates holds the Core reference to feature activation flags, so - // we can set and query enablement of the deduplication feature. - renameDuplicates activationflags.ActivationManager + // activationManager holds the Core reference to feature activation flags, so + // we can set and query enablement of the scim and deduplication feature. + activationManager activationflags.ActivationManager activationErrorHandler Sealer // activateDeduplicationDone is a channel used for synchronization in testing @@ -132,6 +132,12 @@ type IdentityStore struct { // scimEnabled is used to indicate if SCIM paths are enabled and if SCIM operations can be performed. scimEnabled bool + + // scimCleanupCtx is the context shared by all SCIM client cleanup goroutines. + // It is derived from the active context and cancelled on seal/standby. + scimCleanupCtx context.Context + // scimCleanupCancel cancels all background SCIM client cleanup goroutines. + scimCleanupCancel context.CancelFunc } type groupDiff struct { diff --git a/vault/identity_store_util.go b/vault/identity_store_util.go index 947cbef442..bd72445270 100644 --- a/vault/identity_store_util.go +++ b/vault/identity_store_util.go @@ -106,6 +106,8 @@ func (i *IdentityStore) loadArtifacts(ctx context.Context, isActive bool) error return fmt.Errorf("failed to load SCIM clients: %w", err) } + i.startSCIMDeletingClientCleanup(ctx, isActive) + return nil } @@ -119,10 +121,19 @@ func (i *IdentityStore) loadArtifacts(ctx context.Context, isActive bool) error // If the identity deduplication cleanup flag is activated, instead // deal with duplicate entities and groups by renaming with a -UUID // suffix. N.B. *entity alias* duplicates will still be merged as before. - if i.renameDuplicates.IsActivationFlagEnabled(activationflags.IdentityDeduplication) { + if i.activationManager.IsActivationFlagEnabled(activationflags.IdentityDeduplication) { i.conflictResolver = &renameResolver{i.logger} } + // Restore the in-memory scimEnabled flag from the persisted activation + // flags. The ActivationFunc callback only fires on API writes and storage + // invalidations, not on startup, so we must explicitly check the flag here + // to ensure SCIM paths remain available after seal/unseal. + if i.activationManager.IsActivationFlagEnabled(activationflags.SCIMEnablement) { + i.logger.Info("restoring SCIM enablement from persisted activation flags") + i.scimEnabled = true + } + // Load everything when MemDB is set to operate on lower cased names. // errDuplicateIdentityName below should only happen if we're using the // errorResolver (i.e. identity deduplication is not activated) and we @@ -1445,6 +1456,55 @@ func (i *IdentityStore) MemDBAliases(ws memdb.WatchSet, groupAlias bool) (memdb. return iter, nil } +// MemDBAliasesByScimClientIDInTxn returns all entity aliases belonging to the +// given SCIM client within the namespace derived from ctx. +func (i *IdentityStore) MemDBAliasesByScimClientIDInTxn(ctx context.Context, txn *memdb.Txn, scimClientID string) ([]*identity.Alias, error) { + if txn == nil { + return nil, fmt.Errorf("nil txn") + } + if scimClientID == "" { + return nil, fmt.Errorf("empty scim client id") + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + + iter, err := txn.Get(entityAliasesTable, "scim_client_id", ns.ID, scimClientID) + if err != nil { + return nil, fmt.Errorf("failed to lookup aliases using scim client id: %w", err) + } + + var aliases []*identity.Alias + for raw := iter.Next(); raw != nil; raw = iter.Next() { + alias, ok := raw.(*identity.Alias) + if !ok { + return nil, fmt.Errorf("failed to declare the type of fetched alias") + } + cloned, err := alias.Clone() + if err != nil { + return nil, err + } + aliases = append(aliases, cloned) + } + + return aliases, nil +} + +// MemDBAliasesByScimClientID returns all entity aliases belonging to the given +// SCIM client within the namespace derived from ctx. +func (i *IdentityStore) MemDBAliasesByScimClientID(ctx context.Context, scimClientID string) ([]*identity.Alias, error) { + if scimClientID == "" { + return nil, fmt.Errorf("empty scim client id") + } + + txn := i.db.Txn(false) + defer txn.Abort() + + return i.MemDBAliasesByScimClientIDInTxn(ctx, txn, scimClientID) +} + func (i *IdentityStore) MemDBUpsertEntityInTxn(txn *memdb.Txn, entity *identity.Entity) error { if txn == nil { return fmt.Errorf("nil txn") From 97320b37979434bd8c8153bb2c700f7ccc2efee4 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 11:16:34 -0400 Subject: [PATCH 064/468] add new blackbox sdk tests for the billing endpoint (#12601) (#12885) * add new blackbox sdk tests for the billing endpoint * fix the test: we do not want to allow access to endpoint from admin namespace * run new tests on cloud * call cluster healthy assertion instead of raft healthy assertion * add helpers to properly identiy parent namespace, fix tests * more fixes * verify root namespace inaccessible on HVD * remove the new file, remove the root test, move the test to system test file * enhance the test by adding assertions against admin namespace * use NewTestCluster inside the namespace restriction unit test to more realistically rely on system backend restriction * try a fix * revert the last change * log the response * debug: log the response * debugging * bug fix * revert bug fix * use raw read operation instead * add root test * cleanup of comments and debug logs * feedback * clarify parent ns method * feedback * feedback * feedback Co-authored-by: Amir Aslamov --- .../testcluster/blackbox/session_util.go | 33 +++++++++++ vault/external_tests/blackbox/system_test.go | 55 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/sdk/helper/testcluster/blackbox/session_util.go b/sdk/helper/testcluster/blackbox/session_util.go index c079a52b3e..9022a3dbb2 100644 --- a/sdk/helper/testcluster/blackbox/session_util.go +++ b/sdk/helper/testcluster/blackbox/session_util.go @@ -4,6 +4,7 @@ package blackbox import ( + "os" "time" "github.com/hashicorp/vault/api" @@ -41,3 +42,35 @@ func (s *Session) WithRootNamespace(fn func() (*api.Secret, error)) (*api.Secret return fn() } + +// WithParentNamespace temporarily switches to the parent namespace (e.g., "admin" in HVD) +// and executes the provided function, then restores the original namespace. +func (s *Session) WithParentNamespace(fn func() (*api.Secret, error)) (*api.Secret, error) { + s.t.Helper() + + oldNamespace := s.Client.Namespace() + defer s.Client.SetNamespace(oldNamespace) + + // Get the parent namespace from environment (e.g., "admin" in HVD) + parentNS := s.GetParentNamespace() + s.Client.SetNamespace(parentNS) + + return fn() +} + +// GetParentNamespace returns the namespace from VAULT_NAMESPACE environment variable. +// The blackbox test framework auto-creates a unique child namespace for each test +// (e.g., "admin/bbsdk-xxxxx") for isolation. VAULT_NAMESPACE contains the base namespace +// (e.g., "admin"), which is the parent of the test's namespace. +// Example: VAULT_NAMESPACE="admin" → test runs in "admin/bbsdk-xxxxx" → returns "admin" +// Note: This doesn't traverse the namespace hierarchy - it simply returns VAULT_NAMESPACE, +// which happens to be the parent of the test namespace. +func (s *Session) GetParentNamespace() string { + ns := os.Getenv("VAULT_NAMESPACE") + // If VAULT_NAMESPACE is not set, default to root namespace (empty string). + // This handles cases where tests run in non-namespaced environments. + if ns == "" { + return "" + } + return ns +} diff --git a/vault/external_tests/blackbox/system_test.go b/vault/external_tests/blackbox/system_test.go index ffc790a047..a24750e4c4 100644 --- a/vault/external_tests/blackbox/system_test.go +++ b/vault/external_tests/blackbox/system_test.go @@ -4,9 +4,12 @@ package blackbox import ( + "context" "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" + "github.com/stretchr/testify/require" ) // TestUnsealedStatus verifies that the Vault cluster is unsealed and healthy @@ -118,3 +121,55 @@ func TestNodeRemovalAndRejoin(t *testing.T) { t.Log("Successfully verified raft cluster stability for node operations") } + +// TestBillingOverviewNamespaceRestrictions verifies that sys/billing/overview +// returns appropriate errors when called from different namespace levels in HVD. +// In HVD, tests run in admin/bbsdk-xxxxx, and this test verifies: +// - Calling from base namespace (admin) returns "unsupported path" +// - Calling from root namespace (empty) returns "permission denied" +func TestBillingOverviewNamespaceRestrictions(t *testing.T) { + v := blackbox.New(t) + + // Verify cluster stability first + v.AssertClusterHealthy() + + // Check if we're in HVD (has base namespace from VAULT_NAMESPACE) + baseNS := v.GetParentNamespace() + if baseNS == "" { + t.Skip("Skipping namespace restriction tests - no base namespace configured (not in HVD)") + } + + testCases := []struct { + name string + namespaceSwitcher func(func() (*api.Secret, error)) (*api.Secret, error) + expectedError string + }{ + { + name: "base_namespace_unsupported", + namespaceSwitcher: v.WithParentNamespace, + expectedError: "unsupported path", + }, + { + name: "root_namespace_permission_denied", + namespaceSwitcher: v.WithRootNamespace, + expectedError: "permission denied", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var rawResp *api.Response + var err error + _, err = tc.namespaceSwitcher(func() (*api.Secret, error) { + var readErr error + rawResp, readErr = v.Client.Logical().ReadRawWithContext(context.Background(), "sys/billing/overview") + if readErr != nil { + return nil, readErr + } + // Parse the raw response to get the error + return v.Client.Logical().ParseRawResponseAndCloseBody(rawResp, nil) + }) + require.ErrorContains(t, err, tc.expectedError) + }) + } +} From 48925d76fbb0d0cc18278fe5adba894b677e56a9 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 14:04:06 -0400 Subject: [PATCH 065/468] Fix conditional to use new tool (#12836) (#12866) * Fix conditional to use new tool * use event name instead Co-authored-by: Luis (LT) Carbonell --- .github/workflows/build.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da67c28018..069fcd4ff1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -393,24 +393,35 @@ jobs: secrets: inherit hcp-image: + # Logic: Run HCP image job if: + # if needs.setup.outputs.is-ent-branch != 'true' then + # false + # elseif needs.setup.outputs.workflow-trigger == 'schedule' then + # true + # elseif needs.setup.outputs.workflow-trigger == 'pull_request' and ( + # contains(fromJSON(needs.setup.outputs.changed-files).groups, 'hcp') or + # contains(fromJSON(needs.setup.outputs.labels), 'hcp/build-image') or + # contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') + # ) then + # true + # end if: | - needs.setup.outputs.is-ent-branch == 'true' && ( - needs.setup.outputs.workflow-trigger == 'schedule' || + ( needs.setup.outputs.is-ent-branch != 'true' ) && false || + ( needs.setup.outputs.workflow-trigger == 'schedule' ) && true || ( needs.setup.outputs.workflow-trigger == 'pull_request' && - ( + ( contains(fromJSON(needs.setup.outputs.changed-files).groups, 'hcp') || contains(fromJSON(needs.setup.outputs.changed-files).groups, 'hcp') || contains(fromJSON(needs.setup.outputs.labels), 'hcp/build-image') || contains(fromJSON(needs.setup.outputs.labels), 'hcp/test') ) - ) - ) + ) && true needs: - setup - artifacts-ent uses: ./.github/workflows/build-hcp-image.yml with: - pull-request: ${{ needs.setup.outputs.workflow-trigger == 'pull_request' && github.event.pull_request && github.event.pull_request.number || '' }} - branch: ${{ needs.setup.outputs.workflow-trigger == 'schedule' && 'main' || '' }} + pull-request: ${{ github.event_name == 'pull_request' && github.event.pull_request && github.event.pull_request.number || '' }} + branch: ${{ github.event_name == 'schedule' && 'main' || '' }} create-aws-image: true create-azure-image: false hcp-environment: int From f992222562484c188ecefe25fe615e455b40234f Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 14:20:36 -0400 Subject: [PATCH 066/468] hooks(pre-push): handle ssh protocol prefix in git URLs * hooks(pre-push): handle ssh protocol prefix in git URLs Handle optional URL prefix and suffixes when checking for enterprise. Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .hooks/pre-push | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.hooks/pre-push b/.hooks/pre-push index 3e49740464..0e4b3a0a55 100755 --- a/.hooks/pre-push +++ b/.hooks/pre-push @@ -45,6 +45,9 @@ set -eou pipefail +# We need extglob in order to parse remote_url_is_enterprise +shopt -s extglob + # fail takes two arguments. The first is the failure reasons and the second is # an explanation. Both will be written to STDERR. The script will then exit 1. fail() { @@ -60,7 +63,7 @@ fail() { # that of hashicorp/vault-enterprise, otherwise it returns 1. remote_url_is_enterprise() { case "$1" in - git@github.com:hashicorp/vault-enterprise* | https://github.com/hashicorp/vault-enterprise*) + ?(ssh://)git@github.com@(:|/)hashicorp/vault-enterprise?(.git) | https://github.com/hashicorp/vault-enterprise?(.git)) return 0 ;; *) From 21ac80276ffab102dfbcf0c5092479e55fd65530 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 15:24:09 -0400 Subject: [PATCH 067/468] enos(ldap/static-roles): always encode write bodies as JSON (#12792) (#12893) While investigating a failure during another code review[0] I noticed that we were using key/value pairs when when executing `vault write`. That was a problem because we ran into a situtation where the password started with an `@`, which `vault write` infers to be a localtion on disk[1]. This change updates static-roles.sh fixes that issue as writes are always written as JSON instead of key/value pairs. As I was there I choose to improve the script in several ways: - All Vault command executions now capture both STDOUT and STDERR. When commands fail, the captured output is included in error. - Function-local variables are now properly scoped with the `local` - Some comment changes for clarity (obviously subjective for me) [0]: https://github.com/hashicorp/vault-enterprise/actions/runs/22748142932/job/65978391382?pr=12001#step:17:159 [1]: https://developer.hashicorp.com/vault/docs/commands/write Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- enos/enos-variables.hcl | 6 +- enos/enos.vars.hcl | 3 + .../scripts/ldap/static-roles.sh | 598 ++++++++++++------ 3 files changed, 417 insertions(+), 190 deletions(-) diff --git a/enos/enos-variables.hcl b/enos/enos-variables.hcl index ba899a1f4f..7d3688ae8f 100644 --- a/enos/enos-variables.hcl +++ b/enos/enos-variables.hcl @@ -202,17 +202,17 @@ variable "verify_aws_secrets_engine" { variable "verify_kmip_secrets_engine" { description = "If true we'll verify KMIP secrets engines behavior" type = bool - default = false + default = true } variable "verify_ldap_secrets_engine" { description = "If true we'll verify LDAP secrets engines behavior" type = bool - default = false + default = true } variable "verify_log_secrets" { description = "If true and var.vault_enable_audit_devices is true we'll verify that the audit log does not contain unencrypted secrets. Requires var.vault_radar_license_path to be set to a valid license file." type = bool - default = false + default = false // Only because it requires a Vault Radar license } diff --git a/enos/enos.vars.hcl b/enos/enos.vars.hcl index b57d6c54db..ada3ca0147 100644 --- a/enos/enos.vars.hcl +++ b/enos/enos.vars.hcl @@ -113,3 +113,6 @@ // vault_revision is the git sha of Vault artifact we are testing. Some validations will expect the vault // binary and cluster to report this revision. // vault_revision = "df733361af26f8bb29b63704168bbc5ab8d083de" + +// vault_radar_license_path is the path to a valid Vault Radar path. +// vault_radar_license_path = "./support/vault-radar.hclic" diff --git a/enos/modules/verify_secrets_engines/scripts/ldap/static-roles.sh b/enos/modules/verify_secrets_engines/scripts/ldap/static-roles.sh index 0fd524006d..8cc347b883 100644 --- a/enos/modules/verify_secrets_engines/scripts/ldap/static-roles.sh +++ b/enos/modules/verify_secrets_engines/scripts/ldap/static-roles.sh @@ -5,7 +5,7 @@ set -euo pipefail fail() { - echo "ERROR: $1" 1>&2 + printf "\nERROR: %s\n" "$1" 1>&2 exit 1 } @@ -46,39 +46,56 @@ refresh_ldap_bind_credentials() { log "Refreshing LDAP bind credentials from static role" local current_bind_pw - - current_bind_pw=$( - "$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" \ + if ! current_bind_pw=$( + "$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1 \ | jq -r '.data.password' - ) + ); then + fail "Failed to read current bindpass from ${MOUNT}/static-cred/${STATIC_ROLE_MAIN}: ${current_bind_pw}" + fi + [[ -n "$current_bind_pw" ]] || fail "Failed to get current bindpass from ${MOUNT}/static-cred/${STATIC_ROLE_MAIN}; returned value was blank" - [[ -n "$current_bind_pw" ]] || fail "Failed to read current bind password" - - "$binpath" write "${MOUNT}/config" \ - binddn="${LDAP_USER_DN}" \ - bindpass="${current_bind_pw}" \ - url="ldap://${LDAP_SERVER}:${LDAP_PORT}" \ - userdn="ou=users,dc=${LDAP_USERNAME},dc=com" \ - userattr="uid" \ - > /dev/null \ - || fail "Failed to update LDAP config with refreshed bind credentials" + if ! output=$( + "$binpath" write -format=json "${MOUNT}/config" - << EOF 2>&1 +{ + "binddn": "${LDAP_USER_DN}", + "bindpass": "${current_bind_pw}", + "url": "ldap://${LDAP_SERVER}:${LDAP_PORT}", + "userdn": "ou=users,dc=${LDAP_USERNAME},dc=com", + "userattr": "uid" +} +EOF + ); then + fail "Failed to update LDAP config with refreshed bind credentials: ${output}" + fi } # TEST 1: Create Static Role (take over existing LDAP account) test_create_static_role() { echo "Create Static Role (take over existing LDAP account)" - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - dn="${LDAP_USER_DN}" \ - username="${LDAP_USER}" \ - rotation_period="${ROTATION_SHORT}" > /dev/null \ - || fail "Failed to create static role" - ROLE_JSON=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") + local output + if ! output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "dn": "${LDAP_USER_DN}", + "username": "${LDAP_USER}", + "rotation_period": "${ROTATION_SHORT}" +} +EOF + ); then + fail "Failed to create static role: ${output}" + fi + local role_json + if ! role_json=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static role: ${role_json}" + fi + + # Verify role was created with correct username and DN if ! jq -e \ '.data.username == "'"${LDAP_USER}"'" and .data.dn == "'"${LDAP_USER_DN}"'"' \ - <<< "$ROLE_JSON" > /dev/null; then + <<< "$role_json" > /dev/null; then fail "Static role created with incorrect attributes" fi } @@ -86,17 +103,29 @@ test_create_static_role() { # TEST 2: Create Role Without Immediate Rotation test_create_without_initial_rotation() { echo "Create Role Without Immediate Rotation" - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_SKIP}" \ - dn="${LDAP_USER_DN}" \ - username="${LDAP_USER}-skip" \ - rotation_period="${ROTATION_LONG}" \ - skip_initial_rotation=true > /dev/null - ROLE_JSON=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_SKIP}") + local output + if ! output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_SKIP}" - << EOF 2>&1 +{ + "dn": "${LDAP_USER_DN}", + "username": "${LDAP_USER}-skip", + "rotation_period": "${ROTATION_LONG}", + "skip_initial_rotation": true +} +EOF + ); then + fail "Failed to create static role with skip_initial_rotation: ${output}" + fi + + local role_json + if ! role_json=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_SKIP}" 2>&1); then + fail "Failed to read static role: ${role_json}" + fi if ! jq -e \ '.data.skip_initial_rotation == true' \ - <<< "$ROLE_JSON" > /dev/null; then + <<< "$role_json" > /dev/null; then fail "skip_initial_rotation was not honored" fi } @@ -104,27 +133,56 @@ test_create_without_initial_rotation() { # TEST 3: Update Static Role (allowed + forbidden updates) test_update_static_role() { echo "Update Static Role (allowed + forbidden updates)" - old_role_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") - OLD_PERIOD=$(jq -r '.data.rotation_period' <<< "$old_role_output") - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - rotation_period="${ROTATION_LONG}" > /dev/null + local old_role_output + if ! old_role_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static role before update: ${old_role_output}" + fi - new_role_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") - NEW_PERIOD=$(jq -r '.data.rotation_period' <<< "$new_role_output") + local old_period + old_period=$(jq -r '.data.rotation_period' <<< "$old_role_output") - [[ -n "$NEW_PERIOD" ]] || fail "rotation_period missing after update" - [[ "$OLD_PERIOD" != "$NEW_PERIOD" ]] || fail "rotation_period did not change" + local output + if ! output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "rotation_period": "${ROTATION_LONG}" +} +EOF + ); then + fail "Failed to update rotation_period: ${output}" + fi - # Forbidden: username - if "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - username="invalid-user" > /dev/null; then + local new_role_output + if ! new_role_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static role after update: ${new_role_output}" + fi + + local new_period + new_period=$(jq -r '.data.rotation_period' <<< "$new_role_output") + + [[ -n "$new_period" ]] || fail "rotation_period missing after update" + [[ "$old_period" != "$new_period" ]] || fail "rotation_period did not change" + + # Verify forbidden update: username (should fail) + if output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "username": "invalid-user" +} +EOF + ); then fail "Updating username should not be allowed" fi - # Forbidden: DN - if "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - dn="uid=invalid,ou=users,dc=enos,dc=com" > /dev/null; then + # Verify forbidden update: DN (should fail) + if output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "dn": "uid=invalid,ou=users,dc=enos,dc=com" +} +EOF + ); then fail "Updating DN should not be allowed" fi } @@ -132,27 +190,31 @@ test_update_static_role() { # TEST 4: Read Static Role test_read_static_role() { echo "Read Static Role" - ROLE_JSON=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") - # Stable fields + local role_json + if ! role_json=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static role: ${role_json}" + fi + + # Verify username and DN match expected values if ! jq -e \ '.data.username == "'"${LDAP_USER}"'" and .data.dn == "'"${LDAP_USER_DN}"'"' \ - <<< "$ROLE_JSON" > /dev/null; then + <<< "$role_json" > /dev/null; then fail "Static role returned incorrect username or DN" fi - # rotation_period must exist (string value is normalized by Vault) + # Verify rotation_period exists (value is normalized by Vault) if ! jq -e \ '.data | has("rotation_period")' \ - <<< "$ROLE_JSON" > /dev/null; then + <<< "$role_json" > /dev/null; then fail "rotation_period missing" fi - # last_rotation_time may be null or absent early; ensure key exists OR is null-safe + # Verify last_rotation_time exists (may be null initially) if ! jq -e \ '.data | has("last_rotation_time") or (.data.last_rotation_time == null)' \ - <<< "$ROLE_JSON" > /dev/null; then + <<< "$role_json" > /dev/null; then fail "last_rotation_time missing" fi } @@ -160,11 +222,16 @@ test_read_static_role() { # TEST 5: List Static Roles with hierarchical support test_list_static_roles() { echo "List Static Roles with hierarchical support" - roles=$( - "$binpath" list "${MOUNT}/static-role" \ - | jq -r '.[]' - ) + local roles + if ! roles=$( + "$binpath" list "${MOUNT}/static-role" 2>&1 \ + | jq -r '.[]' + ); then + fail "Failed to list static roles: ${roles}" + fi + + # Verify main role appears in the list if ! grep -qx "${STATIC_ROLE_MAIN}" <<< "$roles"; then fail "Static role ${STATIC_ROLE_MAIN} not found in role list" fi @@ -173,15 +240,21 @@ test_list_static_roles() { # TEST 6: Request Static Credentials test_request_static_credentials() { echo "Request Static Credentials" - CREDS=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") + local creds + if ! creds=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static credentials: ${creds}" + fi + + # Verify password exists and is non-empty if ! jq -e '.data.password | length > 0' \ - <<< "$CREDS" > /dev/null; then + <<< "$creds" > /dev/null; then fail "Password missing from static credentials" fi + # Verify TTL is positive if ! jq -e '.data.ttl > 0' \ - <<< "$CREDS" > /dev/null; then + <<< "$creds" > /dev/null; then fail "Invalid TTL returned" fi } @@ -189,114 +262,179 @@ test_request_static_credentials() { # TEST 7: Manual Password Rotation test_manual_password_rotation() { echo "Manual Password Rotation" - old_cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - OLD_PW=$(jq -r '.data.password' <<< "$old_cred_output") + local old_cred_output + if ! old_cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials before rotation: ${old_cred_output}" + fi + + local old_password + old_password=$(jq -r '.data.password' <<< "$old_cred_output") + + # Refresh LDAP bind credentials before rotation refresh_ldap_bind_credentials - "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" > /dev/null - new_cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - NEW_PW=$(jq -r '.data.password' <<< "$new_cred_output") + local output + if ! output=$("$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to manually rotate password: ${output}" + fi - [[ "$OLD_PW" != "$NEW_PW" ]] \ + local new_cred_output + if ! new_cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after rotation: ${new_cred_output}" + fi + + local new_password + new_password=$(jq -r '.data.password' <<< "$new_cred_output") + + # Verify password changed after manual rotation + [[ "$old_password" != "$new_password" ]] \ || fail "Manual password rotation did not change password" } # TEST 8: Automatic Password Rotation test_automatic_password_rotation() { echo "Automatic Password Rotation" - # Ensure role exists and is managed - "$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" > /dev/null \ - || fail "Static role does not exist" - # Capture password BEFORE rotation - cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW_BEFORE=$(jq -r '.data.password' <<< "$cred_before_output") - [[ -n "$PW_BEFORE" ]] || fail "Initial password missing" + local output + # Verify role exists and is managed + if ! output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Static role does not exist: ${output}" + fi - # Ensure automatic rotation is enabled - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - rotation_period="${ROTATION_LONG}" > /dev/null \ - || fail "Failed to enable automatic rotation" + local cred_before_output + if ! cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials before rotation: ${cred_before_output}" + fi - # Trigger rotation path explicitly (scheduler-safe) + local password_before + password_before=$(jq -r '.data.password' <<< "$cred_before_output") + [[ -n "$password_before" ]] || fail "Initial password missing" + + # Enable automatic rotation + if ! output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "rotation_period": "${ROTATION_LONG}" +} +EOF + ); then + fail "Failed to enable automatic rotation: ${output}" + fi + + # Refresh LDAP bind credentials and trigger rotation refresh_ldap_bind_credentials - "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" > /dev/null \ - || fail "Failed to trigger password rotation" - # Capture password AFTER rotation - cred_after_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW_AFTER=$(jq -r '.data.password' <<< "$cred_after_output") - [[ -n "$PW_AFTER" ]] || fail "Rotated password missing" + if ! output=$("$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to trigger password rotation: ${output}" + fi - # Password must change - [[ "$PW_BEFORE" != "$PW_AFTER" ]] \ + local cred_after_output + if ! cred_after_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after rotation: ${cred_after_output}" + fi + + local password_after + password_after=$(jq -r '.data.password' <<< "$cred_after_output") + [[ -n "$password_after" ]] || fail "Rotated password missing" + + # Verify password changed after rotation + [[ "$password_before" != "$password_after" ]] \ || fail "Password did not change after rotation" } # TEST 9: Custom Password Generation test_custom_password_generation() { echo "Custom Password Generation" - "$binpath" write "sys/policies/password/${PASSWORD_POLICY}" \ - policy=' -length = 20 -rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" } -rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" } -rule "charset" { charset = "0123456789" } -' > /dev/null \ - || fail "Failed to create password policy" + local output + # Create custom password policy + if ! output=$( + "$binpath" write -format=json "sys/policies/password/${PASSWORD_POLICY}" - << EOF 2>&1 +{ + "policy": "length = 20\n\nrule \"charset\" { charset = \"abcdefghijklmnopqrstuvwxyz\" }\nrule \"charset\" { charset = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" }\nrule \"charset\" { charset = \"0123456789\" }" +} +EOF + ); then + fail "Failed to create password policy: ${output}" + fi - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" \ - password_policy="${PASSWORD_POLICY}" \ - rotation_period="${ROTATION_SHORT}" \ - > /dev/null \ - || fail "Failed to attach password policy to static role" + # Attach password policy to static role + if ! output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - << EOF 2>&1 +{ + "password_policy": "${PASSWORD_POLICY}", + "rotation_period": "${ROTATION_SHORT}" +} +EOF + ); then + fail "Failed to attach password policy to static role: ${output}" + fi - # Force rotation so policy is applied + # Refresh LDAP bind credentials and force rotation to apply policy refresh_ldap_bind_credentials - "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" \ - > /dev/null \ - || fail "Failed to rotate password with custom policy" - cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW=$(jq -r '.data.password' <<< "$cred_output") + if ! output=$("$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to rotate password with custom policy: ${output}" + fi - [[ "${#PW}" -ge 20 ]] || fail "Password policy not applied" + local cred_output + if ! cred_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after policy rotation: ${cred_output}" + fi + + local password + password=$(jq -r '.data.password' <<< "$cred_output") + + # Verify password meets minimum length requirement from policy + [[ "${#password}" -ge 20 ]] || fail "Password policy not applied (expected length >= 20, got ${#password})" } # TEST 10: Check Password TTL test_check_password_ttl() { echo "Check Password TTL" - CREDS1=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - TTL1=$(jq -r '.data.ttl' <<< "$CREDS1") - # TTL must exist and be numeric - if ! [[ "$TTL1" =~ ^[0-9]+$ ]]; then + local creds_first + if ! creds_first=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials for TTL check: ${creds_first}" + fi + + local ttl_first + ttl_first=$(jq -r '.data.ttl' <<< "$creds_first") + + # Verify TTL exists and is numeric + if ! [[ "$ttl_first" =~ ^[0-9]+$ ]]; then fail "TTL is missing or not a number" fi - # TTL must be positive - if ! [[ "$TTL1" -gt 0 ]]; then + # Verify TTL is positive + if ! [[ "$ttl_first" -gt 0 ]]; then fail "TTL is not positive" fi sleep 3 - CREDS2=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - TTL2=$(jq -r '.data.ttl' <<< "$CREDS2") + local creds_second + if ! creds_second=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after wait: ${creds_second}" + fi - if ! [[ "$TTL2" =~ ^[0-9]+$ ]]; then + local ttl_second + ttl_second=$(jq -r '.data.ttl' <<< "$creds_second") + + if ! [[ "$ttl_second" =~ ^[0-9]+$ ]]; then fail "TTL missing after wait" fi - # TTL should decrease (or rotate and reset) - if [[ "$TTL2" -ge "$TTL1" ]]; then - # If it increased, password must have rotated - PW1=$(jq -r '.data.password' <<< "$CREDS1") - PW2=$(jq -r '.data.password' <<< "$CREDS2") + # Verify TTL decreased or password rotated (which resets TTL) + if [[ "$ttl_second" -ge "$ttl_first" ]]; then + local password_first + password_first=$(jq -r '.data.password' <<< "$creds_first") - if [[ "$PW1" == "$PW2" ]]; then + local password_second + password_second=$(jq -r '.data.password' <<< "$creds_second") + + if [[ "$password_first" == "$password_second" ]]; then fail "TTL did not decrease and password did not rotate" fi fi @@ -305,46 +443,78 @@ test_check_password_ttl() { # TEST 11: Verify Last Vault Rotation is Present test_verify_last_rotation_time() { echo "Verify Last Vault Rotation is Present" - ROLE_JSON=$( - "$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" - ) || fail "Failed to read static role" + local role_json + if ! role_json=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read static role: ${role_json}" + fi + + # Verify last_vault_rotation field exists in role metadata jq -e '.data | has("last_vault_rotation")' \ - <<< "$ROLE_JSON" > /dev/null \ + <<< "$role_json" > /dev/null \ || fail "last_vault_rotation is missing" } # TEST 12: Verify WAL Recovery on Startup test_wal_recovery_on_startup() { echo "Verify WAL Recovery on Startup" - cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW1=$(jq -r '.data.password' <<< "$cred_before_output") - role_before_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") - T1=$(jq -r '.data.last_rotation_time' <<< "$role_before_output") + local cred_before_output + if ! cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials before WAL test: ${cred_before_output}" + fi - # Break LDAP to force rotation failure - "$binpath" write "${MOUNT}/config" \ - binddn="${LDAP_USER_DN}" \ - bindpass="wrong-password" \ - url="ldap://${LDAP_SERVER}:${LDAP_PORT}" \ - userdn="ou=users,dc=${LDAP_USERNAME},dc=com" \ - userattr="uid" > /dev/null + local password_before + password_before=$(jq -r '.data.password' <<< "$cred_before_output") - # Trigger rotation (WAL written, rotation fails) + local role_before_output + if ! role_before_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read role before WAL test: ${role_before_output}" + fi + + local rotation_time_before + rotation_time_before=$(jq -r '.data.last_rotation_time' <<< "$role_before_output") + + local output + # Intentionally break LDAP config to force rotation failure + if ! output=$( + "$binpath" write -format=json "${MOUNT}/config" - << EOF 2>&1 +{ + "binddn": "${LDAP_USER_DN}", + "bindpass": "wrong-password", + "url": "ldap://${LDAP_SERVER}:${LDAP_PORT}", + "userdn": "ou=users,dc=${LDAP_USERNAME},dc=com", + "userattr": "uid" +} +EOF + ); then + fail "Failed to break LDAP config for WAL test: ${output}" + fi + + # Trigger rotation (WAL written, but rotation should fail) "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" \ > /dev/null 2>&1 || true - # Assert rotation did NOT complete - cred_after_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW2=$(jq -r '.data.password' <<< "$cred_after_output") + local cred_after_output + if ! cred_after_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after failed rotation: ${cred_after_output}" + fi - role_after_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}") - T2=$(jq -r '.data.last_rotation_time' <<< "$role_after_output") + local password_after + password_after=$(jq -r '.data.password' <<< "$cred_after_output") - [[ "$PW1" == "$PW2" ]] \ + local role_after_output + if ! role_after_output=$("$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read role after failed rotation: ${role_after_output}" + fi + + local rotation_time_after + rotation_time_after=$(jq -r '.data.last_rotation_time' <<< "$role_after_output") + + # Verify rotation did NOT complete (password and metadata unchanged) + [[ "$password_before" == "$password_after" ]] \ || fail "Password changed despite failed rotation" - [[ "$T1" == "$T2" ]] \ + [[ "$rotation_time_before" == "$rotation_time_after" ]] \ || fail "Rotation metadata updated despite failure" # Restore LDAP config after intentional failure @@ -354,35 +524,61 @@ test_wal_recovery_on_startup() { # TEST 13: Verify Rotation Retry on Failure test_rotation_retry_on_failure() { echo "Verify Rotation Retry on Failure" - # Initial password - cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW1=$(jq -r '.data.password' <<< "$cred_before_output") - # Break LDAP to force rotation failure - "$binpath" write "${MOUNT}/config" \ - binddn="uid=invalid,ou=users,dc=enos,dc=com" \ - bindpass="wrong-password" \ - url="ldap://${LDAP_SERVER}:${LDAP_PORT}" \ - userdn="ou=users,dc=${LDAP_USERNAME},dc=com" \ - userattr="uid" > /dev/null + local cred_before_output + if ! cred_before_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials before retry test: ${cred_before_output}" + fi - # First rotation attempt (fails, WAL created) + local password_initial + password_initial=$(jq -r '.data.password' <<< "$cred_before_output") + + local output + # Intentionally break LDAP config to force rotation failure + if ! output=$( + "$binpath" write -format=json "${MOUNT}/config" - << EOF 2>&1 +{ + "binddn": "uid=invalid,ou=users,dc=enos,dc=com", + "bindpass": "wrong-password", + "url": "ldap://${LDAP_SERVER}:${LDAP_PORT}", + "userdn": "ou=users,dc=${LDAP_USERNAME},dc=com", + "userattr": "uid" +} +EOF + ); then + fail "Failed to break LDAP config for retry test: ${output}" + fi + + # First rotation attempt (should fail, WAL created) "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" \ > /dev/null 2>&1 || true - # Password must remain unchanged - cred_after_first_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW2=$(jq -r '.data.password' <<< "$cred_after_first_output") - [[ "$PW1" == "$PW2" ]] \ + local cred_after_first_output + if ! cred_after_first_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after first retry: ${cred_after_first_output}" + fi + + local password_after_first + password_after_first=$(jq -r '.data.password' <<< "$cred_after_first_output") + + # Verify password unchanged after first failed rotation + [[ "$password_initial" == "$password_after_first" ]] \ || fail "Password changed on failed rotation" - # Second retry attempt (still fails, same WAL reused) + # Second rotation attempt (should still fail, same WAL reused) "$binpath" write -f "${MOUNT}/rotate-role/${STATIC_ROLE_MAIN}" \ > /dev/null 2>&1 || true - cred_after_second_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW3=$(jq -r '.data.password' <<< "$cred_after_second_output") - [[ "$PW1" == "$PW3" ]] \ + local cred_after_second_output + if ! cred_after_second_output=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after second retry: ${cred_after_second_output}" + fi + + local password_after_second + password_after_second=$(jq -r '.data.password' <<< "$cred_after_second_output") + + # Verify password unchanged across retries (WAL consistency) + [[ "$password_initial" == "$password_after_second" ]] \ || fail "Password changed across retries (WAL inconsistency)" # Restore LDAP config after intentional failure @@ -392,21 +588,28 @@ test_rotation_retry_on_failure() { # TEST 14: Managed User Tracking (duplicate username) test_duplicate_user_management() { echo "Managed User Tracking (duplicate username)" + + local output + local status set +e - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_DUP}" \ - rotation_period="${ROTATION_LONG}" \ - dn="${LDAP_USER_DN}" \ - username="${LDAP_USER}" \ - > /dev/null 2>&1 - STATUS=$? + output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_DUP}" - << EOF 2>&1 +{ + "rotation_period": "${ROTATION_LONG}", + "dn": "${LDAP_USER_DN}", + "username": "${LDAP_USER}" +} +EOF + ) + status=$? set -e - # Must fail - if [[ $STATUS -eq 0 ]]; then + # Verify creation fails (username already managed) + if [[ $status -eq 0 ]]; then fail "Duplicate username was incorrectly allowed" fi - # Role must NOT exist + # Verify role does NOT exist if "$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_DUP}" > /dev/null 2>&1; then fail "Duplicate role was created despite username already managed" fi @@ -415,19 +618,28 @@ test_duplicate_user_management() { # TEST 15: Verify Password Rotation Not Happening test_password_rotation_not_happening() { echo "Verify Password Rotation Not Happening" - # First credential read - CREDS1=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW1=$(jq -r '.data.password' <<< "$CREDS1") + + local creds_first + if ! creds_first=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials for negative test: ${creds_first}" + fi + + local password_first + password_first=$(jq -r '.data.password' <<< "$creds_first") # Short wait (well below rotation_period) sleep 2 - # Second credential read - CREDS2=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}") - PW2=$(jq -r '.data.password' <<< "$CREDS2") + local creds_second + if ! creds_second=$("$binpath" read "${MOUNT}/static-cred/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to read credentials after wait: ${creds_second}" + fi - # Password MUST NOT change - if [[ "$PW1" != "$PW2" ]]; then + local password_second + password_second=$(jq -r '.data.password' <<< "$creds_second") + + # Verify password did NOT change (negative test) + if [[ "$password_first" != "$password_second" ]]; then fail "Password rotated unexpectedly in negative test" fi } @@ -435,21 +647,28 @@ test_password_rotation_not_happening() { # TEST 16: Verify Failure of Create Role due to Username Already Managed test_failure_create_role_username_already_managed() { echo "Verify Failure of Create Role due to Username Already Managed" + + local output + local status set +e - "$binpath" write "${MOUNT}/static-role/${STATIC_ROLE_DUP}" \ - dn="${LDAP_USER_DN}" \ - username="${LDAP_USER}" \ - rotation_period="${ROTATION_LONG}" \ - > /dev/null 2>&1 - STATUS=$? + output=$( + "$binpath" write -format=json "${MOUNT}/static-role/${STATIC_ROLE_DUP}" - << EOF 2>&1 +{ + "dn": "${LDAP_USER_DN}", + "username": "${LDAP_USER}", + "rotation_period": "${ROTATION_LONG}" +} +EOF + ) + status=$? set -e - # Must fail - if [[ $STATUS -eq 0 ]]; then + # Verify creation fails (username already managed) + if [[ $status -eq 0 ]]; then fail "Expected failure when creating role with managed username" fi - # Role must NOT exist + # Verify role does NOT exist if "$binpath" read "${MOUNT}/static-role/${STATIC_ROLE_DUP}" > /dev/null 2>&1; then fail "Duplicate role was created despite username already managed" fi @@ -458,7 +677,12 @@ test_failure_create_role_username_already_managed() { # Cleanup cleanup() { echo "Deleting all roles" - "$binpath" delete "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" + + local output + if ! output=$("$binpath" delete "${MOUNT}/static-role/${STATIC_ROLE_MAIN}" 2>&1); then + fail "Failed to delete static role ${STATIC_ROLE_MAIN}: ${output}" + fi + # Note: STATIC_ROLE_SKIP cleanup commented out (test currently disabled) # "$binpath" delete "${MOUNT}/static-role/${STATIC_ROLE_SKIP}" } From 8b3ebfc1fe1d2156f0fa6f1ed8a6e6d1435bd5ed Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 16:38:33 -0400 Subject: [PATCH 068/468] add deprecated comments (#12791) (#12898) Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- builtin/logical/pki/issuing/issue_common.go | 1 + builtin/logical/pki/issuing/roles.go | 27 +++++++++++---------- builtin/logical/pki/path_roles.go | 9 ++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/builtin/logical/pki/issuing/issue_common.go b/builtin/logical/pki/issuing/issue_common.go index 370168716a..7296c53066 100644 --- a/builtin/logical/pki/issuing/issue_common.go +++ b/builtin/logical/pki/issuing/issue_common.go @@ -624,6 +624,7 @@ func ValidateNames(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, } } + // Deprecated: AllowTokenDisplayName is retained for backward compatibility but has not been writeable since v0.4.0 if role.AllowTokenDisplayName { if name == entityInfo.DisplayName { continue diff --git a/builtin/logical/pki/issuing/roles.go b/builtin/logical/pki/issuing/roles.go index 68f25b4fc6..30c90e7a86 100644 --- a/builtin/logical/pki/issuing/roles.go +++ b/builtin/logical/pki/issuing/roles.go @@ -29,19 +29,20 @@ const ( ) type RoleEntry struct { - LeaseMax string `json:"lease_max"` - Lease string `json:"lease"` - DeprecatedMaxTTL string `json:"max_ttl"` - DeprecatedTTL string `json:"ttl"` - TTL time.Duration `json:"ttl_duration"` - MaxTTL time.Duration `json:"max_ttl_duration"` - AllowLocalhost bool `json:"allow_localhost"` - AllowedBaseDomain string `json:"allowed_base_domain"` - AllowedDomainsOld string `json:"allowed_domains,omitempty"` - AllowedDomains []string `json:"allowed_domains_list"` - AllowedDomainsTemplate bool `json:"allowed_domains_template"` - AllowBaseDomain bool `json:"allow_base_domain"` - AllowBareDomains bool `json:"allow_bare_domains"` + LeaseMax string `json:"lease_max"` + Lease string `json:"lease"` + DeprecatedMaxTTL string `json:"max_ttl"` + DeprecatedTTL string `json:"ttl"` + TTL time.Duration `json:"ttl_duration"` + MaxTTL time.Duration `json:"max_ttl_duration"` + AllowLocalhost bool `json:"allow_localhost"` + AllowedBaseDomain string `json:"allowed_base_domain"` + AllowedDomainsOld string `json:"allowed_domains,omitempty"` + AllowedDomains []string `json:"allowed_domains_list"` + AllowedDomainsTemplate bool `json:"allowed_domains_template"` + AllowBaseDomain bool `json:"allow_base_domain"` + AllowBareDomains bool `json:"allow_bare_domains"` + // Deprecated: AllowTokenDisplayName is retained for backward compatibility but has not been writeable since v0.4.0 AllowTokenDisplayName bool `json:"allow_token_displayname"` AllowSubdomains bool `json:"allow_subdomains"` AllowGlobDomains bool `json:"allow_glob_domains"` diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 065a6713e1..51e42ee8ba 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -59,10 +59,11 @@ value or the value of max_ttl, whichever is shorter.`, set, defaults to the system maximum lease TTL.`, }, "allow_token_displayname": { - Type: framework.TypeBool, - Required: true, - Description: `Whether to allow "localhost" and "localdomain" -as a valid common name in a request, independent of allowed_domains value.`, + Type: framework.TypeBool, + Description: `Deprecated. If set, clients can request certificates matching +the value of Display Name from the requesting token. Remember, this stacks with +the other CN options, including allow_subdomains. Defaults to false.`, + Deprecated: true, }, "allow_localhost": { From 383e2267ed8c329116a8aeb4f04281d9149a776f Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 10 Mar 2026 17:04:36 -0400 Subject: [PATCH 069/468] Fix GitHub Actions expression evaluation error in build workflow (#12884) (#12901) * Fix GitHub Actions expression evaluation error in build workflow - Add hcp-setup job with explicit step-by-step parameter validation - Replace problematic inline expressions with debuggable logic steps - Use proper fallback values (0 instead of '') for number type inputs - Resolve 'Unexpected value' error on scheduled runs - Maintain existing workflow logic and conditional behavior - Add clear logging for troubleshooting parameter resolution * Fix type conversion for pull-request number in build workflow - Use fromJSON() to convert string output to number type - Resolves type mismatch error in reusable workflow input Co-authored-by: Luis (LT) Carbonell --- .github/workflows/build.yml | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 069fcd4ff1..8d0bd2b5c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -392,6 +392,44 @@ jobs: web-ui-cache-key: ${{ needs.ui.outputs.cache-key }} secrets: inherit + # Setup HCP input parameters with proper validation + hcp-setup: + runs-on: ubuntu-latest + outputs: + pull-request-number: ${{ steps.pr-logic.outputs.pull-request-number }} + branch-name: ${{ steps.branch-logic.outputs.branch-name }} + steps: + - id: pr-logic + name: Determine pull request number + run: | + echo "Analyzing PR context for event: ${{ github.event_name }}" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUM="${{ github.event.pull_request.number }}" + if [[ -n "$PR_NUM" && "$PR_NUM" != "null" ]]; then + echo "PR context detected, using number: $PR_NUM" + else + echo "PR context but no valid number found, using 0" + PR_NUM="0" + fi + else + PR_NUM="0" + echo "Non-PR context, using fallback value: $PR_NUM" + fi + echo "pull-request-number=$PR_NUM" >> "$GITHUB_OUTPUT" + + - id: branch-logic + name: Determine branch name + run: | + echo "Analyzing branch context for event: ${{ github.event_name }}" + if [[ "${{ github.event_name }}" == "schedule" ]]; then + BRANCH="main" + echo "Schedule context, using branch: $BRANCH" + else + BRANCH="" + echo "Non-schedule context, using empty branch" + fi + echo "branch-name=$BRANCH" >> "$GITHUB_OUTPUT" + hcp-image: # Logic: Run HCP image job if: # if needs.setup.outputs.is-ent-branch != 'true' then @@ -418,10 +456,11 @@ jobs: needs: - setup - artifacts-ent + - hcp-setup uses: ./.github/workflows/build-hcp-image.yml with: - pull-request: ${{ github.event_name == 'pull_request' && github.event.pull_request && github.event.pull_request.number || '' }} - branch: ${{ github.event_name == 'schedule' && 'main' || '' }} + pull-request: ${{ fromJSON(needs.hcp-setup.outputs.pull-request-number) }} + branch: ${{ needs.hcp-setup.outputs.branch-name }} create-aws-image: true create-azure-image: false hcp-environment: int From aedb2da1ff18cfcfdd0802b264f7e105169038e2 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 09:23:31 -0400 Subject: [PATCH 070/468] use is_ent_branch (#12672) (#12685) Co-authored-by: Matthew Irish <39469+meirish@users.noreply.github.com> --- .github/actions/metadata/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/metadata/action.yml b/.github/actions/metadata/action.yml index 8556a866c9..70801142eb 100644 --- a/.github/actions/metadata/action.yml +++ b/.github/actions/metadata/action.yml @@ -156,6 +156,7 @@ runs: if [ "$is_enterprise_repo" = 'true' ]; then base_ref='${{ github.event.pull_request.base.ref || github.event.base_ref || github.ref_name || github.event.branch || github.ref }}' is_ent_repo='true' + is_ent_branch='true' is_ce_in_enterprise=$([[ $base_ref == ce/* ]] && echo "true" || echo "false") if [ "$is_ce_in_enterprise" = 'true' ]; then is_enterprise="false" @@ -192,7 +193,7 @@ runs: echo "compute-small=${compute_small}" echo "go-tags=${go_tags}" echo "is-ce-in-enterprise=${is_ce_in_enterprise}" - echo "is-ent-branch=${is_enterprise}" + echo "is-ent-branch=${is_ent_branch}" echo "is-ent-repo=${is_ent_repo}" echo "vault-version-metadata=${version_metadata}" } | tee -a "$GITHUB_OUTPUT" From 921dc42cdc6fd7870df54db4d2ca3e501b79d351 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 10:02:35 -0400 Subject: [PATCH 071/468] Add token header guardrails (#12749) (#12857) Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- changelog/_12749.txt | 3 + command/server.go | 5 +- http/handler.go | 26 ++ http/testing.go | 12 +- http/token_header_size_test.go | 511 +++++++++++++++++++++ http/util.go | 55 +++ internalshared/configutil/listener.go | 16 + internalshared/configutil/listener_test.go | 22 + 8 files changed, 646 insertions(+), 4 deletions(-) create mode 100644 changelog/_12749.txt create mode 100644 http/token_header_size_test.go diff --git a/changelog/_12749.txt b/changelog/_12749.txt new file mode 100644 index 0000000000..9136ce5989 --- /dev/null +++ b/changelog/_12749.txt @@ -0,0 +1,3 @@ +```release-note:security +http: Added configurable `max_token_header_size` listener option (default 8 KB) to bound the size of authentication token headers (`X-Vault-Token` and `Authorization: Bearer`), preventing a potential denial-of-service attack via oversized header contents. The stdlib-level `MaxHeaderBytes` backstop is also now set on the HTTP server. Set `max_token_header_size = -1` to disable the limit. +``` diff --git a/command/server.go b/command/server.go index 3a6445ad82..031fe98937 100644 --- a/command/server.go +++ b/command/server.go @@ -691,6 +691,7 @@ func (c *ServerCommand) runRecoveryMode() int { ReadTimeout: 30 * time.Second, IdleTimeout: 5 * time.Minute, ErrorLog: c.logger.StandardLogger(nil), + MaxHeaderBytes: vaulthttp.TokenHeaderMaxBytes(ln.Config), } go server.Serve(ln.Listener) @@ -1516,7 +1517,8 @@ func (c *ServerCommand) Run(args []string) int { core.SetClusterHandler(vaulthttp.Handler.Handler(&vault.HandlerProperties{ Core: core, ListenerConfig: &configutil.Listener{ - DisableJSONLimitParsing: true, + DisableJSONLimitParsing: true, + DisableTokenHeaderSizeParsing: true, }, })) @@ -3194,6 +3196,7 @@ func startHttpServers(c *ServerCommand, core *vault.Core, config *server.Config, ReadTimeout: 30 * time.Second, IdleTimeout: 5 * time.Minute, ErrorLog: c.logger.StandardLogger(nil), + MaxHeaderBytes: vaulthttp.TokenHeaderMaxBytes(ln.Config), } // override server defaults with config values for read/write/idle timeouts if configured diff --git a/http/handler.go b/http/handler.go index 00d0d13876..59230666c3 100644 --- a/http/handler.go +++ b/http/handler.go @@ -94,6 +94,12 @@ const ( // to pass the snapshot ID VaultSnapshotRecoverHeader = "X-Vault-Recover-Snapshot-Id" + // DefaultMaxTokenHeaderSize is the default maximum size in bytes for an + // authentication token passed in the X-Vault-Token and Authorization: Bearer + // headers. This is to prevent a denial of service attack via unbounded + // header values. Can be overridden per listener. + DefaultMaxTokenHeaderSize = 8 * 1024 // 8 KB + // CustomMaxJSONDepth specifies the maximum nesting depth of a JSON object. // This limit is designed to prevent stack exhaustion attacks from deeply // nested JSON payloads, which could otherwise lead to a denial-of-service @@ -181,6 +187,25 @@ var ( oidcProtectedPathRegex = regexp.MustCompile(`^identity/oidc/provider/\w(([\w-.]+)?\w)?/userinfo$`) ) +// TokenHeaderMaxBytes returns the http.Server.MaxHeaderBytes value for the +// given listener configuration. A negative CustomMaxTokenHeaderSize disables +// the limit; zero falls back to DefaultMaxTokenHeaderSize. +func TokenHeaderMaxBytes(lnConfig *configutil.Listener) int { + if lnConfig == nil { + return DefaultMaxTokenHeaderSize + } + switch { + case lnConfig.CustomMaxTokenHeaderSize < 0: + // Limit explicitly disabled; leave http.Server.MaxHeaderBytes unset so + // the stdlib default (1 MB) applies. + return 0 + case lnConfig.CustomMaxTokenHeaderSize > 0: + return int(lnConfig.CustomMaxTokenHeaderSize) + default: + return DefaultMaxTokenHeaderSize + } +} + func init() { alwaysRedirectPaths.AddPaths([]string{ "sys/storage/raft/snapshot", @@ -313,6 +338,7 @@ func handler(props *vault.HandlerProperties) http.Handler { wrappedHandler = rateLimitQuotaWrapping(wrappedHandler, core) wrappedHandler = entWrapGenericHandler(core, wrappedHandler, props) wrappedHandler = wrapMaxRequestSizeHandler(wrappedHandler, props) + wrappedHandler = wrapTokenHeaderSizeHandler(wrappedHandler, props) wrappedHandler = priority.WrapRequestPriorityHandler(wrappedHandler) // Add an extra wrapping handler if the DisablePrintableCheck listener diff --git a/http/testing.go b/http/testing.go index 795582ee58..c29c35c79c 100644 --- a/http/testing.go +++ b/http/testing.go @@ -36,10 +36,16 @@ func TestServerWithListenerAndProperties(tb testing.TB, ln net.Listener, addr st mux.Handle("/_test/auth", http.HandlerFunc(testHandleAuth)) mux.Handle("/", Handler.Handler(props)) + var lnConfig *configutil.Listener + if props != nil { + lnConfig = props.ListenerConfig + } + server := &http.Server{ - Addr: ln.Addr().String(), - Handler: mux, - ErrorLog: core.Logger().StandardLogger(nil), + Addr: ln.Addr().String(), + Handler: mux, + ErrorLog: core.Logger().StandardLogger(nil), + MaxHeaderBytes: TokenHeaderMaxBytes(lnConfig), } go server.Serve(ln) } diff --git a/http/token_header_size_test.go b/http/token_header_size_test.go new file mode 100644 index 0000000000..40fa39b2b8 --- /dev/null +++ b/http/token_header_size_test.go @@ -0,0 +1,511 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package http + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "testing" + "time" + + cleanhttp "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/internalshared/configutil" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/vault" + "github.com/stretchr/testify/require" +) + +// sendTokenHeaderRequest builds and dispatches an HTTP GET to addr, placing +// token in the named header. For "Authorization", the value is wrapped as a +// Bearer token per RFC 6750. +func sendTokenHeaderRequest(t *testing.T, client *http.Client, addr, headerName, token string) (*http.Response, error) { + t.Helper() + req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil) + require.NoError(t, err) + if headerName == "Authorization" { + req.Header.Set("Authorization", "Bearer "+token) + } else { + req.Header.Set(headerName, token) + } + return client.Do(req) +} + +// newTestClusterForTokenHeader creates a NewTestCluster with default settings +// and returns the cluster, an HTTP client configured with TLS, and the address. +func newTestClusterForTokenHeader(t *testing.T, opts *vault.TestClusterOptions) (*http.Client, string) { + t.Helper() + if opts == nil { + opts = &vault.TestClusterOptions{} + } + opts.HandlerFunc = Handler + cluster := vault.NewTestCluster(t, nil, opts) + cluster.Start() + + core := cluster.Cores[0] + transport := cleanhttp.DefaultPooledTransport() + transport.TLSClientConfig = core.TLSConfig() + httpClient := &http.Client{Transport: transport, Timeout: 15 * time.Second} + return httpClient, core.Client.Address() +} + +// TestTokenHeader_ExceedsDefaultLimit_IsRejected verifies that tokens larger than +// DefaultMaxTokenHeaderSize are rejected with 400 before token validation occurs. +func TestTokenHeader_ExceedsDefaultLimit_IsRejected(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, nil) + + cases := []struct { + name string + headerSize int + headerName string + }{ + { + name: "just-over-limit-x-vault-token", + headerSize: DefaultMaxTokenHeaderSize + 1, + headerName: consts.AuthHeaderName, + }, + { + name: "just-over-limit-authorization-bearer", + headerSize: DefaultMaxTokenHeaderSize + 1, + headerName: "Authorization", + }, + { + name: "100kb-x-vault-token", + headerSize: 100 * 1024, + headerName: consts.AuthHeaderName, + }, + { + name: "100kb-authorization-bearer", + headerSize: 100 * 1024, + headerName: "Authorization", + }, + { + name: "900kb-x-vault-token", + headerSize: 900 * 1024, + headerName: consts.AuthHeaderName, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + token := strings.Repeat("x", tc.headerSize) + resp, err := sendTokenHeaderRequest(t, client, addr, tc.headerName, token) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusBadRequest, resp.StatusCode, + "token of %d bytes must be rejected by Vault middleware (want 400, not 403)", + tc.headerSize) + }) + } +} + +// TestTokenHeader_WithinDefaultLimit_IsProcessed verifies that tokens at or +// below DefaultMaxTokenHeaderSize reach token validation normally (403, not 400). +func TestTokenHeader_WithinDefaultLimit_IsProcessed(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, nil) + + cases := []struct { + name string + headerSize int + headerName string + }{ + { + name: "512b-x-vault-token", + headerSize: 512, + headerName: consts.AuthHeaderName, + }, + { + name: "512b-authorization-bearer", + headerSize: 512, + headerName: "Authorization", + }, + { + name: "4kb-x-vault-token", + headerSize: 4 * 1024, + headerName: consts.AuthHeaderName, + }, + { + name: "at-limit-x-vault-token", + headerSize: DefaultMaxTokenHeaderSize, + headerName: consts.AuthHeaderName, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + token := strings.Repeat("x", tc.headerSize) + resp, err := sendTokenHeaderRequest(t, client, addr, tc.headerName, token) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusForbidden, resp.StatusCode, + "token of %d bytes is within the limit and must reach validation", tc.headerSize) + }) + } +} + +// TestTokenHeader_ConfigurableLimit_Enforced verifies that an operator can +// lower the token header size limit via the listener configuration, and that +// Vault enforces the configured value rather than DefaultMaxTokenHeaderSize. +func TestTokenHeader_ConfigurableLimit_Enforced(t *testing.T) { + t.Parallel() + + const customLimit = 1024 // 1 KB — well below the default 8 KB + client, addr := newTestClusterForTokenHeader(t, &vault.TestClusterOptions{ + DefaultHandlerProperties: vault.HandlerProperties{ + ListenerConfig: &configutil.Listener{ + CustomMaxTokenHeaderSize: customLimit, + }, + }, + }) + + oversized := strings.Repeat("x", customLimit+1) + resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, oversized) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusBadRequest, resp.StatusCode, + "token exceeding custom limit of %d bytes must be rejected with 400", customLimit) + + atLimit := strings.Repeat("x", customLimit) + resp2, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, atLimit) + require.NoError(t, err) + defer resp2.Body.Close() + + require.Equal(t, http.StatusForbidden, resp2.StatusCode, + "token at the custom limit (%d bytes) must reach validation", customLimit) +} + +// TestTokenHeader_MultipleAuthorizationHeaders_BypassPrevented verifies that an +// oversized Bearer token is rejected even when a non-Bearer Authorization header +// precedes it in the request. +func TestTokenHeader_MultipleAuthorizationHeaders_BypassPrevented(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, nil) + + oversizedBearer := strings.Repeat("x", DefaultMaxTokenHeaderSize+1) + + req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil) + require.NoError(t, err) + req.Header.Add("Authorization", "Basic dXNlcjpwYXNz") + req.Header.Add("Authorization", "Bearer "+oversizedBearer) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusBadRequest, resp.StatusCode, + "oversized Bearer token must be rejected even when a non-Bearer Authorization header precedes it") +} + +// TestTokenHeader_DisabledLimit_AllowsOversizedToken verifies that setting +// max_token_header_size = -1 in the listener config fully disables the check, +// allowing tokens larger than DefaultMaxTokenHeaderSize to reach validation. +func TestTokenHeader_DisabledLimit_AllowsOversizedToken(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, &vault.TestClusterOptions{ + DefaultHandlerProperties: vault.HandlerProperties{ + ListenerConfig: &configutil.Listener{ + CustomMaxTokenHeaderSize: -1, + }, + }, + }) + + oversized := strings.Repeat("x", DefaultMaxTokenHeaderSize*2) + resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, oversized) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusForbidden, resp.StatusCode, + "with max_token_header_size = -1 the size guard must not fire; token must reach validation") +} + +// TestTokenHeader_ErrorResponseFormat verifies that oversized-token rejections +// return a valid Vault JSON error envelope with an "errors" array. +func TestTokenHeader_ErrorResponseFormat(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, nil) + + token := strings.Repeat("x", DefaultMaxTokenHeaderSize+1) + resp, err := sendTokenHeaderRequest(t, client, addr, consts.AuthHeaderName, token) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var envelope struct { + Errors []string `json:"errors"` + } + require.NoError(t, json.Unmarshal(body, &envelope), + "response body must be valid JSON: %s", string(body)) + require.NotEmpty(t, envelope.Errors, + "response must contain at least one error message") + require.Contains(t, envelope.Errors[0], "authentication token", + "error message must describe the token size violation") +} + +// TestTokenHeader_NoAuthHeader_Unaffected verifies that requests with no +// authentication header pass through wrapTokenHeaderSizeHandler unchanged. +func TestTokenHeader_NoAuthHeader_Unaffected(t *testing.T) { + t.Parallel() + + client, addr := newTestClusterForTokenHeader(t, nil) + + req, err := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusForbidden, resp.StatusCode, + "requests with no auth header must not be rejected by the size guard") +} + +// BenchmarkTokenHeader_ProcessingCost measures the per-request overhead of +// wrapTokenHeaderSizeHandler at increasing token sizes. +func BenchmarkTokenHeader_ProcessingCost(b *testing.B) { + core, _, _ := vault.TestCoreUnsealed(b) + ln, addr := TestServer(b, core) + defer ln.Close() + + sizes := []struct { + label string + bytes int + }{ + {"1KB", 1 * 1024}, + {"8KB", 8 * 1024}, // at default limit + {"64KB", 64 * 1024}, // above default limit — fast-rejected by wrapTokenHeaderSizeHandler + {"512KB", 512 * 1024}, + } + + client := cleanhttp.DefaultClient() + client.Timeout = 30 * time.Second + + for _, sz := range sizes { + token := strings.Repeat("x", sz.bytes) + b.Run(fmt.Sprintf("size=%s", sz.label), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + req, _ := http.NewRequest(http.MethodGet, addr+"/v1/auth/token/lookup-self", nil) + req.Header.Set(consts.AuthHeaderName, token) + resp, err := client.Do(req) + if err == nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + } + }) + } +} + +// TestTokenHeaderMaxBytes_NilConfig verifies that a nil listener config +// returns DefaultMaxTokenHeaderSize. +func TestTokenHeaderMaxBytes_NilConfig(t *testing.T) { + t.Parallel() + require.Equal(t, DefaultMaxTokenHeaderSize, TokenHeaderMaxBytes(nil)) +} + +// TestTokenHeaderMaxBytes_ZeroCustomSize verifies that a zero CustomMaxTokenHeaderSize +// falls back to DefaultMaxTokenHeaderSize. +func TestTokenHeaderMaxBytes_ZeroCustomSize(t *testing.T) { + t.Parallel() + lnConfig := &configutil.Listener{} // CustomMaxTokenHeaderSize == 0 + require.Equal(t, DefaultMaxTokenHeaderSize, TokenHeaderMaxBytes(lnConfig)) +} + +// TestTokenHeaderMaxBytes_CustomSize verifies that a positive CustomMaxTokenHeaderSize +// is returned verbatim. +func TestTokenHeaderMaxBytes_CustomSize(t *testing.T) { + t.Parallel() + lnConfig := &configutil.Listener{CustomMaxTokenHeaderSize: 4096} + require.Equal(t, 4096, TokenHeaderMaxBytes(lnConfig)) +} + +// TestTokenHeaderMaxBytes_Disabled verifies that max_token_header_size = -1 +// returns 0, leaving http.Server.MaxHeaderBytes at the Go stdlib default. +func TestTokenHeaderMaxBytes_Disabled(t *testing.T) { + t.Parallel() + lnConfig := &configutil.Listener{CustomMaxTokenHeaderSize: -1} + require.Equal(t, 0, TokenHeaderMaxBytes(lnConfig)) +} + +// TestTokenHeader_StdlibBackstop_NonAuthHeaderRejected verifies that setting +// MaxHeaderBytes on http.Server rejects oversized non-authentication headers that +// wrapTokenHeaderSizeHandler does not inspect. +func TestTokenHeader_StdlibBackstop_NonAuthHeaderRejected(t *testing.T) { + t.Parallel() + + core, _, _ := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + + oversizedValue := strings.Repeat("x", DefaultMaxTokenHeaderSize*2) + + req, err := http.NewRequest(http.MethodGet, addr+"/v1/sys/health", nil) + require.NoError(t, err) + req.Header.Set("X-Evil-Header", oversizedValue) + + client := cleanhttp.DefaultClient() + client.Timeout = 15 * time.Second + resp, err := client.Do(req) + if err == nil { + defer resp.Body.Close() + require.NotEqual(t, http.StatusOK, resp.StatusCode, + "oversized non-auth header must not reach the application layer") + } +} + +// TestTokenHeaderMaxBytes_ServerUsesCorrectDefault verifies that an http.Server +// built with TokenHeaderMaxBytes(nil) enforces the limit on any header. +func TestTokenHeaderMaxBytes_ServerUsesCorrectDefault(t *testing.T) { + t.Parallel() + + reached := false + inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reached = true + w.WriteHeader(http.StatusOK) + }) + + srv := &http.Server{ + Handler: inner, + MaxHeaderBytes: TokenHeaderMaxBytes(nil), + } + + // Start on a random port. + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + go func() { + if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { + t.Errorf("srv.Serve: %v", err) + } + }() + t.Cleanup(func() { srv.Close() }) + + addr := "http://" + ln.Addr().String() + + t.Run("within_limit_reaches_handler", func(t *testing.T) { + reached = false + req, err := http.NewRequest(http.MethodGet, addr+"/", nil) + require.NoError(t, err) + req.Header.Set("X-Test", strings.Repeat("a", DefaultMaxTokenHeaderSize-200)) + resp, err := cleanhttp.DefaultClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + require.True(t, reached, "handler should have been called for a within-limit request") + }) + + t.Run("exceeds_limit_rejected_by_stdlib", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, addr+"/", nil) + require.NoError(t, err) + // 2× to exceed the stdlib's effective limit (MaxHeaderBytes + 4096). + req.Header.Set("X-Evil", strings.Repeat("x", DefaultMaxTokenHeaderSize*2)) + resp, err := cleanhttp.DefaultClient().Do(req) + if err == nil { + defer resp.Body.Close() + require.NotEqual(t, http.StatusOK, resp.StatusCode, + "stdlib must reject a request whose headers exceed MaxHeaderBytes") + } + }) +} + +// TestTokenHeader_ClusterListener_SkipsCheck verifies that the cluster listener +// does not re-enforce the token header size limit on forwarded requests. +// This prevents a regression where a user raising CustomMaxTokenHeaderSize above +// the default would see forwarded requests rejected by the active node's cluster +// listener, which only knows the default limit (same failure mode as the JSON +// limits regression fixed by DisableJSONLimitParsing). +func TestTokenHeader_ClusterListener_SkipsCheck(t *testing.T) { + // A token that exceeds the default limit but would be allowed by a custom + // API-listener limit of 16 KB. The cluster listener must pass it through + // without re-checking. + oversizedForDefault := strings.Repeat("a", DefaultMaxTokenHeaderSize+100) + + // Cluster listener is configured identically to how server.go sets it up: + // DisableTokenHeaderSizeParsing = true, no CustomMaxTokenHeaderSize override. + clusterProps := &vault.HandlerProperties{ + ListenerConfig: &configutil.Listener{ + DisableTokenHeaderSizeParsing: true, + }, + } + + reached := false + inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reached = true + w.WriteHeader(http.StatusOK) + }) + + clusterHandler := wrapTokenHeaderSizeHandler(inner, clusterProps) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + srv := &http.Server{Handler: clusterHandler} + go func() { + if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { + t.Errorf("srv.Serve: %v", err) + } + }() + t.Cleanup(func() { srv.Close() }) + + addr := "http://" + ln.Addr().String() + + t.Run("oversized_token_passes_cluster_listener", func(t *testing.T) { + reached = false + req, err := http.NewRequest(http.MethodGet, addr+"/", nil) + require.NoError(t, err) + req.Header.Set(consts.AuthHeaderName, oversizedForDefault) + resp, err := cleanhttp.DefaultClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, + "cluster listener must not re-enforce the token size limit on forwarded requests") + require.True(t, reached, "inner handler should be reached on the cluster listener") + }) + + t.Run("api_listener_still_enforces_default_limit", func(t *testing.T) { + // Confirm the same token is rejected by a normal API-listener handler + // (no DisableTokenHeaderSizeParsing), so we know the test token really does + // exceed the default limit. + apiProps := &vault.HandlerProperties{ + ListenerConfig: &configutil.Listener{}, + } + apiHandler := wrapTokenHeaderSizeHandler(inner, apiProps) + + apiLn, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + apiSrv := &http.Server{Handler: apiHandler} + go func() { + if err := apiSrv.Serve(apiLn); err != nil && err != http.ErrServerClosed { + t.Errorf("apiSrv.Serve: %v", err) + } + }() + t.Cleanup(func() { apiSrv.Close() }) + + req, err := http.NewRequest(http.MethodGet, "http://"+apiLn.Addr().String()+"/", nil) + require.NoError(t, err) + req.Header.Set(consts.AuthHeaderName, oversizedForDefault) + resp, err := cleanhttp.DefaultClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode, + "API listener must reject a token that exceeds the default size limit") + }) +} diff --git a/http/util.go b/http/util.go index b89255888a..3efef4a092 100644 --- a/http/util.go +++ b/http/util.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/limits" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -23,6 +24,8 @@ import ( var nonVotersAllowed = false +const maxTokenHeaderSizeDefault = 0 + // ctxKeyRoleBasedQuota is used to signal that role-based quota resolution // is needed for the request. type ctxKeyRoleBasedQuota struct{} @@ -45,6 +48,58 @@ func resetBodyIfRead(r *http.Request, buf *bytes.Buffer) *http.Request { return r } +// wrapTokenHeaderSizeHandler rejects requests whose authentication token header +// exceeds the configured size limit. +func wrapTokenHeaderSizeHandler(handler http.Handler, props *vault.HandlerProperties) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var maxTokenHeaderSize int64 + if props.ListenerConfig != nil { + // Skip the check on the cluster listener. Forwarded requests have + // already been validated at the API listener on the originating node, + // so re-checking here would cause a regression when a custom limit + // larger than the default is configured. + if props.ListenerConfig.DisableTokenHeaderSizeParsing { + handler.ServeHTTP(w, r) + return + } + maxTokenHeaderSize = props.ListenerConfig.CustomMaxTokenHeaderSize + } + if maxTokenHeaderSize == maxTokenHeaderSizeDefault { + maxTokenHeaderSize = DefaultMaxTokenHeaderSize + } + + if maxTokenHeaderSize > 0 { + bearerPrefix := "Bearer " + tokenLen := int64(len(r.Header.Get(consts.AuthHeaderName))) + if tokenLen > maxTokenHeaderSize { + respondError(w, http.StatusBadRequest, + fmt.Errorf("authentication token exceeds maximum allowed header size of %d bytes", maxTokenHeaderSize)) + return + } + + // Iterate all Authorization headers to mirror getTokenFromReq, which + // ranges over r.Header["Authorization"] to find the first Bearer value. + // Using r.Header.Get would only check the first header and could be + // bypassed by placing a non-Bearer Authorization header first. + for _, v := range r.Header["Authorization"] { + if !strings.HasPrefix(v, bearerPrefix) { + continue + } + bearerTokenLen := int64(len(strings.TrimSpace(v[len(bearerPrefix):]))) + if bearerTokenLen > maxTokenHeaderSize { + respondError(w, http.StatusBadRequest, + fmt.Errorf("authentication token exceeds maximum allowed header size of %d bytes", maxTokenHeaderSize)) + return + } + // Only the first Bearer value is used by getTokenFromReq; stop after checking it. + break + } + } + + handler.ServeHTTP(w, r) + }) +} + // wrapMaxRequestSizeHandler limits the size of the request body to the // configured size func wrapMaxRequestSizeHandler(handler http.Handler, props *vault.HandlerProperties) http.Handler { diff --git a/internalshared/configutil/listener.go b/internalshared/configutil/listener.go index d0413f8aed..07f0d8a201 100644 --- a/internalshared/configutil/listener.go +++ b/internalshared/configutil/listener.go @@ -176,6 +176,15 @@ type Listener struct { // CustomMaxJSONToken determines the maximum number of tokens in a JSON. CustomMaxJSONTokenRaw interface{} `hcl:"max_json_token"` CustomMaxJSONToken int64 `hcl:"-"` + + // DisableTokenHeaderSizeParsing disables the token header size check. This is only applicable + // to the listener config passed into the Cluster listener since forwarded requests have already + // been checked via the API listener on the originating node. + DisableTokenHeaderSizeParsing bool `hcl:"-"` + + // CustomMaxTokenHeaderSize defines the maximum allowed size in bytes for an authentication token header. + CustomMaxTokenHeaderSizeRaw interface{} `hcl:"max_token_header_size"` + CustomMaxTokenHeaderSize int64 `hcl:"-"` } // AgentAPI allows users to select which parts of the Agent API they want enabled. @@ -499,6 +508,13 @@ func (l *Listener) parseRequestSettings() error { return err } + if err := parseAndClearInt(&l.CustomMaxTokenHeaderSizeRaw, &l.CustomMaxTokenHeaderSize); err != nil { + return fmt.Errorf("error parsing max_token_header_size: %w", err) + } + // A negative value disables the check entirely, matching the max_request_size + // convention. Unlike the CustomMaxJSON* fields, negative is intentionally + // allowed here (not an error). + return nil } diff --git a/internalshared/configutil/listener_test.go b/internalshared/configutil/listener_test.go index 4620f164c6..730ed1dd17 100644 --- a/internalshared/configutil/listener_test.go +++ b/internalshared/configutil/listener_test.go @@ -232,6 +232,8 @@ func TestListener_parseRequestSettings(t *testing.T) { expectedCustomMaxJSONArrayElementCount int64 rawCustomMaxJSONToken any expectedCustomMaxJSONToken int64 + rawCustomMaxTokenHeaderSize any + expectedCustomMaxTokenHeaderSize int64 isErrorExpected bool errorMessage string }{ @@ -323,6 +325,23 @@ func TestListener_parseRequestSettings(t *testing.T) { expectedCustomMaxJSONToken: 500000, isErrorExpected: false, }, + "max-token-header-size-bad": { + rawCustomMaxTokenHeaderSize: "badvalue", + isErrorExpected: true, + errorMessage: "error parsing max_token_header_size", + }, + // -1 is valid: it disables the token header size limit entirely, + // following the same convention as max_request_size = -1. + "max-token-header-size-disable": { + rawCustomMaxTokenHeaderSize: "-1", + expectedCustomMaxTokenHeaderSize: -1, + isErrorExpected: false, + }, + "max-token-header-size-good": { + rawCustomMaxTokenHeaderSize: "4096", + expectedCustomMaxTokenHeaderSize: 4096, + isErrorExpected: false, + }, } for name, tc := range tests { @@ -341,6 +360,7 @@ func TestListener_parseRequestSettings(t *testing.T) { CustomMaxJSONObjectEntryCountRaw: tc.rawCustomMaxJSONObjectEntryCount, CustomMaxJSONArrayElementCountRaw: tc.rawCustomMaxJSONArrayElementCount, CustomMaxJSONTokenRaw: tc.rawCustomMaxJSONToken, + CustomMaxTokenHeaderSizeRaw: tc.rawCustomMaxTokenHeaderSize, } err := l.parseRequestSettings() @@ -357,6 +377,7 @@ func TestListener_parseRequestSettings(t *testing.T) { require.Equal(t, tc.expectedCustomMaxJSONObjectEntryCount, l.CustomMaxJSONObjectEntryCount) require.Equal(t, tc.expectedCustomMaxJSONArrayElementCount, l.CustomMaxJSONArrayElementCount) require.Equal(t, tc.expectedCustomMaxJSONToken, l.CustomMaxJSONToken) + require.Equal(t, tc.expectedCustomMaxTokenHeaderSize, l.CustomMaxTokenHeaderSize) require.Equal(t, tc.expectedRequireRequestHeader, l.RequireRequestHeader) require.Equal(t, tc.expectedDisableRequestLimiter, l.DisableRequestLimiter) require.Equal(t, tc.expectedDuration, l.MaxRequestDuration) @@ -367,6 +388,7 @@ func TestListener_parseRequestSettings(t *testing.T) { require.Nil(t, l.CustomMaxJSONObjectEntryCountRaw) require.Nil(t, l.CustomMaxJSONArrayElementCountRaw) require.Nil(t, l.CustomMaxJSONTokenRaw) + require.Nil(t, l.CustomMaxTokenHeaderSizeRaw) require.Nil(t, l.MaxRequestDurationRaw) require.Nil(t, l.RequireRequestHeaderRaw) require.Nil(t, l.DisableRequestLimiterRaw) From d34cb72e684be97ec2c84279686a1bdab606ef79 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 10:30:48 -0400 Subject: [PATCH 072/468] Add counting for SSH certs and OTPs (#12368) (#12755) * add cert counting for ssh * add system view and fix errors * add otp counting and change units for certs * add storage tests * fix census errors * run make fmt * use incrementer and change storage to match rfc * run make fmt * fix interface and remove parameter * fix errors * Update builtin/logical/ssh/path_creds_create.go * remove error check * add ssh counts to billing endpoint * fix error * add test case * add ssh metric to test * add get functions and tests * fix format * create function for ssh metrics * refactoring and add test cases * replace test check * add ssh to billing overview test --------- Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Co-authored-by: Victor Rodriguez Rizo --- api/sys_billing_test.go | 23 +- builtin/logical/ssh/backend.go | 15 +- builtin/logical/ssh/path_creds_create.go | 2 + builtin/logical/ssh/path_issue_sign.go | 2 + sdk/logical/certificate_counter.go | 27 +- vault/billing/billing_counts.go | 2 + vault/consumption_billing_util.go | 167 +++++++++++ vault/consumption_billing_util_test.go | 261 ++++++++++++++++++ vault/extended_system_view.go | 9 +- vault/external_tests/api/sys_billing_test.go | 1 + vault/logical_system_helpers.go | 15 + vault/logical_system_use_case_billing.go | 35 +++ vault/logical_system_use_case_billing_test.go | 78 ++++++ 13 files changed, 630 insertions(+), 7 deletions(-) diff --git a/api/sys_billing_test.go b/api/sys_billing_test.go index 7b816b3831..65b177b705 100644 --- a/api/sys_billing_test.go +++ b/api/sys_billing_test.go @@ -33,7 +33,7 @@ func TestSys_BillingOverview(t *testing.T) { currentMonth := resp.Months[0] require.Equal(t, "2026-01", currentMonth.Month) require.Equal(t, "2026-01-14T10:49:00Z", currentMonth.UpdatedAt) - require.Len(t, currentMonth.UsageMetrics, 8, "should have all 8 metrics") + require.Len(t, currentMonth.UsageMetrics, 9, "should have all 9 metrics") // Create a map to verify all expected metrics are present metricsMap := make(map[string]UsageMetric) @@ -51,6 +51,7 @@ func TestSys_BillingOverview(t *testing.T) { "data_protection_calls", "pki_units", "managed_keys", + "ssh_units", } for _, metricName := range expectedMetrics { @@ -86,6 +87,10 @@ func TestSys_BillingOverview(t *testing.T) { require.Equal(t, "external_plugins", externalPluginsMetric.MetricName) require.NotNil(t, externalPluginsMetric.MetricData) require.Contains(t, externalPluginsMetric.MetricData, "total") + + sshMetric := metricsMap["ssh_units"] + require.Contains(t, sshMetric.MetricData, "total") + require.Contains(t, sshMetric.MetricData, "metric_details") } func mockVaultBillingHandler(w http.ResponseWriter, _ *http.Request) { @@ -200,6 +205,22 @@ const billingOverviewResponse = `{ } ] } + }, + { + "metric_name": "ssh_units", + "metric_data": { + "total": 8.4, + "metric_details": [ + { + "type": "otp_units", + "count": 5 + }, + { + "type": "certificate_units", + "count": 3.4 + } + ] + } } ] }, diff --git a/builtin/logical/ssh/backend.go b/builtin/logical/ssh/backend.go index c64c98a47c..13aad152eb 100644 --- a/builtin/logical/ssh/backend.go +++ b/builtin/logical/ssh/backend.go @@ -18,10 +18,11 @@ const operationPrefixSSH = "ssh" type backend struct { *framework.Backend - view logical.Storage - salt *salt.Salt - saltMutex sync.RWMutex - backendUUID string + view logical.Storage + salt *salt.Salt + saltMutex sync.RWMutex + backendUUID string + sshCertificateCounter logical.CertificateCounter } func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { @@ -84,6 +85,12 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { BackendType: logical.TypeLogical, } + if sshCertCounterSysView, ok := conf.System.(logical.CertificateCountSystemView); ok { + b.sshCertificateCounter = sshCertCounterSysView.GetCertificateCounter() + } else { + b.sshCertificateCounter = logical.NewNullCertificateCounter() + } + b.backendUUID = conf.BackendUUID return &b, nil } diff --git a/builtin/logical/ssh/path_creds_create.go b/builtin/logical/ssh/path_creds_create.go index 0f27c6b5de..3827803369 100644 --- a/builtin/logical/ssh/path_creds_create.go +++ b/builtin/logical/ssh/path_creds_create.go @@ -203,6 +203,8 @@ func (b *backend) GenerateOTPCredential(ctx context.Context, req *logical.Reques if err := req.Storage.Put(ctx, newEntry); err != nil { return "", err } + + b.sshCertificateCounter.Increment().AddSSHOTP() return otp, nil } diff --git a/builtin/logical/ssh/path_issue_sign.go b/builtin/logical/ssh/path_issue_sign.go index f0a87e3dbf..baf5574d38 100644 --- a/builtin/logical/ssh/path_issue_sign.go +++ b/builtin/logical/ssh/path_issue_sign.go @@ -129,6 +129,8 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic return nil, nil, errors.New("error marshaling signed certificate") } + b.sshCertificateCounter.Increment().AddSSHCertificate(ttl) + response := &logical.Response{ Data: map[string]interface{}{ "serial_number": strconv.FormatUint(certificate.Serial, 16), diff --git a/sdk/logical/certificate_counter.go b/sdk/logical/certificate_counter.go index 0504c06d4d..9f29f66d97 100644 --- a/sdk/logical/certificate_counter.go +++ b/sdk/logical/certificate_counter.go @@ -6,6 +6,7 @@ package logical import ( "crypto/x509" "math" + "time" ) // CertificateCounter is an interface for incrementing the count of issued and stored @@ -28,16 +29,20 @@ type CertCount struct { // purposes. Each certificate's billable units = (Validity Hours ÷ 730), rounded to 4 decimal // places. PkiDurationAdjustedCerts float64 + SSHIssuedCerts float64 + SSHIssuedOTPs uint64 } func (i *CertCount) Add(other CertCount) { i.IssuedCerts += other.IssuedCerts i.StoredCerts += other.StoredCerts i.PkiDurationAdjustedCerts += other.PkiDurationAdjustedCerts + i.SSHIssuedCerts += other.SSHIssuedCerts + i.SSHIssuedOTPs += other.SSHIssuedOTPs } func (i *CertCount) IsZero() bool { - return i.IssuedCerts == 0 && i.StoredCerts == 0 && i.PkiDurationAdjustedCerts == 0 + return i.IssuedCerts == 0 && i.StoredCerts == 0 && i.PkiDurationAdjustedCerts == 0 && i.SSHIssuedCerts == 0 && i.SSHIssuedOTPs == 0 } // durationAdjustedCertificateCount calculates the billable units for a certificate based on its @@ -58,6 +63,8 @@ func durationAdjustedCertificateCount(validitySeconds int64) float64 { type CertCountIncrementer interface { AddIssuedCertificate(stored bool, cert *x509.Certificate) CertCountIncrementer + AddSSHCertificate(ttl time.Duration) CertCountIncrementer + AddSSHOTP() CertCountIncrementer } type certCountIncrementer struct { @@ -88,3 +95,21 @@ func (c *certCountIncrementer) AddIssuedCertificate(stored bool, cert *x509.Cert return c } + +func (c *certCountIncrementer) AddSSHCertificate(ttl time.Duration) CertCountIncrementer { + count := CertCount{ + SSHIssuedCerts: durationAdjustedCertificateCount(int64(ttl.Seconds())), + } + + c.counter.AddCount(count) + + return c +} + +func (c *certCountIncrementer) AddSSHOTP() CertCountIncrementer { + c.counter.AddCount(CertCount{ + SSHIssuedOTPs: 1, + }) + + return c +} diff --git a/vault/billing/billing_counts.go b/vault/billing/billing_counts.go index 1e43a9d9f1..1ee8cb0884 100644 --- a/vault/billing/billing_counts.go +++ b/vault/billing/billing_counts.go @@ -29,6 +29,8 @@ const ( KmipEnabledPrefix = "kmipEnabled/" PkiDurationAdjustedCountPrefix = "normalizedCertsIssued/" MetricsLastUpdatedAtPrefix = "metricsLastUpdatedAt/" + SSHCertificateMetric = "ssh/normalized-certs-issued" + SSHOTPMetric = "ssh/credential-count" BillingWriteInterval = 10 * time.Minute // pluginCountsSendTimeout is the timeout for sending plugin counts to the active node diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 64c05b32b7..9e19de8947 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hashicorp/vault/helper/timeutil" + "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault/billing" ) @@ -758,3 +759,169 @@ func (c *Core) UpdateMetricsLastUpdateTime(ctx context.Context, currentMonth, up return c.storeMetricsLastUpdateTimeLocked(ctx, billing.LocalPrefix, normalizedMonth, updateTime) } + +// GetStoredSSHDurationAdjustedCertCount retrieves the stored SSH duration-adjusted certificate count +// for the specified month. The count is stored as a float64. +// Returns 0 if no count has been stored for the given month. +func (c *Core) GetStoredSSHDurationAdjustedCertCount(ctx context.Context, currentMonth time.Time) (float64, error) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return 0, errors.New("consumption billing is not initialized") + } + + cb.BillingStorageLock.RLock() + defer cb.BillingStorageLock.RUnlock() + + return c.getStoredSSHDurationAdjustedCertCountLocked(ctx, billing.LocalPrefix, currentMonth) +} + +func (c *Core) getStoredSSHDurationAdjustedCertCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (float64, error) { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHCertificateMetric) + + view, ok := c.GetBillingSubView() + if !ok { + return 0, errors.New("error reading SSH duration-adjusted count: billing subview not available") + } + + se, err := view.Get(ctx, billingPath) + if se == nil || err != nil { + return 0, err + } + + var certCount float64 + err = se.DecodeJSON(&certCount) + if err != nil { + return 0, fmt.Errorf("error decoding current SSH duration adjusted cert count: %w", err) + } + + return certCount, nil +} + +func (c *Core) UpdateStoredSSHDurationAdjustedCertCount(ctx context.Context, currentMonth time.Time, certCount float64) (float64, error) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return 0, ErrConsumptionBillingNotInitialized + } + cb.BillingStorageLock.Lock() + defer cb.BillingStorageLock.Unlock() + storedCertCount, err := c.getStoredSSHDurationAdjustedCertCountLocked(ctx, billing.LocalPrefix, currentMonth) + if err != nil { + return 0, err + } + + err = c.storeSSHDurationAdjustedCertCountLocked(ctx, billing.LocalPrefix, currentMonth, certCount+storedCertCount) + if err != nil { + return 0, err + } + + return certCount, nil +} + +func (c *Core) storeSSHDurationAdjustedCertCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, certCount float64) error { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHCertificateMetric) + + countBytes, err := jsonutil.EncodeJSON(certCount) + if err != nil { + return err + } + + entry := &logical.StorageEntry{ + Key: billingPath, + Value: countBytes, + } + + view, ok := c.GetBillingSubView() + if !ok { + return nil + } + return view.Put(ctx, entry) +} + +// GetStoredSSHOTPCount retrieves the stored SSH OTP count +// for the specified month. The count is stored as a uint64. +// Returns 0 if no count has been stored for the given month. +func (c *Core) GetStoredSSHOTPCount(ctx context.Context, currentMonth time.Time) (uint64, error) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return 0, errors.New("consumption billing is not initialized") + } + + cb.BillingStorageLock.RLock() + defer cb.BillingStorageLock.RUnlock() + + return c.getStoredSSHOTPCountLocked(ctx, billing.LocalPrefix, currentMonth) +} + +func (c *Core) getStoredSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (uint64, error) { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHOTPMetric) + + view, ok := c.GetBillingSubView() + if !ok { + return 0, errors.New("error reading SSH OTP count: billing subview not available") + } + + se, err := view.Get(ctx, billingPath) + if se == nil || err != nil { + return 0, err + } + + var otpCount uint64 + err = se.DecodeJSON(&otpCount) + if err != nil { + return 0, fmt.Errorf("error decoding current OTP cert count: %w", err) + } + + return otpCount, nil +} + +func (c *Core) UpdateStoredSSHOTPCount(ctx context.Context, currentMonth time.Time, otpCount uint64) (uint64, error) { + c.consumptionBillingLock.RLock() + cb := c.consumptionBilling + c.consumptionBillingLock.RUnlock() + + if cb == nil { + return 0, ErrConsumptionBillingNotInitialized + } + cb.BillingStorageLock.Lock() + defer cb.BillingStorageLock.Unlock() + storedOTPCount, err := c.getStoredSSHOTPCountLocked(ctx, billing.LocalPrefix, currentMonth) + if err != nil { + return 0, err + } + + err = c.storeSSHOTPCountLocked(ctx, billing.LocalPrefix, currentMonth, otpCount+storedOTPCount) + if err != nil { + return 0, err + } + + return otpCount, nil +} + +func (c *Core) storeSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, otpCount uint64) error { + billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHOTPMetric) + + countBytes, err := jsonutil.EncodeJSON(otpCount) + if err != nil { + return err + } + + entry := &logical.StorageEntry{ + Key: billingPath, + Value: countBytes, + } + + view, ok := c.GetBillingSubView() + if !ok { + return nil + } + return view.Put(ctx, entry) +} diff --git a/vault/consumption_billing_util_test.go b/vault/consumption_billing_util_test.go index b0d8c60d40..6fa127d2b0 100644 --- a/vault/consumption_billing_util_test.go +++ b/vault/consumption_billing_util_test.go @@ -6,6 +6,7 @@ package vault import ( "context" "fmt" + "math" "testing" "time" @@ -23,6 +24,7 @@ import ( logicalDatabase "github.com/hashicorp/vault/builtin/logical/database" logicalNomad "github.com/hashicorp/vault/builtin/logical/nomad" logicalRabbitMQ "github.com/hashicorp/vault/builtin/logical/rabbitmq" + "github.com/hashicorp/vault/builtin/logical/ssh" "github.com/hashicorp/vault/builtin/logical/totp" "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/helper/namespace" @@ -809,6 +811,265 @@ func TestTransitDataProtectionCallCounts(t *testing.T) { require.Equal(t, uint64(0), core.GetInMemoryTransitDataProtectionCallCounts(), "Counter should still be 0") } +// TestSSHCertCounts tests that we correctly store and track the SSH certificate counts +func TestSSHCertCounts(t *testing.T) { + standardDuration := 730.0 + validityHours := float64(60*60*24) / 3600.0 + units := validityHours / standardDuration + // Round to 4 decimal places + expectedCertUnit := math.Round(units*10000) / 10000 + + t.Parallel() + coreConfig := &CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "ssh": ssh.Factory, + }, + } + + core, _, root := TestCoreUnsealedWithConfig(t, coreConfig) + + // Mount SSH backend + req := logical.TestRequest(t, logical.CreateOperation, "sys/mounts/ssh") + req.Data["type"] = "ssh" + req.ClientToken = root + ctx := namespace.RootContext(context.Background()) + _, err := core.HandleRequest(ctx, req) + require.NoError(t, err) + + // Create a certificate + req = logical.TestRequest(t, logical.CreateOperation, "ssh/config/ca") + req.ClientToken = root + _, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/roles/test") + req.ClientToken = root + req.Data["key_type"] = "ca" + req.Data["allow_user_certificates"] = true + req.Data["allow_empty_principals"] = true + req.Data["ttl"] = "1d" + _, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/issue/test") + req.ClientToken = root + resp, err := core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Verify that the SSH counter is incremented + require.Equal(t, expectedCertUnit, core.certCountManager.GetCounts().SSHIssuedCerts) + + // Test sign endpoint + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/sign/test") + req.Data["public_key"] = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBp4mozY/snvG/+pkgv4xYifIFB2ov3gAvAqXgFqNpj vault-enterprise-key" + req.ClientToken = root + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Verify that the SSH counter is incremented + require.Equal(t, expectedCertUnit*2, core.certCountManager.GetCounts().SSHIssuedCerts) + + // Now test persisting the summed counts - store and retrieve counts + // First, update the SSH cert counts (this will sum current counter with stored value) + currentCount := core.certCountManager.GetCounts().SSHIssuedCerts + core.certCountManager.StopConsumerJob() + + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedCerts, "Counter should be reset after update") + + // Retrieve the stored counts + storedCounts, err := core.GetStoredSSHDurationAdjustedCertCount(ctx, time.Now()) + require.NoError(t, err) + require.Equal(t, currentCount, storedCounts) + + core.certCountManager.StartConsumerJob(core.consumeCertCounts) + + // Perform more operations to increase the counter + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/issue/test") + req.ClientToken = root + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Counter should now be 1 cert + require.Equal(t, expectedCertUnit, core.certCountManager.GetCounts().SSHIssuedCerts) + + // Update counts again - should sum the new count with the stored count + core.certCountManager.StopConsumerJob() + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedCerts, "Counter should be reset after update") + + // Verify stored counts are now the sum + summedCounts, err := core.GetStoredSSHDurationAdjustedCertCount(ctx, time.Now()) + require.NoError(t, err) + + expectedSum := currentCount + expectedCertUnit + require.Equal(t, expectedSum, summedCounts, "Count should be sum of stored and current") + + core.certCountManager.StartConsumerJob(core.consumeCertCounts) + + // Add more operations without manually resetting + for i := 0; i < 3; i++ { + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/sign/test") + req.Data["public_key"] = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBp4mozY/snvG/+pkgv4xYifIFB2ov3gAvAqXgFqNpj vault-enterprise-key" + req.ClientToken = root + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + } + + // Counter should be 3 certs + require.Equal(t, expectedCertUnit*3, core.certCountManager.GetCounts().SSHIssuedCerts) + + // Update counts - should sum 3 with the previous stored sum + core.certCountManager.StopConsumerJob() + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedCerts, "Counter should be reset after update") + + // Verify stored counts + storedCounts, err = core.GetStoredSSHDurationAdjustedCertCount(ctx, time.Now()) + require.NoError(t, err) + expectedSum += expectedCertUnit * 3 + require.Equal(t, expectedSum, storedCounts) +} + +// TestSSHOTPCounts tests that we correctly store and track the SSH OTP counts +func TestSSHOTPCounts(t *testing.T) { + t.Parallel() + coreConfig := &CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "ssh": ssh.Factory, + }, + } + + core, _, root := TestCoreUnsealedWithConfig(t, coreConfig) + + // Mount SSH backend + req := logical.TestRequest(t, logical.CreateOperation, "sys/mounts/ssh") + req.Data["type"] = "ssh" + req.ClientToken = root + ctx := namespace.RootContext(context.Background()) + _, err := core.HandleRequest(ctx, req) + require.NoError(t, err) + + // Create a certificate + req = logical.TestRequest(t, logical.CreateOperation, "ssh/config/ca") + req.ClientToken = root + _, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/roles/test") + req.ClientToken = root + req.Data["key_type"] = "otp" + req.Data["default_user"] = "user" + _, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/config/zeroaddress") + req.ClientToken = root + req.Data["roles"] = "test" + _, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test") + req.ClientToken = root + req.Data["ip"] = "1.2.3.4" + resp, err := core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Verify that the SSH counter is incremented + require.Equal(t, uint64(1), core.certCountManager.GetCounts().SSHIssuedOTPs) + + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test") + req.ClientToken = root + req.Data["ip"] = "1.2.3.4" + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Verify that the SSH counter is incremented + require.Equal(t, uint64(2), core.certCountManager.GetCounts().SSHIssuedOTPs) + + // Now test persisting the summed counts - store and retrieve counts + // First, update the SSH cert counts (this will sum current counter with stored value) + currentCount := core.certCountManager.GetCounts().SSHIssuedOTPs + core.certCountManager.StopConsumerJob() + + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + + // Retrieve the stored counts + storedCounts, err := core.GetStoredSSHOTPCount(ctx, time.Now()) + require.NoError(t, err) + require.Equal(t, currentCount, storedCounts) + + core.certCountManager.StartConsumerJob(core.consumeCertCounts) + + // Perform more operations to increase the counter + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test") + req.ClientToken = root + req.Data["ip"] = "1.2.3.4" + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + // Counter should now be 1 + require.Equal(t, uint64(1), core.certCountManager.GetCounts().SSHIssuedOTPs) + + // Update counts again - should sum the new count with the stored count + core.certCountManager.StopConsumerJob() + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + + // Verify stored counts are now the sum + summedCounts, err := core.GetStoredSSHOTPCount(ctx, time.Now()) + require.NoError(t, err) + + expectedSum := currentCount + 1 + require.Equal(t, expectedSum, summedCounts, "Count should be sum of stored and current") + + core.certCountManager.StartConsumerJob(core.consumeCertCounts) + + // Add more operations without manually resetting + for i := 0; i < 3; i++ { + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test") + req.ClientToken = root + req.Data["ip"] = "1.2.3.4" + resp, err = core.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + } + + // Counter should be 3 + require.Equal(t, uint64(3), core.certCountManager.GetCounts().SSHIssuedOTPs) + + // Update counts - should sum 3 with the previous stored sum + core.certCountManager.StopConsumerJob() + time.Sleep(20 * time.Millisecond) + + // Verify the counter was reset after update + require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + + // Verify stored counts + storedCounts, err = core.GetStoredSSHOTPCount(ctx, time.Now()) + require.NoError(t, err) + expectedSum += 3 + require.Equal(t, expectedSum, storedCounts) +} + func addRoleToStorage(t *testing.T, core *Core, mount string, key string, numberOfKeys int) { raw, ok := core.router.root.Get(mount + "/") if !ok { diff --git a/vault/extended_system_view.go b/vault/extended_system_view.go index 12c20bb7e0..e9b1b2fda1 100644 --- a/vault/extended_system_view.go +++ b/vault/extended_system_view.go @@ -13,7 +13,10 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) -var _ logical.ExtendedSystemView = (*extendedSystemViewImpl)(nil) +var ( + _ logical.ExtendedSystemView = (*extendedSystemViewImpl)(nil) + _ logical.CertificateCountSystemView = (*extendedSystemViewImpl)(nil) +) type extendedSystemViewImpl struct { dynamicSystemView @@ -152,3 +155,7 @@ func (e extendedSystemViewImpl) DeregisterWellKnownRedirect(ctx context.Context, func (e extendedSystemViewImpl) GetPinnedPluginVersion(ctx context.Context, pluginType consts.PluginType, pluginName string) (*pluginutil.PinnedVersion, error) { return e.core.pluginCatalog.GetPinnedVersion(ctx, pluginType, pluginName) } + +func (e extendedSystemViewImpl) GetCertificateCounter() logical.CertificateCounter { + return e.core.GetCertificateCounter() +} diff --git a/vault/external_tests/api/sys_billing_test.go b/vault/external_tests/api/sys_billing_test.go index 11078b8db8..4400a3af92 100644 --- a/vault/external_tests/api/sys_billing_test.go +++ b/vault/external_tests/api/sys_billing_test.go @@ -172,6 +172,7 @@ func Test_BillingOverview_EmptyCluster(t *testing.T) { "data_protection_calls": false, "pki_units": false, "managed_keys": false, + "ssh_units": false, } for _, metric := range currentMonth.UsageMetrics { diff --git a/vault/logical_system_helpers.go b/vault/logical_system_helpers.go index 5b5dda1449..4ce29be3cb 100644 --- a/vault/logical_system_helpers.go +++ b/vault/logical_system_helpers.go @@ -332,6 +332,21 @@ func (c *Core) consumeCertCounts(inc logical.CertCount) { unconsumed.PkiDurationAdjustedCerts = 0 } + c.logger.Info("storing SSH counts", "sshDurationAdjustedCount", inc.SSHIssuedCerts, "sshOTPCount", inc.SSHIssuedOTPs) + _, err = c.UpdateStoredSSHDurationAdjustedCertCount(c.activeContext, time.Now(), inc.SSHIssuedCerts) + if err != nil { + c.logger.Error("error storing SSH duration adjusted certificate count", "error", err) + } else { + unconsumed.SSHIssuedCerts = 0 + } + + _, err = c.UpdateStoredSSHOTPCount(c.activeContext, time.Now(), inc.SSHIssuedOTPs) + if err != nil { + c.logger.Error("error storing SSH OTP count", "error", err) + } else { + unconsumed.SSHIssuedOTPs = 0 + } + default: c.logger.Error("Unexpected HA state when consuming certificate counts", "ha_state", haState) } diff --git a/vault/logical_system_use_case_billing.go b/vault/logical_system_use_case_billing.go index a39d6a9bc5..e532413173 100644 --- a/vault/logical_system_use_case_billing.go +++ b/vault/logical_system_use_case_billing.go @@ -210,6 +210,12 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti }, }) + sshCounts, err := b.buildSSHMetric(ctx, month) + if err != nil { + return nil, err + } + usageMetrics = append(usageMetrics, sshCounts) + dataUpdatedAt := b.Core.computeUpdatedAt(ctx, month, currentMonth) monthStr := month.Format("2006-01") @@ -486,3 +492,32 @@ func (c *Core) getThirdPartyPluginCounts(ctx context.Context, month time.Time) ( return thirdPartyPluginCounts, nil } + +func (b *SystemBackend) buildSSHMetric(ctx context.Context, month time.Time) (map[string]interface{}, error) { + certCounts, err := b.Core.GetStoredSSHDurationAdjustedCertCount(ctx, month) + if err != nil { + return nil, fmt.Errorf("error retrieving SSH duration-adjuested cert counts for current month: %w", err) + } + + otpCounts, err := b.Core.GetStoredSSHOTPCount(ctx, month) + if err != nil { + return nil, fmt.Errorf("error retrieving SSH OTP counts for current month: %w", err) + } + + return map[string]interface{}{ + "metric_name": "ssh_units", + "metric_data": map[string]interface{}{ + "total": certCounts + float64(otpCounts), + "metric_details": []map[string]interface{}{ + { + "type": "otp_units", + "count": otpCounts, + }, + { + "type": "certificate_units", + "count": certCounts, + }, + }, + }, + }, nil +} diff --git a/vault/logical_system_use_case_billing_test.go b/vault/logical_system_use_case_billing_test.go index bc5d2eceba..e5763605fd 100644 --- a/vault/logical_system_use_case_billing_test.go +++ b/vault/logical_system_use_case_billing_test.go @@ -11,6 +11,7 @@ import ( logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" logicalAws "github.com/hashicorp/vault/builtin/logical/aws" logicalDatabase "github.com/hashicorp/vault/builtin/logical/database" + logicalSsh "github.com/hashicorp/vault/builtin/logical/ssh" logicalTransit "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/pluginconsts" @@ -172,6 +173,7 @@ func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) { pluginconsts.SecretEngineAWS: logicalAws.Factory, pluginconsts.SecretEngineDatabase: logicalDatabase.Factory, pluginconsts.SecretEngineTransit: logicalTransit.Factory, + pluginconsts.SecretEngineSsh: logicalSsh.Factory, }, }) b := c.systemBackend @@ -233,6 +235,53 @@ func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) { _, err = c.HandleRequest(ctx, req) require.NoError(t, err) + // Create SSH certificate and OTP + req = logical.TestRequest(t, logical.CreateOperation, "sys/mounts/ssh") + req.Data["type"] = "ssh" + req.ClientToken = root + resp, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/config/ca") + req.ClientToken = root + resp, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/roles/test-cert") + req.ClientToken = root + req.Data["key_type"] = "ca" + req.Data["allow_user_certificates"] = true + req.Data["allow_empty_principals"] = true + req.Data["ttl"] = "1d" + _, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/issue/test-cert") + req.ClientToken = root + resp, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/roles/test-otp") + req.ClientToken = root + req.Data["key_type"] = "otp" + req.Data["default_user"] = "user" + _, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.CreateOperation, "ssh/config/zeroaddress") + req.ClientToken = root + req.Data["roles"] = "test-otp" + _, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + + req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test-otp") + req.ClientToken = root + req.Data["ip"] = "1.2.3.4" + resp, err = c.HandleRequest(ctx, req) + require.NoError(t, err) + require.Nil(t, resp.Error()) + // Update all metrics currentMonth := time.Now() _, err = c.UpdateMaxKvCounts(ctx, billing.LocalPrefix, currentMonth) @@ -244,6 +293,12 @@ func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) { _, err = c.UpdateTransitCallCounts(ctx, currentMonth) require.NoError(t, err) + _, err = c.UpdateStoredSSHDurationAdjustedCertCount(ctx, currentMonth, c.certCountManager.GetCounts().SSHIssuedCerts) + require.NoError(t, err) + + _, err = c.UpdateStoredSSHOTPCount(ctx, currentMonth, c.certCountManager.GetCounts().SSHIssuedOTPs) + require.NoError(t, err) + // Make a request to the billing overview endpoint req = logical.TestRequest(t, logical.ReadOperation, "billing/overview") req.Data["refresh_data"] = true @@ -366,6 +421,24 @@ func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) { require.True(t, ok, "managed_keys total should be int") require.GreaterOrEqual(t, total, 0) require.Contains(t, metricData, "metric_details") + + case "ssh_units": + require.Contains(t, metricData, "total") + total, ok := metricData["total"].(float64) + require.True(t, ok, "ssh_units total should be float64") + require.GreaterOrEqual(t, total, float64(0)) + + require.Contains(t, metricData, "metric_details") + metricDetails, ok := metricData["metric_details"].([]map[string]interface{}) + require.True(t, ok, "metric_details should be []map[string]interface{}") + require.NotEmpty(t, metricDetails) + require.Equal(t, len(metricDetails), 2) + + require.Equal(t, metricDetails[0]["type"], "otp_units") + require.GreaterOrEqual(t, metricDetails[0]["count"], uint64(0)) + + require.Equal(t, metricDetails[1]["type"], "certificate_units") + require.GreaterOrEqual(t, metricDetails[1]["count"], float64(0)) } } @@ -469,6 +542,7 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) { "data_protection_calls": false, "pki_units": false, "managed_keys": false, + "ssh_units": false, } for _, metric := range usageMetrics { @@ -522,6 +596,10 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) { details, ok := metricData["metric_details"].([]map[string]interface{}) require.True(t, ok, "%s metric_details should be array", metricName) require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName) + case "ssh_units": + total, ok := metricData["total"].(float64) + require.True(t, ok, "ssh_units total should be float64") + require.Equal(t, float64(0), total, "ssh_units total should be 0") } } From 6674b4358a399cf290e932c020366490c3b3cacb Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 12:14:28 -0400 Subject: [PATCH 073/468] Backport Add census counting for ssh into ce/main (#12918) * no-op commit * Add census counting for ssh (#12396) * add cert counting for ssh * add system view and fix errors * add otp counting and change units for certs * add storage tests * fix census errors * run make fmt * use incrementer and change storage to match rfc * run make fmt * fix interface and remove parameter * fix errors * Update builtin/logical/ssh/path_creds_create.go Co-authored-by: Victor Rodriguez Rizo * remove error check * add ssh counts to billing endpoint * fix error * add test case * add ssh metric to test * add get functions and tests * fix format * create function for ssh metrics * refactoring and add test cases * replace test check * add census counting for ssh * fix read calls * fix test * add otp test * move ssh functions * address feedback * fix hash * fix hash * change otp count * fix tests * change version and hash --------- Co-authored-by: Victor Rodriguez Rizo # Conflicts: # sdk/logical/certificate_counter.go # vault/census_manager_schema_ent.go # vault/census_manager_schema_ent_test.go # vault/consumption_billing_license_utilization_ent.go # vault/consumption_billing_util.go # vault/consumption_billing_util_test.go # vault/external_tests/billing/consumption_billing_license_utilization_census_ent_test.go # vault/logical_system_use_case_billing_test.go --------- Co-authored-by: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Co-authored-by: Victor Rodriguez Rizo --- sdk/logical/certificate_counter.go | 4 ++-- vault/consumption_billing_util.go | 10 ++++---- vault/consumption_billing_util_test.go | 24 ++++++++++--------- vault/logical_system_use_case_billing_test.go | 2 +- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/sdk/logical/certificate_counter.go b/sdk/logical/certificate_counter.go index 9f29f66d97..a93afbf06a 100644 --- a/sdk/logical/certificate_counter.go +++ b/sdk/logical/certificate_counter.go @@ -30,7 +30,7 @@ type CertCount struct { // places. PkiDurationAdjustedCerts float64 SSHIssuedCerts float64 - SSHIssuedOTPs uint64 + SSHIssuedOTPs float64 } func (i *CertCount) Add(other CertCount) { @@ -108,7 +108,7 @@ func (c *certCountIncrementer) AddSSHCertificate(ttl time.Duration) CertCountInc func (c *certCountIncrementer) AddSSHOTP() CertCountIncrementer { c.counter.AddCount(CertCount{ - SSHIssuedOTPs: 1, + SSHIssuedOTPs: 0.0014, }) return c diff --git a/vault/consumption_billing_util.go b/vault/consumption_billing_util.go index 9e19de8947..5e3e3ccd7e 100644 --- a/vault/consumption_billing_util.go +++ b/vault/consumption_billing_util.go @@ -846,7 +846,7 @@ func (c *Core) storeSSHDurationAdjustedCertCountLocked(ctx context.Context, loca // GetStoredSSHOTPCount retrieves the stored SSH OTP count // for the specified month. The count is stored as a uint64. // Returns 0 if no count has been stored for the given month. -func (c *Core) GetStoredSSHOTPCount(ctx context.Context, currentMonth time.Time) (uint64, error) { +func (c *Core) GetStoredSSHOTPCount(ctx context.Context, currentMonth time.Time) (float64, error) { c.consumptionBillingLock.RLock() cb := c.consumptionBilling c.consumptionBillingLock.RUnlock() @@ -861,7 +861,7 @@ func (c *Core) GetStoredSSHOTPCount(ctx context.Context, currentMonth time.Time) return c.getStoredSSHOTPCountLocked(ctx, billing.LocalPrefix, currentMonth) } -func (c *Core) getStoredSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (uint64, error) { +func (c *Core) getStoredSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time) (float64, error) { billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHOTPMetric) view, ok := c.GetBillingSubView() @@ -874,7 +874,7 @@ func (c *Core) getStoredSSHOTPCountLocked(ctx context.Context, localPathPrefix s return 0, err } - var otpCount uint64 + var otpCount float64 err = se.DecodeJSON(&otpCount) if err != nil { return 0, fmt.Errorf("error decoding current OTP cert count: %w", err) @@ -883,7 +883,7 @@ func (c *Core) getStoredSSHOTPCountLocked(ctx context.Context, localPathPrefix s return otpCount, nil } -func (c *Core) UpdateStoredSSHOTPCount(ctx context.Context, currentMonth time.Time, otpCount uint64) (uint64, error) { +func (c *Core) UpdateStoredSSHOTPCount(ctx context.Context, currentMonth time.Time, otpCount float64) (float64, error) { c.consumptionBillingLock.RLock() cb := c.consumptionBilling c.consumptionBillingLock.RUnlock() @@ -906,7 +906,7 @@ func (c *Core) UpdateStoredSSHOTPCount(ctx context.Context, currentMonth time.Ti return otpCount, nil } -func (c *Core) storeSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, otpCount uint64) error { +func (c *Core) storeSSHOTPCountLocked(ctx context.Context, localPathPrefix string, currentMonth time.Time, otpCount float64) error { billingPath := billing.GetMonthlyBillingMetricPath(localPathPrefix, currentMonth, billing.SSHOTPMetric) countBytes, err := jsonutil.EncodeJSON(otpCount) diff --git a/vault/consumption_billing_util_test.go b/vault/consumption_billing_util_test.go index 6fa127d2b0..5789046400 100644 --- a/vault/consumption_billing_util_test.go +++ b/vault/consumption_billing_util_test.go @@ -943,6 +943,8 @@ func TestSSHCertCounts(t *testing.T) { // TestSSHOTPCounts tests that we correctly store and track the SSH OTP counts func TestSSHOTPCounts(t *testing.T) { + expectedOTPUnit := 0.0014 + t.Parallel() coreConfig := &CoreConfig{ LogicalBackends: map[string]logical.Factory{ @@ -987,7 +989,7 @@ func TestSSHOTPCounts(t *testing.T) { require.Nil(t, resp.Error()) // Verify that the SSH counter is incremented - require.Equal(t, uint64(1), core.certCountManager.GetCounts().SSHIssuedOTPs) + require.Equal(t, expectedOTPUnit, core.certCountManager.GetCounts().SSHIssuedOTPs) req = logical.TestRequest(t, logical.UpdateOperation, "ssh/creds/test") req.ClientToken = root @@ -997,7 +999,7 @@ func TestSSHOTPCounts(t *testing.T) { require.Nil(t, resp.Error()) // Verify that the SSH counter is incremented - require.Equal(t, uint64(2), core.certCountManager.GetCounts().SSHIssuedOTPs) + require.Equal(t, expectedOTPUnit*2, core.certCountManager.GetCounts().SSHIssuedOTPs) // Now test persisting the summed counts - store and retrieve counts // First, update the SSH cert counts (this will sum current counter with stored value) @@ -1007,7 +1009,7 @@ func TestSSHOTPCounts(t *testing.T) { time.Sleep(20 * time.Millisecond) // Verify the counter was reset after update - require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") // Retrieve the stored counts storedCounts, err := core.GetStoredSSHOTPCount(ctx, time.Now()) @@ -1024,21 +1026,21 @@ func TestSSHOTPCounts(t *testing.T) { require.NoError(t, err) require.Nil(t, resp.Error()) - // Counter should now be 1 - require.Equal(t, uint64(1), core.certCountManager.GetCounts().SSHIssuedOTPs) + // Counter should now be 1 unit + require.Equal(t, expectedOTPUnit, core.certCountManager.GetCounts().SSHIssuedOTPs) // Update counts again - should sum the new count with the stored count core.certCountManager.StopConsumerJob() time.Sleep(20 * time.Millisecond) // Verify the counter was reset after update - require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") // Verify stored counts are now the sum summedCounts, err := core.GetStoredSSHOTPCount(ctx, time.Now()) require.NoError(t, err) - expectedSum := currentCount + 1 + expectedSum := currentCount + expectedOTPUnit require.Equal(t, expectedSum, summedCounts, "Count should be sum of stored and current") core.certCountManager.StartConsumerJob(core.consumeCertCounts) @@ -1053,20 +1055,20 @@ func TestSSHOTPCounts(t *testing.T) { require.Nil(t, resp.Error()) } - // Counter should be 3 - require.Equal(t, uint64(3), core.certCountManager.GetCounts().SSHIssuedOTPs) + // Counter should be 3 units + require.Equal(t, expectedOTPUnit*3, core.certCountManager.GetCounts().SSHIssuedOTPs) // Update counts - should sum 3 with the previous stored sum core.certCountManager.StopConsumerJob() time.Sleep(20 * time.Millisecond) // Verify the counter was reset after update - require.Equal(t, uint64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") + require.Equal(t, float64(0), core.certCountManager.GetCounts().SSHIssuedOTPs, "Counter should be reset after update") // Verify stored counts storedCounts, err = core.GetStoredSSHOTPCount(ctx, time.Now()) require.NoError(t, err) - expectedSum += 3 + expectedSum += 3 * expectedOTPUnit require.Equal(t, expectedSum, storedCounts) } diff --git a/vault/logical_system_use_case_billing_test.go b/vault/logical_system_use_case_billing_test.go index e5763605fd..5dfa699869 100644 --- a/vault/logical_system_use_case_billing_test.go +++ b/vault/logical_system_use_case_billing_test.go @@ -435,7 +435,7 @@ func TestSystemBackend_BillingOverview_MetricFormats(t *testing.T) { require.Equal(t, len(metricDetails), 2) require.Equal(t, metricDetails[0]["type"], "otp_units") - require.GreaterOrEqual(t, metricDetails[0]["count"], uint64(0)) + require.GreaterOrEqual(t, metricDetails[0]["count"], float64(0)) require.Equal(t, metricDetails[1]["type"], "certificate_units") require.GreaterOrEqual(t, metricDetails[1]["count"], float64(0)) From 2ce86cb36780555e257f57fdc99ba612b3818cd1 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 13:16:13 -0400 Subject: [PATCH 074/468] Set default minimum for pki certs (#12905) (#12921) Co-authored-by: divyaac --- sdk/logical/certificate_counter.go | 7 ++++++- sdk/logical/certificate_counter_test.go | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/logical/certificate_counter.go b/sdk/logical/certificate_counter.go index a93afbf06a..811f6fe73e 100644 --- a/sdk/logical/certificate_counter.go +++ b/sdk/logical/certificate_counter.go @@ -58,7 +58,12 @@ func durationAdjustedCertificateCount(validitySeconds int64) float64 { validityHours := float64(validitySeconds) / 3600.0 units := validityHours / standardDuration // Round to 4 decimal places - return math.Round(units*10000) / 10000 + ret := math.Round(units*10000) / 10000 + if ret == 0.0 && validitySeconds > 0 { + // Ensure we don't return 0.0, which would be interpreted as no billable units. + return 0.0001 + } + return ret } type CertCountIncrementer interface { diff --git a/sdk/logical/certificate_counter_test.go b/sdk/logical/certificate_counter_test.go index 4d449878bf..889cee2e26 100644 --- a/sdk/logical/certificate_counter_test.go +++ b/sdk/logical/certificate_counter_test.go @@ -17,7 +17,7 @@ func Test_durationAdjustedCertificateCount(t *testing.T) { { name: "zero duration", validitySeconds: 0, - want: 0.0, + want: 0, // If the duration is zero, the normalized unit should be zero }, { name: "1 hour", @@ -72,12 +72,12 @@ func Test_durationAdjustedCertificateCount(t *testing.T) { { name: "very small duration - 1 second", validitySeconds: 1, - want: 0.0, // 1/3600/730 = 0.00000038... rounds to 0.0 + want: 0.0001, // 1/3600/730 = 0.00000038... rounds to 0.0 but should return default minimum 0.0001 }, { name: "very small duration - 60 seconds", validitySeconds: 60, - want: 0.0, // 60/3600/730 = 0.000023... rounds to 0.0 + want: 0.0001, // 60/3600/730 = 0.000023... rounds to 0.0 and should return default minimum 0.0001 }, { name: "very small duration - 600 seconds", From c852fa2542a1140788d9486622aa3eab4025999a Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 13:53:58 -0400 Subject: [PATCH 075/468] UI: Cert auth method Vault UI (#12890) (#12922) * add cert auth method to ui * add test coverage * add changelog Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- changelog/_12890.txt | 3 ++ ui/app/components/auth/form/cert.ts | 42 ++++++++++++++++ ui/app/utils/auth-form-helpers.ts | 4 +- ui/tests/helpers/auth/auth-helpers.ts | 3 ++ ui/tests/helpers/auth/response-stubs.ts | 39 ++++++++++++++- .../components/auth/form/base-test.js | 48 +++++++++++++++++++ .../auth/page/method-authentication-test.js | 15 ++++++ ui/types/vault/auth/methods.d.ts | 6 +++ 8 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 changelog/_12890.txt create mode 100644 ui/app/components/auth/form/cert.ts diff --git a/changelog/_12890.txt b/changelog/_12890.txt new file mode 100644 index 0000000000..a1405ec451 --- /dev/null +++ b/changelog/_12890.txt @@ -0,0 +1,3 @@ +```release-note:feature +**UI TLS Certificate login: Add UI login support for the TLS certificate (cert) authentication. +``` diff --git a/ui/app/components/auth/form/cert.ts b/ui/app/components/auth/form/cert.ts new file mode 100644 index 0000000000..02c82e12f2 --- /dev/null +++ b/ui/app/components/auth/form/cert.ts @@ -0,0 +1,42 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import AuthBase from './base'; + +import type { CertLoginApiResponse } from 'vault/vault/auth/methods'; + +/** + * @module Auth::Form::Cert + * see Auth::Base + * */ + +export default class AuthFormCert extends AuthBase { + loginFields = [ + { + name: 'name', + label: 'Role name', + helperText: 'Leave blank to match any certificate role.', + }, + ]; + + async loginRequest(formData: { name: string; path: string }) { + const { path, name } = formData; + + const { auth } = (await this.api.auth.certLogin(path, { + name, + })) as CertLoginApiResponse; + + const { cert_name, common_name } = auth?.metadata || {}; + const displayName = + cert_name && common_name ? `${cert_name}/${common_name}` : cert_name || common_name || ''; + + return this.normalizeAuthResponse(auth, { + authMountPath: path, + displayName, + token: auth.client_token, + ttl: auth.lease_duration, + }); + } +} diff --git a/ui/app/utils/auth-form-helpers.ts b/ui/app/utils/auth-form-helpers.ts index 68315a1858..2526538bd1 100644 --- a/ui/app/utils/auth-form-helpers.ts +++ b/ui/app/utils/auth-form-helpers.ts @@ -9,7 +9,7 @@ * which includes all the methods that can be enabled and mounted. */ -const BASE_LOGIN_METHODS = ['github', 'jwt', 'ldap', 'oidc', 'okta', 'radius', 'token', 'userpass']; +const BASE_LOGIN_METHODS = ['github', 'jwt', 'ldap', 'oidc', 'okta', 'radius', 'token', 'cert', 'userpass']; export const ENTERPRISE_LOGIN_METHODS = ['saml']; @@ -19,7 +19,7 @@ export const supportedTypes = (isEnterprise: boolean) => { // this ensures no unexpected params are injected and submitted in the login form // 'namespace' and 'path' are intentionally omitted because they are handled explicitly -export const POSSIBLE_FIELDS = ['role', 'jwt', 'password', 'token', 'username']; +export const POSSIBLE_FIELDS = ['role', 'jwt', 'password', 'token', 'username', 'name']; // maps OIDC provider domain to display name for oidc-jwt auth form export const DOMAIN_PROVIDER_MAP = { diff --git a/ui/tests/helpers/auth/auth-helpers.ts b/ui/tests/helpers/auth/auth-helpers.ts index 656fb826b0..4285d02da7 100644 --- a/ui/tests/helpers/auth/auth-helpers.ts +++ b/ui/tests/helpers/auth/auth-helpers.ts @@ -75,6 +75,7 @@ export const fillInLoginFields = async (loginFields: LoginFields, { toggleOption }; const LOGIN_DATA = { + cert: { name: 'app-client' }, token: { token: 'mysupersecuretoken' }, username: { username: 'matilda', password: 'some-password' }, role: { role: 'some-dev' }, @@ -94,6 +95,8 @@ export const AUTH_METHOD_LOGIN_DATA = { oidc: LOGIN_DATA.role, jwt: LOGIN_DATA.jwt, saml: LOGIN_DATA.role, + // cert is its own thing + cert: LOGIN_DATA.cert, }; // Mock response for `sys/internal/ui/mounts` diff --git a/ui/tests/helpers/auth/response-stubs.ts b/ui/tests/helpers/auth/response-stubs.ts index dd5c90a5e5..47a304a5d2 100644 --- a/ui/tests/helpers/auth/response-stubs.ts +++ b/ui/tests/helpers/auth/response-stubs.ts @@ -28,6 +28,31 @@ const BASE_REQUEST_DATA = { }; export const RESPONSE_STUBS = { + cert: { + ...BASE_REQUEST_DATA, + data: null, + auth: { + client_token: 'hvs.myvaultgeneratedgithubtoken', + accessor: 'bSdIXwG3bor6qPpsXYPXDesS', + policies: ['app-policy', 'default'], + token_policies: ['app-policy', 'default'], + metadata: { + authority_key_id: '57:4e:bd:2b:c5:64:57:18:85:fe:ba:65:74:fe:51:d8:07:b3:c8:7c', + cert_name: 'app-client', + common_name: '123-vault-auth-client', + serial_number: '203007388920717447074631125239238186446913014802', + subject_key_id: 'a8:09:b6:a2:2d:ac:a7:ad:67:6c:ff:b2:23:ef:eb:cd:c4:b7:68:eb', + }, + lease_duration: 2764800, + renewable: true, + entity_id: '9caa5721-97dc-e082-5b37-6f21ae6effbd', + token_type: 'service', + orphan: true, + mfa_requirement: null, + num_uses: 0, + }, + mount_type: '', + }, github: { ...BASE_REQUEST_DATA, data: null, @@ -317,9 +342,19 @@ export const RESPONSE_STUBS = { }, }; -// Once the auth service authentication method is simplified and no longer handles every auth type -// the "backend" key should be completely removable export const TOKEN_DATA = { + cert: { + authMethodType: 'cert', + authMountPath: 'cert', + displayName: `${RESPONSE_STUBS.cert.auth.metadata.cert_name}/${RESPONSE_STUBS.cert.auth.metadata.common_name}`, + entityId: RESPONSE_STUBS.cert.auth.entity_id, + policies: RESPONSE_STUBS.cert.auth.policies, + renewable: RESPONSE_STUBS.cert.auth.renewable, + token: RESPONSE_STUBS.cert.auth.client_token, + tokenExpirationEpoch: 1752352843223, + ttl: RESPONSE_STUBS.cert.auth.lease_duration, + userRootNamespace: '', + }, github: { authMethodType: 'github', authMountPath: 'github', diff --git a/ui/tests/integration/components/auth/form/base-test.js b/ui/tests/integration/components/auth/form/base-test.js index 3f14d0f150..33768d890d 100644 --- a/ui/tests/integration/components/auth/form/base-test.js +++ b/ui/tests/integration/components/auth/form/base-test.js @@ -32,6 +32,54 @@ module('Integration | Component | auth | form | base', function (hooks) { }; }); + module('cert', function (hooks) { + hooks.beforeEach(function () { + this.setup('cert', 'certLogin'); + this.assertSubmit = (assert, loginRequestArgs, loginData) => { + const [path, { name }] = loginRequestArgs; + // if path is included in loginData, a custom path was submitted + const expectedPath = loginData?.path || this.authType; + assert.strictEqual(path, expectedPath, 'it calls certLogin with expected path'); + assert.strictEqual(name, loginData.name, 'it calls certLogin with name'); + }; + this.renderComponent = ({ yieldBlock = false } = {}) => { + if (yieldBlock) { + return render(hbs` + + <:advancedSettings> + + + + `); + } + return render(hbs` + `); + }; + }); + + hooks.afterEach(function () { + this.authenticateStub.restore(); + }); + + authFormTestHelper(test); + + test('it renders custom label', async function (assert) { + await this.renderComponent(); + const id = find(GENERAL.inputByAttr('name')).id; + assert.dom(`#label-${id}`).hasText('Role name'); + }); + }); + module('github', function (hooks) { hooks.beforeEach(function () { this.setup('github', 'githubLogin'); diff --git a/ui/tests/integration/components/auth/page/method-authentication-test.js b/ui/tests/integration/components/auth/page/method-authentication-test.js index 5617bfa00a..c110c125d2 100644 --- a/ui/tests/integration/components/auth/page/method-authentication-test.js +++ b/ui/tests/integration/components/auth/page/method-authentication-test.js @@ -91,6 +91,21 @@ module('Integration | Component | auth | page | method authentication', function window.localStorage.clear(); }); + module('cert', function (hooks) { + hooks.beforeEach(async function () { + this.authType = 'cert'; + this.loginData = { name: 'app-client' }; + this.path = this.authType; + this.response = RESPONSE_STUBS.cert; + this.tokenName = 'vault-cert☃1'; + this.stubRequests = () => { + this.server.post(`/auth/${this.path}/login`, () => this.response); + }; + }); + + methodAuthenticationTests(test); + }); + module('github', function (hooks) { hooks.beforeEach(async function () { this.authType = 'github'; diff --git a/ui/types/vault/auth/methods.d.ts b/ui/types/vault/auth/methods.d.ts index 9ae3163692..6ef0c12a80 100644 --- a/ui/types/vault/auth/methods.d.ts +++ b/ui/types/vault/auth/methods.d.ts @@ -41,6 +41,12 @@ interface AuthResponseDataKey extends SharedAuthResponseData { } // METHOD SPECIFIC RESPONSES +export interface CertLoginApiResponse extends ApiResponse { + auth: AuthResponseAuthKey & { + metadata: { cert_name: string; common_name: string }; + }; +} + export interface GithubLoginApiResponse extends ApiResponse { auth: AuthResponseAuthKey & { metadata: { org: string; username: string }; From 2ef4c502215eb4bb0ea50a07f16c7ade860706a2 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 14:21:10 -0400 Subject: [PATCH 076/468] Add audit log entries for new token type (#12747) (#12908) * Add audit log entries for enterprise JWT token fields * Reduce enterprise token field comment detail - simplify enterprise token comments in sdk/logical/request.go - remove verbose wording about issuer/audience/authorization semantics * Fix TestAudit_JWT_DelegationToken permission denied error The test was failing with 'permission denied' when using a delegation token (JWT with act claim) to access cubbyhole. The root causes were: 1. RAR (Rich Authorization Request) check: The JWT contained 'authorization_details' constraints that only allowed access to 'secret/data/users/alice' and 'secret/data/config/general', but the test was attempting to access 'cubbyhole/test'. The RAR check in PerformRARCheck() was correctly denying this mismatch. 2. Missing entity policies for actor ACL: For delegation tokens, the actor's ACL is built solely from entity identity policies (not token policies like 'default'). Without explicit policies on the actor entity, the delegation ACL intersection check would fail. Fixes: - Removed 'authorization_details' from the test JWT since the test is about verifying audit log entries for delegation tokens, not RAR constraints - Added 'default' policy to both subject and actor entities to ensure both ACLs allow cubbyhole access for the delegation token intersection - Updated test assertions to match the simplified JWT (removed authorization_details verification) * Use require.NoError instead of t.Fatalf for error check * Add explicit checks for auth field presence before type assertion Adds separate checks to verify the 'auth' and 'metadata' fields exist in the map before attempting type assertion, preventing potential panics and improving test clarity. * test: tighten request metadata merge assertions * test: simplify enterprise metadata assertions * test: split enterprise metadata merge coverage * style: apply gofumpt to entry formatter tests * test: add godoc for enterprise token metadata test --------- Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- audit/entry_formatter.go | 63 ++++ audit/entry_formatter_test.go | 573 +++++++++++++++++++++++++++++++++- audit/hashstructure_test.go | 65 ++++ sdk/logical/auth.go | 6 + sdk/logical/auth_test.go | 55 ++++ sdk/logical/request.go | 21 +- vault/request_handling.go | 22 +- vault/request_handling_ce.go | 14 +- 8 files changed, 810 insertions(+), 9 deletions(-) create mode 100644 sdk/logical/auth_test.go diff --git a/audit/entry_formatter.go b/audit/entry_formatter.go index 90b33221c6..d89743c4e1 100644 --- a/audit/entry_formatter.go +++ b/audit/entry_formatter.go @@ -5,6 +5,7 @@ package audit import ( "context" + "encoding/json" "errors" "fmt" "maps" @@ -253,6 +254,46 @@ func clone[V any](s V) (V, error) { return s2.(V), err } +// mergeEnterpriseTokenMetadata injects enterprise token fields from a logical.Request +// into the audit auth's Metadata map. +func mergeEnterpriseTokenMetadata(a *auth, req *logical.Request) error { + if a == nil || req == nil { + return nil + } + + if req.EnterpriseTokenMetadata == "" && + req.EnterpriseTokenIssuer == "" && + len(req.EnterpriseTokenAudience) == 0 && + len(req.EnterpriseTokenAuthorizationDetails) == 0 { + return nil + } + + if a.Metadata == nil { + a.Metadata = make(map[string]string) + } + if req.EnterpriseTokenMetadata != "" { + a.Metadata["enterprise_token_metadata"] = req.EnterpriseTokenMetadata + } + if req.EnterpriseTokenIssuer != "" { + a.Metadata["enterprise_token_issuer"] = req.EnterpriseTokenIssuer + } + if len(req.EnterpriseTokenAudience) > 0 { + audJSON, err := json.Marshal(req.EnterpriseTokenAudience) + if err != nil { + return fmt.Errorf("unable to marshal enterprise token audience for audit: %w", err) + } + a.Metadata["enterprise_token_audience"] = string(audJSON) + } + if len(req.EnterpriseTokenAuthorizationDetails) > 0 { + authzJSON, err := json.Marshal(req.EnterpriseTokenAuthorizationDetails) + if err != nil { + return fmt.Errorf("unable to marshal enterprise token authorization details for audit: %w", err) + } + a.Metadata["enterprise_token_authorization_details"] = string(authzJSON) + } + return nil +} + // newAuth takes a logical.Auth and the number of remaining client token uses // (which should be supplied from the logical.Request's client token), and creates // an audit auth. @@ -281,6 +322,18 @@ func newAuth(input *logical.Auth, tokenRemainingUses int) (*auth, error) { return nil, fmt.Errorf("unable to clone logical auth: metadata: %w", err) } + if input.ActorEntityID != "" || input.ActorEntityName != "" { + if metadata == nil { + metadata = make(map[string]string) + } + if input.ActorEntityID != "" { + metadata["actor_entity_id"] = input.ActorEntityID + } + if input.ActorEntityName != "" { + metadata["actor_entity_name"] = input.ActorEntityName + } + } + policies, err := clone(input.Policies) if err != nil { return nil, fmt.Errorf("unable to clone logical auth: policies: %w", err) @@ -535,6 +588,10 @@ func (f *entryFormatter) createEntry(ctx context.Context, a *Event) (*entry, err return nil, fmt.Errorf("cannot convert auth: %w", err) } + if err := mergeEnterpriseTokenMetadata(auth, data.Request); err != nil { + return nil, err + } + req, err := newRequest(data.Request, ns) if err != nil { return nil, fmt.Errorf("cannot convert request: %w", err) @@ -548,6 +605,12 @@ func (f *entryFormatter) createEntry(ctx context.Context, a *Event) (*entry, err return nil, fmt.Errorf("cannot convert response: %w", err) } + if resp != nil && resp.Auth != nil { + if err := mergeEnterpriseTokenMetadata(resp.Auth, data.Request); err != nil { + return nil, err + } + } + // If the plugin's response contained any additional audit request fields, // lets populate them on our original request. if data.Response != nil && data.Response.SupplementalAuditRequestData != nil { diff --git a/audit/entry_formatter_test.go b/audit/entry_formatter_test.go index 73f63a67ed..ea68d10ff9 100644 --- a/audit/entry_formatter_test.go +++ b/audit/entry_formatter_test.go @@ -26,8 +26,7 @@ import ( "github.com/stretchr/testify/require" ) -const testFormatJSONReqBasicStrFmt = ` -{ +const testFormatJSONReqBasicStrFmt = `{ "time": "2015-08-05T13:45:46Z", "type": "request", "auth": { @@ -60,6 +59,43 @@ const testFormatJSONReqBasicStrFmt = ` } ` +const testFormatJSONEnterpriseTokenStrFmt = `{ + "time": "2015-08-05T13:45:46Z", + "type": "request", + "auth": { + "client_token": "%s", + "accessor": "bar", + "display_name": "testtoken", + "policies": [ + "root" + ], + "no_default_policy": true, + "metadata": { + "actor_entity_id": "actor-entity-789", + "actor_entity_name": "actor-service", + "enterprise_token_metadata": "test-token-123" + }, + "entity_id": "foobarentity", + "token_type": "service", + "token_ttl": 14400, + "token_issue_time": "2020-05-28T13:40:18-05:00" + }, + "request": { + "operation": "update", + "path": "/foo", + "data": null, + "wrap_ttl": 60, + "remote_address": "127.0.0.1", + "headers": { + "foo": [ + "bar" + ] + } + }, + "error": "this is an error" +} +` + // testHeaderFormatter is a stub to prevent the need to import the vault package // to bring in vault.HeadersConfig for testing. type testHeaderFormatter struct { @@ -495,6 +531,435 @@ func BenchmarkAuditFileSink_Process(b *testing.B) { }) } +// TestEntryFormatter_ActorAuth ensures that actor entity fields and EnterpriseToken* +// fields are correctly mapped into auth metadata from logical.Auth and logical.Request. +func TestEntryFormatter_ActorAuth(t *testing.T) { + t.Parallel() + + authTests := map[string]struct { + Input *logical.Auth + ExpectedActorEntityID string + ExpectedActorEntityName string + }{ + "actor-fields-present": { + Input: &logical.Auth{ + EntityID: "subject-123", + DisplayName: "subject-user", + ActorEntityID: "actor-456", + ActorEntityName: "actor-service", + TokenType: logical.TokenTypeDefault, + }, + ExpectedActorEntityID: "actor-456", + ExpectedActorEntityName: "actor-service", + }, + "actor-fields-absent": { + Input: &logical.Auth{ + EntityID: "subject-123", + DisplayName: "subject-user", + TokenType: logical.TokenTypeDefault, + }, + }, + } + + for name, tc := range authTests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + result, err := newAuth(tc.Input, 0) + require.NoError(t, err) + require.NotNil(t, result) + if tc.ExpectedActorEntityID != "" { + got, ok := result.Metadata["actor_entity_id"] + require.True(t, ok, "actor_entity_id should be present in auth metadata") + require.Equal(t, tc.ExpectedActorEntityID, got) + } else { + _, ok := result.Metadata["actor_entity_id"] + require.False(t, ok, "actor_entity_id should be absent in auth metadata") + } + if tc.ExpectedActorEntityName != "" { + got, ok := result.Metadata["actor_entity_name"] + require.True(t, ok, "actor_entity_name should be present in auth metadata") + require.Equal(t, tc.ExpectedActorEntityName, got) + } else { + _, ok := result.Metadata["actor_entity_name"] + require.False(t, ok, "actor_entity_name should be absent in auth metadata") + } + }) + } +} + +// TestMergeEnterpriseTokenMetadata verifies enterprise token claims are copied into auth metadata. +func TestMergeEnterpriseTokenMetadata(t *testing.T) { + t.Parallel() + + requestTests := map[string]struct { + Input *logical.Request + ExpectedMetadata string + ExpectedIssuer string + }{ + "metadata-present": { + Input: &logical.Request{ID: "req-1", EnterpriseTokenMetadata: "token-abc"}, + ExpectedMetadata: "token-abc", + }, + "metadata-absent": { + Input: &logical.Request{ID: "req-2"}, + ExpectedMetadata: "", + }, + "issuer-present": { + Input: &logical.Request{ + ID: "req-3", + EnterpriseTokenMetadata: "token-xyz", + EnterpriseTokenIssuer: "https://issuer.example.com", + }, + ExpectedMetadata: "token-xyz", + ExpectedIssuer: "https://issuer.example.com", + }, + } + + for name, tc := range requestTests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + a := &auth{} + err := mergeEnterpriseTokenMetadata(a, tc.Input) + require.NoError(t, err) + if tc.ExpectedMetadata == "" && tc.ExpectedIssuer == "" { + require.Nil(t, a.Metadata) + } + + assertMetadataField := func(key, want string) { + t.Helper() + got, ok := a.Metadata[key] + if want == "" { + require.False(t, ok, "%s should be absent in auth metadata", key) + return + } + require.True(t, ok, "%s should be present in auth metadata", key) + require.Equal(t, want, got) + } + + assertMetadataField("enterprise_token_metadata", tc.ExpectedMetadata) + assertMetadataField("enterprise_token_issuer", tc.ExpectedIssuer) + }) + } +} + +// TestEntryFormatter_Process_JSON_EnterpriseToken verifies that enterprise token fields +// (actor_entity_id, actor_entity_name, enterprise_token_metadata, enterprise_token_issuer, +// enterprise_token_audience, enterprise_token_authorization_details) are correctly +// serialized into auth.metadata in the JSON audit output, and absent when not set. +func TestEntryFormatter_Process_JSON_EnterpriseToken(t *testing.T) { + t.Parallel() + + staticSalt := newStaticSalt(t) + + authzDetails := []logical.AuthorizationDetail{ + {"type": "payment_initiation", "currency": "USD"}, + } + + cases := map[string]struct { + Auth *logical.Auth + Req *logical.Request + WantActorEntityID string + WantActorEntityName string + WantMetadata string + WantIssuer string + WantAudience string + WantAuthorizationDetails string + }{ + "enterprise-token-with-actor-and-authorization-details": { + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + EntityID: "subject-entity-123", + ActorEntityID: "actor-entity-456", + ActorEntityName: "actor-service", + Policies: []string{"default"}, + TokenType: logical.TokenTypeDefault, + }, + Req: &logical.Request{ + Operation: logical.ReadOperation, + Path: "/cubbyhole/test", + EnterpriseTokenMetadata: "test-token-abc", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault"}, + EnterpriseTokenAuthorizationDetails: authzDetails, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + }, + WantActorEntityID: "actor-entity-456", + WantActorEntityName: "actor-service", + WantMetadata: "test-token-abc", + WantIssuer: "https://issuer.example.com", + WantAudience: `["vault"]`, + WantAuthorizationDetails: `[{"currency":"USD","type":"payment_initiation"}]`, + }, + "enterprise-token-base-fields-only": { + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + EntityID: "subject-entity-123", + Policies: []string{"default"}, + TokenType: logical.TokenTypeDefault, + }, + Req: &logical.Request{ + Operation: logical.ReadOperation, + Path: "/cubbyhole/test", + EnterpriseTokenMetadata: "test-token-xyz", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault"}, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + }, + WantMetadata: "test-token-xyz", + WantIssuer: "https://issuer.example.com", + WantAudience: `["vault"]`, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + cfg, err := newFormatterConfig(&testHeaderFormatter{}, map[string]string{ + "hmac_accessor": "false", + }) + require.NoError(t, err) + formatter, err := newEntryFormatter("test", cfg, staticSalt, hclog.NewNullLogger()) + require.NoError(t, err) + + in := &logical.LogInput{ + Auth: tc.Auth, + Request: tc.Req, + } + + auditEvent, err := newEvent(RequestType) + require.NoError(t, err) + auditEvent.Data = in + + e := &eventlogger.Event{ + Type: event.AuditType.AsEventType(), + CreatedAt: time.Now(), + Formatted: make(map[string][]byte), + Payload: auditEvent, + } + + e2, err := formatter.Process(nshelper.RootContext(nil), e) + require.NoError(t, err) + + jsonBytes, ok := e2.Format(jsonFormat.String()) + require.True(t, ok) + require.Positive(t, len(jsonBytes)) + + var result entry + require.NoError(t, json.Unmarshal(jsonBytes, &result)) + + require.NotNil(t, result.Auth) + require.Equal(t, tc.WantActorEntityID, result.Auth.Metadata["actor_entity_id"]) + require.Equal(t, tc.WantActorEntityName, result.Auth.Metadata["actor_entity_name"]) + + require.NotNil(t, result.Request) + require.Equal(t, tc.WantMetadata, result.Auth.Metadata["enterprise_token_metadata"]) + require.Equal(t, tc.WantIssuer, result.Auth.Metadata["enterprise_token_issuer"]) + require.Equal(t, tc.WantAudience, result.Auth.Metadata["enterprise_token_audience"]) + require.Equal(t, tc.WantAuthorizationDetails, result.Auth.Metadata["enterprise_token_authorization_details"]) + }) + } +} + +// TestEntryFormatter_Process_Response_EnterpriseToken verifies that enterprise token +// fields are injected into both the top-level auth.metadata AND the response auth.metadata. +func TestEntryFormatter_Process_Response_EnterpriseToken(t *testing.T) { + t.Parallel() + + staticSalt := newStaticSalt(t) + + cfg, err := newFormatterConfig(&testHeaderFormatter{}, map[string]string{ + "hmac_accessor": "false", + }) + require.NoError(t, err) + formatter, err := newEntryFormatter("test", cfg, staticSalt, hclog.NewNullLogger()) + require.NoError(t, err) + + in := &logical.LogInput{ + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + EntityID: "subject-entity-123", + ActorEntityID: "actor-entity-456", + ActorEntityName: "actor-service", + Policies: []string{"default"}, + TokenType: logical.TokenTypeDefault, + }, + Request: &logical.Request{ + Operation: logical.ReadOperation, + Path: "/secret/data/test", + EnterpriseTokenMetadata: "resp-token-abc", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault", "api"}, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + }, + Response: &logical.Response{ + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + EntityID: "subject-entity-123", + Policies: []string{"default"}, + TokenType: logical.TokenTypeDefault, + }, + Data: map[string]interface{}{ + "value": "secret", + }, + }, + } + + auditEvent, err := newEvent(ResponseType) + require.NoError(t, err) + auditEvent.Data = in + + e := &eventlogger.Event{ + Type: event.AuditType.AsEventType(), + CreatedAt: time.Now(), + Formatted: make(map[string][]byte), + Payload: auditEvent, + } + + e2, err := formatter.Process(nshelper.RootContext(nil), e) + require.NoError(t, err) + + jsonBytes, ok := e2.Format(jsonFormat.String()) + require.True(t, ok) + + var result entry + require.NoError(t, json.Unmarshal(jsonBytes, &result)) + + // Top-level auth must have enterprise token fields in metadata + require.NotNil(t, result.Auth) + require.Equal(t, "actor-entity-456", result.Auth.Metadata["actor_entity_id"]) + require.Equal(t, "actor-service", result.Auth.Metadata["actor_entity_name"]) + require.Equal(t, "resp-token-abc", result.Auth.Metadata["enterprise_token_metadata"]) + require.Equal(t, "https://issuer.example.com", result.Auth.Metadata["enterprise_token_issuer"]) + require.Equal(t, `["vault","api"]`, result.Auth.Metadata["enterprise_token_audience"]) + + // Response auth must also have enterprise token fields in metadata + require.NotNil(t, result.Response) + require.NotNil(t, result.Response.Auth) + require.Equal(t, "resp-token-abc", result.Response.Auth.Metadata["enterprise_token_metadata"]) + require.Equal(t, "https://issuer.example.com", result.Response.Auth.Metadata["enterprise_token_issuer"]) + require.Equal(t, `["vault","api"]`, result.Response.Auth.Metadata["enterprise_token_audience"]) +} + +// TestEntryFormatter_EnterpriseTokenFieldsNotOnRequestOrAuthTopLevel verifies that +// enterprise token fields do NOT appear as top-level JSON keys on the request or auth +// structs — they must only be in auth.metadata. +func TestEntryFormatter_EnterpriseTokenFieldsNotOnRequestOrAuthTopLevel(t *testing.T) { + t.Parallel() + + staticSalt := newStaticSalt(t) + + cfg, err := newFormatterConfig(&testHeaderFormatter{}, map[string]string{ + "hmac_accessor": "false", + }) + require.NoError(t, err) + formatter, err := newEntryFormatter("test", cfg, staticSalt, hclog.NewNullLogger()) + require.NoError(t, err) + + in := &logical.LogInput{ + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + EntityID: "foobarentity", + ActorEntityID: "actor-entity-789", + ActorEntityName: "actor-service", + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + }, + Request: &logical.Request{ + Operation: logical.ReadOperation, + Path: "/secret/data/test", + EnterpriseTokenMetadata: "test-token-123", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault"}, + EnterpriseTokenAuthorizationDetails: []logical.AuthorizationDetail{{"type": "access"}}, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + }, + } + + auditEvent, err := newEvent(RequestType) + require.NoError(t, err) + auditEvent.Data = in + + e := &eventlogger.Event{ + Type: event.AuditType.AsEventType(), + CreatedAt: time.Now(), + Formatted: make(map[string][]byte), + Payload: auditEvent, + } + + e2, err := formatter.Process(nshelper.RootContext(nil), e) + require.NoError(t, err) + + jsonBytes, ok := e2.Format(jsonFormat.String()) + require.True(t, ok) + + var raw map[string]json.RawMessage + require.NoError(t, json.Unmarshal(jsonBytes, &raw)) + + // Auth top-level must NOT have actor_entity_id or actor_entity_name + var authMap map[string]json.RawMessage + require.NoError(t, json.Unmarshal(raw["auth"], &authMap)) + _, hasActorEntityID := authMap["actor_entity_id"] + _, hasActorEntityName := authMap["actor_entity_name"] + require.False(t, hasActorEntityID, "actor_entity_id must not be a top-level auth field") + require.False(t, hasActorEntityName, "actor_entity_name must not be a top-level auth field") + + // Request top-level must NOT have any enterprise_token_* fields + var reqMap map[string]json.RawMessage + require.NoError(t, json.Unmarshal(raw["request"], &reqMap)) + for key := range reqMap { + require.False(t, strings.HasPrefix(key, "enterprise_token_"), + "request must not have top-level enterprise_token_ field: %s", key) + } + + // But auth.metadata MUST have them + var metadataMap map[string]string + require.NoError(t, json.Unmarshal(authMap["metadata"], &metadataMap)) + entityID, ok := metadataMap["actor_entity_id"] + require.True(t, ok) + require.Equal(t, "actor-entity-789", entityID) + + entityName, ok := metadataMap["actor_entity_name"] + require.True(t, ok) + require.Equal(t, "actor-service", entityName) + + tokenMetadata, ok := metadataMap["enterprise_token_metadata"] + require.True(t, ok) + require.Equal(t, "test-token-123", tokenMetadata) + + tokenIssuer, ok := metadataMap["enterprise_token_issuer"] + require.True(t, ok) + require.Equal(t, "https://issuer.example.com", tokenIssuer) + + tokenAudience, ok := metadataMap["enterprise_token_audience"] + require.True(t, ok) + require.Equal(t, `["vault"]`, tokenAudience) + + tokenAuthzDetails, ok := metadataMap["enterprise_token_authorization_details"] + require.True(t, ok) + require.Contains(t, tokenAuthzDetails, `"type":"access"`) +} + // TestEntryFormatter_Process_Request exercises entryFormatter process an event // with varying inputs. func TestEntryFormatter_Process_Request(t *testing.T) { @@ -788,6 +1253,40 @@ func TestEntryFormatter_Process_JSON(t *testing.T) { "@cee: ", expectedResultStr, }, + "auth, request with enterprise token": { + &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + EntityID: "foobarentity", + ActorEntityID: "actor-entity-789", + ActorEntityName: "actor-service", + NoDefaultPolicy: true, + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + LeaseOptions: logical.LeaseOptions{ + TTL: time.Hour * 4, + IssueTime: issueTime, + }, + }, + &logical.Request{ + Operation: logical.UpdateOperation, + Path: "/foo", + EnterpriseTokenMetadata: "test-token-123", + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + WrapInfo: &logical.RequestWrapInfo{ + TTL: 60 * time.Second, + }, + Headers: map[string][]string{ + "foo": {"bar"}, + }, + }, + errors.New("this is an error"), + "", + fmt.Sprintf(testFormatJSONEnterpriseTokenStrFmt, ss.salt.GetIdentifiedHMAC("foo")), + }, } for name, tc := range cases { @@ -831,7 +1330,7 @@ func TestEntryFormatter_Process_JSON(t *testing.T) { expectedJSON := new(entry) - if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil { + if err := jsonutil.DecodeJSON([]byte(tc.ExpectedStr), &expectedJSON); err != nil { t.Fatalf("bad json: %s", err) } expectedJSON.Request.Namespace = &namespace{ID: "root"} @@ -1190,7 +1689,73 @@ func TestEntryFormatter_Process_NoMutation(t *testing.T) { require.NotEqual(t, e2, e) } -// TestEntryFormatter_Process_Panic tries to send data into the entryFormatter +// TestEntryFormatter_Process_NoMutation_WithEnterpriseToken verifies that +// formatting an event carrying enterprise token fields does not mutate the +// original logical.Request. The EnterpriseToken* fields must be identical on +// the input after Process returns. +func TestEntryFormatter_Process_NoMutation_WithEnterpriseToken(t *testing.T) { + t.Parallel() + + cfg, err := newFormatterConfig(&testHeaderFormatter{}, nil) + require.NoError(t, err) + staticSalt := newStaticSalt(t) + formatter, err := newEntryFormatter("no-mutation-ent-token", cfg, staticSalt, hclog.NewNullLogger()) + require.NoError(t, err) + require.NotNil(t, formatter) + + authzDetails := []logical.AuthorizationDetail{ + { + "type": "vault:path_access", + "path_constraint": "secret/data/users/alice", + "action": "read", + }, + } + + in := &logical.LogInput{ + Auth: &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + EntityID: "subject-entity-123", + ActorEntityID: "actor-entity-456", + ActorEntityName: "actor-service", + DisplayName: "testtoken", + Policies: []string{"default"}, + TokenType: logical.TokenTypeService, + }, + Request: &logical.Request{ + Operation: logical.ReadOperation, + Path: "/cubbyhole/test", + EnterpriseTokenMetadata: "test-token-abc", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault", "api"}, + EnterpriseTokenAuthorizationDetails: authzDetails, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + }, + } + + // Snapshot the enterprise token field values before processing. + wantMetadata := in.Request.EnterpriseTokenMetadata + wantIssuer := in.Request.EnterpriseTokenIssuer + wantAudience := append([]string(nil), in.Request.EnterpriseTokenAudience...) + + e := fakeEvent(t, RequestType, in) + + e2, err := formatter.Process(nshelper.RootContext(nil), e) + require.NoError(t, err) + require.NotNil(t, e2) + + // The event pointer must differ — no in-place mutation. + require.NotEqual(t, e2, e) + + // The original request's enterprise token fields must be unchanged. + require.Equal(t, wantMetadata, in.Request.EnterpriseTokenMetadata) + require.Equal(t, wantIssuer, in.Request.EnterpriseTokenIssuer) + require.Equal(t, wantAudience, in.Request.EnterpriseTokenAudience) + require.Equal(t, authzDetails, in.Request.EnterpriseTokenAuthorizationDetails) +} + // which will currently cause a panic when a response is formatted due to the // underlying hashing that is done with reflectwalk. func TestEntryFormatter_Process_Panic(t *testing.T) { diff --git a/audit/hashstructure_test.go b/audit/hashstructure_test.go index 5901599253..092e453ee1 100644 --- a/audit/hashstructure_test.go +++ b/audit/hashstructure_test.go @@ -432,3 +432,68 @@ func TestHashWalker_TimeStructs(t *testing.T) { } } } + +// TestCopy_request_EnterpriseTokenFields verifies that copystructure.Copy +// correctly deep-copies a logical.Request that carries enterprise token fields, +// including EnterpriseTokenAuthorizationDetails which is []map[string]any and +// would silently lose data under a shallow copy. +func TestCopy_request_EnterpriseTokenFields(t *testing.T) { + expected := logical.Request{ + Data: map[string]interface{}{ + "foo": "bar", + }, + EnterpriseTokenMetadata: "test-token-abc", + EnterpriseTokenIssuer: "https://issuer.example.com", + EnterpriseTokenAudience: []string{"vault", "api"}, + EnterpriseTokenAuthorizationDetails: []logical.AuthorizationDetail{ + { + "type": "vault:path_access", + "path_constraint": "secret/data/users/alice", + "action": "read", + }, + { + "type": "vault:path_access", + "path_constraint": "secret/data/config/general", + "action": "update", + }, + }, + } + arg := expected + + dup, err := copystructure.Copy(&arg) + require.NoError(t, err) + + arg2 := dup.(*logical.Request) + require.EqualValues(t, expected, *arg2) +} + +// TestHashRequest_EnterpriseTokenFieldsInMetadata verifies that enterprise token +// fields stored in auth.Metadata are not HMAC'd by hashAuth. These values are +// not secrets and must appear as cleartext in the audit log. +func TestHashRequest_EnterpriseTokenFieldsInMetadata(t *testing.T) { + // Enterprise token fields are now stored in auth.Metadata by createEntry(). + // Verify that hashAuth does not HMAC metadata values. + auditAuth := &auth{ + ClientToken: "secret-token", + Metadata: map[string]string{ + "enterprise_token_metadata": "test-token-xyz", + "enterprise_token_issuer": "https://issuer.example.com", + "actor_entity_id": "actor-123", + "actor_entity_name": "actor-service", + }, + } + + salter := &testSalter{} + err := hashAuth(context.Background(), salter, auditAuth, false) + require.NoError(t, err) + + // ClientToken must be HMAC'd — it is a secret. + require.NotEqual(t, "secret-token", auditAuth.ClientToken) + require.Contains(t, auditAuth.ClientToken, "hmac-sha256:") + + // Metadata values must pass through unchanged — they are not secrets. + require.Equal(t, "test-token-xyz", auditAuth.Metadata["enterprise_token_metadata"]) + require.Equal(t, "https://issuer.example.com", auditAuth.Metadata["enterprise_token_issuer"]) + require.Equal(t, "actor-123", auditAuth.Metadata["actor_entity_id"]) + require.Equal(t, "actor-service", auditAuth.Metadata["actor_entity_name"]) +} diff --git a/sdk/logical/auth.go b/sdk/logical/auth.go index 86a225b575..9ab8fac2d4 100644 --- a/sdk/logical/auth.go +++ b/sdk/logical/auth.go @@ -90,6 +90,12 @@ type Auth struct { // matching groups, the entity ID of the user will be added. GroupAliases []*Alias `json:"group_aliases" mapstructure:"group_aliases" structs:"group_aliases"` + // ActorEntityID is the entity ID of the actor performing the request. + ActorEntityID string `json:"actor_entity_id,omitempty" mapstructure:"actor_entity_id" structs:"actor_entity_id"` + + // ActorEntityName is the name of the actor entity performing the request. + ActorEntityName string `json:"actor_entity_name,omitempty" mapstructure:"actor_entity_name" structs:"actor_entity_name"` + // The set of CIDRs that this token can be used with BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` diff --git a/sdk/logical/auth_test.go b/sdk/logical/auth_test.go new file mode 100644 index 0000000000..9f48c459d0 --- /dev/null +++ b/sdk/logical/auth_test.go @@ -0,0 +1,55 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: MPL-2.0 + +package logical + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestAuth_ActorEntityFields verifies that ActorEntityID and ActorEntityName +// round-trip correctly through JSON marshalling and unmarshalling, and that +// both fields appear with their expected JSON keys when set. +func TestAuth_ActorEntityFields(t *testing.T) { + t.Parallel() + + auth := &Auth{ + EntityID: "subject-entity-123", + DisplayName: "subject-user", + ActorEntityID: "actor-entity-456", + ActorEntityName: "actor-service", + } + + data, err := json.Marshal(auth) + require.NoError(t, err) + require.Contains(t, string(data), `"actor_entity_id":"actor-entity-456"`) + require.Contains(t, string(data), `"actor_entity_name":"actor-service"`) + + var auth2 Auth + err = json.Unmarshal(data, &auth2) + require.NoError(t, err) + require.Equal(t, "actor-entity-456", auth2.ActorEntityID) + require.Equal(t, "actor-service", auth2.ActorEntityName) +} + +// TestAuth_ActorEntityFields_OmitEmpty verifies that ActorEntityID and +// ActorEntityName are omitted from the JSON output when not set, preventing +// empty actor fields from appearing in audit entries for requests without +// an actor entity. +func TestAuth_ActorEntityFields_OmitEmpty(t *testing.T) { + t.Parallel() + + auth := &Auth{ + EntityID: "subject-entity-123", + DisplayName: "subject-user", + // ActorEntityID and ActorEntityName intentionally not set + } + + data, err := json.Marshal(auth) + require.NoError(t, err) + require.NotContains(t, string(data), "actor_entity_id") + require.NotContains(t, string(data), "actor_entity_name") +} diff --git a/sdk/logical/request.go b/sdk/logical/request.go index 596d97c5db..8184764f0f 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -86,6 +86,10 @@ func IndexStateFromContext(ctx context.Context) *WALState { return s } +// AuthorizationDetail stores one enterprise token authorization detail object. +// The "type" field is required. +type AuthorizationDetail map[string]any + // Request is a struct that stores the parameters and context of a request // being made to Vault. It is used to abstract the details of the higher level // request protocol from the handlers. @@ -137,9 +141,18 @@ type Request struct { // hashed. ClientToken string `json:"client_token" structs:"client_token" mapstructure:"client_token" sentinel:""` - // EnterpriseTokenMetadata is used to store metadata related to enterprise token based requests. + // EnterpriseTokenMetadata stores enterprise token metadata. EnterpriseTokenMetadata string `json:"enterprise_token_metadata" structs:"enterprise_token_metadata" mapstructure:"enterprise_token_metadata" sentinel:""` + // EnterpriseTokenIssuer stores the enterprise token issuer. + EnterpriseTokenIssuer string `json:"enterprise_token_issuer,omitempty" structs:"enterprise_token_issuer" mapstructure:"enterprise_token_issuer"` + + // EnterpriseTokenAudience stores enterprise token audience values. + EnterpriseTokenAudience []string `json:"enterprise_token_audience,omitempty" structs:"enterprise_token_audience" mapstructure:"enterprise_token_audience"` + + // EnterpriseTokenAuthorizationDetails stores enterprise token authorization details. + EnterpriseTokenAuthorizationDetails []AuthorizationDetail `json:"enterprise_token_authorization_details,omitempty" structs:"enterprise_token_authorization_details" mapstructure:"enterprise_token_authorization_details"` + // ClientTokenAccessor is provided to the core so that the it can get // logged as part of request audit logging. ClientTokenAccessor string `json:"client_token_accessor" structs:"client_token_accessor" mapstructure:"client_token_accessor" sentinel:""` @@ -190,6 +203,12 @@ type Request struct { // to make this request EntityID string `json:"entity_id" structs:"entity_id" mapstructure:"entity_id" sentinel:""` + // ActorEntityID is the entity ID of the actor in a request. + ActorEntityID string `json:"actor_entity_id" structs:"actor_entity_id" mapstructure:"actor_entity_id" sentinel:""` + + // ActorEntityName is the name of the actor entity in a request. + ActorEntityName string `json:"actor_entity_name" structs:"actor_entity_name" mapstructure:"actor_entity_name" sentinel:""` + // PolicyOverride indicates that the requestor wishes to override // soft-mandatory Sentinel policies PolicyOverride bool `json:"policy_override" structs:"policy_override" mapstructure:"policy_override"` diff --git a/vault/request_handling.go b/vault/request_handling.go index fe307f33e0..e318c7044c 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -242,7 +242,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req var secondEntity *identity.Entity if IsEnterpriseToken(req.ClientToken) { - isValidEnterpriseToken, tokenMetadataContainer, entity, entity2, err := c.validateEnterpriseTokenAndFetchEntity(ctx, req.ClientToken) + isValidEnterpriseToken, tokenMetadataContainer, entity, actorEntity, err := c.validateEnterpriseTokenAndFetchEntity(ctx, req.ClientToken) if err != nil { c.logger.Error("failed to validate enterprise token", "error", err) } @@ -250,8 +250,11 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, logical.ErrPermissionDenied } req.EnterpriseTokenMetadata = getEnterpriseTokenMetadata(tokenMetadataContainer) - secondEntity = entity2 - err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity) + req.EnterpriseTokenIssuer = getEnterpriseTokenIssuer(tokenMetadataContainer) + req.EnterpriseTokenAudience = getEnterpriseTokenAudience(tokenMetadataContainer) + req.EnterpriseTokenAuthorizationDetails = getEnterpriseTokenAuthorizationDetails(tokenMetadataContainer) + secondEntity = actorEntity + err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity, actorEntity) if err != nil { return nil, nil, nil, nil, multierror.Append(err, errors.New("failed in processing enterprise token")) } @@ -282,6 +285,14 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, multierror.Append(logical.ErrPermissionDenied, logical.ErrInvalidToken) } + if secondEntity != nil { + if req.Auth == nil { + req.Auth = &logical.Auth{} + } + req.Auth.ActorEntityID = secondEntity.ID + req.Auth.ActorEntityName = secondEntity.Name + } + // CIDR checks bind all tokens except non-expiring root tokens if te.TTL != 0 && len(te.BoundCIDRs) > 0 { var valid bool @@ -572,6 +583,11 @@ func (c *Core) CheckToken(ctx context.Context, req *logical.Request, unauth bool req.ClientID = clientID } + if req.Auth != nil { + auth.ActorEntityID = req.Auth.ActorEntityID + auth.ActorEntityName = req.Auth.ActorEntityName + } + twoStepRecover := req.Operation == logical.RecoverOperation && req.RecoverSourcePath != "" && req.RecoverSourcePath != req.Path var alternateRecoverCapability *logical.Operation if twoStepRecover { diff --git a/vault/request_handling_ce.go b/vault/request_handling_ce.go index cbf26bae73..64e0953aea 100644 --- a/vault/request_handling_ce.go +++ b/vault/request_handling_ce.go @@ -17,7 +17,7 @@ func (c *Core) validateEnterpriseTokenAndFetchEntity(ctx context.Context, tokenS return false, nil, nil, nil, errors.New("not implemented") } -func (c *Core) createAndStoreEnterpriseTokenEntry(ctx context.Context, req *logical.Request, allClaims map[string]interface{}, entity *identity.Entity) error { +func (c *Core) createAndStoreEnterpriseTokenEntry(ctx context.Context, req *logical.Request, allClaims map[string]interface{}, entity *identity.Entity, actorEntity *identity.Entity) error { return nil } @@ -25,6 +25,18 @@ func getEnterpriseTokenMetadata(_ map[string]interface{}) string { return "" } +func getEnterpriseTokenIssuer(_ map[string]interface{}) string { + return "" +} + +func getEnterpriseTokenAudience(_ map[string]interface{}) []string { + return nil +} + +func getEnterpriseTokenAuthorizationDetails(_ map[string]interface{}) []logical.AuthorizationDetail { + return nil +} + func (c *Core) performSecondaryEntityTokenChecks(_ context.Context, _ *ACL, _ *identity.Entity, _ map[string][]string) (*ACL, error) { return nil, errors.New("not implemented") } From 99fec827712818f5a26df97077cf9ce14bbdc7a4 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 15:28:36 -0400 Subject: [PATCH 077/468] Backport Add an authenticated mode to rekey endpoints into ce/main (#12925) --- changelog/_12712.txt | 3 + command/server.go | 4 + command/server/config.go | 9 + command/server/config_test_helpers.go | 1 + http/handler.go | 48 +- http/sys_config_state_test.go | 1 + http/sys_rekey.go | 279 +-------- http/sys_rekey_test.go | 307 +++++++--- sdk/helper/docker/testhelpers.go | 6 +- sdk/helper/testcluster/docker/environment.go | 25 + sdk/helper/testcluster/types.go | 1 + sdk/logical/response.go | 6 + sdk/logical/response_util.go | 37 +- vault/core.go | 47 ++ .../external_tests/api/sys_rekey_ext_test.go | 1 + .../sys_rekey_config_reload_test.go | 187 ++++++ vault/logical_system.go | 554 ++++++++++++++++-- vault/logical_system_paths.go | 276 ++++++++- vault/plugin_reload.go | 6 +- vault/rekey.go | 98 ++-- vault/rekey_test.go | 80 +-- vault/router.go | 16 +- vault/router_test.go | 4 +- vault/testing.go | 36 +- 24 files changed, 1520 insertions(+), 512 deletions(-) create mode 100644 changelog/_12712.txt create mode 100644 vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go diff --git a/changelog/_12712.txt b/changelog/_12712.txt new file mode 100644 index 0000000000..d53727d36d --- /dev/null +++ b/changelog/_12712.txt @@ -0,0 +1,3 @@ +```release-note:change +core: sys/rekey endpoints are now authenticated by default, with the old unauthenticated behaviour enabled by setting the new HCL config key enable_unauthenticated_access to include the value "rekey". +``` diff --git a/command/server.go b/command/server.go index 031fe98937..e6e4db308a 100644 --- a/command/server.go +++ b/command/server.go @@ -2350,6 +2350,9 @@ func (c *ServerCommand) Reload(lock *sync.RWMutex, reloadFuncs *map[string][]rel // Set Introspection Endpoint to enabled with new value in the config after reload core.ReloadIntrospectionEndpointEnabled() + // Reload unauthenticated endpoints override configuration + core.ReloadEnableUnauthenticatedAccess() + // Send a message that we reloaded. This prevents "guessing" sleep times // in tests. select { @@ -3002,6 +3005,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical. AdministrativeNamespacePath: config.AdministrativeNamespacePath, ObservationSystemConfig: config.Observations, ReportingScanDirectory: config.ReportingScanDirectory, + EnableUnauthenticatedAccess: config.EnableUnauthenticatedAccess, } if c.flagDev { diff --git a/command/server/config.go b/command/server/config.go index f960f404ca..9888da47c5 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -56,6 +56,8 @@ type Config struct { Experiments []string `hcl:"experiments"` + EnableUnauthenticatedAccess []string `hcl:"enable_unauthenticated_access"` + CacheSize int `hcl:"cache_size"` DisableCache bool `hcl:"-"` DisableCacheRaw interface{} `hcl:"disable_cache"` @@ -520,6 +522,11 @@ func (c *Config) Merge(c2 *Config) *Config { result.Experiments = mergeExperiments(c.Experiments, c2.Experiments) + result.EnableUnauthenticatedAccess = c.EnableUnauthenticatedAccess + if len(c2.EnableUnauthenticatedAccess) > 0 { + result.EnableUnauthenticatedAccess = c2.EnableUnauthenticatedAccess + } + return result } @@ -1415,6 +1422,8 @@ func (c *Config) Sanitized() map[string]interface{} { "log_requests_level": c.LogRequestsLevel, "experiments": c.Experiments, + "enable_unauthenticated_access": c.EnableUnauthenticatedAccess, + "detect_deadlocks": c.DetectDeadlocks, "imprecise_lease_role_tracking": c.ImpreciseLeaseRoleTracking, diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index 3974232bd9..a1ac8f1ab9 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -943,6 +943,7 @@ func testConfig_Sanitized(t *testing.T) { "post_unseal_trace_directory": "/tmp", "remove_irrevocable_lease_after": (30 * 24 * time.Hour) / time.Second, "allow_audit_log_prefixing": false, + "enable_unauthenticated_access": []string(nil), } addExpectedEntSanitizedConfig(expected, []string{"http"}) diff --git a/http/handler.go b/http/handler.go index 59230666c3..aefa8e7957 100644 --- a/http/handler.go +++ b/http/handler.go @@ -236,6 +236,19 @@ var _ vault.HandlerHandler = HandlerFunc(func(props *vault.HandlerProperties) ht // handler returns an http.Handler for the API. This can be used on // its own to mount the Vault API within another web server. func handler(props *vault.HandlerProperties) http.Handler { + handlerUnauth := handlerWithUnauthRekey(props, true) + handlerAuth := handlerWithUnauthRekey(props, false) + + return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + if props.Core.GetEnableUnauthRekey() { + handlerUnauth.ServeHTTP(writer, req) + } else { + handlerAuth.ServeHTTP(writer, req) + } + }) +} + +func handlerWithUnauthRekey(props *vault.HandlerProperties, unauthRekey bool) http.Handler { core := props.Core // Create the muxer to handle the actual endpoints @@ -275,12 +288,18 @@ func handler(props *vault.HandlerProperties) http.Handler { handleAuditNonLogical(core, handleSysGenerateRootAttempt(core, vault.GenerateStandardRootTokenStrategy)))) mux.Handle("/v1/sys/generate-root/update", handleRequestForwarding(core, handleAuditNonLogical(core, handleSysGenerateRootUpdate(core, vault.GenerateStandardRootTokenStrategy)))) - mux.Handle("/v1/sys/rekey/init", handleRequestForwarding(core, handleSysRekeyInit(core, false))) - mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false))) - mux.Handle("/v1/sys/rekey/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, false))) - mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true))) - mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true))) - mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true))) + + // Register rekey endpoints as unauthenticated handlers only if unauthRekey is true. + // When false (the default), these endpoints will be handled by the sys backend as authenticated endpoints. + if unauthRekey { + mux.Handle("/v1/sys/rekey/init", handleRequestForwarding(core, handleSysRekeyInit(core, false))) + mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false))) + mux.Handle("/v1/sys/rekey/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, false))) + mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true))) + mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true))) + mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true))) + } + mux.Handle("/v1/sys/storage/raft/bootstrap", handleSysRaftBootstrap(core)) mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core)) mux.Handle("/v1/sys/internal/ui/feature-flags", handleSysInternalFeatureFlags(core)) @@ -1482,6 +1501,23 @@ func respondErrorCommon(w http.ResponseWriter, req *logical.Request, resp *logic respondErrorAndData(w, statusCode, data, newErr) return true } + if body := resp.Data[logical.HTTPRawBodyError]; body != nil { + if code := resp.Data[logical.HTTPStatusCode]; code != nil { + if i, ok := code.(int); ok { + // Defensively ignore non-int status codes + statusCode = i + } + } + switch v := body.(type) { + case string: + logical.RespondWithBody(w, statusCode, v) + case []byte: + logical.RespondWithBody(w, statusCode, string(v)) + default: + respondError(w, http.StatusInternalServerError, fmt.Errorf("unable to decode body: %w", newErr)) + } + return true + } } respondError(w, statusCode, newErr) return true diff --git a/http/sys_config_state_test.go b/http/sys_config_state_test.go index a837911851..6921e626b8 100644 --- a/http/sys_config_state_test.go +++ b/http/sys_config_state_test.go @@ -183,6 +183,7 @@ func TestSysConfigState_Sanitized(t *testing.T) { "post_unseal_trace_directory": "", "remove_irrevocable_lease_after": json.Number("0"), "allow_audit_log_prefixing": false, + "enable_unauthenticated_access": nil, } if tc.expectedHAStorageOutput != nil { diff --git a/http/sys_rekey.go b/http/sys_rekey.go index a9d9618fa5..a9a526604e 100644 --- a/http/sys_rekey.go +++ b/http/sys_rekey.go @@ -5,14 +5,10 @@ package http import ( "context" - "encoding/base64" - "encoding/hex" - "errors" "fmt" "net/http" "time" - "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/vault" ) @@ -51,94 +47,25 @@ func handleSysRekeyInit(core *vault.Core, recovery bool) http.Handler { } func handleSysRekeyInitGet(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { - barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx) - if barrierConfErr != nil { - respondError(w, http.StatusInternalServerError, barrierConfErr) - return - } - if barrierConfig == nil { - respondError(w, http.StatusBadRequest, fmt.Errorf("server is not yet initialized")) - return - } - - // Get the rekey configuration - rekeyConf, err := core.RekeyConfig(recovery) + status, code, err := vault.HandleSysRekeyInitGet(ctx, core, recovery, true) if err != nil { - respondError(w, err.Code(), err) + respondError(w, code, err) return } - sealThreshold, err := core.RekeyThreshold(ctx, recovery) - if err != nil { - respondError(w, err.Code(), err) - return - } - - // Format the status - status := &RekeyStatusResponse{ - Started: false, - T: 0, - N: 0, - Required: sealThreshold, - } - if rekeyConf != nil { - // Get the progress - started, progress, err := core.RekeyProgress(recovery, false) - if err != nil { - respondError(w, err.Code(), err) - return - } - - status.Nonce = rekeyConf.Nonce - status.Started = started - status.T = rekeyConf.SecretThreshold - status.N = rekeyConf.SecretShares - status.Progress = progress - status.VerificationRequired = rekeyConf.VerificationRequired - status.VerificationNonce = rekeyConf.VerificationNonce - if rekeyConf.PGPKeys != nil && len(rekeyConf.PGPKeys) != 0 { - pgpFingerprints, err := pgpkeys.GetFingerprints(rekeyConf.PGPKeys, nil) - if err != nil { - respondError(w, http.StatusInternalServerError, err) - return - } - status.PGPFingerprints = pgpFingerprints - status.Backup = rekeyConf.Backup - } - } respondOk(w, status) } func handleSysRekeyInitPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { // Parse the request - var req RekeyRequest + var req *vault.RekeyRequest if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil { respondError(w, http.StatusBadRequest, err) return } - - if req.Backup && len(req.PGPKeys) == 0 { - respondError(w, http.StatusBadRequest, fmt.Errorf("cannot request a backup of the new keys without providing PGP keys for encryption")) - return - } - - if len(req.PGPKeys) > 0 && len(req.PGPKeys) != req.SecretShares { - respondError(w, http.StatusBadRequest, fmt.Errorf("incorrect number of PGP keys for rekey")) - return - } - - // Initialize the rekey - err := core.RekeyInit(&vault.SealConfig{ - SecretShares: req.SecretShares, - SecretThreshold: req.SecretThreshold, - StoredShares: req.StoredShares, - PGPKeys: req.PGPKeys, - Backup: req.Backup, - VerificationRequired: req.RequireVerification, - Created: time.Now().UTC(), - }, recovery) + code, err := vault.HandleSysRekeyInitPut(core, recovery, req, true) if err != nil { - respondError(w, err.Code(), err) + respondError(w, code, err) return } @@ -146,13 +73,13 @@ func handleSysRekeyInitPut(ctx context.Context, core *vault.Core, recovery bool, } func handleSysRekeyInitDelete(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { - var req RekeyDeleteRequest + var req vault.RekeyDeleteRequest if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil { respondError(w, http.StatusBadRequest, err) return } - if err := core.RekeyCancel(recovery, req.Nonce, 10*time.Minute); err != nil { + if err := core.RekeyCancel(recovery, req.Nonce, 10*time.Minute, true); err != nil { respondError(w, err.Code(), err) return } @@ -168,67 +95,26 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler { } // Parse the request - var req RekeyUpdateRequest + var req vault.RekeyUpdateRequest if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil { respondError(w, http.StatusBadRequest, err) return } - if req.Key == "" { - respondError( - w, http.StatusBadRequest, - errors.New("'key' must be specified in request body as JSON")) - return - } - - // Decode the key, which is base64 or hex encoded - min, max := core.BarrierKeyLength() - key, err := hex.DecodeString(req.Key) - // We check min and max here to ensure that a string that is base64 - // encoded but also valid hex will not be valid and we instead base64 - // decode it - if err != nil || len(key) < min || len(key) > max { - key, err = base64.StdEncoding.DecodeString(req.Key) - if err != nil { - respondError( - w, http.StatusBadRequest, - errors.New("'key' must be a valid hex or base64 string")) - return - } - } ctx, cancel := core.GetContext() defer cancel() - // Use the key to make progress on rekey - result, rekeyErr := core.RekeyUpdate(ctx, key, req.Nonce, recovery) - if rekeyErr != nil { - respondError(w, rekeyErr.Code(), rekeyErr) + result, code, err := vault.HandleSysRekeyUpdatePut(ctx, core, recovery, &req, true) + if err != nil { + respondError(w, code, err) + return + } + if result != nil { + respondOk(w, result) return } - // Format the response - resp := &RekeyUpdateResponse{} - if result != nil { - resp.Complete = true - resp.Nonce = req.Nonce - resp.Backup = result.Backup - resp.PGPFingerprints = result.PGPFingerprints - resp.VerificationRequired = result.VerificationRequired - resp.VerificationNonce = result.VerificationNonce - - // Encode the keys - keys := make([]string, 0, len(result.SecretShares)) - keysB64 := make([]string, 0, len(result.SecretShares)) - for _, k := range result.SecretShares { - keys = append(keys, hex.EncodeToString(k)) - keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k)) - } - resp.Keys = keys - resp.KeysB64 = keysB64 - respondOk(w, resp) - } else { - handleSysRekeyInitGet(ctx, core, recovery, w, r) - } + handleSysRekeyInitGet(r.Context(), core, recovery, w, r) }) } @@ -266,47 +152,17 @@ func handleSysRekeyVerify(core *vault.Core, recovery bool) http.Handler { } func handleSysRekeyVerifyGet(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { - barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx) - if barrierConfErr != nil { - respondError(w, http.StatusInternalServerError, barrierConfErr) - return - } - if barrierConfig == nil { - respondError(w, http.StatusBadRequest, fmt.Errorf("server is not yet initialized")) - return - } - - // Get the rekey configuration - rekeyConf, err := core.RekeyConfig(recovery) + status, code, err := vault.HandleSysRekeyVerifyGet(ctx, core, recovery, true) if err != nil { - respondError(w, err.Code(), err) - return - } - if rekeyConf == nil { - respondError(w, http.StatusBadRequest, errors.New("no rekey configuration found")) + respondError(w, code, err) return } - // Get the progress - started, progress, err := core.RekeyProgress(recovery, true) - if err != nil { - respondError(w, err.Code(), err) - return - } - - // Format the status - status := &RekeyVerificationStatusResponse{ - Started: started, - Nonce: rekeyConf.VerificationNonce, - T: rekeyConf.SecretThreshold, - N: rekeyConf.SecretShares, - Progress: progress, - } respondOk(w, status) } func handleSysRekeyVerifyDelete(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { - if err := core.RekeyVerifyRestart(recovery); err != nil { + if err := core.RekeyVerifyRestart(recovery, true); err != nil { respondError(w, err.Code(), err) return } @@ -316,112 +172,25 @@ func handleSysRekeyVerifyDelete(ctx context.Context, core *vault.Core, recovery func handleSysRekeyVerifyPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) { // Parse the request - var req RekeyVerificationUpdateRequest + var req vault.RekeyVerificationUpdateRequest if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil { respondError(w, http.StatusBadRequest, err) return } - if req.Key == "" { - respondError( - w, http.StatusBadRequest, - errors.New("'key' must be specified in request body as JSON")) - return - } - - // Decode the key, which is base64 or hex encoded - min, max := core.BarrierKeyLength() - key, err := hex.DecodeString(req.Key) - // We check min and max here to ensure that a string that is base64 - // encoded but also valid hex will not be valid and we instead base64 - // decode it - if err != nil || len(key) < min || len(key) > max { - key, err = base64.StdEncoding.DecodeString(req.Key) - if err != nil { - respondError( - w, http.StatusBadRequest, - errors.New("'key' must be a valid hex or base64 string")) - return - } - } ctx, cancel := core.GetContext() defer cancel() - // Use the key to make progress on rekey - result, rekeyErr := core.RekeyVerify(ctx, key, req.Nonce, recovery) - if rekeyErr != nil { - respondError(w, rekeyErr.Code(), rekeyErr) + resp, code, err := vault.HandleSysRekeyVerifyPut(ctx, core, recovery, true, &req) + if err != nil { + respondError(w, code, err) return } // Format the response - resp := &RekeyVerificationUpdateResponse{} - if result != nil { - resp.Complete = true - resp.Nonce = result.Nonce + if resp != nil { respondOk(w, resp) } else { handleSysRekeyVerifyGet(ctx, core, recovery, w, r) } } - -type RekeyRequest struct { - SecretShares int `json:"secret_shares"` - SecretThreshold int `json:"secret_threshold"` - StoredShares int `json:"stored_shares"` - PGPKeys []string `json:"pgp_keys"` - Backup bool `json:"backup"` - RequireVerification bool `json:"require_verification"` -} - -type RekeyStatusResponse struct { - Nonce string `json:"nonce"` - Started bool `json:"started"` - T int `json:"t"` - N int `json:"n"` - Progress int `json:"progress"` - Required int `json:"required"` - PGPFingerprints []string `json:"pgp_fingerprints"` - Backup bool `json:"backup"` - VerificationRequired bool `json:"verification_required"` - VerificationNonce string `json:"verification_nonce,omitempty"` -} - -type RekeyUpdateRequest struct { - Nonce string - Key string -} - -type RekeyUpdateResponse struct { - Nonce string `json:"nonce"` - Complete bool `json:"complete"` - Keys []string `json:"keys"` - KeysB64 []string `json:"keys_base64"` - PGPFingerprints []string `json:"pgp_fingerprints"` - Backup bool `json:"backup"` - VerificationRequired bool `json:"verification_required"` - VerificationNonce string `json:"verification_nonce,omitempty"` -} - -type RekeyVerificationUpdateRequest struct { - Nonce string `json:"nonce"` - Key string `json:"key"` -} - -type RekeyVerificationStatusResponse struct { - Nonce string `json:"nonce"` - Started bool `json:"started"` - T int `json:"t"` - N int `json:"n"` - Progress int `json:"progress"` -} - -type RekeyVerificationUpdateResponse struct { - Nonce string `json:"nonce"` - Complete bool `json:"complete"` -} - -type RekeyDeleteRequest struct { - Nonce string `json:"nonce"` - Key string `json:"key"` -} diff --git a/http/sys_rekey_test.go b/http/sys_rekey_test.go index babe79c179..d5d8d06ce6 100644 --- a/http/sys_rekey_test.go +++ b/http/sys_rekey_test.go @@ -7,20 +7,21 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net/http" "reflect" "testing" "github.com/go-test/deep" - "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" "github.com/hashicorp/vault/vault" + "github.com/stretchr/testify/require" ) // Test to check if the API errors out when wrong number of PGP keys are // supplied for rekey func TestSysRekey_Init_pgpKeysEntriesForRekey(t *testing.T) { cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), + HandlerFunc: Handler, + NumCores: 1, }) cluster.Start() defer cluster.Cleanup() @@ -37,44 +38,93 @@ func TestSysRekey_Init_pgpKeysEntriesForRekey(t *testing.T) { } func TestSysRekey_Init_Status(t *testing.T) { - t.Run("status-barrier-default", func(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), - }) - cluster.Start() - defer cluster.Cleanup() - cl := cluster.Cores[0].Client - - resp, err := cl.Logical().Read("sys/rekey/init") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := resp.Data - expected := map[string]interface{}{ - "started": false, - "t": json.Number("0"), - "n": json.Number("0"), - "progress": json.Number("0"), - "required": json.Number("3"), - "pgp_fingerprints": interface{}(nil), - "backup": false, - "nonce": "", - "verification_required": false, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: Handler, + NumCores: 1, }) + defer cluster.Cleanup() + cl := cluster.Cores[0].Client + + testCases := []struct { + name string + enableUnauthRekey bool + useToken bool + expectError bool + }{ + { + name: "default-unauthenticated", + enableUnauthRekey: true, + useToken: false, + expectError: false, + }, + { + name: "default-authenticated", + enableUnauthRekey: true, + useToken: true, + expectError: false, + }, + { + name: "auth-required-without-token", + enableUnauthRekey: false, + useToken: false, + expectError: true, + }, + { + name: "auth-required-with-token", + enableUnauthRekey: false, + useToken: true, + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cluster.Cores[0].Core.SetEnableUnauthRekey(tc.enableUnauthRekey) + + if tc.useToken { + cl.SetToken(cluster.RootToken) + } else { + cl.SetToken("") + } + + resp, err := cl.Logical().Read("sys/rekey/init") + + if tc.expectError { + if err == nil { + t.Fatal("expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := resp.Data + expected := map[string]interface{}{ + "started": false, + "t": json.Number("0"), + "n": json.Number("0"), + "progress": json.Number("0"), + "required": json.Number("3"), + "pgp_fingerprints": interface{}(nil), + "backup": false, + "nonce": "", + "verification_required": false, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + } + }) + } } func TestSysRekey_Init_Setup(t *testing.T) { t.Run("init-barrier-barrier-key", func(t *testing.T) { cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), + HandlerFunc: Handler, + NumCores: 1, }) cluster.Start() defer cluster.Cleanup() @@ -142,10 +192,9 @@ func TestSysRekey_Init_Setup(t *testing.T) { func TestSysRekey_Init_Cancel(t *testing.T) { t.Run("cancel-barrier-barrier-key", func(t *testing.T) { cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: Handler, - RequestResponseCallback: schema.ResponseValidatingCallback(t), + HandlerFunc: Handler, + NumCores: 1, }) - cluster.Start() defer cluster.Cleanup() cl := cluster.Cores[0].Client @@ -198,73 +247,147 @@ func TestSysRekey_badKey(t *testing.T) { } func TestSysRekey_Update(t *testing.T) { - t.Run("rekey-barrier-barrier-key", func(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) + testCases := []struct { + name string + enableUnauthRekey bool + useToken bool + expectInitError bool + expectUpdateError bool + }{ + { + name: "unauthenticated", + enableUnauthRekey: true, + useToken: false, + expectInitError: false, + expectUpdateError: false, + }, + { + name: "authenticated", + enableUnauthRekey: true, + useToken: true, + expectInitError: false, + expectUpdateError: false, + }, + { + name: "auth-required", + enableUnauthRekey: false, + useToken: true, + expectInitError: false, + expectUpdateError: false, + }, + { + name: "auth-required-no-token", + enableUnauthRekey: false, + useToken: false, + expectInitError: true, + expectUpdateError: true, + }, + } - resp := testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{ - "secret_shares": 5, - "secret_threshold": 3, - }) - var rekeyStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rekeyStatus) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conf := &vault.CoreConfig{} + if tc.enableUnauthRekey { + conf.EnableUnauthenticatedAccess = []string{"rekey"} + } + cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{ + DisableTLS: true, + HandlerFunc: Handler, + NumCores: 1, + }) + defer cluster.Cleanup() + cl := cluster.Cores[0].Client - var actual map[string]interface{} - var expected map[string]interface{} + reqToken := "" + if tc.useToken { + reqToken = cl.Token() + } + addr := cl.Address() - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{ - "nonce": rekeyStatus["nonce"].(string), - "key": hex.EncodeToString(key), + resp := testHttpPut(t, reqToken, addr+"/v1/sys/rekey/init", map[string]interface{}{ + "secret_shares": 5, + "secret_threshold": 3, }) - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": true, - "nonce": rekeyStatus["nonce"].(string), - "backup": false, - "pgp_fingerprints": interface{}(nil), - "required": json.Number("3"), - "t": json.Number("3"), - "n": json.Number("5"), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "verification_required": false, + if tc.expectInitError { + testResponseStatus(t, resp, http.StatusForbidden) + return } + + var rekeyStatus map[string]interface{} testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) + testResponseBody(t, resp, &rekeyStatus) - if i+1 == len(keys) { - delete(expected, "started") - delete(expected, "required") - delete(expected, "t") - delete(expected, "n") - delete(expected, "progress") - expected["complete"] = true - expected["keys"] = actual["keys"] - expected["keys_base64"] = actual["keys_base64"] + var actual map[string]interface{} + var expected map[string]interface{} + + if !tc.expectUpdateError { + // Test with a bad key to ensure that we format errors the same way + resp = testHttpPut(t, reqToken, addr+"/v1/sys/rekey/update", map[string]interface{}{ + "nonce": rekeyStatus["nonce"].(string), + "key": hex.EncodeToString([]byte("badkey")), + }) + testResponseStatus(t, resp, http.StatusBadRequest) + testResponseBody(t, resp, &actual) + require.Equal(t, map[string]any{"errors": []any{"key is shorter than minimum 16 bytes"}}, actual) } - if i+1 < len(keys) && (actual["nonce"] == nil || actual["nonce"].(string) == "") { - t.Fatalf("expected a nonce, i is %d, actual is %#v", i, actual) + for i, key := range cluster.BarrierKeys { + resp = testHttpPut(t, reqToken, addr+"/v1/sys/rekey/update", map[string]interface{}{ + "nonce": rekeyStatus["nonce"].(string), + "key": hex.EncodeToString(key), + }) + + if tc.expectUpdateError { + testResponseStatus(t, resp, http.StatusForbidden) + return + } + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "started": true, + "nonce": rekeyStatus["nonce"].(string), + "backup": false, + "pgp_fingerprints": interface{}(nil), + "required": json.Number("3"), + "t": json.Number("3"), + "n": json.Number("5"), + "progress": json.Number(fmt.Sprintf("%d", i+1)), + "verification_required": false, + } + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + + if i+1 == len(cluster.BarrierKeys) { + delete(expected, "started") + delete(expected, "required") + delete(expected, "t") + delete(expected, "n") + delete(expected, "progress") + expected["complete"] = true + expected["keys"] = actual["keys"] + expected["keys_base64"] = actual["keys_base64"] + } + + if i+1 < len(cluster.BarrierKeys) && (actual["nonce"] == nil || actual["nonce"].(string) == "") { + t.Fatalf("expected a nonce, i is %d, actual is %#v", i, actual) + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: \n%#v\nactual: \n%#v", expected, actual) + } } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: \n%#v\nactual: \n%#v", expected, actual) + retKeys := actual["keys"].([]interface{}) + if len(retKeys) != 5 { + t.Fatalf("bad: %#v", retKeys) } - } - - retKeys := actual["keys"].([]interface{}) - if len(retKeys) != 5 { - t.Fatalf("bad: %#v", retKeys) - } - keysB64 := actual["keys_base64"].([]interface{}) - if len(keysB64) != 5 { - t.Fatalf("bad: %#v", keysB64) - } - }) + keysB64 := actual["keys_base64"].([]interface{}) + if len(keysB64) != 5 { + t.Fatalf("bad: %#v", keysB64) + } + }) + } } func TestSysRekey_ReInitUpdate(t *testing.T) { diff --git a/sdk/helper/docker/testhelpers.go b/sdk/helper/docker/testhelpers.go index 60ee1fcd45..a1ba1c712f 100644 --- a/sdk/helper/docker/testhelpers.go +++ b/sdk/helper/docker/testhelpers.go @@ -443,7 +443,7 @@ func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*St } for from, to := range d.RunOptions.CopyFromTo { - if err := copyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil { + if err := CopyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil { _ = d.DockerAPI.ContainerRemove(ctx, c.ID, container.RemoveOptions{}) return nil, err } @@ -500,7 +500,7 @@ func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*St func (d *Runner) RefreshFiles(ctx context.Context, containerID string) error { for from, to := range d.RunOptions.CopyFromTo { - if err := copyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil { + if err := CopyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil { // TODO too drastic? _ = d.DockerAPI.ContainerRemove(ctx, containerID, container.RemoveOptions{}) return err @@ -555,7 +555,7 @@ func (d *Runner) Restart(ctx context.Context, containerID string) error { return d.DockerAPI.NetworkConnect(ctx, d.RunOptions.NetworkID, containerID, ends) } -func copyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error { +func CopyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error { srcInfo, err := archive.CopyInfoSourcePath(from, false) if err != nil { return fmt.Errorf("error copying from source %q: %v", from, err) diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index 2adc9f30a8..b2985c9ec4 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -1021,6 +1021,31 @@ func (n *DockerClusterNode) Restart(ctx context.Context) error { return nil } +func (n *DockerClusterNode) Signal(ctx context.Context, signal string) error { + return n.DockerAPI.ContainerKill(ctx, n.Container.ID, signal) +} + +func (n *DockerClusterNode) UpdateConfig(ctx context.Context, config *testcluster.VaultNodeConfig) error { + // Marshal the config to JSON + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + // Write the config to the work directory + configPath := filepath.Join(n.WorkDir, "user.json") + if err := os.WriteFile(configPath, configJSON, 0o644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + // Copy the updated config to the container + if err := dockhelper.CopyToContainer(ctx, n.DockerAPI, n.Container.ID, configPath, "/vault/config/user.json"); err != nil { + return fmt.Errorf("failed to copy config to container: %w", err) + } + + return nil +} + func (n *DockerClusterNode) AddNetworkDelay(ctx context.Context, delay time.Duration, targetIP string) error { ip := net.ParseIP(targetIP) if ip == nil { diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go index d61a55d2e0..23835062aa 100644 --- a/sdk/helper/testcluster/types.go +++ b/sdk/helper/testcluster/types.go @@ -83,6 +83,7 @@ type VaultNodeConfig struct { EnableResponseHeaderRaftNodeID bool `json:"enable_response_header_raft_node_id"` LicensePath string `json:"license_path"` FeatureFlags []string `json:"feature_flags,omitempty"` + EnableUnauthenticatedAccess []string `json:"enable_unauthenticated_access,omitempty"` } type ClusterNode struct { diff --git a/sdk/logical/response.go b/sdk/logical/response.go index f80cb83093..a673ceb0ca 100644 --- a/sdk/logical/response.go +++ b/sdk/logical/response.go @@ -29,6 +29,12 @@ const ( // avoided like the HTTPContentType. The value must be a byte slice. HTTPRawBody = "http_raw_body" + // HTTPRawBodyError is similar to HTTPRawBody. The difference is that + // HTTPRawBodyError is specifically intended for endpoints that want to manage + // their error response directly. This was added to mitigate the risk of + // causing regressions in the error responses of existing HTTPRawBody users. + HTTPRawBodyError = "http_raw_body_error" + // HTTPStatusCode is the response code of the HTTP body that goes with the HTTPContentType. // This can only be specified for non-secrets, and should should be similarly // avoided like the HTTPContentType. The value must be an integer. diff --git a/sdk/logical/response_util.go b/sdk/logical/response_util.go index 3408524aca..231dc08ca9 100644 --- a/sdk/logical/response_util.go +++ b/sdk/logical/response_util.go @@ -4,6 +4,7 @@ package logical import ( + "bytes" "encoding/json" "errors" "fmt" @@ -196,24 +197,38 @@ func AdjustErrorStatusCode(status *int, err error) { } } +type errorResponse struct { + Errors []string `json:"errors"` +} + +// GenerateNonLogicalErrorResponse returns a struct that can be serialized to +// JSON as part of reporting an error to callers. It is used by some older APIs +// that live in the http layer which don't use logical.Response, as well as by +// some that do but are emulating the older ones. +func GenerateNonLogicalErrorResponse(status int, err error) *errorResponse { + resp := &errorResponse{Errors: make([]string, 0, 1)} + if err != nil { + resp.Errors = append(resp.Errors, err.Error()) + } + + return resp +} + func RespondError(w http.ResponseWriter, status int, err error) { AdjustErrorStatusCode(&status, err) defer IncrementResponseStatusCodeMetric(status) + var b bytes.Buffer + enc := json.NewEncoder(&b) + enc.Encode(GenerateNonLogicalErrorResponse(status, err)) + RespondWithBody(w, status, b.String()) +} + +func RespondWithBody(w http.ResponseWriter, status int, body string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - - type ErrorResponse struct { - Errors []string `json:"errors"` - } - resp := &ErrorResponse{Errors: make([]string, 0, 1)} - if err != nil { - resp.Errors = append(resp.Errors, err.Error()) - } - - enc := json.NewEncoder(w) - enc.Encode(resp) + w.Write([]byte(body)) } func RespondErrorAndData(w http.ResponseWriter, status int, data interface{}, err error) { diff --git a/vault/core.go b/vault/core.go index 5c79682c7c..2da158e6ae 100644 --- a/vault/core.go +++ b/vault/core.go @@ -274,6 +274,11 @@ type Core struct { // the generate-root process simply to talk to the new follower cluster. devToken string + // enableUnauthRekey controls whether rekey endpoints are registered as + // unauthenticated endpoints (true) or as authenticated sys backend + // endpoints (false, default). + enableUnauthRekey *atomic.Bool + // HABackend may be available depending on the physical backend ha physical.HABackend @@ -971,6 +976,12 @@ type CoreConfig struct { // ReportingScanDirectory is where files generated by /sys/reporting/scan will go. ReportingScanDirectory string + + // EnableUnauthenticatedAccess is a list of endpoint names that should be + // accessible without authentication, despite them being by default authenticated. + // These aren't the actual paths to endpoints, but rather specific values that + // identify groups of endpoints, e.g. "rekey" refers to the sys/rekey/* endpoints. + EnableUnauthenticatedAccess []string } // GetServiceRegistration returns the config's ServiceRegistration, or nil if it does @@ -1155,6 +1166,7 @@ func CreateCore(conf *CoreConfig) (*Core, error) { periodicLeaderRefreshInterval: conf.PeriodicLeaderRefreshInterval, rpcLastSuccessfulHeartbeat: new(atomic.Value), reportingScanDirectory: conf.ReportingScanDirectory, + enableUnauthRekey: new(atomic.Bool), } c.certCountManager = cert_count.InitCertificateCountManager(c.logger) @@ -1437,6 +1449,15 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.clusterAddrBridge = conf.ClusterAddrBridge c.licenseReloadCh = conf.LicenseReload + + // Check if "rekey" is in the EnableUnauthenticatedAccess list + for _, endpoint := range conf.EnableUnauthenticatedAccess { + if endpoint == "rekey" { + c.enableUnauthRekey.Store(true) + break + } + } + return c, nil } @@ -4434,6 +4455,24 @@ func (c *Core) ReloadIntrospectionEndpointEnabled() { c.introspectionEnabled = conf.(*server.Config).EnableIntrospectionEndpoint } +func (c *Core) ReloadEnableUnauthenticatedAccess() { + conf := c.rawConfig.Load() + if conf == nil { + return + } + + // Check if "rekey" is in the EnableUnauthenticatedAccess list + enableRekey := false + for _, endpoint := range conf.(*server.Config).EnableUnauthenticatedAccess { + if endpoint == "rekey" { + enableRekey = true + break + } + } + + c.enableUnauthRekey.Store(enableRekey) +} + type PeerNode struct { Hostname string `json:"hostname"` APIAddress string `json:"api_address"` @@ -4918,3 +4957,11 @@ var errRemovedHANode = errors.New("node has been removed from the HA cluster") func (c *Core) CoreNumber() int { return c.coreNumber } + +func (c *Core) GetEnableUnauthRekey() bool { + return c.enableUnauthRekey.Load() +} + +func (c *Core) SetEnableUnauthRekey(val bool) { + c.enableUnauthRekey.Store(val) +} diff --git a/vault/external_tests/api/sys_rekey_ext_test.go b/vault/external_tests/api/sys_rekey_ext_test.go index 0a584ed247..960a206f10 100644 --- a/vault/external_tests/api/sys_rekey_ext_test.go +++ b/vault/external_tests/api/sys_rekey_ext_test.go @@ -40,6 +40,7 @@ func TestSysRekey_Verification(t *testing.T) { func testSysRekey_Verification(t *testing.T, recovery bool, legacyShamir bool) { opts := &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, + NumCores: 1, } switch { case recovery: diff --git a/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go b/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go new file mode 100644 index 0000000000..0f40a8bcf8 --- /dev/null +++ b/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go @@ -0,0 +1,187 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package system_binary + +import ( + "os" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster" + "github.com/hashicorp/vault/sdk/helper/testcluster/docker" + "github.com/stretchr/testify/require" +) + +// waitForRekeyInConfig polls sys/config/state/sanitized until the rekey endpoint +// appears or disappears from enable_unauthenticated_access based on shouldBePresent. +func waitForRekeyInConfig(t *testing.T, client *api.Client, rootToken string, shouldBePresent bool) { + clientWithAuth, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientWithAuth.SetToken(rootToken) + + require.Eventually(t, func() bool { + resp, err := clientWithAuth.Logical().Read("sys/config/state/sanitized") + if err != nil { + t.Logf("error reading config state: %v", err) + return false + } + if resp == nil || resp.Data == nil { + t.Logf("nil response or data from config state") + return false + } + + override, ok := resp.Data["enable_unauthenticated_access"] + if !ok { + // If the field is not present, rekey is not in the override list + return !shouldBePresent + } + + // Check if override contains "rekey" + rekeyFound := false + if overrideSlice, ok := override.([]interface{}); ok { + for _, v := range overrideSlice { + if str, ok := v.(string); ok && str == "rekey" { + rekeyFound = true + break + } + } + } + + if shouldBePresent { + return rekeyFound + } + return !rekeyFound + }, 10*time.Second, 100*time.Millisecond, "rekey presence in enable_unauthenticated_access did not match expected state") +} + +// TestSysRekey_ConfigReload tests that the rekey status endpoint can be toggled +// between requiring authentication and not requiring authentication by using +// the enable_unauthenticated_access config option and reloading the config. +func TestSysRekey_ConfigReload(t *testing.T) { + binary := os.Getenv("VAULT_BINARY") + if binary == "" { + t.Skip("only running docker test when $VAULT_BINARY present") + } + + nodeConfig := &testcluster.VaultNodeConfig{ + LogLevel: "TRACE", + } + opts := &docker.DockerClusterOptions{ + ImageRepo: "hashicorp/vault", + ImageTag: "latest", + VaultBinary: binary, + DisableMlock: true, + ClusterOptions: testcluster.ClusterOptions{ + NumCores: 1, + VaultNodeConfig: nodeConfig, + }, + } + + cluster := docker.NewTestDockerCluster(t, opts) + defer cluster.Cleanup() + + node := cluster.Nodes()[0].(*docker.DockerClusterNode) + client := node.APIClient() + rootToken := cluster.GetRootToken() + + // Test 1: Without enable_unauthenticated_access, rekey status should require auth + t.Run("requires-auth-by-default", func(t *testing.T) { + // Try without token - should fail + clientNoAuth, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientNoAuth.SetToken("") + + _, err = clientNoAuth.Logical().Read("sys/rekey/init") + require.Error(t, err, "expected error when accessing rekey status without token") + require.Contains(t, err.Error(), "permission denied", "error should indicate permission denied") + + // Try with token - should succeed + clientWithAuth, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientWithAuth.SetToken(rootToken) + + resp, err := clientWithAuth.Logical().Read("sys/rekey/init") + require.NoError(t, err, "should succeed with valid token") + require.NotNil(t, resp, "response should not be nil") + require.NotNil(t, resp.Data, "response data should not be nil") + require.False(t, resp.Data["started"].(bool), "rekey should not be started") + }) + + // Test 2: Update config to enable unauthenticated rekey and reload + t.Run("enable-unauthenticated-via-config-reload", func(t *testing.T) { + // Create updated config with enable_unauthenticated_access + nodeConfig.EnableUnauthenticatedAccess = []string{"rekey"} + + // Update the config and copy it to the container + err := node.UpdateConfig(t.Context(), nodeConfig) + require.NoError(t, err, "failed to update config") + + // Send SIGHUP to reload the configuration + err = node.Signal(t.Context(), "SIGHUP") + require.NoError(t, err, "failed to send SIGHUP") + + // Wait for rekey to appear in enable_unauthenticated_access + waitForRekeyInConfig(t, client, rootToken, true) + + // Now test that rekey status works without auth + clientNoAuth, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientNoAuth.SetToken("") + + resp, err := clientNoAuth.Logical().Read("sys/rekey/init") + require.NoError(t, err, "should succeed without token after config reload") + require.NotNil(t, resp, "response should not be nil") + require.NotNil(t, resp.Data, "response data should not be nil") + require.False(t, resp.Data["started"].(bool), "rekey should not be started") + + // Verify it still works with auth + clientWithAuth2, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientWithAuth2.SetToken(rootToken) + + resp2, err := clientWithAuth2.Logical().Read("sys/rekey/init") + require.NoError(t, err, "should still succeed with valid token") + require.NotNil(t, resp2, "response should not be nil") + require.NotNil(t, resp2.Data, "response data should not be nil") + require.False(t, resp2.Data["started"].(bool), "rekey should not be started") + }) + + // Test 3: Remove the override and reload to restore auth requirement + t.Run("restore-auth-requirement-via-config-reload", func(t *testing.T) { + // Create config without enable_unauthenticated_access + nodeConfig.EnableUnauthenticatedAccess = nil + + // Update the config and copy it to the container + err := node.UpdateConfig(t.Context(), nodeConfig) + require.NoError(t, err, "failed to update config") + + // Send SIGHUP to reload the configuration + err = node.Signal(t.Context(), "SIGHUP") + require.NoError(t, err, "failed to send SIGHUP") + + // Wait for rekey to be removed from enable_unauthenticated_access + waitForRekeyInConfig(t, client, rootToken, false) + + // Now test that rekey status requires auth again + clientNoAuth, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientNoAuth.SetToken("") + + _, err = clientNoAuth.Logical().Read("sys/rekey/init") + require.Error(t, err, "should fail without token after restoring auth requirement") + require.Contains(t, err.Error(), "permission denied", "error should indicate permission denied") + + // Verify it still works with auth + clientWithAuth2, err := client.Clone() + require.NoError(t, err, "failed to clone client") + clientWithAuth2.SetToken(rootToken) + + resp, err := clientWithAuth2.Logical().Read("sys/rekey/init") + require.NoError(t, err, "should succeed with valid token") + require.NotNil(t, resp, "response should not be nil") + require.NotNil(t, resp.Data, "response data should not be nil") + require.False(t, resp.Data["started"].(bool), "rekey should not be started") + }) +} diff --git a/vault/logical_system.go b/vault/logical_system.go index d018091d14..e830a62d78 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -4,6 +4,7 @@ package vault import ( + "bytes" "context" crand "crypto/rand" "crypto/sha256" @@ -14,6 +15,7 @@ import ( "errors" "fmt" "hash" + "io" "math/rand" "net/http" "net/url" @@ -43,6 +45,7 @@ import ( "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/monitor" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/helper/random" "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/framework" @@ -106,6 +109,57 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf raftChallengeLimiter: rate.NewLimiter(rate.Limit(RaftChallengesPerSecond), RaftInitialChallengeLimit), } + // Build the unauthenticated paths list. Rekey paths are conditionally added based on + // the enableUnauthRekey configuration (retrieved from Core). + unauthenticatedPaths := []string{ + "wrapping/lookup", + "wrapping/pubkey", + "replication/status", + "internal/specs/openapi", + "internal/ui/authenticated-messages", + "internal/ui/unauthenticated-messages", + "internal/ui/mounts", + "internal/ui/mounts/*", + "internal/ui/namespaces", + "replication/performance/status", + "replication/dr/status", + "replication/dr/secondary/promote", + "replication/dr/secondary/disable", + "replication/dr/secondary/recover", + "replication/dr/secondary/update-primary", + "replication/dr/secondary/operation-token/delete", + "replication/dr/secondary/license", + "replication/dr/secondary/license/signed", + "replication/dr/secondary/license/status", + "replication/dr/secondary/sys/config/reload/license", + "replication/dr/secondary/reindex", + "storage/raft/bootstrap/challenge", + "storage/raft/bootstrap/answer", + "init", + "seal-status", + "unseal", + "leader", + "health", + "generate-root/attempt", + "generate-root/update", + "decode-token", + "mfa/validate", + } + + // Note that while rekeyPaths are not part of unauthenticatedPaths, that's + // because they are defined both here and in http.handler. The latter ones + // are unauthenticated and don't use the logical framework. They are enabled + // only when Core.enableUnauthRekey is true, and being more specific paths + // than the v1/sys mux path they take precedence when enabled. + rekeyPaths := []string{ + "rekey/init", + "rekey/update", + "rekey/verify", + "rekey-recovery-key/init", + "rekey-recovery-key/update", + "rekey-recovery-key/verify", + } + b.Backend = &framework.Backend{ RunningVersion: versions.DefaultBuiltinVersion, Help: strings.TrimSpace(sysHelpRoot), @@ -147,46 +201,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf "step-down", }, - Unauthenticated: []string{ - "wrapping/lookup", - "wrapping/pubkey", - "replication/status", - "internal/specs/openapi", - "internal/ui/authenticated-messages", - "internal/ui/unauthenticated-messages", - "internal/ui/mounts", - "internal/ui/mounts/*", - "internal/ui/namespaces", - "replication/performance/status", - "replication/dr/status", - "replication/dr/secondary/promote", - "replication/dr/secondary/disable", - "replication/dr/secondary/recover", - "replication/dr/secondary/update-primary", - "replication/dr/secondary/operation-token/delete", - "replication/dr/secondary/license", - "replication/dr/secondary/license/signed", - "replication/dr/secondary/license/status", - "replication/dr/secondary/sys/config/reload/license", - "replication/dr/secondary/reindex", - "storage/raft/bootstrap/challenge", - "storage/raft/bootstrap/answer", - "init", - "seal-status", - "unseal", - "leader", - "health", - "generate-root/attempt", - "generate-root/update", - "decode-token", - "rekey/init", - "rekey/update", - "rekey/verify", - "rekey-recovery-key/init", - "rekey-recovery-key/update", - "rekey-recovery-key/verify", - "mfa/validate", - }, + Unauthenticated: unauthenticatedPaths, LocalStorage: []string{ expirationSubPath, @@ -199,6 +214,8 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf SealWrapStorage: []string{ managedKeyRegistrySubPath, }, + + Binary: rekeyPaths, }, } b.Backend.PathsSpecial.Unauthenticated = append(b.Backend.PathsSpecial.Unauthenticated, entUnauthenticatedPaths()...) @@ -1373,6 +1390,463 @@ func (b *SystemBackend) handleRekeyDeleteRecovery(ctx context.Context, req *logi return b.handleRekeyDelete(ctx, req, data, true) } +type RekeyStatusResponse struct { + Nonce string `json:"nonce"` + Started bool `json:"started"` + T int `json:"t"` + N int `json:"n"` + Progress int `json:"progress"` + Required int `json:"required"` + PGPFingerprints []string `json:"pgp_fingerprints"` + Backup bool `json:"backup"` + VerificationRequired bool `json:"verification_required"` + VerificationNonce string `json:"verification_nonce,omitempty"` +} + +func HandleSysRekeyInitGet(ctx context.Context, core *Core, recovery bool, grabLock bool) (*RekeyStatusResponse, int, error) { + barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx) + if barrierConfErr != nil { + return nil, http.StatusInternalServerError, barrierConfErr + } + if barrierConfig == nil { + return nil, http.StatusBadRequest, fmt.Errorf("server is not yet initialized") + } + + // Get the rekey configuration + rekeyConf, err := core.RekeyConfig(recovery, grabLock) + if err != nil { + return nil, err.Code(), err + } + + sealThreshold, err := core.RekeyThreshold(ctx, recovery, grabLock) + if err != nil { + return nil, err.Code(), err + } + + // Format the status + status := &RekeyStatusResponse{ + Started: false, + T: 0, + N: 0, + Required: sealThreshold, + } + if rekeyConf != nil { + // Get the progress + started, progress, err := core.RekeyProgress(recovery, false, grabLock) + if err != nil { + return nil, err.Code(), err + } + + status.Nonce = rekeyConf.Nonce + status.Started = started + status.T = rekeyConf.SecretThreshold + status.N = rekeyConf.SecretShares + status.Progress = progress + status.VerificationRequired = rekeyConf.VerificationRequired + status.VerificationNonce = rekeyConf.VerificationNonce + if rekeyConf.PGPKeys != nil && len(rekeyConf.PGPKeys) != 0 { + pgpFingerprints, err := pgpkeys.GetFingerprints(rekeyConf.PGPKeys, nil) + if err != nil { + return nil, http.StatusInternalServerError, err + } + status.PGPFingerprints = pgpFingerprints + status.Backup = rekeyConf.Backup + } + } + return status, 0, nil +} + +type RekeyRequest struct { + SecretShares int `json:"secret_shares"` + SecretThreshold int `json:"secret_threshold"` + StoredShares int `json:"stored_shares"` + PGPKeys []string `json:"pgp_keys"` + Backup bool `json:"backup"` + RequireVerification bool `json:"require_verification"` +} + +func HandleSysRekeyInitPut(core *Core, recovery bool, req *RekeyRequest, grabLock bool) (int, error) { + if req.Backup && len(req.PGPKeys) == 0 { + return http.StatusBadRequest, fmt.Errorf("cannot request a backup of the new keys without providing PGP keys for encryption") + } + + if len(req.PGPKeys) > 0 && len(req.PGPKeys) != req.SecretShares { + return http.StatusBadRequest, fmt.Errorf("incorrect number of PGP keys for rekey") + } + + // Initialize the rekey + err := core.RekeyInit(&SealConfig{ + SecretShares: req.SecretShares, + SecretThreshold: req.SecretThreshold, + StoredShares: req.StoredShares, + PGPKeys: req.PGPKeys, + Backup: req.Backup, + VerificationRequired: req.RequireVerification, + Created: time.Now().UTC(), + }, recovery, grabLock) + if err != nil { + return err.Code(), err + } + return http.StatusOK, nil +} + +type RekeyUpdateRequest struct { + Nonce string + Key string +} + +type RekeyUpdateResponse struct { + Nonce string `json:"nonce"` + Complete bool `json:"complete"` + Keys []string `json:"keys"` + KeysB64 []string `json:"keys_base64"` + PGPFingerprints []string `json:"pgp_fingerprints"` + Backup bool `json:"backup"` + VerificationRequired bool `json:"verification_required"` + VerificationNonce string `json:"verification_nonce,omitempty"` +} + +func HandleSysRekeyUpdatePut(ctx context.Context, core *Core, recovery bool, req *RekeyUpdateRequest, grabLock bool) (*RekeyUpdateResponse, int, error) { + if req.Key == "" { + return nil, http.StatusBadRequest, errors.New("'key' must be specified in request body as JSON") + } + + // Decode the key, which is base64 or hex encoded + min, max := core.BarrierKeyLength() + key, err := hex.DecodeString(req.Key) + // We check min and max here to ensure that a string that is base64 + // encoded but also valid hex will not be valid and we instead base64 + // decode it + if err != nil || len(key) < min || len(key) > max { + key, err = base64.StdEncoding.DecodeString(req.Key) + if err != nil { + return nil, http.StatusBadRequest, errors.New("'key' must be a valid hex or base64 string") + } + } + + // Use the key to make progress on rekey + result, rekeyErr := core.RekeyUpdate(ctx, key, req.Nonce, recovery, grabLock) + + if rekeyErr != nil { + return nil, rekeyErr.Code(), rekeyErr + } + + // Format the response + resp := &RekeyUpdateResponse{} + if result != nil { + resp.Complete = true + resp.Nonce = req.Nonce + resp.Backup = result.Backup + resp.PGPFingerprints = result.PGPFingerprints + resp.VerificationRequired = result.VerificationRequired + resp.VerificationNonce = result.VerificationNonce + + // Encode the keys + keys := make([]string, 0, len(result.SecretShares)) + keysB64 := make([]string, 0, len(result.SecretShares)) + for _, k := range result.SecretShares { + keys = append(keys, hex.EncodeToString(k)) + keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k)) + } + resp.Keys = keys + resp.KeysB64 = keysB64 + return resp, 0, nil + } + return nil, 0, nil +} + +type RekeyVerifyStatusResponse struct { + Nonce string `json:"nonce"` + Started bool `json:"started"` + T int `json:"t"` + N int `json:"n"` + Progress int `json:"progress"` +} + +func HandleSysRekeyVerifyGet(ctx context.Context, core *Core, recovery bool, grabLock bool) (*RekeyVerifyStatusResponse, int, error) { + barrierConfig, err := core.SealAccess().BarrierConfig(ctx) + if err != nil { + return nil, http.StatusInternalServerError, err + } + if barrierConfig == nil { + return nil, http.StatusBadRequest, fmt.Errorf("server is not yet initialized") + } + + // Get the rekey configuration + rekeyConf, rekeyErr := core.RekeyConfig(recovery, grabLock) + if rekeyErr != nil { + return nil, rekeyErr.Code(), rekeyErr + } + if rekeyConf == nil { + return nil, http.StatusBadRequest, fmt.Errorf("no rekey configuration found") + } + + // Get the progress + started, progress, rekeyErr := core.RekeyProgress(recovery, true, grabLock) + if rekeyErr != nil { + return nil, rekeyErr.Code(), rekeyErr + } + + // Format the status + status := &RekeyVerifyStatusResponse{ + Started: started, + Nonce: rekeyConf.VerificationNonce, + T: rekeyConf.SecretThreshold, + N: rekeyConf.SecretShares, + Progress: progress, + } + return status, 0, nil +} + +type RekeyVerificationUpdateRequest struct { + Nonce string `json:"nonce"` + Key string `json:"key"` +} + +type RekeyVerificationUpdateResponse struct { + Nonce string `json:"nonce"` + Complete bool `json:"complete"` +} + +func HandleSysRekeyVerifyPut(ctx context.Context, core *Core, recovery bool, grabLock bool, req *RekeyVerificationUpdateRequest) (*RekeyVerificationUpdateResponse, int, error) { + if req.Key == "" { + return nil, http.StatusBadRequest, errors.New("'key' must be specified in request body as JSON") + } + + // Decode the key, which is base64 or hex encoded + min, max := core.BarrierKeyLength() + key, err := hex.DecodeString(req.Key) + // We check min and max here to ensure that a string that is base64 + // encoded but also valid hex will not be valid and we instead base64 + // decode it + if err != nil || len(key) < min || len(key) > max { + key, err = base64.StdEncoding.DecodeString(req.Key) + if err != nil { + return nil, http.StatusBadRequest, errors.New("'key' must be a valid hex or base64 string") + } + } + + // Use the key to make progress on rekey + result, rekeyErr := core.RekeyVerify(ctx, key, req.Nonce, recovery, grabLock) + if rekeyErr != nil { + return nil, rekeyErr.Code(), rekeyErr + } + if result != nil { + return &RekeyVerificationUpdateResponse{ + Nonce: result.Nonce, + Complete: result.Complete, + }, http.StatusOK, nil + } + return nil, 0, nil +} + +// handleRekeyInit handles the rekey/init endpoint for both barrier and recovery keys +func (b *SystemBackend) handleRekeyInit( + ctx context.Context, + req *logical.Request, + recovery bool, +) (*logical.Response, error) { + // Check replication state + repState := b.Core.ReplicationState() + if repState.HasState(consts.ReplicationPerformanceSecondary) { + return logical.ErrorResponse("rekeying can only be performed on the primary cluster when replication is activated"), nil + } + + // Check if recovery key is supported + if recovery && !b.Core.SealAccess().RecoveryKeySupported() { + return logical.ErrorResponse("recovery rekeying not supported"), nil + } + + switch req.Operation { + case logical.ReadOperation: + return b.handleRekeyInitGet(ctx, recovery) + case logical.UpdateOperation: + return b.handleRekeyInitPut(ctx, recovery) + case logical.DeleteOperation: + return b.handleRekeyInitDelete(ctx, recovery) + default: + return nil, logical.ErrUnsupportedOperation + } +} + +// getJSONBody populates the out struct with the contents of the HTTP request body +// and returns (nil, nil), or on error returns values that a handler can return +// for failure. This is intended for older APIs that don't use the framework. +func getJSONBody(ctx context.Context, out any) (*logical.Response, error) { + body, ok := logical.ContextOriginalBodyValue(ctx) + if !ok { + return nonLogicalError(http.StatusInternalServerError, fmt.Errorf("failed to retrieve request body")) + } + err := jsonutil.DecodeJSONFromReader(body, out) + if err != nil && err != io.EOF { + return nonLogicalError(http.StatusBadRequest, fmt.Errorf("failed to parse JSON input: %w", err)) + } + return nil, nil +} + +// nonLogicalError creates an error response for older handlers that don't follow +// current vault response conventions. +func nonLogicalError(code int, err error) (*logical.Response, error) { + logical.AdjustErrorStatusCode(&code, err) + defer logical.IncrementResponseStatusCodeMetric(code) + + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(logical.GenerateNonLogicalErrorResponse(code, err)) + + resp, _ := logical.RespondWithStatusCode(nil, nil, code) + resp.Data[logical.HTTPRawBodyError] = buf.String() + return resp, err +} + +// nonLogicalResponse takes the result of a request handler, and either returns +// an error response if err is non nil, or serializes the val into the response. +// It uses the HTTP raw body field in response data, since this is for older +// APIs that don't follow our usual response format. +func nonLogicalResponse(val any, code int, err error) (*logical.Response, error) { + if err != nil { + return nonLogicalError(code, err) + } + + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(val) + resp, _ := logical.RespondWithStatusCode(nil, nil, http.StatusOK) + resp.Data[logical.HTTPRawBody] = buf.String() + return resp, nil +} + +func (b *SystemBackend) handleRekeyInitBarrier(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyInit(ctx, req, false) +} + +func (b *SystemBackend) handleRekeyInitRecovery(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyInit(ctx, req, true) +} + +func (b *SystemBackend) handleRekeyInitGet(ctx context.Context, recovery bool) (*logical.Response, error) { + status, code, err := HandleSysRekeyInitGet(ctx, b.Core, recovery, false) + return nonLogicalResponse(status, code, err) +} + +func (b *SystemBackend) handleRekeyInitPut(ctx context.Context, recovery bool) (*logical.Response, error) { + var req RekeyRequest + resp, err := getJSONBody(ctx, &req) + if err != nil { + return resp, err + } + + code, err := HandleSysRekeyInitPut(b.Core, recovery, &req, false) + if err != nil { + return nonLogicalError(code, err) + } + + return b.handleRekeyInitGet(ctx, recovery) +} + +type RekeyDeleteRequest struct { + Nonce string `json:"nonce"` + Key string `json:"key"` +} + +func (b *SystemBackend) handleRekeyInitDelete(ctx context.Context, recovery bool) (*logical.Response, error) { + var req RekeyDeleteRequest + resp, err := getJSONBody(ctx, &req) + if err != nil { + return resp, err + } + + if err := b.Core.RekeyCancel(recovery, req.Nonce, 10*time.Minute, false); err != nil { + return nil, fmt.Errorf("failed to cancel rekey: %w", err) + } + + return nil, nil +} + +// handleRekeyUpdate handles the rekey/update endpoint for both barrier and recovery keys +func (b *SystemBackend) handleRekeyUpdate(ctx context.Context, recovery bool) (*logical.Response, error) { + var req RekeyUpdateRequest + resp, err := getJSONBody(ctx, &req) + if err != nil { + return resp, err + } + + // Use the key to make progress on rekey + result, code, err := HandleSysRekeyUpdatePut(ctx, b.Core, recovery, &req, false) + if err == nil && result == nil { + return b.handleRekeyInitGet(ctx, recovery) + } + return nonLogicalResponse(result, code, err) +} + +func (b *SystemBackend) handleRekeyUpdateBarrier(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyUpdate(ctx, false) +} + +func (b *SystemBackend) handleRekeyUpdateRecovery(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyUpdate(ctx, true) +} + +// handleRekeyVerify handles the rekey/verify endpoint for both barrier and recovery keys +func (b *SystemBackend) handleRekeyVerify(ctx context.Context, req *logical.Request, _ *framework.FieldData, recovery bool) (*logical.Response, error) { + repState := b.Core.ReplicationState() + if repState.HasState(consts.ReplicationPerformanceSecondary) { + return logical.ErrorResponse("rekeying can only be performed on the primary cluster when replication is activated"), nil + } + + // Check if recovery key is supported + if recovery && !b.Core.SealAccess().RecoveryKeySupported() { + return logical.ErrorResponse("recovery rekeying not supported"), nil + } + + switch req.Operation { + case logical.ReadOperation: + return b.handleRekeyVerifyGet(ctx, recovery) + case logical.UpdateOperation: + return b.handleRekeyVerifyPut(ctx, recovery) + case logical.DeleteOperation: + return b.handleRekeyVerifyDelete(ctx, recovery) + default: + return nil, logical.ErrUnsupportedOperation + } +} + +func (b *SystemBackend) handleRekeyVerifyBarrier(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyVerify(ctx, req, data, false) +} + +func (b *SystemBackend) handleRekeyVerifyRecovery(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return b.handleRekeyVerify(ctx, req, data, true) +} + +func (b *SystemBackend) handleRekeyVerifyGet(ctx context.Context, recovery bool) (*logical.Response, error) { + status, code, err := HandleSysRekeyVerifyGet(ctx, b.Core, recovery, false) + return nonLogicalResponse(status, code, err) +} + +func (b *SystemBackend) handleRekeyVerifyDelete(ctx context.Context, recovery bool) (*logical.Response, error) { + if err := b.Core.RekeyVerifyRestart(recovery, false); err != nil { + return nil, fmt.Errorf("failed to restart rekey verification: %w", err) + } + + return b.handleRekeyVerifyGet(ctx, recovery) +} + +func (b *SystemBackend) handleRekeyVerifyPut(ctx context.Context, recovery bool) (*logical.Response, error) { + var req RekeyVerificationUpdateRequest + resp, err := getJSONBody(ctx, &req) + if err != nil { + return resp, err + } + + result, code, err := HandleSysRekeyVerifyPut(ctx, b.Core, recovery, false, &RekeyVerificationUpdateRequest{ + Nonce: req.Nonce, + Key: req.Key, + }) + if err == nil && result == nil { + return b.handleRekeyVerifyGet(ctx, recovery) + } + return nonLogicalResponse(result, code, err) +} + func (b *SystemBackend) handleGenerateRootDecodeTokenUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { encodedToken := data.Get("encoded_token").(string) otp := data.Get("otp").(string) diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 32f4405d65..c4180883cc 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -769,7 +769,7 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { respFields := map[string]*framework.FieldSchema{ "nonce": { Type: framework.TypeString, - Required: true, + Required: false, }, "started": { Type: framework.TypeBool, @@ -785,7 +785,7 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { }, "progress": { Type: framework.TypeInt, - Required: true, + Required: false, }, "required": { Type: framework.TypeInt, @@ -793,11 +793,11 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { }, "verification_required": { Type: framework.TypeBool, - Required: true, + Required: false, }, "verification_nonce": { Type: framework.TypeString, - Required: true, + Required: false, }, "backup": { Type: framework.TypeBool, @@ -815,6 +815,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { OperationPrefix: "rekey-attempt", }, + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. Fields: map[string]*framework.FieldSchema{ "secret_shares": { Type: framework.TypeInt, @@ -824,6 +826,10 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Type: framework.TypeInt, Description: "Specifies the number of shares required to reconstruct the unseal key. This must be less than or equal secret_shares. If using Vault HSM with auto-unsealing, this value must be the same as secret_shares.", }, + "stored_shares": { + Type: framework.TypeInt, + Description: "Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as secret_shares.", + }, "pgp_keys": { Type: framework.TypeCommaStringSlice, Description: "Specifies an array of PGP public keys used to encrypt the output unseal keys. Ordering is preserved. The keys must be base64-encoded from their original binary representation. The size of this array must be the same as secret_shares.", @@ -836,10 +842,16 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Type: framework.TypeBool, Description: "Turns on verification functionality", }, + "nonce": { + Type: framework.TypeString, + Description: "Specifies the nonce of the rekey operation. If the rekey was initialized within the last 10 minutes, you must provide the nonce to cancel the operation.", + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "read", OperationSuffix: "progress", @@ -853,6 +865,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Summary: "Reads the configuration and progress of the current rekey attempt.", }, logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "initialize", }, @@ -866,6 +880,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Description: "Only a single rekey attempt can take place at a time, and changing the parameters of a rekey requires canceling and starting a new rekey, which will also provide a new nonce.", }, logical.DeleteOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "cancel", }, @@ -991,6 +1007,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { { Pattern: "rekey/update", + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. Fields: map[string]*framework.FieldSchema{ "key": { Type: framework.TypeString, @@ -1004,6 +1022,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyUpdateBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: "rekey-attempt", OperationVerb: "update", @@ -1068,6 +1088,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { OperationPrefix: "rekey-verification", }, + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. Fields: map[string]*framework.FieldSchema{ "key": { Type: framework.TypeString, @@ -1081,6 +1103,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "read", OperationSuffix: "progress", @@ -1115,6 +1139,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Summary: "Read the configuration and progress of the current rekey verification attempt.", }, logical.DeleteOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "cancel", }, @@ -1149,6 +1175,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { Description: "This clears any progress made and resets the nonce. Unlike a `DELETE` against `sys/rekey/init`, this only resets the current verification operation, not the entire rekey atttempt.", }, logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyBarrier, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "update", }, @@ -1170,6 +1198,246 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path { }, }, }, + { + Pattern: "rekey-recovery-key/init", + + DisplayAttrs: &framework.DisplayAttributes{ + OperationPrefix: "rekey-recovery-key-attempt", + }, + + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. + Fields: map[string]*framework.FieldSchema{ + "secret_shares": { + Type: framework.TypeInt, + Description: "Specifies the number of shares to split the recovery key into.", + }, + "stored_shares": { + Type: framework.TypeInt, + Description: "Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as `secret_shares`.", + }, + "secret_threshold": { + Type: framework.TypeInt, + Description: "Specifies the number of shares required to reconstruct the recovery key.", + }, + "pgp_keys": { + Type: framework.TypeCommaStringSlice, + Description: "Specifies an array of PGP public keys used to encrypt the output recovery keys.", + }, + "backup": { + Type: framework.TypeBool, + Description: "Specifies if using PGP-encrypted keys, whether Vault should also store a plaintext backup of the PGP-encrypted keys.", + }, + "require_verification": { + Type: framework.TypeBool, + Description: "Turns on verification functionality", + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "read", + OperationSuffix: "progress", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: respFields, + }}, + }, + Summary: "Reads the configuration and progress of the current recovery key rekey attempt.", + }, + logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "initialize", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: respFields, + }}, + }, + Summary: "Initializes a new recovery key rekey attempt.", + Description: "Only a single recovery key rekey attempt can take place at a time.", + }, + logical.DeleteOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyInitRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "cancel", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + }}, + }, + Summary: "Cancels any in-progress recovery key rekey.", + Description: "This clears the recovery key rekey settings as well as any progress made.", + }, + }, + }, + { + Pattern: "rekey-recovery-key/update", + + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. + Fields: map[string]*framework.FieldSchema{ + "key": { + Type: framework.TypeString, + Description: "Specifies a single recovery key share.", + }, + "nonce": { + Type: framework.TypeString, + Description: "Specifies the nonce of the rekey attempt.", + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyUpdateRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationPrefix: "rekey-recovery-key-attempt", + OperationVerb: "update", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "nonce": { + Type: framework.TypeString, + Required: true, + }, + "complete": { + Type: framework.TypeBool, + }, + "keys": { + Type: framework.TypeCommaStringSlice, + }, + "keys_base64": { + Type: framework.TypeCommaStringSlice, + }, + "verification_required": { + Type: framework.TypeBool, + Required: true, + }, + "verification_nonce": { + Type: framework.TypeString, + Required: true, + }, + "backup": { + Type: framework.TypeBool, + }, + "pgp_fingerprints": { + Type: framework.TypeCommaStringSlice, + }, + }, + }}, + }, + Summary: "Enter a single recovery key share to progress the rekey of the Vault.", + }, + }, + }, + { + Pattern: "rekey-recovery-key/verify", + + DisplayAttrs: &framework.DisplayAttributes{ + OperationPrefix: "rekey-recovery-key-verification", + }, + + // Note that since we're a binary path we don't actually use these fields, they exist solely for the sake + // of populating the openapi schema. + Fields: map[string]*framework.FieldSchema{ + "key": { + Type: framework.TypeString, + Description: "Specifies a single recovery key share from the new set of shares.", + }, + "nonce": { + Type: framework.TypeString, + Description: "Specifies the nonce of the rekey verification operation.", + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "read", + OperationSuffix: "progress", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "nonce": { + Type: framework.TypeString, + Required: true, + }, + "started": { + Type: framework.TypeBool, + Required: true, + }, + "t": { + Type: framework.TypeInt, + Required: true, + }, + "n": { + Type: framework.TypeInt, + Required: true, + }, + "progress": { + Type: framework.TypeInt, + Required: true, + }, + }, + }}, + }, + Summary: "Read the configuration and progress of the current recovery key rekey verification attempt.", + }, + logical.DeleteOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "cancel", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + }}, + }, + Summary: "Cancel any in-progress recovery key rekey verification operation.", + Description: "This clears any progress made and resets the nonce.", + }, + logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleRekeyVerifyRecovery, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "update", + }, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "nonce": { + Type: framework.TypeString, + Required: true, + }, + "complete": { + Type: framework.TypeBool, + }, + }, + }}, + }, + Summary: "Enter a single new recovery key share to progress the rekey verification operation.", + }, + }, + }, { Pattern: "seal$", diff --git a/vault/plugin_reload.go b/vault/plugin_reload.go index 8ea7ccd925..42d6ef6797 100644 --- a/vault/plugin_reload.go +++ b/vault/plugin_reload.go @@ -271,17 +271,17 @@ func (c *Core) reloadBackendCommon(ctx context.Context, entry *MountEntry, isAut paths := backend.SpecialPaths() if paths != nil { re.rootPaths.Store(pathsToRadix(paths.Root)) - loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated) + loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated) if err != nil { return err } re.loginPaths.Store(loginPathsEntry) - binaryPathsEntry, err := parseUnauthenticatedPaths(paths.Binary) + binaryPathsEntry, err := parseSpecialPaths(paths.Binary) if err != nil { return err } re.binaryPaths.Store(binaryPathsEntry) - allowSnapshotReadPathsEntry, err := parseUnauthenticatedPaths(paths.AllowSnapshotRead) + allowSnapshotReadPathsEntry, err := parseSpecialPaths(paths.AllowSnapshotRead) if err != nil { return err } diff --git a/vault/rekey.go b/vault/rekey.go index e808e6c891..b6bc23ab31 100644 --- a/vault/rekey.go +++ b/vault/rekey.go @@ -63,9 +63,11 @@ type RekeyBackup struct { // the recovery key threshold, depending on whether rekey is being // performed on the recovery key, or whether the seal supports // recovery keys. -func (c *Core) RekeyThreshold(ctx context.Context, recovery bool) (int, logical.HTTPCodedError) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) RekeyThreshold(ctx context.Context, recovery bool, grabLock bool) (int, logical.HTTPCodedError) { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return 0, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -97,9 +99,11 @@ func (c *Core) RekeyThreshold(ctx context.Context, recovery bool) (int, logical. } // RekeyProgress is used to return the rekey progress (num shares). -func (c *Core) RekeyProgress(recovery, verification bool) (bool, int, logical.HTTPCodedError) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) RekeyProgress(recovery, verification, grabLock bool) (bool, int, logical.HTTPCodedError) { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return false, 0, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -128,9 +132,11 @@ func (c *Core) RekeyProgress(recovery, verification bool) (bool, int, logical.HT } // RekeyConfig is used to read the rekey configuration -func (c *Core) RekeyConfig(recovery bool) (*SealConfig, logical.HTTPCodedError) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) RekeyConfig(recovery bool, grabLock bool) (*SealConfig, logical.HTTPCodedError) { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -158,19 +164,19 @@ func (c *Core) RekeyConfig(recovery bool) (*SealConfig, logical.HTTPCodedError) // RekeyInit will either initialize the rekey of barrier or recovery key. // recovery determines whether this is a rekey on the barrier or recovery key. -func (c *Core) RekeyInit(config *SealConfig, recovery bool) logical.HTTPCodedError { +func (c *Core) RekeyInit(config *SealConfig, recovery bool, grabLock bool) logical.HTTPCodedError { if config.SecretThreshold > config.SecretShares { return logical.CodedError(http.StatusBadRequest, "provided threshold greater than the total shares") } if recovery { - return c.RecoveryRekeyInit(config) + return c.RecoveryRekeyInit(config, grabLock) } - return c.BarrierRekeyInit(config) + return c.BarrierRekeyInit(config, grabLock) } // BarrierRekeyInit is used to initialize the rekey settings for the barrier key -func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError { +func (c *Core) BarrierRekeyInit(config *SealConfig, grabLock bool) logical.HTTPCodedError { switch c.seal.BarrierSealConfigType() { case SealConfigTypeShamir: // As of Vault 1.3 all seals use StoredShares==1. The one exception is @@ -210,8 +216,10 @@ func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError { return logical.CodedError(http.StatusInternalServerError, fmt.Errorf("invalid rekey seal configuration: %w", err).Error()) } - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -239,14 +247,14 @@ func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError { c.barrierRekeyConfig.Nonce = nonce c.barrierRekeyConfig.Created = time.Now().UTC() - if c.logger.IsInfo() { - c.logger.Info("rekey initialized", "nonce", c.barrierRekeyConfig.Nonce, "shares", c.barrierRekeyConfig.SecretShares, "threshold", c.barrierRekeyConfig.SecretThreshold, "validation_required", c.barrierRekeyConfig.VerificationRequired) - } + c.logger.Info("rekey initialized", "nonce", c.barrierRekeyConfig.Nonce, + "shares", c.barrierRekeyConfig.SecretShares, "threshold", c.barrierRekeyConfig.SecretThreshold, + "validation_required", c.barrierRekeyConfig.VerificationRequired, "backup", c.barrierRekeyConfig.Backup) return nil } // RecoveryRekeyInit is used to initialize the rekey settings for the recovery key -func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError { +func (c *Core) RecoveryRekeyInit(config *SealConfig, grabLock bool) logical.HTTPCodedError { if config.StoredShares > 0 { return logical.CodedError(http.StatusBadRequest, "stored shares not supported by recovery key") } @@ -261,8 +269,10 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError { return logical.CodedError(http.StatusBadRequest, "recovery keys not supported") } - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -297,11 +307,11 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError { } // RekeyUpdate is used to provide a new key part for the barrier or recovery key. -func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recovery bool) (*RekeyResult, logical.HTTPCodedError) { +func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recovery bool, grabLock bool) (*RekeyResult, logical.HTTPCodedError) { if recovery { - return c.RecoveryRekeyUpdate(ctx, key, nonce) + return c.RecoveryRekeyUpdate(ctx, key, nonce, grabLock) } - return c.BarrierRekeyUpdate(ctx, key, nonce) + return c.BarrierRekeyUpdate(ctx, key, nonce, grabLock) } // BarrierRekeyUpdate is used to provide a new key part. Barrier rekey can be done @@ -309,10 +319,12 @@ func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recove // key. // // N.B.: If recovery keys are used to rekey, the new barrier key shares are not returned. -func (c *Core) BarrierRekeyUpdate(ctx context.Context, key []byte, nonce string) (*RekeyResult, logical.HTTPCodedError) { +func (c *Core) BarrierRekeyUpdate(ctx context.Context, key []byte, nonce string, grabLock bool) (*RekeyResult, logical.HTTPCodedError) { // Ensure we are already unsealed - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -597,10 +609,12 @@ func (c *Core) performBarrierRekey(ctx context.Context, newSealKey []byte) logic } // RecoveryRekeyUpdate is used to provide a new key part -func (c *Core) RecoveryRekeyUpdate(ctx context.Context, key []byte, nonce string) (*RekeyResult, logical.HTTPCodedError) { +func (c *Core) RecoveryRekeyUpdate(ctx context.Context, key []byte, nonce string, grabLock bool) (*RekeyResult, logical.HTTPCodedError) { // Ensure we are already unsealed - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -798,10 +812,12 @@ func (c *Core) performRecoveryRekey(ctx context.Context, newRootKey []byte) logi return nil } -func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recovery bool) (ret *RekeyVerifyResult, retErr logical.HTTPCodedError) { +func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recovery bool, grabLock bool) (ret *RekeyVerifyResult, retErr logical.HTTPCodedError) { // Ensure we are already unsealed - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -913,9 +929,11 @@ func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recove } // RekeyCancel is used to cancel an in-progress rekey -func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline time.Duration) logical.HTTPCodedError { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline time.Duration, grabLock bool) logical.HTTPCodedError { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } @@ -952,9 +970,11 @@ func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline ti } // RekeyVerifyRestart is used to start the verification process over -func (c *Core) RekeyVerifyRestart(recovery bool) logical.HTTPCodedError { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) RekeyVerifyRestart(recovery bool, grabLock bool) logical.HTTPCodedError { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() { return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error()) } diff --git a/vault/rekey_test.go b/vault/rekey_test.go index c99b25374a..08ca240ff8 100644 --- a/vault/rekey_test.go +++ b/vault/rekey_test.go @@ -36,7 +36,7 @@ func TestCore_Rekey_Lifecycle(t *testing.T) { func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) { min, _ := c.barrier.KeyLength() // Verify update not allowed - _, err := c.RekeyUpdate(context.Background(), make([]byte, min), "", recovery) + _, err := c.RekeyUpdate(context.Background(), make([]byte, min), "", recovery, true) expected := "no barrier rekey in progress" if recovery { expected = "no recovery rekey in progress" @@ -46,12 +46,12 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) { } // Should be no progress - if _, _, err := c.RekeyProgress(recovery, false); err == nil { + if _, _, err := c.RekeyProgress(recovery, false, true); err == nil { t.Fatal("expected error from RekeyProgress") } // Should be no config - conf, err := c.RekeyConfig(recovery) + conf, err := c.RekeyConfig(recovery, true) if err != nil { t.Fatalf("err: %v", err) } @@ -60,7 +60,7 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) { } // Cancel should be idempotent - err = c.RekeyCancel(false, "", 10*time.Minute) + err = c.RekeyCancel(false, "", 10*time.Minute, true) if err != nil { t.Fatalf("err: %v", err) } @@ -70,13 +70,13 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) { SecretThreshold: 3, SecretShares: 5, } - err = c.RekeyInit(newConf, recovery) + err = c.RekeyInit(newConf, recovery, true) if err != nil { t.Fatalf("err: %v", err) } // Should get config - conf, err = c.RekeyConfig(recovery) + conf, err = c.RekeyConfig(recovery, true) if err != nil { t.Fatalf("err: %v", err) } @@ -86,13 +86,13 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) { } // Cancel should be clear - err = c.RekeyCancel(recovery, conf.Nonce, 10*time.Minute) + err = c.RekeyCancel(recovery, conf.Nonce, 10*time.Minute, true) if err != nil { t.Fatalf("err: %v", err) } // Should be no config - conf, err = c.RekeyConfig(recovery) + conf, err = c.RekeyConfig(recovery, true) if err != nil { t.Fatalf("err: %v", err) } @@ -114,7 +114,7 @@ func testCore_Rekey_Init_Common(t *testing.T, c *Core, recovery bool) { SecretThreshold: 5, SecretShares: 1, } - err := c.RekeyInit(badConf, recovery) + err := c.RekeyInit(badConf, recovery, true) if err == nil { t.Fatalf("should fail") } @@ -131,13 +131,13 @@ func testCore_Rekey_Init_Common(t *testing.T, c *Core, recovery bool) { newConf.Type = c.seal.RecoverySealConfigType().String() } - err = c.RekeyInit(newConf, recovery) + err = c.RekeyInit(newConf, recovery, true) if err != nil { t.Fatalf("err: %v", err) } // Second should fail - err = c.RekeyInit(newConf, recovery) + err = c.RekeyInit(newConf, recovery, true) if err == nil { t.Fatalf("should fail") } @@ -171,13 +171,13 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro SecretThreshold: 3, SecretShares: 5, } - hErr := c.RekeyInit(newConf, recovery) + hErr := c.RekeyInit(newConf, recovery, true) if hErr != nil { t.Fatalf("err: %v", hErr) } // Fetch new config with generated nonce - rkconf, hErr := c.RekeyConfig(recovery) + rkconf, hErr := c.RekeyConfig(recovery, true) if hErr != nil { t.Fatalf("err: %v", hErr) } @@ -188,7 +188,7 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro // Provide the master/recovery keys var result *RekeyResult for _, key := range keys { - result, err = c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery) + result, err = c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery, true) if err != nil { if !wantRekeyUpdateError { t.Fatalf("err: %v", err) @@ -208,12 +208,12 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro } // Should be no progress - if _, _, err := c.RekeyProgress(recovery, false); err == nil { + if _, _, err := c.RekeyProgress(recovery, false, true); err == nil { t.Fatal("expected error from RekeyProgress") } // Should be no config - conf, hErr := c.RekeyConfig(recovery) + conf, hErr := c.RekeyConfig(recovery, true) if hErr != nil { t.Fatalf("rekey config error: %v", hErr) } @@ -271,13 +271,13 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro SecretThreshold: 1, SecretShares: 1, } - err = c.RekeyInit(newConf, recovery) + err = c.RekeyInit(newConf, recovery, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rkconf, err = c.RekeyConfig(recovery) + rkconf, err = c.RekeyConfig(recovery, true) if err != nil { t.Fatalf("err: %v", err) } @@ -288,14 +288,14 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro // Provide the parts master oldResult := result for i := 0; i < 3; i++ { - result, err = c.RekeyUpdate(context.Background(), TestKeyCopy(oldResult.SecretShares[i]), rkconf.Nonce, recovery) + result, err = c.RekeyUpdate(context.Background(), TestKeyCopy(oldResult.SecretShares[i]), rkconf.Nonce, recovery, true) if err != nil { t.Fatalf("err: %v", err) } // Should be progress if i < 2 { - _, num, err := c.RekeyProgress(recovery, false) + _, num, err := c.RekeyProgress(recovery, false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -367,13 +367,13 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover SecretThreshold: 3, SecretShares: 5, } - err := c.RekeyInit(newConf, recovery) + err := c.RekeyInit(newConf, recovery, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rkconf, err := c.RekeyConfig(recovery) + rkconf, err := c.RekeyConfig(recovery, true) if err != nil { t.Fatalf("err: %v", err) } @@ -382,7 +382,7 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover } // Provide the nonce (invalid) - _, err = c.RekeyUpdate(context.Background(), keys[0], "abcd", recovery) + _, err = c.RekeyUpdate(context.Background(), keys[0], "abcd", recovery, true) if err == nil { t.Fatalf("expected error") } @@ -392,13 +392,13 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover oldkeystr := fmt.Sprintf("%#v", key) key[0]++ newkeystr := fmt.Sprintf("%#v", key) - ret, err := c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery) + ret, err := c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery, true) if err == nil { t.Fatalf("expected error, ret is %#v\noldkeystr: %s\nnewkeystr: %s", *ret, oldkeystr, newkeystr) } // Check progress has been reset - _, num, err := c.RekeyProgress(recovery, false) + _, num, err := c.RekeyProgress(recovery, false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -466,12 +466,12 @@ func TestCore_Rekey_Standby(t *testing.T) { SecretShares: 1, SecretThreshold: 1, } - err = core.RekeyInit(newConf, false) + err = core.RekeyInit(newConf, false, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rkconf, err := core.RekeyConfig(false) + rkconf, err := core.RekeyConfig(false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -480,7 +480,7 @@ func TestCore_Rekey_Standby(t *testing.T) { } var rekeyResult *RekeyResult for _, key := range keys { - rekeyResult, err = core.RekeyUpdate(context.Background(), key, rkconf.Nonce, false) + rekeyResult, err = core.RekeyUpdate(context.Background(), key, rkconf.Nonce, false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -499,12 +499,12 @@ func TestCore_Rekey_Standby(t *testing.T) { TestWaitActive(t, core2) // Rekey the master key again - err = core2.RekeyInit(newConf, false) + err = core2.RekeyInit(newConf, false, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rkconf, err = core2.RekeyConfig(false) + rkconf, err = core2.RekeyConfig(false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -513,7 +513,7 @@ func TestCore_Rekey_Standby(t *testing.T) { } var rekeyResult2 *RekeyResult for _, key := range rekeyResult.SecretShares { - rekeyResult2, err = core2.RekeyUpdate(context.Background(), key, rkconf.Nonce, false) + rekeyResult2, err = core2.RekeyUpdate(context.Background(), key, rkconf.Nonce, false, true) if err != nil { t.Fatalf("err: %v", err) } @@ -542,7 +542,7 @@ func TestSysRekey_Verification_Invalid(t *testing.T) { err := core.BarrierRekeyInit(&SealConfig{ VerificationRequired: true, StoredShares: 1, - }) + }, true) if err == nil { t.Fatal("expected error") @@ -599,11 +599,11 @@ func TestCancelRekey_Nonce(t *testing.T) { t.Skip(t, "recovery rekey not supported") } - err := c.RekeyInit(tc.config, tc.recovery) + err := c.RekeyInit(tc.config, tc.recovery, true) require.NoError(t, err, "rekey init failed") // try to cancel without the nonce - err = c.RekeyCancel(tc.recovery, "", 10*time.Minute) + err = c.RekeyCancel(tc.recovery, "", 10*time.Minute, true) require.Error(t, err, "cancel should have errored") // retrieve the nonce @@ -621,7 +621,7 @@ func TestCancelRekey_Nonce(t *testing.T) { require.NotEmpty(t, nonce, "nonce missing") // cancel successfully - err = c.RekeyCancel(tc.recovery, nonce, 10*time.Minute) + err = c.RekeyCancel(tc.recovery, nonce, 10*time.Minute, true) require.NoError(t, err, "error on rekey cancel") }) } @@ -675,14 +675,14 @@ func TestCancelRekey_Regression(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - c.RekeyCancel(tc.recovery, "", 10*time.Minute) + c.RekeyCancel(tc.recovery, "", 10*time.Minute, true) }() } - err := c.RekeyInit(tc.config, tc.recovery) + err := c.RekeyInit(tc.config, tc.recovery, true) require.NoError(t, err) wg.Wait() - happening, keys, err := c.RekeyProgress(tc.recovery, false) + happening, keys, err := c.RekeyProgress(tc.recovery, false, true) require.NoError(t, err) require.True(t, happening) require.Equal(t, 0, keys) @@ -731,14 +731,14 @@ func TestCancelRekey_AfterDeadline(t *testing.T) { for _, tc := range testCases { t.Run(tc.config.Type, func(t *testing.T) { c := tc.core(t) - err := c.RekeyInit(tc.config, tc.recovery) + err := c.RekeyInit(tc.config, tc.recovery, true) require.NoError(t, err) // ensure that that 10 ms have passed before we cancel time.Sleep(10 * time.Millisecond) // set the deadline to a microsecond, which means we won't need a // nonce to cancel the rekey - err = c.RekeyCancel(tc.recovery, "", time.Microsecond) + err = c.RekeyCancel(tc.recovery, "", time.Microsecond, true) require.NoError(t, err) }) } diff --git a/vault/router.go b/vault/router.go index 3db7fb6caa..e92810224f 100644 --- a/vault/router.go +++ b/vault/router.go @@ -209,25 +209,25 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount } re.tainted.Store(mountEntry.Tainted) re.rootPaths.Store(pathsToRadix(paths.Root)) - loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated) + loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated) if err != nil { return err } re.loginPaths.Store(loginPathsEntry) - binaryPathsEntry, err := parseUnauthenticatedPaths(paths.Binary) + binaryPathsEntry, err := parseSpecialPaths(paths.Binary) if err != nil { return err } re.binaryPaths.Store(binaryPathsEntry) - limitedPathsEntry, err := parseUnauthenticatedPaths(paths.Limited) + limitedPathsEntry, err := parseSpecialPaths(paths.Limited) if err != nil { return err } re.limitedPaths.Store(limitedPathsEntry) - allowSnapshotReadPathsEntry, err := parseUnauthenticatedPaths(paths.AllowSnapshotRead) + allowSnapshotReadPathsEntry, err := parseSpecialPaths(paths.AllowSnapshotRead) if err != nil { return err } @@ -1024,7 +1024,7 @@ func wildcardError(path, msg string) error { return fmt.Errorf("path %q: invalid use of wildcards %s", path, msg) } -func isValidUnauthenticatedPath(path string) (bool, error) { +func isValidSpecialPath(path string) (bool, error) { switch { case strings.Count(path, "*") > 1: return false, wildcardError(path, "(multiple '*' is forbidden)") @@ -1038,13 +1038,13 @@ func isValidUnauthenticatedPath(path string) (bool, error) { return true, nil } -// parseUnauthenticatedPaths converts a list of special paths to a +// parseSpecialPaths converts a list of special paths to a // specialPathsEntry -func parseUnauthenticatedPaths(paths []string) (*specialPathsEntry, error) { +func parseSpecialPaths(paths []string) (*specialPathsEntry, error) { var tempPaths []string tempWildcardPaths := make([]wildcardPath, 0) for _, path := range paths { - if ok, err := isValidUnauthenticatedPath(path); !ok { + if ok, err := isValidSpecialPath(path); !ok { return nil, err } diff --git a/vault/router_test.go b/vault/router_test.go index 1eba959b70..c845e5c0c1 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -571,7 +571,7 @@ func TestParseUnauthenticatedPaths(t *testing.T) { } allPaths := append(paths, wildcardPaths...) - p, err := parseUnauthenticatedPaths(allPaths) + p, err := parseSpecialPaths(allPaths) if err != nil { t.Fatal(err) } @@ -629,7 +629,7 @@ func TestParseUnauthenticatedPaths_Error(t *testing.T) { } for _, tc := range tcases { - _, err := parseUnauthenticatedPaths(tc.paths) + _, err := parseSpecialPaths(tc.paths) if err == nil || err != nil && !strings.Contains(err.Error(), tc.err) { t.Fatalf("bad: path: %s expect: %v got %v", tc.paths, tc.err, err) } diff --git a/vault/testing.go b/vault/testing.go index d2a0fc9625..d2735282e9 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -1173,6 +1173,7 @@ type TestClusterOptions struct { // ABCDLoggerNames names the loggers according to our ABCD convention when generating 4 clusters ABCDLoggerNames bool + DisableTLS bool } type TestPluginConfig struct { @@ -1374,6 +1375,10 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T }) } + scheme := "https" + if opts.DisableTLS { + scheme = "http" + } // // Listener setup // @@ -1430,10 +1435,14 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T tlsConfigs = append(tlsConfigs, tlsConfig) lns := []*TestListener{ { - Listener: tls.NewListener(ln, tlsConfig), - Address: ln.Addr().(*net.TCPAddr), + Address: ln.Addr().(*net.TCPAddr), }, } + if opts.DisableTLS { + lns[0].Listener = ln + } else { + lns[0].Listener = tls.NewListener(ln, tlsConfig) + } listeners = append(listeners, lns) var handler http.Handler = http.NewServeMux() handlers = append(handlers, handler) @@ -1461,8 +1470,8 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T audit.TypeSocket: audit.NewSocketBackend, audit.TypeSyslog: audit.NewSyslogBackend, }, - RedirectAddr: fmt.Sprintf("https://127.0.0.1:%d", listeners[0][0].Address.Port), - ClusterAddr: "https://127.0.0.1:0", + RedirectAddr: fmt.Sprintf(scheme+"://127.0.0.1:%d", listeners[0][0].Address.Port), + ClusterAddr: scheme + "://127.0.0.1:0", DisableMlock: true, EnableUI: true, EnableRaw: true, @@ -1559,6 +1568,7 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T coreConfig.PeriodicLeaderRefreshInterval = base.PeriodicLeaderRefreshInterval coreConfig.ClusterAddrBridge = base.ClusterAddrBridge coreConfig.ObservationSystemConfig = base.ObservationSystemConfig + coreConfig.EnableUnauthenticatedAccess = base.EnableUnauthenticatedAccess testApplyEntBaseConfig(coreConfig, base) } @@ -1856,7 +1866,11 @@ func (testCluster *TestCluster) newCore(t testing.TB, idx int, coreConfig *CoreC firstCoreNumber = opts.FirstCoreNumber } - localConfig.RedirectAddr = fmt.Sprintf("https://127.0.0.1:%d", listeners[0].Address.Port) + scheme := "https" + if opts != nil && opts.DisableTLS { + scheme = "http" + } + localConfig.RedirectAddr = fmt.Sprintf(scheme+"://127.0.0.1:%d", listeners[0].Address.Port) // if opts.SealFunc is provided, use that to generate a seal for the config instead if opts != nil && opts.SealFunc != nil { @@ -1920,10 +1934,10 @@ func (testCluster *TestCluster) newCore(t testing.TB, idx int, coreConfig *CoreC if opts != nil && opts.ClusterLayers != nil { localConfig.ClusterNetworkLayer = opts.ClusterLayers.Layers()[idx] - localConfig.ClusterAddr = "https://" + localConfig.ClusterNetworkLayer.Listeners()[0].Addr().String() + localConfig.ClusterAddr = scheme + "://" + localConfig.ClusterNetworkLayer.Listeners()[0].Addr().String() } if opts != nil && opts.BaseClusterListenPort != 0 { - localConfig.ClusterAddr = fmt.Sprintf("https://127.0.0.1:%d", opts.BaseClusterListenPort+idx) + localConfig.ClusterAddr = fmt.Sprintf(scheme+"://127.0.0.1:%d", opts.BaseClusterListenPort+idx) } switch { @@ -2144,7 +2158,11 @@ func (testCluster *TestCluster) getAPIClient( port int, tlsConfig *tls.Config, ) *api.Client { transport := cleanhttp.DefaultPooledTransport() - transport.TLSClientConfig = tlsConfig.Clone() + scheme := "http" + if opts != nil && !opts.DisableTLS { + scheme = "https" + transport.TLSClientConfig = tlsConfig.Clone() + } if err := http2.ConfigureTransport(transport); err != nil { t.Fatal(err) } @@ -2159,7 +2177,7 @@ func (testCluster *TestCluster) getAPIClient( if config.Error != nil { t.Fatal(config.Error) } - config.Address = fmt.Sprintf("https://127.0.0.1:%d", port) + config.Address = fmt.Sprintf(scheme+"://127.0.0.1:%d", port) config.HttpClient = client config.MaxRetries = 0 apiClient, err := api.NewClient(config) From faba3e06a346dc4f1f46e04343d5299901c251e1 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 11 Mar 2026 19:19:59 -0400 Subject: [PATCH 078/468] Update census schema version for consumption billing metrics (#12926) (#12934) * update census schema version for consumption billing metrics * add changelog Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> --- changelog/_12926.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/_12926.txt diff --git a/changelog/_12926.txt b/changelog/_12926.txt new file mode 100644 index 0000000000..0fd5dfb4e4 --- /dev/null +++ b/changelog/_12926.txt @@ -0,0 +1,3 @@ +```release-note:improvement +license utilization reporting: Added consumption billing metrics. +``` \ No newline at end of file From 1b30f42e06258db3acaf281c719a7e10204ff47c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 03:48:34 -0400 Subject: [PATCH 079/468] VAULT-42859: surface authorization_details from inbound JWT into logical.Auth (#12750) (#12919) Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/logical/auth.go | 7 +++++++ vault/request_handling.go | 2 ++ vault/request_handling_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/sdk/logical/auth.go b/sdk/logical/auth.go index 9ab8fac2d4..c3bfb01a5a 100644 --- a/sdk/logical/auth.go +++ b/sdk/logical/auth.go @@ -124,6 +124,13 @@ type Auth struct { // HTTPRequestPriority contains potential information about the request // priority based on required path capabilities HTTPRequestPriority *uint8 `json:"http_request_priority"` + + // AuthorizationDetails holds fine-grained authorization constraints for the request. + // Each element is a JSON object with at minimum a "type" field. + // It is nil when the token does not carry authorization details. + // It is not included in plugin RPC serialization because it is only needed at request-routing + // time and not during plugin Renew or Revoke operations. + AuthorizationDetails []AuthorizationDetail `json:"authorization_details,omitempty"` } func (a *Auth) GoString() string { diff --git a/vault/request_handling.go b/vault/request_handling.go index e318c7044c..95c085f4e3 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -587,6 +587,8 @@ func (c *Core) CheckToken(ctx context.Context, req *logical.Request, unauth bool auth.ActorEntityID = req.Auth.ActorEntityID auth.ActorEntityName = req.Auth.ActorEntityName } + // Copy authorization details from the request to auth so plugins can access them. + auth.AuthorizationDetails = req.EnterpriseTokenAuthorizationDetails twoStepRecover := req.Operation == logical.RecoverOperation && req.RecoverSourcePath != "" && req.RecoverSourcePath != req.Path var alternateRecoverCapability *logical.Operation diff --git a/vault/request_handling_test.go b/vault/request_handling_test.go index 7fe5ebe1d2..34b7271c40 100644 --- a/vault/request_handling_test.go +++ b/vault/request_handling_test.go @@ -641,3 +641,37 @@ func TestRequestHandling_fetchACLTokenEntryAndEntity_NilRequest(t *testing.T) { require.Error(t, err) require.Equal(t, ErrInternalError, err) } + +// TestAuth_AuthorizationDetails_CopiedFromRequest verifies that logical.Auth.AuthorizationDetails +// matches the authorization details already carried on the request. +func TestAuth_AuthorizationDetails_CopiedFromRequest(t *testing.T) { + t.Parallel() + + details := []logical.AuthorizationDetail{ + {"type": "account_information", "scope": "read"}, + {"type": "payment_initiation", "amount": "100"}, + } + + auth := &logical.Auth{} + req := &logical.Request{ + EnterpriseTokenAuthorizationDetails: details, + } + + // Simulate the assignment performed in CheckToken. + auth.AuthorizationDetails = req.EnterpriseTokenAuthorizationDetails + + require.Equal(t, details, auth.AuthorizationDetails, "auth.AuthorizationDetails must equal req.EnterpriseTokenAuthorizationDetails") +} + +// TestAuth_AuthorizationDetails_NilWhenAbsent verifies that auth.AuthorizationDetails is nil +// when the request does not carry authorization details. +func TestAuth_AuthorizationDetails_NilWhenAbsent(t *testing.T) { + t.Parallel() + + auth := &logical.Auth{} + req := &logical.Request{} + + auth.AuthorizationDetails = req.EnterpriseTokenAuthorizationDetails + + require.Nil(t, auth.AuthorizationDetails) +} From bcff969b2b23c23b9b8290a7eaa04c8908895572 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 06:58:31 -0400 Subject: [PATCH 080/468] Fix JWT JIT standby write forwarding (#12883) (#12938) * vault: fix JWT JIT standby write forwarding Forward JWT token creation/register-auth writes from performance standbys through the token creation RPC path and convert read-only failures to ErrPerfStandbyPleaseForward in fetchACLTokenEntryAndEntity. Add regression coverage for standby forwarding, read-only conversion, and duplicate create idempotency. * vault: address PR review feedback for JWT standby fix - Add TODO comment on pre-existing funcGetErr/err bug in getAuthRegisterFunc error path - Add TODO comments on lockless c.perfStandby reads (consistent with codebase but needs accessor) - Add leader-side duplicate token create idempotency test - Add godoc note explaining why tests use TestCore* helpers instead of NewTestCluster * vault: add godoc comments to all JWT standby tests The codechecker CI linter requires godoc on every test function. * vault: clarify TODO comments for known JWT standby issues Improve TODO comments to explicitly describe each bug, impact, and intended fix: - lockless perfStandby reads and stale/racy HA role observation - wrong error variable returned on getAuthRegisterFunc failure * vault: add Jira link for perfStandby sync TODO * vault: add Jira link for funcGetErr TODO * vault: clarify read-only forward mock in test * vault: document forward-helper overrides across JWT tests * vault: add godoc for duplicate token helper * vault: move JWT standby tests into request_handling_ent_test Relocate JWT perf-standby/JIT tests from the dedicated file into request_handling_ent_test.go and remove the extra test file. * test: add fetchACLTokenEntryAndEntity unit tests for request_handling.go Add 8 new test cases covering the non-enterprise code paths in fetchACLTokenEntryAndEntity: - Empty client token returns ErrPermissionDenied - Unknown token returns ErrPermissionDenied + ErrInvalidToken - Valid root token returns ACL, token entry, and no error - Cached token entry is used instead of performing a lookup - BoundCIDR with no connection info returns ErrPermissionDenied - BoundCIDR with IP outside allowed range returns ErrPermissionDenied - BoundCIDR with IP in allowed range succeeds - Non-expiring root token bypasses CIDR checks (TTL == 0) CIDR tests pre-cache the token entry on the request to isolate the CIDR check logic from expiration manager side effects. * refactor: move forwardCreateTokenRegisterAuthForJWTJIT from package var to Core field Replace the package-level var with a field on Core, matching the idiomatic Vault pattern (e.g. replicationHashFunc, sealReloadFunc). This eliminates mutable global state, makes tests parallel-safe, and follows the established testability pattern. - Add forwardCreateTokenRegisterAuthForJWTJIT field to entCoreExt struct - Initialize it to forwardCreateTokenRegisterAuth in coreInit - Update createAndStoreJwtTokenEntryJIT to call c.forwardCreateTokenRegisterAuthForJWTJIT - Simplify tests: set field on Core instance directly, no save/restore needed * fix: add actorEntity param to test calls after rebase The rebase onto main picked up the actorEntity parameter addition to createAndStoreJwtTokenEntryJIT. Update all test call sites to pass nil for actorEntity since these tests do not exercise delegation tokens. * Move JWT JIT whitebox tests to external_tests Move the perf standby JWT JIT forwarding tests and leader duplicate create test from vault/request_handling_ent_test.go to blackbox external tests using NewTestCluster and the HTTP API. - vault/external_tests/perfstandby/perfstandby_jwt_jit_ent_test.go: ForwardedFromStandby, DuplicateRequestIsIdempotent, StandbyTokenWorksOnActive - vault/external_tests/external_jwt/jwt_jit_ent_test.go: DuplicateCreateIsIdempotent (leader-side) * Remove whitebox reference comments from external tests * Merge JWT JIT tests into perfstandby_token_create_ent_test.go Append the JWT JIT perf standby tests to the existing token create test file instead of creating a separate file. * Remove perfStandby synchronization TODO comments The perfStandby reads in the request path are protected by stateLock.RLock() acquired in switchedLockHandleRequest, so these reads are safe within the request handling call tree. * test: drop redundant TestCluster lifecycle calls --------- Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vault/request_handling.go | 3 + vault/request_handling_test.go | 175 +++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/vault/request_handling.go b/vault/request_handling.go index 95c085f4e3..f1adf8987a 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -256,6 +256,9 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req secondEntity = actorEntity err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity, actorEntity) if err != nil { + if c.perfStandby && errors.Is(err, logical.ErrReadOnly) { + return nil, nil, nil, nil, logical.ErrPerfStandbyPleaseForward + } return nil, nil, nil, nil, multierror.Append(err, errors.New("failed in processing enterprise token")) } } diff --git a/vault/request_handling_test.go b/vault/request_handling_test.go index 34b7271c40..e563121ed3 100644 --- a/vault/request_handling_test.go +++ b/vault/request_handling_test.go @@ -12,6 +12,7 @@ import ( "github.com/armon/go-metrics" "github.com/go-test/deep" + "github.com/hashicorp/go-secure-stdlib/parseutil" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/builtin/credential/approle" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" @@ -675,3 +676,177 @@ func TestAuth_AuthorizationDetails_NilWhenAbsent(t *testing.T) { require.Nil(t, auth.AuthorizationDetails) } + +// TestRequestHandling_fetchACLTokenEntryAndEntity_EmptyToken verifies that a +// request with an empty ClientToken is rejected with ErrPermissionDenied. +func TestRequestHandling_fetchACLTokenEntryAndEntity_EmptyToken(t *testing.T) { + core, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + req := &logical.Request{ClientToken: ""} + _, _, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.Error(t, err) + require.Equal(t, logical.ErrPermissionDenied, err) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_UnknownToken verifies that a +// non-existent token returns ErrPermissionDenied combined with ErrInvalidToken. +func TestRequestHandling_fetchACLTokenEntryAndEntity_UnknownToken(t *testing.T) { + core, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + req := &logical.Request{ClientToken: "hvs.nonexistent-token-id"} + _, _, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.Error(t, err) + require.ErrorIs(t, err, logical.ErrPermissionDenied) + require.ErrorIs(t, err, logical.ErrInvalidToken) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_ValidRootToken verifies the +// happy path: a valid root token returns an ACL, the token entry, and no error. +func TestRequestHandling_fetchACLTokenEntryAndEntity_ValidRootToken(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + req := &logical.Request{ClientToken: root} + acl, te, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.NoError(t, err) + require.NotNil(t, acl) + require.NotNil(t, te) + require.Equal(t, root, te.ID) + require.Contains(t, te.Policies, "root") +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_CachedTokenEntry verifies +// that when a token entry is already cached on the request, the function uses +// the cached entry instead of performing a lookup. +func TestRequestHandling_fetchACLTokenEntryAndEntity_CachedTokenEntry(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + // Look up the root token to get a valid entry + te, err := core.tokenStore.Lookup(ctx, root) + require.NoError(t, err) + require.NotNil(t, te) + + // Pre-cache the entry on the request + req := &logical.Request{ClientToken: root} + req.SetTokenEntry(te) + + acl, returnedTE, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.NoError(t, err) + require.NotNil(t, acl) + // The returned token entry should be the same object we cached + require.Same(t, te, returnedTE) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_NoConnection +// verifies that a token with BoundCIDRs is rejected when the request has no +// connection information. The token entry is pre-cached on the request to +// isolate the CIDR check logic from token storage concerns. +func TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_NoConnection(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + boundCIDRs, err := parseutil.ParseAddrs([]string{"10.0.0.0/8"}) + require.NoError(t, err) + + // Look up the root token to get a valid base entry, then add BoundCIDRs + te, err := core.tokenStore.Lookup(ctx, root) + require.NoError(t, err) + te.TTL = time.Hour + te.BoundCIDRs = boundCIDRs + + req := &logical.Request{ + ClientToken: root, + // No Connection field set + } + req.SetTokenEntry(te) + + _, _, _, _, err = core.fetchACLTokenEntryAndEntity(ctx, req) + + require.Error(t, err) + require.ErrorIs(t, err, logical.ErrPermissionDenied) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_OutOfRange +// verifies that a token with BoundCIDRs is rejected when the request comes +// from an IP outside the allowed range. The token entry is pre-cached on the +// request to isolate the CIDR check logic from token storage concerns. +func TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_OutOfRange(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + boundCIDRs, err := parseutil.ParseAddrs([]string{"10.0.0.0/8"}) + require.NoError(t, err) + + te, err := core.tokenStore.Lookup(ctx, root) + require.NoError(t, err) + te.TTL = time.Hour + te.BoundCIDRs = boundCIDRs + + req := &logical.Request{ + ClientToken: root, + Connection: &logical.Connection{RemoteAddr: "192.168.1.1"}, + } + req.SetTokenEntry(te) + + _, _, _, _, err = core.fetchACLTokenEntryAndEntity(ctx, req) + + require.Error(t, err) + require.ErrorIs(t, err, logical.ErrPermissionDenied) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_InRange verifies +// that a token with BoundCIDRs succeeds when the request comes from an IP +// within the allowed CIDR range. The token entry is pre-cached on the request +// to isolate the CIDR check logic from token storage concerns. +func TestRequestHandling_fetchACLTokenEntryAndEntity_BoundCIDR_InRange(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + boundCIDRs, err := parseutil.ParseAddrs([]string{"10.0.0.0/8"}) + require.NoError(t, err) + + te, err := core.tokenStore.Lookup(ctx, root) + require.NoError(t, err) + te.TTL = time.Hour + te.BoundCIDRs = boundCIDRs + + req := &logical.Request{ + ClientToken: root, + Connection: &logical.Connection{RemoteAddr: "10.1.2.3"}, + } + req.SetTokenEntry(te) + + acl, returnedTE, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.NoError(t, err) + require.NotNil(t, acl) + require.NotNil(t, returnedTE) + require.Equal(t, root, returnedTE.ID) +} + +// TestRequestHandling_fetchACLTokenEntryAndEntity_NonExpiring_RootIgnoresCIDR +// verifies that a non-expiring root token (TTL == 0) bypasses CIDR checks even +// if BoundCIDRs is set, since the CIDR check is gated on TTL != 0. +func TestRequestHandling_fetchACLTokenEntryAndEntity_NonExpiring_RootIgnoresCIDR(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + // Root token has TTL == 0, so CIDR checks should not apply + req := &logical.Request{ + ClientToken: root, + // No Connection, which would fail if CIDR checks ran + } + acl, te, _, _, err := core.fetchACLTokenEntryAndEntity(ctx, req) + + require.NoError(t, err) + require.NotNil(t, acl) + require.NotNil(t, te) + require.Equal(t, time.Duration(0), te.TTL) +} From d408952aca9e445fea95d73ca387d7a7bef72c2d Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:34:09 -0500 Subject: [PATCH 081/468] Remove extra makefile lines (#12940) --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index a7e2e98d79..560b59bd18 100644 --- a/Makefile +++ b/Makefile @@ -239,9 +239,6 @@ proto: check-tools-external $(SED_CMD) -e 's/Id/ID/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' vault/request_forwarding_service.pb.go $(SED_CMD) -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/BoundCidrs/BoundCIDRs/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' helper/identity/types.pb.go helper/identity/mfa/types.pb.go helper/storagepacker/types.pb.go sdk/plugin/pb/backend.pb.go sdk/logical/identity.pb.go vault/activity/activity_log.pb.go - # Enterprise files - $(SED_CMD) -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/protobuf:"/sentinel:"" protobuf:"/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/SPDX-License-IDentifier/SPDX-License-Identifier/' vault/replication_services_ent.pb.go - # This will inject the sentinel struct tags as decorated in the proto files. protoc-go-inject-tag -input=./helper/identity/types.pb.go protoc-go-inject-tag -input=./helper/identity/mfa/types.pb.go From 1a57de40bd936ba7e48fb4b2054b7d18e5229e92 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 12:00:59 -0400 Subject: [PATCH 082/468] Backport Fill out Secret Engine Tests into ce/main (#12927) * no-op commit * Fill out Secret Engine Tests (#12287) * reorg some tests * split tests out * fix test * test cleanup * make ldap work * formatting * whitespace * Make KMIP work * Activate smoke_sdk scenarios * Add gotestsum * tryagain * fix go path install * add debugging * more debug * shrug emoji * Remove debug and increase timeout * syntax * help with polling * disable stepdown test for now * Update vault/external_tests/blackbox/secrets_ldap_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update sdk/helper/testcluster/blackbox/session_raft.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update enos/modules/verify_secrets_engines/modules/create/auth.tf Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update enos/modules/vault_run_blackbox_test/scripts/run-test.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update enos/modules/vault_run_blackbox_test/main.tf Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * arm fix * gotestsum * timing * try this * try this * handle when these already exist * --- --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Cant run smoke_sdk in ce (#12931) --------- Co-authored-by: Luis (LT) Carbonell Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../test-run-enos-scenario-matrix.yml | 4 + .github/workflows/test-run-enos-scenario.yml | 2 + enos/enos-samples-ce-build.hcl | 6 + enos/enos-samples-ce-release.hcl | 6 + enos/enos-scenario-smoke-sdk.hcl | 25 +- .../main.tf | 20 ++ .../scripts/populate-ldap.sh | 97 +++++++ enos/modules/vault_run_blackbox_test/main.tf | 21 +- .../scripts/run-test.sh | 20 +- .../vault_run_blackbox_test/variables.tf | 12 + .../modules/create/kv.tf | 17 +- .../scripts/ldap/setup.sh | 8 +- .../testcluster/blackbox/session_raft.go | 256 ++++++++---------- .../blackbox/auth_engines_test.go | 100 ------- .../blackbox/auth_userpass_test.go | 105 +++++++ .../blackbox/secrets_aws_test.go | 116 ++++++++ .../blackbox/secrets_engines_external_test.go | 63 +++++ .../blackbox/secrets_engines_test.go | 224 ++------------- .../blackbox/secrets_identity_test.go | 132 +++++++++ .../blackbox/secrets_kmip_test.go | 99 +++++++ .../blackbox/secrets_kv_test.go | 46 ++++ .../blackbox/secrets_ldap_test.go | 154 +++++++++++ .../blackbox/secrets_pki_test.go | 109 ++++++++ .../blackbox/secrets_ssh_test.go | 139 ++++++++++ .../blackbox/secrets_transit_test.go | 158 +++++++++++ vault/external_tests/blackbox/smoke_test.go | 3 +- 26 files changed, 1468 insertions(+), 474 deletions(-) create mode 100644 enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh create mode 100644 vault/external_tests/blackbox/auth_userpass_test.go create mode 100644 vault/external_tests/blackbox/secrets_aws_test.go create mode 100644 vault/external_tests/blackbox/secrets_engines_external_test.go create mode 100644 vault/external_tests/blackbox/secrets_identity_test.go create mode 100644 vault/external_tests/blackbox/secrets_kmip_test.go create mode 100644 vault/external_tests/blackbox/secrets_kv_test.go create mode 100644 vault/external_tests/blackbox/secrets_ldap_test.go create mode 100644 vault/external_tests/blackbox/secrets_pki_test.go create mode 100644 vault/external_tests/blackbox/secrets_ssh_test.go create mode 100644 vault/external_tests/blackbox/secrets_transit_test.go diff --git a/.github/workflows/test-run-enos-scenario-matrix.yml b/.github/workflows/test-run-enos-scenario-matrix.yml index 971e526da7..153988e389 100644 --- a/.github/workflows/test-run-enos-scenario-matrix.yml +++ b/.github/workflows/test-run-enos-scenario-matrix.yml @@ -200,6 +200,10 @@ jobs: echo 'ENOS_VAR_verify_ldap_secrets_engine=false' echo 'ENOS_VAR_verify_log_secrets=true' } | tee -a "$GITHUB_ENV" + - uses: ./.github/actions/set-up-go + with: + github-token: ${{ steps.secrets.outputs.github-token }} + - uses: ./.github/actions/install-tools - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 with: # the Terraform wrapper will break Terraform execution in Enos because diff --git a/.github/workflows/test-run-enos-scenario.yml b/.github/workflows/test-run-enos-scenario.yml index 50fe1a3b2f..37a8861925 100644 --- a/.github/workflows/test-run-enos-scenario.yml +++ b/.github/workflows/test-run-enos-scenario.yml @@ -64,6 +64,8 @@ jobs: - uses: ./.github/actions/set-up-go with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} + # Install additional tools like gotestsum that are required for Enos scenarios + - uses: ./.github/actions/install-tools - name: Configure Git run: git config --global url."https://${{ secrets.ELEVATED_GITHUB_TOKEN }}:@github.com".insteadOf "https://github.com" - name: Set up node diff --git a/enos/enos-samples-ce-build.hcl b/enos/enos-samples-ce-build.hcl index d603bc1d67..afd4125c7b 100644 --- a/enos/enos-samples-ce-build.hcl +++ b/enos/enos-samples-ce-build.hcl @@ -24,6 +24,7 @@ sample "build_ce_linux_amd64_deb" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -68,6 +69,7 @@ sample "build_ce_linux_arm64_deb" { } } + subset "proxy" { matrix { arch = ["arm64"] @@ -112,6 +114,7 @@ sample "build_ce_linux_arm64_rpm" { } } + subset "proxy" { matrix { arch = ["arm64"] @@ -156,6 +159,7 @@ sample "build_ce_linux_amd64_rpm" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -206,6 +210,7 @@ sample "build_ce_linux_amd64_zip" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -250,6 +255,7 @@ sample "build_ce_linux_arm64_zip" { } } + subset "proxy" { matrix { arch = ["arm64"] diff --git a/enos/enos-samples-ce-release.hcl b/enos/enos-samples-ce-release.hcl index d0e111b5e2..0dfe24d79d 100644 --- a/enos/enos-samples-ce-release.hcl +++ b/enos/enos-samples-ce-release.hcl @@ -24,6 +24,7 @@ sample "release_ce_linux_amd64_deb" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -68,6 +69,7 @@ sample "release_ce_linux_arm64_deb" { } } + subset "proxy" { matrix { arch = ["arm64"] @@ -112,6 +114,7 @@ sample "release_ce_linux_arm64_rpm" { } } + subset "proxy" { matrix { arch = ["arm64"] @@ -156,6 +159,7 @@ sample "release_ce_linux_amd64_rpm" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -200,6 +204,7 @@ sample "release_ce_linux_amd64_zip" { } } + subset "proxy" { matrix { arch = ["amd64"] @@ -244,6 +249,7 @@ sample "release_ce_linux_arm64_zip" { } } + subset "proxy" { matrix { arch = ["arm64"] diff --git a/enos/enos-scenario-smoke-sdk.hcl b/enos/enos-scenario-smoke-sdk.hcl index 9962956a47..e8412f9500 100644 --- a/enos/enos-scenario-smoke-sdk.hcl +++ b/enos/enos-scenario-smoke-sdk.hcl @@ -70,6 +70,11 @@ scenario "smoke_sdk" { ip_version = ["6"] backend = ["consul"] } + + // smoke_sdk scenario cannot be run in CE + exclude { + edition = ["ce"] + } } terraform_cli = terraform_cli.default @@ -408,14 +413,16 @@ scenario "smoke_sdk" { // Define smoke test suite locals { smoke_tests = [ - "TestStepdownAndLeaderElection", "TestSecretsEngineCreate", + "TestSecretsEngineExternalCreate", "TestUnsealedStatus", "TestVaultVersion", "TestSecretsEngineRead", + "TestSecretsEngineExternalRead", "TestReplicationStatus", "TestUIAssets", - "TestSecretsEngineDelete" + "TestSecretsEngineDelete", + "TestSecretsEngineExternalDelete" ] // Add backend-specific tests @@ -432,18 +439,20 @@ scenario "smoke_sdk" { step "run_blackbox_tests" { description = "Run blackbox SDK smoke tests: ${join(", ", local.smoke_tests_with_backend)}" module = module.vault_run_blackbox_test - depends_on = [step.get_vault_cluster_ips] + depends_on = [step.get_vault_cluster_ips, step.set_up_external_integration_target] providers = { enos = local.enos_provider[matrix.distro] } variables { - leader_host = step.get_vault_cluster_ips.leader_host - leader_public_ip = step.get_vault_cluster_ips.leader_public_ip - vault_root_token = step.create_vault_cluster.root_token - test_names = local.smoke_tests_with_backend - test_package = "./vault/external_tests/blackbox" + leader_host = step.get_vault_cluster_ips.leader_host + leader_public_ip = step.get_vault_cluster_ips.leader_public_ip + vault_root_token = step.create_vault_cluster.root_token + test_names = local.smoke_tests_with_backend + test_package = "./vault/external_tests/blackbox" + integration_host_state = step.set_up_external_integration_target.state + vault_edition = matrix.edition } } diff --git a/enos/modules/set_up_external_integration_target/main.tf b/enos/modules/set_up_external_integration_target/main.tf index c24c230de7..1e9281036c 100755 --- a/enos/modules/set_up_external_integration_target/main.tf +++ b/enos/modules/set_up_external_integration_target/main.tf @@ -66,6 +66,26 @@ resource "enos_remote_exec" "setup_openldap" { } } +# Populate LDAP server with required users and organizational units +resource "enos_remote_exec" "populate_ldap" { + depends_on = [enos_remote_exec.setup_openldap] + + scripts = [abspath("${path.module}/scripts/populate-ldap.sh")] + + environment = { + LDAP_SERVER = local.ldap_server.host.private_ip + LDAP_PORT = local.ldap_server.port + LDAP_ADMIN_PW = local.ldap_server.admin_pw + LDAP_DOMAIN = local.ldap_server.domain + } + + transport = { + ssh = { + host = local.ldap_server.host.public_ip + } + } +} + # Creating KMIP Server using generic container script resource "enos_remote_exec" "create_kmip" { depends_on = [module.install_packages] diff --git a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh new file mode 100644 index 0000000000..146b4823d0 --- /dev/null +++ b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Copyright IBM Corp. 2025 +# SPDX-License-Identifier: BUSL-1.1 + +set -e + +fail() { + echo "$1" 1>&2 + exit 1 +} + +[[ -z "$LDAP_SERVER" ]] && fail "LDAP_SERVER env variable has not been set" +[[ -z "$LDAP_PORT" ]] && fail "LDAP_PORT env variable has not been set" +[[ -z "$LDAP_ADMIN_PW" ]] && fail "LDAP_ADMIN_PW env variable has not been set" +[[ -z "$LDAP_DOMAIN" ]] && fail "LDAP_DOMAIN env variable has not been set" + +echo "OpenLDAP: Checking for OpenLDAP Server Connection: ${LDAP_SERVER}:${LDAP_PORT}" +# Wait for LDAP server to be ready +sleep 10 + +# Extract domain components from LDAP_DOMAIN (e.g., "enos.com" -> "dc=enos,dc=com") +IFS='.' read -ra DOMAIN_PARTS <<< "$LDAP_DOMAIN" +DOMAIN_DN="" +for part in "${DOMAIN_PARTS[@]}"; do + if [[ -n "$DOMAIN_DN" ]]; then + DOMAIN_DN="${DOMAIN_DN},dc=${part}" + else + DOMAIN_DN="dc=${part}" + fi +done + +echo "OpenLDAP: Using domain DN: ${DOMAIN_DN}" +echo "OpenLDAP: Testing connection with admin credentials" + +# Test connection +ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base + +echo "OpenLDAP: Creating organizational units" +# Creating Users and Groups Org Units LDIF file +OU_LDIF="ou.ldif" +cat << EOF > ${OU_LDIF} +dn: ou=users,${DOMAIN_DN} +objectClass: organizationalUnit +ou: users + +dn: ou=groups,${DOMAIN_DN} +objectClass: organizationalUnit +ou: groups +EOF + +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f ${OU_LDIF} || echo "OUs may already exist" + +echo "OpenLDAP: Creating test users" +USER_LDIF="users.ldif" +cat << EOF > ${USER_LDIF} +# User: enos +dn: uid=enos,ou=users,${DOMAIN_DN} +objectClass: inetOrgPerson +sn: enos +cn: enos user +uid: enos +userPassword: ${LDAP_ADMIN_PW} + +# Static-role test user (for LDAP verification tests) +dn: uid=vault-static-user,ou=users,${DOMAIN_DN} +objectClass: inetOrgPerson +sn: vault-static-user +cn: Vault Static User +uid: vault-static-user +userPassword: ${LDAP_ADMIN_PW} + +# Service accounts for library tests +dn: uid=svc-account-1,ou=users,${DOMAIN_DN} +objectClass: inetOrgPerson +sn: svc-account-1 +cn: Service Account 1 +uid: svc-account-1 +userPassword: ${LDAP_ADMIN_PW} + +dn: uid=svc-account-2,ou=users,${DOMAIN_DN} +objectClass: inetOrgPerson +sn: svc-account-2 +cn: Service Account 2 +uid: svc-account-2 +userPassword: ${LDAP_ADMIN_PW} + +dn: uid=svc-delete,ou=users,${DOMAIN_DN} +objectClass: inetOrgPerson +sn: svc-delete +cn: Service Account Delete +uid: svc-delete +userPassword: ${LDAP_ADMIN_PW} +EOF + +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f ${USER_LDIF} || echo "Users may already exist" + +echo "LDAP population completed successfully." diff --git a/enos/modules/vault_run_blackbox_test/main.tf b/enos/modules/vault_run_blackbox_test/main.tf index 05dc3ed757..2076a3adcf 100644 --- a/enos/modules/vault_run_blackbox_test/main.tf +++ b/enos/modules/vault_run_blackbox_test/main.tf @@ -34,12 +34,31 @@ resource "enos_local_exec" "run_blackbox_test" { VAULT_ADDR = var.vault_addr != null ? var.vault_addr : "http://${var.leader_public_ip}:8200" VAULT_TEST_PACKAGE = var.test_package VAULT_TEST_MATRIX = length(var.test_names) > 0 ? local_file.test_matrix.filename : "" + VAULT_EDITION = var.vault_edition + # PATH and Go-related environment variables are inherited from the calling process }, var.vault_namespace != null ? { VAULT_NAMESPACE = var.vault_namespace - } : {}) + } : {}, local.ldap_environment + ) depends_on = [local_file.test_matrix] } +# Local variables for LDAP environment setup +locals { + # Extract LDAP configuration safely, defaulting to empty map if not available + ldap_config = try(var.integration_host_state.ldap, {}) + + # Convert domain (e.g., "enos.com") to DN format (e.g., "dc=enos,dc=com") + domain_dn = try(local.ldap_config.domain, "") != "" ? join(",", [for part in split(".", local.ldap_config.domain) : "dc=${part}"]) : "" + + # Set up LDAP environment variables when LDAP integration is available + ldap_environment = try(local.ldap_config.domain, "") != "" ? { + LDAP_SERVER = "ldap://${local.ldap_config.host.private_ip}:${local.ldap_config.port}" + LDAP_BIND_DN = "cn=admin,${local.domain_dn}" + LDAP_BIND_PASS = local.ldap_config.admin_pw + } : {} +} + # Extract information from the script output locals { json_file_path = try( diff --git a/enos/modules/vault_run_blackbox_test/scripts/run-test.sh b/enos/modules/vault_run_blackbox_test/scripts/run-test.sh index aa5d157e55..10bad74f98 100755 --- a/enos/modules/vault_run_blackbox_test/scripts/run-test.sh +++ b/enos/modules/vault_run_blackbox_test/scripts/run-test.sh @@ -19,12 +19,28 @@ echo "Checking required dependencies..." # Check if Go is installed if ! command -v go &> /dev/null; then - fail "Go is not installed or not in PATH. Please install Go to run tests." + echo "ERROR: Go is not installed or not found in PATH." + echo "" + echo "To resolve this issue:" + echo " • On a developer machine: Install Go from https://golang.org/dl/" + echo " • In CI: Ensure the setup-go action is configured properly" + echo " • If Go is installed elsewhere, add it to your PATH environment variable" + echo "" + fail "Go is required to run blackbox tests." fi +echo "Go version: $(go version)" + # Check if gotestsum is installed (required) if ! command -v gotestsum &> /dev/null; then - fail "gotestsum is not installed or not in PATH. Please install gotestsum: go install gotest.tools/gotestsum@latest" + echo "ERROR: gotestsum is not installed or not found in PATH." + echo "" + echo "To resolve this issue:" + echo " • Run 'make tools' to install required development tools" + echo " • Ensure GOPATH/bin is in your PATH environment variable" + echo " • Or manually install: go install gotest.tools/gotestsum@v1.13.0" + echo "" + fail "gotestsum is required to run blackbox tests." fi # Check if jq is available (needed for parsing test matrix) diff --git a/enos/modules/vault_run_blackbox_test/variables.tf b/enos/modules/vault_run_blackbox_test/variables.tf index 499b44473b..032d1ab391 100644 --- a/enos/modules/vault_run_blackbox_test/variables.tf +++ b/enos/modules/vault_run_blackbox_test/variables.tf @@ -41,3 +41,15 @@ variable "vault_namespace" { description = "The Vault namespace to operate in (for HCP environments). Optional." default = null } + +variable "integration_host_state" { + type = any + description = "The state from the external integration setup" + default = null +} + +variable "vault_edition" { + type = string + description = "The Vault edition (ce, ent, ent.hsm, ent.fips1402, ent.hsm.fips1402)" + default = "ent" +} diff --git a/enos/modules/verify_secrets_engines/modules/create/kv.tf b/enos/modules/verify_secrets_engines/modules/create/kv.tf index 6f004da958..38568153cf 100644 --- a/enos/modules/verify_secrets_engines/modules/create/kv.tf +++ b/enos/modules/verify_secrets_engines/modules/create/kv.tf @@ -61,9 +61,21 @@ resource "enos_remote_exec" "policy_write_kv_writer" { environment = { POLICY_NAME = local.kv_write_policy_name POLICY_CONFIG = <<-EOF - path "${local.kv_mount}/*" { + path "${local.kv_mount}/data/*" { capabilities = ["create", "update", "read", "delete", "list"] } + path "${local.kv_mount}/metadata/*" { + capabilities = ["read", "list", "delete"] + } + path "${local.kv_mount}/delete/*" { + capabilities = ["update"] + } + path "${local.kv_mount}/undelete/*" { + capabilities = ["update"] + } + path "${local.kv_mount}/destroy/*" { + capabilities = ["update"] + } EOF VAULT_ADDR = var.vault_addr VAULT_TOKEN = var.vault_root_token @@ -107,7 +119,8 @@ resource "enos_remote_exec" "kv_put_secret_test" { depends_on = [ enos_remote_exec.secrets_enable_kv_secret, enos_remote_exec.policy_write_kv_writer, - enos_remote_exec.identity_group_kv_writers + enos_remote_exec.identity_group_kv_writers, + enos_remote_exec.auth_login_testuser ] for_each = var.hosts diff --git a/enos/modules/verify_secrets_engines/scripts/ldap/setup.sh b/enos/modules/verify_secrets_engines/scripts/ldap/setup.sh index dfc52d12e3..7632c92b73 100755 --- a/enos/modules/verify_secrets_engines/scripts/ldap/setup.sh +++ b/enos/modules/verify_secrets_engines/scripts/ldap/setup.sh @@ -38,7 +38,7 @@ dn: ou=groups,dc=$LDAP_USERNAME,dc=com objectClass: organizationalUnit ou: groups EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${GROUP_LDIF} +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${GROUP_LDIF} || echo "OUs may already exist" ADMIN_LDIF="admin.ldif" cat << EOF > ${ADMIN_LDIF} @@ -49,7 +49,7 @@ cn: admin description: LDAP administrator userPassword: $LDAP_ADMIN_PW EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${ADMIN_LDIF} +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${ADMIN_LDIF} || echo "Admin may already exist" # Dedicated LDAP user for static role tests LDAP_STATIC_USERNAME="${LDAP_STATIC_USERNAME:-vault-static-user}" @@ -79,7 +79,7 @@ objectClass: groupOfNames cn: devs member: uid=$LDAP_USERNAME,ou=users,dc=$LDAP_USERNAME,dc=com EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${USER_LDIF} +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,dc=${LDAP_USERNAME},dc=com" -w "${LDAP_ADMIN_PW}" -f ${USER_LDIF} || echo "Users may already exist" echo "Vault: Adding vault-admins group and adding existing user to it" ADMIN_GROUP_LDIF="vault-admins.ldif" @@ -93,7 +93,7 @@ EOF ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" \ -D "cn=admin,dc=${LDAP_USERNAME},dc=com" \ -w "${LDAP_ADMIN_PW}" \ - -f ${ADMIN_GROUP_LDIF} + -f ${ADMIN_GROUP_LDIF} || echo "Admin group may already exist" echo "LDAP configuration completed successfully." "$binpath" auth enable "${MOUNT}" > /dev/null 2>&1 || echo "Warning: Vault ldap auth already enabled" diff --git a/sdk/helper/testcluster/blackbox/session_raft.go b/sdk/helper/testcluster/blackbox/session_raft.go index 32cc416125..f7890579e5 100644 --- a/sdk/helper/testcluster/blackbox/session_raft.go +++ b/sdk/helper/testcluster/blackbox/session_raft.go @@ -4,9 +4,11 @@ package blackbox import ( + "errors" "time" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/backoff" "github.com/stretchr/testify/require" ) @@ -182,147 +184,129 @@ func (s *Session) GetClusterNodeCount() int { // WaitForNewLeader waits for a new leader to be elected that is different from initialLeader // and for the cluster to become healthy. For single-node clusters, it just waits for the -// cluster to become healthy again after stepdown. +// cluster to become healthy again after stepdown. Uses reasonable timeouts to detect race conditions early. func (s *Session) WaitForNewLeader(initialLeader string, timeoutSeconds int) { s.t.Helper() + // Use reasonable timeout - if it takes more than a few seconds, there's likely a race condition + if timeoutSeconds > 10 { + s.t.Logf("Warning: timeout of %d seconds is quite high, consider investigating potential race conditions", timeoutSeconds) + } + // Check cluster size to handle single-node case nodeCount := s.GetClusterNodeCount() if nodeCount <= 1 { s.t.Logf("Single-node cluster detected, waiting for cluster to recover after stepdown...") - // For single-node clusters, just wait for the same leader to come back and be healthy - timeout := time.After(time.Duration(timeoutSeconds) * time.Second) - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() + // Use backoff helper for single-node recovery + b := backoff.NewBackoff(20, 100*time.Millisecond, 1*time.Second) // Max ~10 seconds with backoff - for { - select { - case <-timeout: - s.t.Fatalf("Timeout waiting for single-node cluster to recover after %d seconds", timeoutSeconds) - case <-ticker.C: - // Check if cluster is healthy again - secret, err := s.WithRootNamespace(func() (*api.Secret, error) { - return s.Client.Logical().Read("sys/storage/raft/autopilot/state") - }) - if err != nil { - s.t.Logf("Error reading autopilot state: %v, retrying...", err) - continue - } - - if secret == nil { - s.t.Logf("No autopilot state returned, retrying...") - continue - } - - healthy, ok := secret.Data["healthy"].(bool) - if !ok { - s.t.Logf("Autopilot healthy status not found, retrying...") - continue - } - - if healthy { - s.t.Log("Single-node cluster has recovered and is healthy") - return - } else { - s.t.Logf("Single-node cluster not yet healthy, waiting...") - } + err := b.Retry(func() error { + secret, err := s.WithRootNamespace(func() (*api.Secret, error) { + return s.Client.Logical().Read("sys/storage/raft/autopilot/state") + }) + if err != nil { + return err } + if secret == nil { + return errors.New("no autopilot state returned") + } + + healthy, ok := secret.Data["healthy"].(bool) + if !ok { + return errors.New("autopilot healthy status not found") + } + if !healthy { + return errors.New("cluster not yet healthy") + } + + return nil + }) + if err != nil { + s.t.Fatalf("Single-node cluster failed to recover: %v", err) } + + s.t.Log("Single-node cluster has recovered and is healthy") + return } // Multi-node cluster logic - wait for actual leader change - timeout := time.After(time.Duration(timeoutSeconds) * time.Second) - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() + s.t.Logf("Multi-node cluster detected, waiting for new leader election...") - newLeaderFound := false + // Phase 1: Wait for new leader (should be fast) + leaderBackoff := backoff.NewBackoff(20, 100*time.Millisecond, 500*time.Millisecond) // Max ~5 seconds var currentLeader string - for { - select { - case <-timeout: - if newLeaderFound { - s.t.Fatalf("Timeout waiting for cluster to become healthy after %d seconds (new leader: %s)", timeoutSeconds, currentLeader) - } else { - s.t.Fatalf("Timeout waiting for new leader election after %d seconds", timeoutSeconds) - } - case <-ticker.C: - // First, check if a new leader has been elected - if !newLeaderFound { - secret, err := s.WithRootNamespace(func() (*api.Secret, error) { - return s.Client.Logical().Read("sys/leader") - }) - if err != nil { - s.t.Logf("Error reading leader status: %v, retrying...", err) - continue - } - - if secret == nil { - s.t.Logf("No leader data returned, retrying...") - continue - } - - leaderAddress, ok := secret.Data["leader_address"].(string) - if !ok || leaderAddress == "" { - s.t.Logf("No leader address found, retrying...") - continue - } - - if leaderAddress != initialLeader { - s.t.Logf("New leader elected: %s (was: %s)", leaderAddress, initialLeader) - currentLeader = leaderAddress - newLeaderFound = true - } else { - s.t.Logf("Still waiting for new leader, current: %s", leaderAddress) - continue - } - } - - // Once we have a new leader, wait for cluster to be healthy - if newLeaderFound { - secret, err := s.WithRootNamespace(func() (*api.Secret, error) { - return s.Client.Logical().Read("sys/storage/raft/autopilot/state") - }) - if err != nil { - s.t.Logf("Error reading autopilot state: %v, retrying...", err) - continue - } - - if secret == nil { - s.t.Logf("No autopilot state returned, retrying...") - continue - } - - healthy, ok := secret.Data["healthy"].(bool) - if !ok { - s.t.Logf("Autopilot healthy status not found, retrying...") - continue - } - - if healthy { - s.t.Logf("Cluster is now healthy with new leader: %s", currentLeader) - return - } else { - s.t.Logf("Cluster not yet healthy, waiting...") - } - } + err := leaderBackoff.Retry(func() error { + secret, err := s.WithRootNamespace(func() (*api.Secret, error) { + return s.Client.Logical().Read("sys/leader") + }) + if err != nil { + return err } + if secret == nil { + return errors.New("no leader data returned") + } + + leaderAddress, ok := secret.Data["leader_address"].(string) + if !ok || leaderAddress == "" { + return errors.New("no leader address found") + } + + if leaderAddress == initialLeader { + return errors.New("still waiting for new leader") + } + + currentLeader = leaderAddress + return nil + }) + if err != nil { + s.t.Fatalf("Failed to elect new leader: %v", err) } + + s.t.Logf("New leader elected: %s (was: %s)", currentLeader, initialLeader) + + // Phase 2: Wait for cluster health (should also be fast) + healthBackoff := backoff.NewBackoff(20, 100*time.Millisecond, 500*time.Millisecond) // Max ~5 seconds + + err = healthBackoff.Retry(func() error { + secret, err := s.WithRootNamespace(func() (*api.Secret, error) { + return s.Client.Logical().Read("sys/storage/raft/autopilot/state") + }) + if err != nil { + return err + } + if secret == nil { + return errors.New("no autopilot state returned") + } + + healthy, ok := secret.Data["healthy"].(bool) + if !ok { + return errors.New("autopilot healthy status not found") + } + if !healthy { + return errors.New("cluster not yet healthy") + } + + return nil + }) + if err != nil { + s.t.Fatalf("Cluster failed to become healthy with new leader: %v", err) + } + + s.t.Logf("Cluster is now healthy with new leader: %s", currentLeader) } // AssertClusterHealthy verifies that the cluster is healthy, with fallback for managed environments // like HCP where raft APIs may not be accessible. This is the recommended method for general -// cluster health checks in blackbox tests. It includes retry logic for Docker environments -// where the cluster may not be immediately ready. +// cluster health checks in blackbox tests. Uses backoff helper for reasonable retry logic. func (s *Session) AssertClusterHealthy() { s.t.Helper() - // For Docker environments, wait for the cluster to be ready with retry logic - maxRetries := 30 - retryDelay := 2 * time.Second + // Use backoff helper for cluster readiness checks + b := backoff.NewBackoff(15, 200*time.Millisecond, 2*time.Second) // Max ~15 seconds with backoff - for attempt := 1; attempt <= maxRetries; attempt++ { + err := b.Retry(func() error { // Try raft-based health check first (works for self-managed clusters) secret, err := s.WithRootNamespace(func() (*api.Secret, error) { return s.Client.Logical().Read("sys/storage/raft/autopilot/state") @@ -333,16 +317,9 @@ func (s *Session) AssertClusterHealthy() { if healthy, ok := secret.Data["healthy"].(bool); ok && healthy { // Raft API is available and healthy, use full raft health check s.AssertRaftClusterHealthy() - return + return nil } else if ok && !healthy { - // Raft API available but not healthy yet, retry if we have attempts left - if attempt < maxRetries { - s.t.Logf("Cluster not yet healthy (attempt %d/%d), waiting %v...", attempt, maxRetries, retryDelay) - time.Sleep(retryDelay) - continue - } else { - s.t.Fatalf("Cluster failed to become healthy after %d attempts", maxRetries) - } + return errors.New("cluster not yet healthy according to autopilot") } } @@ -351,41 +328,21 @@ func (s *Session) AssertClusterHealthy() { return s.Client.Logical().Read("sys/seal-status") }) if err != nil { - if attempt < maxRetries { - s.t.Logf("Failed to read seal status (attempt %d/%d): %v, retrying in %v...", attempt, maxRetries, err, retryDelay) - time.Sleep(retryDelay) - continue - } - require.NoError(s.t, err, "Failed to read seal status - cluster may be unreachable") + return err } if sealStatus == nil { - if attempt < maxRetries { - s.t.Logf("Seal status response was nil (attempt %d/%d), retrying in %v...", attempt, maxRetries, retryDelay) - time.Sleep(retryDelay) - continue - } - require.NotNil(s.t, sealStatus, "Seal status response was nil") + return errors.New("seal status response was nil") } // Verify cluster is unsealed sealed, ok := sealStatus.Data["sealed"].(bool) if !ok { - if attempt < maxRetries { - s.t.Logf("Could not determine seal status (attempt %d/%d), retrying in %v...", attempt, maxRetries, retryDelay) - time.Sleep(retryDelay) - continue - } - require.True(s.t, ok, "Could not determine seal status") + return errors.New("could not determine seal status") } if sealed { - if attempt < maxRetries { - s.t.Logf("Cluster is sealed (attempt %d/%d), retrying in %v...", attempt, maxRetries, retryDelay) - time.Sleep(retryDelay) - continue - } - require.False(s.t, sealed, "Cluster is sealed") + return errors.New("cluster is sealed") } // If we get here, cluster is unsealed and responsive @@ -394,6 +351,9 @@ func (s *Session) AssertClusterHealthy() { } else { s.t.Log("Cluster health verified (managed environment - raft APIs not accessible)") } - return + return nil + }) + if err != nil { + s.t.Fatalf("Cluster health check failed: %v", err) } } diff --git a/vault/external_tests/blackbox/auth_engines_test.go b/vault/external_tests/blackbox/auth_engines_test.go index 7cc91b5b8a..9e9483ad90 100644 --- a/vault/external_tests/blackbox/auth_engines_test.go +++ b/vault/external_tests/blackbox/auth_engines_test.go @@ -6,7 +6,6 @@ package blackbox import ( "testing" - "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" ) @@ -17,12 +16,10 @@ func TestAuthEngineCreate(t *testing.T) { // Verify we have a healthy cluster first v.AssertClusterHealthy() - // Test userpass auth engine t.Run("UserpassAuth", func(t *testing.T) { testUserpassAuthCreate(t, v) }) - // Stub out remaining auth engine creation tests t.Run("LDAPAuth", func(t *testing.T) { t.Skip("LDAP auth engine create test - implementation pending") }) @@ -55,12 +52,10 @@ func TestAuthEngineRead(t *testing.T) { // Verify we have a healthy cluster first v.AssertClusterHealthy() - // Test userpass auth engine read operations t.Run("UserpassAuth", func(t *testing.T) { testUserpassAuthRead(t, v) }) - // Stub out remaining auth engine read tests t.Run("LDAPAuth", func(t *testing.T) { t.Skip("LDAP auth engine read test - implementation pending") }) @@ -93,12 +88,10 @@ func TestAuthEngineDelete(t *testing.T) { // Verify we have a healthy cluster first v.AssertClusterHealthy() - // Test userpass auth engine delete operations t.Run("UserpassAuth", func(t *testing.T) { testUserpassAuthDelete(t, v) }) - // Stub out remaining auth engine delete tests t.Run("LDAPAuth", func(t *testing.T) { t.Skip("LDAP auth engine delete test - implementation pending") }) @@ -123,96 +116,3 @@ func TestAuthEngineDelete(t *testing.T) { t.Skip("Cert auth engine delete test - implementation pending") }) } - -// Userpass Auth Engine Test Implementation Functions - -func testUserpassAuthCreate(t *testing.T, v *blackbox.Session) { - // Create a policy for our test user - userPolicy := ` - path "*" { - capabilities = ["read", "list"] - } - ` - - // Use common utility to setup userpass auth - userClient := SetupUserpassAuth(v, "testuser", "passtestuser1", "reguser", userPolicy) - - // Verify the auth method was enabled by reading auth mounts - authMounts := v.MustRead("sys/auth") - if authMounts.Data == nil { - t.Fatal("Could not read auth mounts") - } - - // Verify userpass auth method is enabled - if userpassAuth, ok := authMounts.Data["userpass/"]; !ok { - t.Fatal("userpass auth method not found in sys/auth") - } else { - userpassMap := userpassAuth.(map[string]any) - if userpassMap["type"] != "userpass" { - t.Fatalf("Expected userpass auth method type to be 'userpass', got: %v", userpassMap["type"]) - } - } - - // Test that the user session was created successfully - if userClient != nil { - // Login successful, verify we can read basic info - tokenInfo := userClient.MustRead("auth/token/lookup-self") - if tokenInfo.Data == nil { - t.Fatal("Expected user to be able to read own token info after login") - } - t.Log("Userpass login test successful") - } else { - t.Log("Userpass login not available (likely managed environment)") - } - - t.Log("Successfully created userpass auth with user: testuser") -} - -func testUserpassAuthRead(t *testing.T, v *blackbox.Session) { - // Use common utility to setup userpass auth with default policy - userClient := SetupUserpassAuth(v, "readuser", "readpass123", "default", "") - - // Read the user configuration - userConfig := v.MustRead("auth/userpass/users/readuser") - if userConfig.Data == nil { - t.Fatal("Expected to read user configuration") - } - - // Test that the user session was created successfully - if userClient != nil { - // Login successful, verify we can read basic info - tokenInfo := userClient.MustRead("auth/token/lookup-self") - if tokenInfo.Data == nil { - t.Fatal("Expected user to be able to read own token info after login") - } - t.Log("Userpass login test successful") - } else { - t.Log("Userpass login not available (likely managed environment)") - } - - t.Log("Successfully read userpass auth config for user: readuser") -} - -func testUserpassAuthDelete(t *testing.T, v *blackbox.Session) { - // Enable userpass auth method with unique mount for delete test - v.MustEnableAuth("userpass-delete", &api.EnableAuthOptions{Type: "userpass"}) - - // Create a user to delete - userName := "deleteuser" - userPassword := "deletepass123" - v.MustWrite("auth/userpass-delete/users/"+userName, map[string]any{ - "password": userPassword, - "policies": "default", - }) - - // Verify the user exists - userConfig := v.MustRead("auth/userpass-delete/users/" + userName) - if userConfig.Data == nil { - t.Fatal("Expected user to exist before deletion") - } - - // Delete the user - v.MustWrite("auth/userpass-delete/users/"+userName, nil) - - t.Logf("Successfully deleted userpass auth user: %s", userName) -} diff --git a/vault/external_tests/blackbox/auth_userpass_test.go b/vault/external_tests/blackbox/auth_userpass_test.go new file mode 100644 index 0000000000..efdcee300f --- /dev/null +++ b/vault/external_tests/blackbox/auth_userpass_test.go @@ -0,0 +1,105 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testUserpassAuthCreate tests userpass auth engine creation +func testUserpassAuthCreate(t *testing.T, v *blackbox.Session) { + // Create a policy for our test user + userPolicy := ` + path "*" { + capabilities = ["read", "list"] + } + ` + + // Use common utility to setup userpass auth + userClient := SetupUserpassAuth(v, "testuser", "passtestuser1", "reguser", userPolicy) + + // Verify the auth method was enabled by reading auth mounts + authMounts := v.MustRead("sys/auth") + if authMounts.Data == nil { + t.Fatal("Could not read auth mounts") + } + + // Verify userpass auth method is enabled + if userpassAuth, ok := authMounts.Data["userpass/"]; !ok { + t.Fatal("userpass auth method not found in sys/auth") + } else { + userpassMap := userpassAuth.(map[string]any) + if userpassMap["type"] != "userpass" { + t.Fatalf("Expected userpass auth method type to be 'userpass', got: %v", userpassMap["type"]) + } + } + + // Test that the user session was created successfully + if userClient != nil { + // Login successful, verify we can read basic info + tokenInfo := userClient.MustRead("auth/token/lookup-self") + if tokenInfo.Data == nil { + t.Fatal("Expected user to be able to read own token info after login") + } + t.Log("Userpass login test successful") + } else { + t.Log("Userpass login not available (likely managed environment)") + } + + t.Log("Successfully created userpass auth with user: testuser") +} + +// testUserpassAuthRead tests userpass auth engine read operations +func testUserpassAuthRead(t *testing.T, v *blackbox.Session) { + // Use common utility to setup userpass auth with default policy + userClient := SetupUserpassAuth(v, "readuser", "readpass123", "default", "") + + // Read the user configuration + userConfig := v.MustRead("auth/userpass/users/readuser") + if userConfig.Data == nil { + t.Fatal("Expected to read user configuration") + } + + // Test that the user session was created successfully + if userClient != nil { + // Login successful, verify we can read basic info + tokenInfo := userClient.MustRead("auth/token/lookup-self") + if tokenInfo.Data == nil { + t.Fatal("Expected user to be able to read own token info after login") + } + t.Log("Userpass login test successful") + } else { + t.Log("Userpass login not available (likely managed environment)") + } + + t.Log("Successfully read userpass auth config for user: readuser") +} + +// testUserpassAuthDelete tests userpass auth engine delete operations +func testUserpassAuthDelete(t *testing.T, v *blackbox.Session) { + // Enable userpass auth method with unique mount for delete test + v.MustEnableAuth("userpass-delete", &api.EnableAuthOptions{Type: "userpass"}) + + // Create a user to delete + userName := "deleteuser" + userPassword := "deletepass123" + v.MustWrite("auth/userpass-delete/users/"+userName, map[string]any{ + "password": userPassword, + "policies": "default", + }) + + // Verify the user exists + userConfig := v.MustRead("auth/userpass-delete/users/" + userName) + if userConfig.Data == nil { + t.Fatal("Expected user to exist before deletion") + } + + // Delete the user + v.MustWrite("auth/userpass-delete/users/"+userName, nil) + + t.Logf("Successfully deleted userpass auth user: %s", userName) +} diff --git a/vault/external_tests/blackbox/secrets_aws_test.go b/vault/external_tests/blackbox/secrets_aws_test.go new file mode 100644 index 0000000000..040fe0a13e --- /dev/null +++ b/vault/external_tests/blackbox/secrets_aws_test.go @@ -0,0 +1,116 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "os" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testAWSSecretsCreate tests AWS secrets engine creation +func testAWSSecretsCreate(t *testing.T, v *blackbox.Session) { + // Check if AWS credentials are available + accessKey := os.Getenv("AWS_ACCESS_KEY_ID") + secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") + + if accessKey == "" || secretKey == "" { + t.Skip("AWS credentials not available - skipping AWS secrets engine test") + } + + // Enable AWS secrets engine + v.MustEnableSecretsEngine("aws-create", &api.MountInput{Type: "aws"}) + + // Configure AWS secrets engine with root credentials + v.MustWrite("aws-create/config/root", map[string]any{ + "access_key": accessKey, + "secret_key": secretKey, + "region": "us-east-1", + }) + + // Create a role for generating credentials + v.MustWrite("aws-create/roles/test-role", map[string]any{ + "credential_type": "iam_user", + "policy_document": `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:Describe*", + "Resource": "*" + } + ] + }`, + }) + + // Verify role was created by reading it + roleResp := v.MustRead("aws-create/roles/test-role") + if roleResp.Data == nil { + t.Fatal("Expected to read AWS role configuration") + } + + t.Log("Successfully created AWS secrets engine with role") +} + +// testAWSSecretsRead tests AWS secrets engine read operations +func testAWSSecretsRead(t *testing.T, v *blackbox.Session) { + // Check if AWS credentials are available + accessKey := os.Getenv("AWS_ACCESS_KEY_ID") + secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") + + if accessKey == "" || secretKey == "" { + t.Skip("AWS credentials not available - skipping AWS secrets engine test") + } + + // Enable AWS secrets engine + v.MustEnableSecretsEngine("aws-read", &api.MountInput{Type: "aws"}) + + // Configure AWS secrets engine + v.MustWrite("aws-read/config/root", map[string]any{ + "access_key": accessKey, + "secret_key": secretKey, + "region": "us-west-2", + }) + + // Create a role + v.MustWrite("aws-read/roles/read-role", map[string]any{ + "credential_type": "iam_user", + "policy_document": `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:ListBucket", + "Resource": "*" + } + ] + }`, + }) + + // Read the role configuration + roleResp := v.MustRead("aws-read/roles/read-role") + if roleResp.Data == nil { + t.Fatal("Expected to read AWS role configuration") + } + + // Verify role properties + assertions := v.AssertSecret(roleResp) + assertions.Data(). + HasKey("credential_type", "iam_user"). + HasKeyExists("policy_document") + + // Read root configuration (should not expose credentials) + configResp := v.MustRead("aws-read/config/root") + if configResp.Data == nil { + t.Fatal("Expected to read AWS root configuration") + } + + // Verify config properties (credentials should not be returned) + configAssertions := v.AssertSecret(configResp) + configAssertions.Data().HasKey("region", "us-west-2") + + t.Log("Successfully read AWS secrets engine configuration") +} diff --git a/vault/external_tests/blackbox/secrets_engines_external_test.go b/vault/external_tests/blackbox/secrets_engines_external_test.go new file mode 100644 index 0000000000..baba4e3a9f --- /dev/null +++ b/vault/external_tests/blackbox/secrets_engines_external_test.go @@ -0,0 +1,63 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// TestSecretsEngineExternalCreate tests creation/setup of secrets engines that require external infrastructure +// These tests are excluded from cloud environments (HCP/Docker) which don't have access to AWS, LDAP servers, etc. +func TestSecretsEngineExternalCreate(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + t.Run("AWSSecrets", func(t *testing.T) { + testAWSSecretsCreate(t, v) + }) + + t.Run("LDAPSecrets", func(t *testing.T) { + testLDAPSecretsCreate(t, v) + }) + + t.Run("KMIPSecrets", func(t *testing.T) { + testKMIPSecretsCreate(t, v) + }) +} + +// TestSecretsEngineExternalRead tests read operations for secrets engines that require external infrastructure +func TestSecretsEngineExternalRead(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + t.Run("AWSSecrets", func(t *testing.T) { + testAWSSecretsRead(t, v) + }) + + t.Run("LDAPSecrets", func(t *testing.T) { + testLDAPSecretsRead(t, v) + }) + + t.Run("KMIPSecrets", func(t *testing.T) { + testKMIPSecretsRead(t, v) + }) +} + +// TestSecretsEngineExternalDelete tests delete operations for secrets engines that require external infrastructure +func TestSecretsEngineExternalDelete(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + t.Run("LDAPSecrets", func(t *testing.T) { + testLDAPSecretsDelete(t, v) + }) +} diff --git a/vault/external_tests/blackbox/secrets_engines_test.go b/vault/external_tests/blackbox/secrets_engines_test.go index 72f1d7edd3..896ce5582c 100644 --- a/vault/external_tests/blackbox/secrets_engines_test.go +++ b/vault/external_tests/blackbox/secrets_engines_test.go @@ -6,54 +6,31 @@ package blackbox import ( "testing" - "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" ) // TestSecretsEngineCreate tests creation/setup of various secrets engines +// This test covers secrets engines that work in cloud environments (HCP/Docker) func TestSecretsEngineCreate(t *testing.T) { v := blackbox.New(t) // Verify we have a healthy cluster first v.AssertClusterHealthy() - // KV secrets engine tests are now in kvv2_test.go - just test basic enablement here t.Run("KVSecrets", func(t *testing.T) { - SetupKVEngine(v, "kv-create") - // Write and read test data to verify engine works - v.MustWriteKV2("kv-create", "test/path", StandardKVData) - secret := v.MustReadKV2("kv-create", "test/path") - AssertKVData(t, v, secret, StandardKVData) - t.Log("Successfully created and tested KV secrets engine") + testKVSecretsCreate(t, v) }) - // Stub out remaining secret engine creation tests t.Run("PKISecrets", func(t *testing.T) { - t.Skip("PKI secrets engine create test - implementation pending") + testPKISecretsCreate(t, v) }) t.Run("SSHSecrets", func(t *testing.T) { - t.Skip("SSH secrets engine create test - implementation pending") + testSSHSecretsCreate(t, v) }) t.Run("IdentitySecrets", func(t *testing.T) { - t.Skip("Identity secrets engine create test - implementation pending") - }) - - t.Run("AWSSecrets", func(t *testing.T) { - t.Skip("AWS secrets engine create test - implementation pending") - }) - - t.Run("LDAPSecrets", func(t *testing.T) { - t.Skip("LDAP secrets engine create test - implementation pending") - }) - - t.Run("KMIPSecrets", func(t *testing.T) { - t.Skip("KMIP secrets engine create test - implementation pending") - }) - - t.Run("DatabaseSecrets", func(t *testing.T) { - t.Skip("Database secrets engine create test - implementation pending") + testIdentitySecretsCreate(t, v) }) t.Run("TransitSecrets", func(t *testing.T) { @@ -62,228 +39,59 @@ func TestSecretsEngineCreate(t *testing.T) { } // TestSecretsEngineRead tests read operations for various secrets engines +// This test covers secrets engines that work in cloud environments (HCP/Docker) func TestSecretsEngineRead(t *testing.T) { v := blackbox.New(t) // Verify we have a healthy cluster first v.AssertClusterHealthy() - // KV read tests are in kvv2_test.go - test basic read functionality here t.Run("KVSecrets", func(t *testing.T) { - SetupKVEngine(v, "kv-read") - v.MustWriteKV2("kv-read", "read/test", AltKVData) - secret := v.MustReadKV2("kv-read", "read/test") - AssertKVData(t, v, secret, AltKVData) - t.Log("Successfully read KV secrets engine data") + testKVSecretsRead(t, v) }) - // Stub out remaining secret engine read tests t.Run("PKISecrets", func(t *testing.T) { - t.Skip("PKI secrets engine read test - implementation pending") + testPKISecretsRead(t, v) }) t.Run("SSHSecrets", func(t *testing.T) { - t.Skip("SSH secrets engine read test - implementation pending") + testSSHSecretsRead(t, v) }) t.Run("IdentitySecrets", func(t *testing.T) { - t.Skip("Identity secrets engine read test - implementation pending") - }) - - t.Run("AWSSecrets", func(t *testing.T) { - t.Skip("AWS secrets engine read test - implementation pending") - }) - - t.Run("LDAPSecrets", func(t *testing.T) { - t.Skip("LDAP secrets engine read test - implementation pending") - }) - - t.Run("KMIPSecrets", func(t *testing.T) { - t.Skip("KMIP secrets engine read test - implementation pending") - }) - - t.Run("DatabaseSecrets", func(t *testing.T) { - t.Skip("Database secrets engine read test - implementation pending") + testIdentitySecretsRead(t, v) }) t.Run("TransitSecrets", func(t *testing.T) { - t.Skip("Transit secrets engine read test - implementation pending") + testTransitSecretsRead(t, v) }) } // TestSecretsEngineDelete tests delete operations for various secrets engines +// This test covers secrets engines that work in cloud environments (HCP/Docker) func TestSecretsEngineDelete(t *testing.T) { v := blackbox.New(t) // Verify we have a healthy cluster first v.AssertClusterHealthy() - // KV delete tests are in kvv2_test.go - test basic delete functionality here t.Run("KVSecrets", func(t *testing.T) { - SetupKVEngine(v, "kv-delete") - - // Write test data - v.MustWriteKV2("kv-delete", "delete/test", StandardKVData) - - // Verify it exists - secret := v.MustReadKV2("kv-delete", "delete/test") - AssertKVData(t, v, secret, StandardKVData) - - // Delete using KV v2 delete endpoint - v.MustWrite("kv-delete/delete/delete/test", map[string]any{ - "versions": []int{1}, - }) - - t.Log("Successfully deleted KV secrets engine data") + testKVSecretsDelete(t, v) }) - // Stub out remaining secret engine delete tests t.Run("PKISecrets", func(t *testing.T) { - t.Skip("PKI secrets engine delete test - implementation pending") + testPKISecretsDelete(t, v) }) t.Run("SSHSecrets", func(t *testing.T) { - t.Skip("SSH secrets engine delete test - implementation pending") + testSSHSecretsDelete(t, v) }) t.Run("IdentitySecrets", func(t *testing.T) { - t.Skip("Identity secrets engine delete test - implementation pending") - }) - - t.Run("AWSSecrets", func(t *testing.T) { - t.Skip("AWS secrets engine delete test - implementation pending") - }) - - t.Run("LDAPSecrets", func(t *testing.T) { - t.Skip("LDAP secrets engine delete test - implementation pending") - }) - - t.Run("KMIPSecrets", func(t *testing.T) { - t.Skip("KMIP secrets engine delete test - implementation pending") - }) - - t.Run("DatabaseSecrets", func(t *testing.T) { - t.Skip("Database secrets engine delete test - implementation pending") + testIdentitySecretsDelete(t, v) }) t.Run("TransitSecrets", func(t *testing.T) { testTransitSecretsDelete(t, v) }) } - -// Transit Secrets Engine Test Implementation Functions - -func testTransitSecretsCreate(t *testing.T, v *blackbox.Session) { - // Enable transit secrets engine - v.MustEnableSecretsEngine("transit", &api.MountInput{Type: "transit"}) - - // Create an encryption key - keyName := "test-key" - v.MustWrite("transit/keys/"+keyName, map[string]any{ - "type": "aes256-gcm96", - }) - - // Verify the key was created by reading it - keyInfo := v.MustRead("transit/keys/" + keyName) - if keyInfo.Data == nil { - t.Fatal("Expected to read key configuration") - } - - // Verify key type - if keyType, ok := keyInfo.Data["type"]; !ok || keyType != "aes256-gcm96" { - t.Fatalf("Expected key type 'aes256-gcm96', got: %v", keyInfo.Data["type"]) - } - - // Test encryption - plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // base64 encoded "the quick brown fox" - encryptResp := v.MustWrite("transit/encrypt/"+keyName, map[string]any{ - "plaintext": plaintext, - }) - - if encryptResp.Data == nil || encryptResp.Data["ciphertext"] == nil { - t.Fatal("Expected ciphertext in encryption response") - } - - ciphertext := encryptResp.Data["ciphertext"].(string) - t.Logf("Encrypted ciphertext: %s", ciphertext[:20]+"...") - - // Test decryption - decryptResp := v.MustWrite("transit/decrypt/"+keyName, map[string]any{ - "ciphertext": ciphertext, - }) - - if decryptResp.Data == nil || decryptResp.Data["plaintext"] == nil { - t.Fatal("Expected plaintext in decryption response") - } - - decryptedText := decryptResp.Data["plaintext"].(string) - if decryptedText != plaintext { - t.Fatalf("Decrypted text doesn't match original. Expected: %s, Got: %s", plaintext, decryptedText) - } - - t.Log("Successfully created transit secrets engine and tested encryption/decryption") -} - -func testTransitSecretsRead(t *testing.T, v *blackbox.Session) { - // Enable transit secrets engine with unique mount - v.MustEnableSecretsEngine("transit-read", &api.MountInput{Type: "transit"}) - - // Create an encryption key - keyName := "read-test-key" - v.MustWrite("transit-read/keys/"+keyName, map[string]any{ - "type": "aes256-gcm96", - "exportable": false, - }) - - // Read the key configuration - keyInfo := v.MustRead("transit-read/keys/" + keyName) - if keyInfo.Data == nil { - t.Fatal("Expected to read key configuration") - } - - // Verify key properties - assertions := v.AssertSecret(keyInfo) - assertions.Data(). - HasKey("type", "aes256-gcm96"). - HasKey("exportable", false). - HasKeyExists("keys"). - HasKeyExists("latest_version") - - t.Log("Successfully read transit secrets engine key configuration") -} - -func testTransitSecretsDelete(t *testing.T, v *blackbox.Session) { - // Enable transit secrets engine with unique mount - v.MustEnableSecretsEngine("transit-delete", &api.MountInput{Type: "transit"}) - - // Create an encryption key - keyName := "delete-test-key" - v.MustWrite("transit-delete/keys/"+keyName, map[string]any{ - "type": "aes256-gcm96", - }) - - // Verify the key exists - keyInfo := v.MustRead("transit-delete/keys/" + keyName) - if keyInfo.Data == nil { - t.Fatal("Expected key to exist before deletion") - } - - // Configure the key to allow deletion (transit keys require this) - v.MustWrite("transit-delete/keys/"+keyName+"/config", map[string]any{ - "deletion_allowed": true, - }) - - // Delete the key - _, err := v.Client.Logical().Delete("transit-delete/keys/" + keyName) - if err != nil { - t.Fatalf("Failed to delete transit key: %v", err) - } - - // Verify the key is deleted by attempting to read it - readSecret, err := v.Client.Logical().Read("transit-delete/keys/" + keyName) - if err == nil && readSecret != nil { - t.Fatal("Expected key to be deleted, but it still exists") - } - - t.Logf("Successfully deleted transit key: %s", keyName) -} diff --git a/vault/external_tests/blackbox/secrets_identity_test.go b/vault/external_tests/blackbox/secrets_identity_test.go new file mode 100644 index 0000000000..b1acbf1f9b --- /dev/null +++ b/vault/external_tests/blackbox/secrets_identity_test.go @@ -0,0 +1,132 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testIdentitySecretsCreate tests Identity secrets engine creation +func testIdentitySecretsCreate(t *testing.T, v *blackbox.Session) { + // Identity is a built-in secrets engine, no need to enable it + // Create an entity + entityResp := v.MustWrite("identity/entity", map[string]any{ + "name": "test-entity", + "policies": []string{"default"}, + "metadata": map[string]string{ + "team": "engineering", + "env": "test", + }, + }) + + if entityResp.Data == nil { + t.Fatal("Expected entity creation response") + } + + // Verify entity was created + assertions := v.AssertSecret(entityResp) + assertions.Data().HasKeyExists("id") + + entityID := entityResp.Data["id"].(string) + + // Create an entity alias (requires an auth mount) + v.MustEnableAuth("userpass-identity", &api.EnableAuthOptions{Type: "userpass"}) + + // Get the accessor for the auth mount + authsResp := v.MustRead("sys/auth") + if authsResp.Data == nil { + t.Fatal("Expected to read auth mounts") + } + + var accessor string + if authData, ok := authsResp.Data["userpass-identity/"]; ok { + if authMap, ok := authData.(map[string]any); ok { + accessor = authMap["accessor"].(string) + } + } + + if accessor == "" { + t.Fatal("Failed to get auth mount accessor") + } + + // Create entity alias + aliasResp := v.MustWrite("identity/entity-alias", map[string]any{ + "name": "test-user", + "canonical_id": entityID, + "mount_accessor": accessor, + }) + + if aliasResp.Data == nil { + t.Fatal("Expected entity alias creation response") + } + + aliasAssertions := v.AssertSecret(aliasResp) + aliasAssertions.Data().HasKeyExists("id") + + t.Log("Successfully created identity entity and alias") +} + +// testIdentitySecretsRead tests Identity secrets engine read operations +func testIdentitySecretsRead(t *testing.T, v *blackbox.Session) { + // Create an entity + entityResp := v.MustWrite("identity/entity", map[string]any{ + "name": "read-entity", + "policies": []string{"default"}, + "metadata": map[string]string{ + "purpose": "testing", + }, + }) + + entityID := entityResp.Data["id"].(string) + + // Read the entity by ID + readResp := v.MustRead("identity/entity/id/" + entityID) + if readResp.Data == nil { + t.Fatal("Expected to read entity") + } + + // Verify entity properties + assertions := v.AssertSecret(readResp) + assertions.Data(). + HasKey("name", "read-entity"). + HasKeyExists("id"). + HasKeyExists("policies") + + // Read entity by name + nameResp := v.MustRead("identity/entity/name/read-entity") + if nameResp.Data == nil { + t.Fatal("Expected to read entity by name") + } + + nameAssertions := v.AssertSecret(nameResp) + nameAssertions.Data(). + HasKey("name", "read-entity"). + HasKey("id", entityID) + + t.Log("Successfully read identity entity by ID and name") +} + +// testIdentitySecretsDelete tests Identity secrets engine delete operations +func testIdentitySecretsDelete(t *testing.T, v *blackbox.Session) { + entityResp := v.MustWrite("identity/entity", map[string]any{ + "name": "delete-entity", + }) + entityID := entityResp.Data["id"].(string) + readResp := v.MustRead("identity/entity/id/" + entityID) + if readResp.Data == nil { + t.Fatal("Expected entity to exist before deletion") + } + _, err := v.Client.Logical().Delete("identity/entity/id/" + entityID) + if err != nil { + t.Fatalf("Failed to delete entity: %v", err) + } + deletedResp, err := v.Client.Logical().Read("identity/entity/id/" + entityID) + if err == nil && deletedResp != nil { + t.Fatal("Expected entity to be deleted, but it still exists") + } + t.Logf("Successfully deleted identity entity: %s", entityID) +} diff --git a/vault/external_tests/blackbox/secrets_kmip_test.go b/vault/external_tests/blackbox/secrets_kmip_test.go new file mode 100644 index 0000000000..a4db4a0e12 --- /dev/null +++ b/vault/external_tests/blackbox/secrets_kmip_test.go @@ -0,0 +1,99 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "os" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testKMIPSecretsCreate tests KMIP secrets engine creation +func testKMIPSecretsCreate(t *testing.T, v *blackbox.Session) { + // Check if this is Vault Enterprise (KMIP is enterprise-only) + edition := os.Getenv("VAULT_EDITION") + if edition == "ce" || edition == "" { + t.Skip("KMIP secrets engine is only available in Vault Enterprise") + } + + // Enable KMIP secrets engine + v.MustEnableSecretsEngine("kmip-create", &api.MountInput{Type: "kmip"}) + + // Configure KMIP secrets engine + v.MustWrite("kmip-create/config", map[string]any{ + "listen_addrs": []string{"0.0.0.0:5696"}, + "server_hostnames": []string{"localhost"}, + "default_tls_client_ttl": "24h", + }) + + // Create a KMIP scope + v.MustWrite("kmip-create/scope/test-scope", map[string]any{}) + + // Create a KMIP role within the scope + v.MustWrite("kmip-create/scope/test-scope/role/test-role", map[string]any{ + "operation_all": true, + }) + + // Verify role was created by reading it + roleResp := v.MustRead("kmip-create/scope/test-scope/role/test-role") + if roleResp.Data == nil { + t.Fatal("Expected to read KMIP role configuration") + } + + t.Log("Successfully created KMIP secrets engine with scope and role") +} + +// testKMIPSecretsRead tests KMIP secrets engine read operations +func testKMIPSecretsRead(t *testing.T, v *blackbox.Session) { + // Check if this is Vault Enterprise (KMIP is enterprise-only) + edition := os.Getenv("VAULT_EDITION") + if edition == "ce" || edition == "" { + t.Skip("KMIP secrets engine is only available in Vault Enterprise") + } + + // Enable KMIP secrets engine + v.MustEnableSecretsEngine("kmip-read", &api.MountInput{Type: "kmip"}) + + // Configure KMIP secrets engine + v.MustWrite("kmip-read/config", map[string]any{ + "listen_addrs": []string{"0.0.0.0:5697"}, + "server_hostnames": []string{"localhost"}, + }) + + // Create a scope + v.MustWrite("kmip-read/scope/read-scope", map[string]any{}) + + // Create a role + v.MustWrite("kmip-read/scope/read-scope/role/read-role", map[string]any{ + "operation_activate": true, + "operation_create": true, + "operation_get": true, + }) + + // Read the role configuration + roleResp := v.MustRead("kmip-read/scope/read-scope/role/read-role") + if roleResp.Data == nil { + t.Fatal("Expected to read KMIP role configuration") + } + + // Verify role properties + assertions := v.AssertSecret(roleResp) + assertions.Data(). + HasKey("operation_activate", true). + HasKey("operation_create", true). + HasKey("operation_get", true) + + // Note: Reading individual scopes is not supported (returns 405) + // The KMIP API only supports listing scopes, not reading them individually + + // Read KMIP configuration + configResp := v.MustRead("kmip-read/config") + if configResp.Data == nil { + t.Fatal("Expected to read KMIP configuration") + } + + t.Log("Successfully read KMIP secrets engine configuration") +} diff --git a/vault/external_tests/blackbox/secrets_kv_test.go b/vault/external_tests/blackbox/secrets_kv_test.go new file mode 100644 index 0000000000..08faa648ba --- /dev/null +++ b/vault/external_tests/blackbox/secrets_kv_test.go @@ -0,0 +1,46 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testKVSecretsCreate tests KV secrets engine creation +func testKVSecretsCreate(t *testing.T, v *blackbox.Session) { + // KV secrets engine tests are now in kvv2_test.go - just test basic enablement here + SetupKVEngine(v, "kv-create") + + // Write and read test data to verify engine works + v.MustWriteKV2("kv-create", "test/path", StandardKVData) + secret := v.MustReadKV2("kv-create", "test/path") + AssertKVData(t, v, secret, StandardKVData) + + t.Log("Successfully created and tested KV secrets engine") +} + +// testKVSecretsRead tests KV secrets engine read operations +func testKVSecretsRead(t *testing.T, v *blackbox.Session) { + // KV read tests are in kvv2_test.go - test basic read functionality here + SetupKVEngine(v, "kv-read") + v.MustWriteKV2("kv-read", "read/test", AltKVData) + secret := v.MustReadKV2("kv-read", "read/test") + AssertKVData(t, v, secret, AltKVData) + + t.Log("Successfully read KV secrets engine data") +} + +// testKVSecretsDelete tests KV secrets engine delete operations +func testKVSecretsDelete(t *testing.T, v *blackbox.Session) { + SetupKVEngine(v, "kv-delete") + v.MustWriteKV2("kv-delete", "delete/test", StandardKVData) + secret := v.MustReadKV2("kv-delete", "delete/test") + AssertKVData(t, v, secret, StandardKVData) + v.MustWrite("kv-delete/delete/delete/test", map[string]any{ + "versions": []int{1}, + }) + t.Log("Successfully deleted KV secrets engine data") +} diff --git a/vault/external_tests/blackbox/secrets_ldap_test.go b/vault/external_tests/blackbox/secrets_ldap_test.go new file mode 100644 index 0000000000..1cac6565b8 --- /dev/null +++ b/vault/external_tests/blackbox/secrets_ldap_test.go @@ -0,0 +1,154 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "os" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testLDAPSecretsCreate tests LDAP secrets engine creation +func testLDAPSecretsCreate(t *testing.T, v *blackbox.Session) { + // Check if LDAP server configuration is available from integration host + ldapServer := os.Getenv("LDAP_SERVER") + ldapBindDN := os.Getenv("LDAP_BIND_DN") + ldapBindPass := os.Getenv("LDAP_BIND_PASS") + + if ldapServer == "" || ldapBindDN == "" || ldapBindPass == "" { + t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") + } + + // Enable LDAP secrets engine + v.MustEnableSecretsEngine("ldap-create", &api.MountInput{Type: "ldap"}) + + // Configure LDAP secrets engine with integration server details + v.MustWrite("ldap-create/config", map[string]any{ + "binddn": ldapBindDN, + "bindpass": ldapBindPass, + "url": ldapServer, + "userdn": "ou=users,dc=enos,dc=com", + "userattr": "uid", + }) + + // Create a static role for password rotation using the user created by the integration setup + v.MustWrite("ldap-create/static-role/test-role", map[string]any{ + "username": "enos", + "dn": "uid=enos,ou=users,dc=enos,dc=com", + "rotation_period": "24h", + }) + + // Verify role was created by reading it + roleResp := v.MustRead("ldap-create/static-role/test-role") + if roleResp.Data == nil { + t.Fatal("Expected to read LDAP static role configuration") + } + + t.Log("Successfully created LDAP secrets engine with static role") +} + +// testLDAPSecretsRead tests LDAP secrets engine read operations +func testLDAPSecretsRead(t *testing.T, v *blackbox.Session) { + // Check if LDAP server configuration is available from integration host + ldapServer := os.Getenv("LDAP_SERVER") + ldapBindDN := os.Getenv("LDAP_BIND_DN") + ldapBindPass := os.Getenv("LDAP_BIND_PASS") + + if ldapServer == "" || ldapBindDN == "" || ldapBindPass == "" { + t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") + } + + // Enable LDAP secrets engine + v.MustEnableSecretsEngine("ldap-read", &api.MountInput{Type: "ldap"}) + + // Configure LDAP secrets engine with integration server details + v.MustWrite("ldap-read/config", map[string]any{ + "binddn": ldapBindDN, + "bindpass": ldapBindPass, + "url": ldapServer, + "userdn": "ou=users,dc=enos,dc=com", + "userattr": "uid", + }) + + // Create a library set for service account management + v.MustWrite("ldap-read/library/test-set", map[string]any{ + "service_account_names": []string{"svc-account-1", "svc-account-2"}, + "ttl": "10h", + "max_ttl": "20h", + "disable_check_in_enforcement": false, + }) + + // Read the library set configuration + libraryResp := v.MustRead("ldap-read/library/test-set") + if libraryResp.Data == nil { + t.Fatal("Expected to read LDAP library set configuration") + } + + // Verify library set properties + assertions := v.AssertSecret(libraryResp) + assertions.Data(). + HasKeyExists("service_account_names"). + HasKeyExists("ttl"). + HasKeyExists("max_ttl") + + // Read configuration (should not expose bind password) + configResp := v.MustRead("ldap-read/config") + if configResp.Data == nil { + t.Fatal("Expected to read LDAP configuration") + } + + t.Log("Successfully read LDAP secrets engine configuration") +} + +// testLDAPSecretsDelete tests LDAP secrets engine delete operations +func testLDAPSecretsDelete(t *testing.T, v *blackbox.Session) { + // Check if LDAP server configuration is available from integration host + ldapServer := os.Getenv("LDAP_SERVER") + ldapBindDN := os.Getenv("LDAP_BIND_DN") + ldapBindPass := os.Getenv("LDAP_BIND_PASS") + + if ldapServer == "" || ldapBindDN == "" || ldapBindPass == "" { + t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") + } + + // Enable LDAP secrets engine + v.MustEnableSecretsEngine("ldap-delete", &api.MountInput{Type: "ldap"}) + + // Configure LDAP secrets engine with integration server details + v.MustWrite("ldap-delete/config", map[string]any{ + "binddn": ldapBindDN, + "bindpass": ldapBindPass, + "url": ldapServer, + "userdn": "ou=users,dc=enos,dc=com", + "userattr": "uid", + }) + + // Create a library set + v.MustWrite("ldap-delete/library/delete-set", map[string]any{ + "service_account_names": []string{"svc-delete"}, + "ttl": "1h", + }) + + // Verify library set exists + libraryResp := v.MustRead("ldap-delete/library/delete-set") + if libraryResp.Data == nil { + t.Fatal("Expected library set to exist before deletion") + } + + // Delete the library set + _, err := v.Client.Logical().Delete("ldap-delete/library/delete-set") + if err != nil { + t.Fatalf("Failed to delete LDAP library set: %v", err) + } + + // Verify library set is deleted + deletedResp, err := v.Client.Logical().Read("ldap-delete/library/delete-set") + if err == nil && deletedResp != nil { + t.Fatal("Expected library set to be deleted, but it still exists") + } + + t.Log("Successfully deleted LDAP library set") +} diff --git a/vault/external_tests/blackbox/secrets_pki_test.go b/vault/external_tests/blackbox/secrets_pki_test.go new file mode 100644 index 0000000000..e30a72689d --- /dev/null +++ b/vault/external_tests/blackbox/secrets_pki_test.go @@ -0,0 +1,109 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testPKISecretsCreate tests PKI secrets engine creation +func testPKISecretsCreate(t *testing.T, v *blackbox.Session) { + // Enable PKI secrets engine + v.MustEnableSecretsEngine("pki-create", &api.MountInput{Type: "pki"}) + + // Configure max TTL for the mount + err := v.Client.Sys().TuneMount("pki-create", api.MountConfigInput{ + MaxLeaseTTL: "87600h", + }) + if err != nil { + t.Fatalf("Failed to tune PKI mount: %v", err) + } + + // Generate root CA + rootResp := v.MustWrite("pki-create/root/generate/internal", map[string]any{ + "common_name": "test-root-ca.example.com", + "ttl": "8760h", + "key_type": "rsa", + "key_bits": 2048, + }) + + if rootResp.Data == nil { + t.Fatal("Expected root CA generation response") + } + + // Verify root CA was created + assertions := v.AssertSecret(rootResp) + assertions.Data(). + HasKeyExists("certificate"). + HasKeyExists("issuing_ca"). + HasKeyExists("serial_number") + + // Create a role for issuing certificates + v.MustWrite("pki-create/roles/test-role", map[string]any{ + "allowed_domains": []string{"example.com"}, + "allow_subdomains": true, + "max_ttl": "72h", + "key_type": "rsa", + "key_bits": 2048, + }) + + // Verify role was created by reading it + roleResp := v.MustRead("pki-create/roles/test-role") + if roleResp.Data == nil { + t.Fatal("Expected to read role configuration") + } + + t.Log("Successfully created PKI secrets engine with root CA and role") +} + +// testPKISecretsRead tests PKI secrets engine read operations +func testPKISecretsRead(t *testing.T, v *blackbox.Session) { + // Setup PKI engine with root CA + roleName := v.MustSetupPKIRoot("pki-read") + + // Read the role configuration + roleResp := v.MustRead("pki-read/roles/" + roleName) + if roleResp.Data == nil { + t.Fatal("Expected to read role configuration") + } + + // Verify role properties + assertions := v.AssertSecret(roleResp) + assertions.Data(). + HasKey("allow_subdomains", true). + HasKeyExists("max_ttl"). + HasKeyExists("allowed_domains") + + // Read CA certificate + caResp := v.MustRead("pki-read/cert/ca") + if caResp.Data == nil { + t.Fatal("Expected to read CA certificate") + } + + assertions = v.AssertSecret(caResp) + assertions.Data().HasKeyExists("certificate") + + t.Log("Successfully read PKI secrets engine configuration and certificates") +} + +// testPKISecretsDelete tests PKI secrets engine delete operations +func testPKISecretsDelete(t *testing.T, v *blackbox.Session) { + roleName := v.MustSetupPKIRoot("pki-delete") + roleResp := v.MustRead("pki-delete/roles/" + roleName) + if roleResp.Data == nil { + t.Fatal("Expected role to exist before deletion") + } + _, err := v.Client.Logical().Delete("pki-delete/roles/" + roleName) + if err != nil { + t.Fatalf("Failed to delete PKI role: %v", err) + } + deletedResp, err := v.Client.Logical().Read("pki-delete/roles/" + roleName) + if err == nil && deletedResp != nil { + t.Fatal("Expected role to be deleted, but it still exists") + } + t.Logf("Successfully deleted PKI role: %s", roleName) +} diff --git a/vault/external_tests/blackbox/secrets_ssh_test.go b/vault/external_tests/blackbox/secrets_ssh_test.go new file mode 100644 index 0000000000..5d4b39b69e --- /dev/null +++ b/vault/external_tests/blackbox/secrets_ssh_test.go @@ -0,0 +1,139 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// testSSHSecretsCreate tests SSH secrets engine creation +func testSSHSecretsCreate(t *testing.T, v *blackbox.Session) { + // Enable SSH secrets engine + v.MustEnableSecretsEngine("ssh-create", &api.MountInput{Type: "ssh"}) + + // Generate CA key pair + caResp := v.MustWrite("ssh-create/config/ca", map[string]any{ + "generate_signing_key": true, + }) + + if caResp.Data == nil { + t.Fatal("Expected CA generation response") + } + + // Verify CA was created + assertions := v.AssertSecret(caResp) + assertions.Data().HasKeyExists("public_key") + + // Create an SSH role for signing certificates + v.MustWrite("ssh-create/roles/test-role", map[string]any{ + "key_type": "ca", + "allow_user_certificates": true, + "allowed_users": "*", + "default_user": "ubuntu", + "ttl": "30m", + "max_ttl": "24h", + }) + + // Verify role was created by reading it + roleResp := v.MustRead("ssh-create/roles/test-role") + if roleResp.Data == nil { + t.Fatal("Expected to read SSH role configuration") + } + + // Verify role properties + roleAssertions := v.AssertSecret(roleResp) + roleAssertions.Data(). + HasKey("key_type", "ca"). + HasKey("allow_user_certificates", true). + HasKey("default_user", "ubuntu") + + t.Log("Successfully created SSH secrets engine with CA and role") +} + +// testSSHSecretsRead tests SSH secrets engine read operations +func testSSHSecretsRead(t *testing.T, v *blackbox.Session) { + // Enable SSH secrets engine + v.MustEnableSecretsEngine("ssh-read", &api.MountInput{Type: "ssh"}) + + // Generate CA + v.MustWrite("ssh-read/config/ca", map[string]any{ + "generate_signing_key": true, + }) + + // Create a role + v.MustWrite("ssh-read/roles/read-role", map[string]any{ + "key_type": "ca", + "allow_user_certificates": true, + "allowed_users": "testuser", + "default_user": "testuser", + "ttl": "1h", + }) + + // Read the role configuration + roleResp := v.MustRead("ssh-read/roles/read-role") + if roleResp.Data == nil { + t.Fatal("Expected to read SSH role configuration") + } + + // Verify role properties + assertions := v.AssertSecret(roleResp) + assertions.Data(). + HasKey("key_type", "ca"). + HasKey("allow_user_certificates", true). + HasKey("allowed_users", "testuser"). + HasKey("default_user", "testuser") + + // Read CA public key + publicKeyResp := v.MustRead("ssh-read/config/ca") + if publicKeyResp.Data == nil { + t.Fatal("Expected to read CA public key") + } + + assertions = v.AssertSecret(publicKeyResp) + assertions.Data().HasKeyExists("public_key") + + t.Log("Successfully read SSH secrets engine configuration") +} + +// testSSHSecretsDelete tests SSH secrets engine delete operations +func testSSHSecretsDelete(t *testing.T, v *blackbox.Session) { + // Enable SSH secrets engine + v.MustEnableSecretsEngine("ssh-delete", &api.MountInput{Type: "ssh"}) + + // Generate CA + v.MustWrite("ssh-delete/config/ca", map[string]any{ + "generate_signing_key": true, + }) + + // Create a role + v.MustWrite("ssh-delete/roles/delete-role", map[string]any{ + "key_type": "ca", + "allow_user_certificates": true, + "allowed_users": "*", + "default_user": "ubuntu", + }) + + // Verify role exists + roleResp := v.MustRead("ssh-delete/roles/delete-role") + if roleResp.Data == nil { + t.Fatal("Expected role to exist before deletion") + } + + // Delete the role + _, err := v.Client.Logical().Delete("ssh-delete/roles/delete-role") + if err != nil { + t.Fatalf("Failed to delete SSH role: %v", err) + } + + // Verify role is deleted + deletedResp, err := v.Client.Logical().Read("ssh-delete/roles/delete-role") + if err == nil && deletedResp != nil { + t.Fatal("Expected role to be deleted, but it still exists") + } + + t.Log("Successfully deleted SSH role") +} diff --git a/vault/external_tests/blackbox/secrets_transit_test.go b/vault/external_tests/blackbox/secrets_transit_test.go new file mode 100644 index 0000000000..dc95f1fb7d --- /dev/null +++ b/vault/external_tests/blackbox/secrets_transit_test.go @@ -0,0 +1,158 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// TestTransitSecretsEngineCreate tests Transit secrets engine creation and basic operations +func TestTransitSecretsEngineCreate(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + testTransitSecretsCreate(t, v) +} + +// TestTransitSecretsEngineRead tests Transit secrets engine read operations +func TestTransitSecretsEngineRead(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + testTransitSecretsRead(t, v) +} + +// TestTransitSecretsEngineDelete tests Transit secrets engine delete operations +func TestTransitSecretsEngineDelete(t *testing.T) { + v := blackbox.New(t) + + // Verify we have a healthy cluster first + v.AssertClusterHealthy() + + testTransitSecretsDelete(t, v) +} + +// Transit Secrets Engine Test Implementation Functions + +func testTransitSecretsCreate(t *testing.T, v *blackbox.Session) { + // Enable transit secrets engine + v.MustEnableSecretsEngine("transit", &api.MountInput{Type: "transit"}) + + // Create an encryption key + keyName := "test-key" + v.MustWrite("transit/keys/"+keyName, map[string]any{ + "type": "aes256-gcm96", + }) + + // Verify the key was created by reading it + keyInfo := v.MustRead("transit/keys/" + keyName) + if keyInfo.Data == nil { + t.Fatal("Expected to read key configuration") + } + + // Verify key type + if keyType, ok := keyInfo.Data["type"]; !ok || keyType != "aes256-gcm96" { + t.Fatalf("Expected key type 'aes256-gcm96', got: %v", keyInfo.Data["type"]) + } + + // Test encryption + plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // base64 encoded "the quick brown fox" + encryptResp := v.MustWrite("transit/encrypt/"+keyName, map[string]any{ + "plaintext": plaintext, + }) + + if encryptResp.Data == nil || encryptResp.Data["ciphertext"] == nil { + t.Fatal("Expected ciphertext in encryption response") + } + + ciphertext := encryptResp.Data["ciphertext"].(string) + t.Logf("Encrypted ciphertext: %s", ciphertext[:20]+"...") + + // Test decryption + decryptResp := v.MustWrite("transit/decrypt/"+keyName, map[string]any{ + "ciphertext": ciphertext, + }) + + if decryptResp.Data == nil || decryptResp.Data["plaintext"] == nil { + t.Fatal("Expected plaintext in decryption response") + } + + decryptedText := decryptResp.Data["plaintext"].(string) + if decryptedText != plaintext { + t.Fatalf("Decrypted text doesn't match original. Expected: %s, Got: %s", plaintext, decryptedText) + } + + t.Log("Successfully created transit secrets engine and tested encryption/decryption") +} + +func testTransitSecretsRead(t *testing.T, v *blackbox.Session) { + // Enable transit secrets engine with unique mount + v.MustEnableSecretsEngine("transit-read", &api.MountInput{Type: "transit"}) + + // Create an encryption key + keyName := "read-test-key" + v.MustWrite("transit-read/keys/"+keyName, map[string]any{ + "type": "aes256-gcm96", + "exportable": false, + }) + + // Read the key configuration + keyInfo := v.MustRead("transit-read/keys/" + keyName) + if keyInfo.Data == nil { + t.Fatal("Expected to read key configuration") + } + + // Verify key properties + assertions := v.AssertSecret(keyInfo) + assertions.Data(). + HasKey("type", "aes256-gcm96"). + HasKey("exportable", false). + HasKeyExists("keys"). + HasKeyExists("latest_version") + + t.Log("Successfully read transit secrets engine key configuration") +} + +func testTransitSecretsDelete(t *testing.T, v *blackbox.Session) { + // Enable transit secrets engine with unique mount + v.MustEnableSecretsEngine("transit-delete", &api.MountInput{Type: "transit"}) + + // Create an encryption key + keyName := "delete-test-key" + v.MustWrite("transit-delete/keys/"+keyName, map[string]any{ + "type": "aes256-gcm96", + }) + + // Verify the key exists + keyInfo := v.MustRead("transit-delete/keys/" + keyName) + if keyInfo.Data == nil { + t.Fatal("Expected key to exist before deletion") + } + + // Configure the key to allow deletion (transit keys require this) + v.MustWrite("transit-delete/keys/"+keyName+"/config", map[string]any{ + "deletion_allowed": true, + }) + + // Delete the key + _, err := v.Client.Logical().Delete("transit-delete/keys/" + keyName) + if err != nil { + t.Fatalf("Failed to delete transit key: %v", err) + } + + // Verify the key is deleted by attempting to read it + readSecret, err := v.Client.Logical().Read("transit-delete/keys/" + keyName) + if err == nil && readSecret != nil { + t.Fatal("Expected key to be deleted, but it still exists") + } + + t.Logf("Successfully deleted transit key: %s", keyName) +} diff --git a/vault/external_tests/blackbox/smoke_test.go b/vault/external_tests/blackbox/smoke_test.go index 461cab6d32..88692d23e9 100644 --- a/vault/external_tests/blackbox/smoke_test.go +++ b/vault/external_tests/blackbox/smoke_test.go @@ -52,7 +52,8 @@ func TestStepdownAndLeaderElection(t *testing.T) { v.MustStepDownLeader() // Wait for new leader election (with timeout) - v.WaitForNewLeader(initialLeader, 120) + // Use generous timeout to handle network latency and complex backend coordination + v.WaitForNewLeader(initialLeader, 300) // Verify cluster is still healthy after leader change/recovery v.AssertRaftClusterHealthy() From fa8681a66676dcda12dcbe623504831be76aab41 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 14:03:48 -0400 Subject: [PATCH 083/468] Fix data race in identity tests (#12941) (#12948) Co-authored-by: Kuba Wieczorek --- vault/identity_store.go | 3 +++ vault/identity_store_test.go | 15 +++++++++++---- vault/identity_store_util.go | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/vault/identity_store.go b/vault/identity_store.go index 64f56e5da0..c297549fce 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -51,6 +51,9 @@ func (i *IdentityStore) GetDisableLowerCasedNames() bool { return i.disableLowerCasedNames } +// resetDB callers must hold the write lock on i.lock before calling, to ensure +// that no other goroutine is reading from or writing to the database while it +// gets reset. func (i *IdentityStore) resetDB() error { var err error diff --git a/vault/identity_store_test.go b/vault/identity_store_test.go index ffc1c799c5..bfbd0a40ed 100644 --- a/vault/identity_store_test.go +++ b/vault/identity_store_test.go @@ -1506,6 +1506,9 @@ func identityStoreLoadingIsDeterministic(t *testing.T, flags *determinismTestFla CredentialBackends: map[string]logical.Factory{ "userpass": credUserpass.Factory, }, + ActivityLogConfig: ActivityLogCoreConfig{ + DisableTimers: true, + }, } c, sealKeys, rootToken := TestCoreUnsealedWithConfig(t, cfg) @@ -1681,13 +1684,15 @@ func identityStoreLoadingIsDeterministic(t *testing.T, flags *determinismTestFla var prevErr error for i := 0; i < 10; i++ { + c.identityStore.lock.Lock() err := c.identityStore.resetDB() + if err == nil { + logger.Info(" ==> BEGIN LOAD ARTIFACTS", "i", i) + err = c.identityStore.loadArtifacts(ctx, true) + } + c.identityStore.lock.Unlock() require.NoError(t, err) - logger.Info(" ==> BEGIN LOAD ARTIFACTS", "i", i) - - err = c.identityStore.loadArtifacts(ctx, true) - if i > 0 { require.Equal(t, prevErr, err) } @@ -1833,7 +1838,9 @@ func TestIdentityStoreLoadingDuplicateReporting(t *testing.T) { // Setup a logger we can use to capture unseal logs logBuf, stopCapture := startLogCapture(t, logger) + c.identityStore.lock.Lock() err = c.identityStore.loadArtifacts(ctx, true) + c.identityStore.lock.Unlock() stopCapture() require.NoError(t, err) diff --git a/vault/identity_store_util.go b/vault/identity_store_util.go index bd72445270..bfdc17fcca 100644 --- a/vault/identity_store_util.go +++ b/vault/identity_store_util.go @@ -40,7 +40,7 @@ var ( ) // loadArtifacts is responsible for loading entities, groups, and aliases from -// storage into MemDB. +// storage into MemDB. The caller should hold the identity store lock. func (i *IdentityStore) loadArtifacts(ctx context.Context, isActive bool) error { if i == nil { return nil From cd2c9b0304bf07dc412e5cdae63c42d11120713c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 14:10:28 -0400 Subject: [PATCH 084/468] [VAULT-42684] UI: add playwright coverage for kmip binary tests (#12945) (#12949) Co-authored-by: Shannon Roberts (Beagin) --- ui/e2e/tests/superuser/kmip.spec.ts | 134 ++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 ui/e2e/tests/superuser/kmip.spec.ts diff --git a/ui/e2e/tests/superuser/kmip.spec.ts b/ui/e2e/tests/superuser/kmip.spec.ts new file mode 100644 index 0000000000..488993dcf1 --- /dev/null +++ b/ui/e2e/tests/superuser/kmip.spec.ts @@ -0,0 +1,134 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; + +test('kmip workflow', async ({ page }) => { + await page.goto('dashboard'); + + await test.step('enable KMIP secrets engine', async () => { + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('KMIP - enabled').click(); + await page.getByRole('textbox', { name: 'Path' }).fill('kmip-builtin'); + await page.getByRole('button', { name: 'Method Options' }).click(); + await page.getByRole('textbox', { name: 'Description' }).fill('This is a kmip mount.'); + await page.getByRole('checkbox', { name: 'Local' }).check(); + await page.getByRole('button', { name: 'Enable engine' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect( + page.getByText('Successfully mounted the kmip secrets engine at kmip-builtin') + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('configure KMIP engine', async () => { + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Configure' }).click(); + + await page.locator('label').filter({ hasText: 'Default TLS Client TTL Lease' }).click(); + await page.getByLabel('TLS CA Key type').selectOption('rsa'); + await page.getByRole('textbox', { name: 'TLS CA Key bits' }).fill('2048'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully configured KMIP engine')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + await expect(page.locator('[data-test-row-value="Default TLS client TTL"]')).toContainText('0'); + await expect(page.locator('span[data-test-row-value="TLS CA key type"]')).toContainText('rsa'); + await expect(page.locator('span[data-test-row-value="TLS CA key bits"]')).toContainText('2048'); + }); + + await test.step('update general settings', async () => { + await page.getByRole('link', { name: 'General settings' }).click(); + await page.getByText('Engine type kmip').click(); + await page.getByRole('textbox', { name: 'Description' }).click(); + await page.getByRole('textbox', { name: 'Description' }).press('ControlOrMeta+a'); + await page.getByRole('textbox', { name: 'Description' }).fill('abcdefg'); + await page.getByRole('button', { name: 'Save changes' }).click(); + + await expect(page.getByText('Configuration saved')).toBeVisible(); + await expect(page.getByText('Engine settings successfully updated.')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('create a scope', async () => { + await page.getByRole('link', { name: 'Exit configuration' }).click(); + await expect(page.locator('section')).toContainText('KMIP Secrets Engine'); + await expect(page.locator('section')).toContainText( + "First, let's create a scope that our roles and credentials will belong to. A client can only access objects within their role's scope." + ); + await page.getByRole('link', { name: 'Create scope' }).click(); + await page.getByRole('textbox', { name: 'Name' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('kmip-scope'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully created scope kmip-scope')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('create a role', async () => { + await page.getByRole('button', { name: 'More options' }).click(); + await page.getByRole('link', { name: 'View scope', exact: true }).click(); + + await page.locator('div').filter({ hasText: 'No roles in this scope yet' }).nth(3).click(); + await page.getByRole('link', { name: 'Create role' }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).click(); + await page.getByRole('textbox', { name: 'Name', exact: true }).fill('kmip-role'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully saved role kmip-role')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('generate credentials', async () => { + await page.getByRole('button', { name: 'More options' }).click(); + await page.getByRole('link', { name: 'View credentials', exact: true }).click(); + await expect(page.getByText('No credentials yet for this')).toBeVisible(); + await expect(page.getByText('You can generate new')).toBeVisible(); + await page.getByLabel('toolbar actions').getByRole('link', { name: 'Generate credentials' }).click(); + await page.getByLabel('Certificate format').selectOption('pem_bundle'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully generated credentials from role kmip-role.')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('revoke credentials', async () => { + await page.getByRole('button', { name: 'Revoke credentials' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully revoked credentials.')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + await expect(page.getByText('No credentials yet for this')).toBeVisible(); + }); + + await test.step('delete role', async () => { + await page.getByRole('link', { name: 'kmip-scope' }).click(); + await page.getByRole('button', { name: 'More options' }).click(); + await page.getByRole('button', { name: 'Delete role' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await expect(page.getByText('No roles in this scope yet')).toBeVisible(); + await expect(page.getByText('Roles let you generate')).toBeVisible(); + }); + + await test.step('delete scope', async () => { + await page.getByRole('link', { name: 'kmip-builtin' }).click(); + await page.getByRole('button', { name: 'More options' }).click(); + await page.getByRole('button', { name: 'Delete scope' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await expect(page.getByText('KMIP Secrets Engine')).toBeVisible(); + await expect(page.getByText("First, let's create a scope")).toBeVisible(); + }); +}); From 7f893c6ea3d106df2ee5859532cd691ab505e5ab Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 14:16:30 -0400 Subject: [PATCH 085/468] [VAULT-42686] UI: add playwright coverage for keymgmt binary tests (#12906) (#12950) * [VAULT-42686] UI: add playwright coverage for keymgmt binary tests * use test.step instead of comments * cleanup Co-authored-by: Shannon Roberts (Beagin) --- .../keymgmt-mount-external.ent.spec.ts | 136 ++++++++++++ .../keymgmt-tune-external.ent.spec.ts | 204 ++++++++++++++++++ ui/e2e/tests/superuser/keymgmt.spec.ts | 86 ++++++++ 3 files changed, 426 insertions(+) create mode 100644 ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts create mode 100644 ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts create mode 100644 ui/e2e/tests/superuser/keymgmt.spec.ts diff --git a/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts b/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts new file mode 100644 index 0000000000..d7245f0898 --- /dev/null +++ b/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts @@ -0,0 +1,136 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; + +const PINNED_PLUGIN_DATA = { + data: { + name: 'vault-plugin-secrets-keymgmt', + type: 'secret', + version: 'v0.17.0+ent', + }, +}; + +const PLUGIN_CATALOG_DATA = { + request_id: 'request_id', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + detailed: [ + { + builtin: true, + deprecation_status: 'supported', + name: 'keymgmt', + type: 'secret', + version: 'v0.18.1+builtin', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: '', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.16.0+ent', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.17.0+ent', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.18.0+ent', + }, + ], + }, +}; + +test('mount external keymgmt workflow', async ({ page }) => { + await test.step('mock the keymgmt pinned version response', async () => { + await page.route('**v1/sys/plugins/pins/secret/vault-plugin-secrets-keymgmt', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(PINNED_PLUGIN_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await test.step('mock the plugin catalog response to return builtin and external keymgmt plugins', async () => { + await page.route('**v1/sys/plugins/catalog', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(PLUGIN_CATALOG_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await page.goto('dashboard'); + + await test.step('navigate to enable Key Management engine', async () => { + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('Key Management - enabled').click(); + await page.getByRole('textbox', { name: 'Path' }).fill('keymgmt-external'); + }); + + await test.step('verify builtin and external plugin type options are visible', async () => { + await expect(page.getByText('Built-in plugin Preregistered')).toBeVisible(); + await expect(page.getByText('External plugin External')).toBeVisible(); + await expect(page.getByText('Plugin version Required')).not.toBeVisible(); + }); + + await test.step('selecting external plugin type shows plugin version dropdown', async () => { + await page.locator('label:nth-child(2) > .hds-form-radio-card__control-wrapper').click(); + await expect(page.getByText('Plugin version Required')).toBeVisible(); + await expect(page.getByLabel('Plugin version Required')).toContainText( + 'v0.17.0+ent (pinned) v0.16.0+ent v0.18.0+ent' + ); + }); + + await test.step('pinned version is selected by default with no warning', async () => { + await expect(page.getByLabel('Version differs from pinned')).not.toBeVisible(); + }); + + await test.step('selecting a non-pinned version shows a warning', async () => { + await page.getByLabel('Plugin version Required').selectOption('v0.16.0+ent'); + await expect(page.getByLabel('Version differs from pinned')).toContainText( + 'You have selected v0.16.0+ent, but version v0.17.0+ent is pinned for this plugin. Enabling the engine with this version will override the pinned version for this mount.' + ); + }); + + await test.step('re-selecting the pinned version clears the warning', async () => { + await page.getByLabel('Plugin version Required').selectOption('v0.17.0+ent'); + await expect(page.getByLabel('Version differs from pinned')).not.toBeVisible(); + }); + + await test.step('enabling engine shows error with external plugin name', async () => { + await page.getByRole('button', { name: 'Enable engine' }).click(); + await expect(page.getByLabel('Error')).toContainText( + 'plugin not found in the catalog: vault-plugin-secrets-keymgmt' + ); + }); +}); diff --git a/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts b/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts new file mode 100644 index 0000000000..6acdd0ac4e --- /dev/null +++ b/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts @@ -0,0 +1,204 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; + +const PINNED_PLUGIN_DATA = { + data: { + name: 'vault-plugin-secrets-keymgmt', + type: 'secret', + version: 'v0.17.0+ent', + }, +}; + +const PLUGIN_CATALOG_DATA = { + request_id: 'request_id', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + detailed: [ + { + builtin: true, + deprecation_status: 'supported', + name: 'keymgmt', + type: 'secret', + version: 'v0.18.1+builtin', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: '', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.16.0+ent', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.17.0+ent', + }, + { + builtin: false, + name: 'vault-plugin-secrets-keymgmt', + sha256: 'sha256', + type: 'secret', + version: 'v0.18.0+ent', + }, + ], + }, +}; + +const KEYMGMT_EXTERNAL_MOUNT_DATA = { + request_id: 'request_id', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + accessor: 'vault-plugin-secrets-keymgmt_accessor', + config: { + default_lease_ttl: 2764800, + force_no_cache: false, + listing_visibility: 'hidden', + max_lease_ttl: 2764800, + }, + description: '', + external_entropy_access: false, + local: false, + options: {}, + path: 'keymgmt-external/', + plugin_version: 'v0.17.0+ent', + running_plugin_version: 'v0.17.0+ent', + running_sha256: 'sha256', + seal_wrap: false, + type: 'vault-plugin-secrets-keymgmt', + uuid: 'uuid', + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: '', +}; + +const UPDATED_KEYMGMT_EXTERNAL_MOUNT_DATA = { + ...KEYMGMT_EXTERNAL_MOUNT_DATA, + data: { + ...KEYMGMT_EXTERNAL_MOUNT_DATA.data, + plugin_version: 'v0.18.0+ent', + running_plugin_version: 'v0.18.0+ent', + }, +}; + +test('tune external keymgmt workflow', async ({ page }) => { + await test.step('mock the keymgmt pinned version response', async () => { + await page.route('**v1/sys/plugins/pins/secret/vault-plugin-secrets-keymgmt', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(PINNED_PLUGIN_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await test.step('mock the plugin catalog response to return builtin and external keymgmt plugins', async () => { + await page.route('**v1/sys/plugins/catalog', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(PLUGIN_CATALOG_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await test.step('mock the keymgmt external mount response', async () => { + await page.route('**/v1/sys/internal/ui/mounts/keymgmt-external', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(KEYMGMT_EXTERNAL_MOUNT_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await test.step('mock the initial keymgmt external tunde response', async () => { + await page.route('**/v1/sys/mounts/keymgmt-external/tune', async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 204, + contentType: 'application/json', + }); + } else { + await route.continue(); + } + }); + }); + + await page.goto('dashboard'); + + await test.step("navigate to the external keymgmt mount's general settings page", async () => { + await page.goto('secrets-engines/keymgmt-external/list'); + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Configure' }).click(); + await expect(page.getByRole('heading', { level: 1 })).toContainText('keymgmt-external configuration'); + await expect(page.getByRole('link', { name: 'General settings' })).toBeVisible(); + await expect(page.getByRole('paragraph').nth(2)).toContainText('vault-plugin-secrets-keymgmt'); + await expect(page.getByRole('paragraph').nth(3)).toContainText('v0.17.0+ent (Pinned)'); + }); + + await test.step('verify that selecting an unpinned version shows the override message', async () => { + await page.getByLabel('Update version to:').selectOption('v0.16.0+ent'); + await expect(page.getByLabel('Override pinned version')).toContainText( + 'You have selected v0.16.0+ent, but version v0.17.0+ent is pinned for this plugin. Updating to this version will override the pinned version for this mount.' + ); + }); + + await test.step('reset the version selection and verify that the override message goes away', async () => { + await page.getByLabel('Update version to:').selectOption(''); + await expect(page.getByLabel('Override pinned version')).not.toBeVisible(); + }); + + await test.step('verify that selecting an unpinned version shows the override message', async () => { + await page.getByLabel('Update version to:').selectOption('v0.18.0+ent'); + await expect(page.getByLabel('Override pinned version')).toContainText( + 'You have selected v0.18.0+ent, but version v0.17.0+ent is pinned for this plugin. Updating to this version will override the pinned version for this mount.' + ); + }); + + await test.step('mock updated mount response after tuning with a new plugin version', async () => { + await page.route('**/v1/sys/internal/ui/mounts/keymgmt-external', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(UPDATED_KEYMGMT_EXTERNAL_MOUNT_DATA), + }); + } else { + await route.continue(); + } + }); + }); + + await page.getByRole('button', { name: 'Save changes' }).click(); +}); diff --git a/ui/e2e/tests/superuser/keymgmt.spec.ts b/ui/e2e/tests/superuser/keymgmt.spec.ts new file mode 100644 index 0000000000..4b8371f1bd --- /dev/null +++ b/ui/e2e/tests/superuser/keymgmt.spec.ts @@ -0,0 +1,86 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; + +test('keymgmt workflow', async ({ page }) => { + await test.step('mock the distribution response', async () => { + await page.route('**/v1/keymgmt-builtin/kms/test-provider/key/test-key', async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + }); + } else { + await route.continue(); + } + }); + }); + + await page.goto('dashboard'); + + await test.step('enable Key Management secrets engine', async () => { + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('Key Management - enabled').click(); + await page.getByRole('textbox', { name: 'Path' }).fill('keymgmt-builtin'); + await page.getByRole('button', { name: 'Method Options' }).click(); + await page.getByRole('textbox', { name: 'Description' }).fill('This is a keymgmt mount.'); + await page.getByRole('checkbox', { name: 'Local' }).check(); + await page.getByRole('button', { name: 'Enable engine' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect( + page.getByText('Successfully mounted the keymgmt secrets engine at keymgmt-builtin') + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('create a provider', async () => { + await page.getByRole('link', { name: 'Create provider' }).click(); + await page.getByLabel('Type').selectOption('azurekeyvault'); + await page.getByRole('textbox', { name: 'Provider name' }).fill('test-provider'); + await page.getByRole('textbox', { name: 'Key Vault instance name' }).fill('keyvault-name'); + await page.getByRole('textbox', { name: 'client_id' }).fill('a0454cd1-e28e-405e-bc50-7477fa8a00b7'); + await page.getByRole('textbox', { name: 'client_secret' }).fill('eR%HizuCVEpAKgeaUEx'); + await page.getByRole('textbox', { name: 'tenant_id' }).fill('cd4bf224-d114-4f96-9bbc-b8f45751c43f'); + await page.getByRole('button', { name: 'Create provider' }).click(); + + await expect(page.locator('span').filter({ hasText: 'test-provider' })).toBeVisible(); + await expect(page.getByText('Azure Key Vault')).toBeVisible(); + await expect(page.getByText('keyvault-name')).toBeVisible(); + await expect(page.getByText('None')).toBeVisible(); + }); + + await test.step('create a key', async () => { + await page.getByRole('link', { name: 'Keys' }).click(); + await page.getByRole('link', { name: 'Create key' }).click(); + await page.getByRole('textbox', { name: 'Key name' }).fill('test-key'); + await page.getByRole('checkbox', { name: 'Allow deletion' }).check(); + await page.getByRole('button', { name: 'Create key' }).click(); + + await expect(page.locator('section')).toContainText('test-key'); + await expect(page.locator('section')).toContainText('rsa-2048'); + await expect(page.locator('section')).toContainText('Yes'); + }); + + await test.step('distribute key to provider', async () => { + await page.getByLabel('toolbar actions').getByRole('button', { name: 'Distribute key' }).click(); + await page.getByText('Search').click(); + await page.getByRole('option', { name: 'test-provider' }).click(); + await page.getByText('Encrypt').click(); + await page.getByText('Decrypt').click(); + await page.getByText('Sign').click(); + await page.getByText('Verify').click(); + await page.getByText('Wrap', { exact: true }).click(); + await page.getByText('Unwrap').click(); + await page.getByRole('radio', { name: 'HSM' }).check(); + await page.getByRole('button', { name: 'Distribute key' }).click(); + + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully distributed key test-key to test-provider')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); +}); From 8e9772f516c1f6e604a810f1b987da4435bfa2db Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 15:17:33 -0400 Subject: [PATCH 086/468] Backport events: fix missed events with multiple event clients into ce/main (#12867) Co-authored-by: Theron Voran --- go.mod | 2 +- vault/eventbus/bus.go | 10 +- vault/eventbus/bus_test.go | 2 +- vault/eventbus/filter.go | 161 +++++++++---- vault/eventbus/filter_test.go | 434 ++++++++++++++++++++++++++++++++-- 5 files changed, 546 insertions(+), 63 deletions(-) diff --git a/go.mod b/go.mod index 839b7ad268..b0dd9de888 100644 --- a/go.mod +++ b/go.mod @@ -233,7 +233,6 @@ require ( google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.10 gopkg.in/ory-am/dockertest.v3 v3.3.4 - k8s.io/apimachinery v0.34.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 layeh.com/radius v0.0.0-20231213012653-1006025d24f8 ) @@ -255,6 +254,7 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + k8s.io/apimachinery v0.34.1 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/vault/eventbus/bus.go b/vault/eventbus/bus.go index 4952c40679..7903341d1d 100644 --- a/vault/eventbus/bus.go +++ b/vault/eventbus/bus.go @@ -288,7 +288,9 @@ func (bus *EventBus) subscribeInternal(ctx context.Context, namespacePathPattern if err != nil { return nil, nil, err } - bus.filters.addPattern(bus.filters.self, namespacePathPatterns, pattern) + // use filterNodeID as the "subscription id" when storing a subscriber + // pattern + bus.filters.addPattern(bus.filters.self, namespacePathPatterns, pattern, filterNodeID) bus.filters.addClusterWidePattern(namespacePathPatterns, pattern) } err = bus.broker.RegisterNode(eventlogger.NodeID(filterNodeID), filterNode) @@ -304,8 +306,10 @@ func (bus *EventBus) subscribeInternal(ctx context.Context, namespacePathPattern ctx, cancel := context.WithCancel(ctx) asyncNode := newAsyncNode(ctx, bus.logger, bus.broker, func() { if clusterNode == nil { - bus.filters.removePattern(bus.filters.self, namespacePathPatterns, pattern) - bus.filters.removeClusterWidePattern(namespacePathPatterns, pattern) + // use filterNodeID as the "subscription id" when removing a + // subscriber pattern + bus.filters.removePattern(bus.filters.self, namespacePathPatterns, pattern, filterNodeID) + bus.filters.makeClusterWideFilters() } }) err = bus.broker.RegisterNode(eventlogger.NodeID(sinkNodeID), asyncNode) diff --git a/vault/eventbus/bus_test.go b/vault/eventbus/bus_test.go index f9da6df963..a4e6fe7d29 100644 --- a/vault/eventbus/bus_test.go +++ b/vault/eventbus/bus_test.go @@ -832,7 +832,7 @@ func TestSubscribeClusterNode(t *testing.T) { bus.Start() - bus.filters.addPattern("somecluster", []string{""}, "abc*") + bus.filters.addPattern("somecluster", []string{""}, "abc*", "uuid") ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(cancelFunc) ch, cancel2, err := bus.NewClusterNodeSubscription(ctx, "somecluster") diff --git a/vault/eventbus/filter.go b/vault/eventbus/filter.go index ed914cccda..a511d27f64 100644 --- a/vault/eventbus/filter.go +++ b/vault/eventbus/filter.go @@ -6,15 +6,16 @@ package eventbus import ( "context" "fmt" + "maps" "slices" "sort" "strings" "sync" + "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/logical" "github.com/ryanuber/go-glob" - "k8s.io/apimachinery/pkg/util/sets" ) // clusterWide is used to collect and keep track of subscriber patterns from all @@ -48,9 +49,65 @@ func (p pattern) isEmpty() bool { return p.namespacePatterns == "" && p.eventTypePattern == "" } +// patternSet is a map of patterns to subscription id's +type patternSet map[pattern][]string + +func newPatternSet() patternSet { + return make(patternSet) +} + +// Insert a pattern into the pattern set for the given subscription ID, +// maintaining subscription IDs in sorted order for search efficiency when +// inserting. +func (ps patternSet) Insert(p pattern, subscriptionID string) patternSet { + position, found := slices.BinarySearch(ps[p], subscriptionID) + if !found { + ps[p] = slices.Insert(ps[p], position, subscriptionID) + } + return ps +} + +// Delete removes the given subscriptionID from the pattern, if that association +// exists. +func (ps patternSet) Delete(p pattern, subscriptionID string) patternSet { + if ps == nil { + return nil + } + if ids, ok := ps[p]; ok { + if i := slices.Index(ids, subscriptionID); i >= 0 { + ps[p] = append(ids[:i], ids[i+1:]...) + } + if len(ps[p]) == 0 { + delete(ps, p) + } + } + return ps +} + +func (ps patternSet) Clear() { + clear(ps) +} + +// Difference returns the set of patterns from ps not in check. The subscriber +// ID slices are also considered when checking for differences. +func (ps patternSet) Difference(check patternSet) patternSet { + diff := newPatternSet() + for p := range ps { + if _, ok := check[p]; !ok { + diff[p] = ps[p] + } else { + subIDdiff := strutil.Difference(ps[p], check[p], false) + if len(subIDdiff) > 0 { + diff[p] = subIDdiff + } + } + } + return diff +} + // ClusterNodeFilter keeps track of all patterns that a particular cluster node is interested in. type ClusterNodeFilter struct { - patterns sets.Set[pattern] + patterns patternSet } // match checks if the given ns and eventType matches any pattern in the cluster node's filter. @@ -93,20 +150,19 @@ func (f *Filters) String() string { func (nf *ClusterNodeFilter) String() string { var x []string - l := nf.patterns.UnsortedList() - for _, v := range l { - x = append(x, v.String()) + for p, subscriberIDs := range nf.patterns { + x = append(x, fmt.Sprintf("%s: %v", p.String(), subscriberIDs)) } slices.Sort(x) return strings.Join(x, ",") } func (f *Filters) addClusterWidePattern(namespacePatterns []string, eventTypePattern string) { - f.addPattern(clusterWide, namespacePatterns, eventTypePattern) + f.addPattern(clusterWide, namespacePatterns, eventTypePattern, clusterWide) } func (f *Filters) removeClusterWidePattern(namespacePatterns []string, eventTypePattern string) { - f.removePattern(clusterWide, namespacePatterns, eventTypePattern) + f.removePattern(clusterWide, namespacePatterns, eventTypePattern, clusterWide) } func (f *Filters) clearClusterWidePatterns() { @@ -155,9 +211,12 @@ func (f *Filters) clearClusterNodePatterns(c clusterNodeID) { func (f *Filters) copyPatternWithLock(c clusterNodeID) *ClusterNodeFilter { filters := &ClusterNodeFilter{} if got, ok := f.filters[c]; ok { - filters.patterns = got.patterns.Clone() + filters.patterns = maps.Clone(got.patterns) + for key, slice := range filters.patterns { + filters.patterns[key] = slices.Clone(slice) + } } else { - filters.patterns = sets.New[pattern]() + filters.patterns = newPatternSet() } return filters } @@ -166,13 +225,15 @@ func (f *Filters) makeClusterWideFilters() { defer f.notify(clusterWide) f.lock.Lock() defer f.lock.Unlock() - newPatterns := sets.New[pattern]() + newPatterns := newPatternSet() for c, cf := range f.filters { if c == clusterWide { continue } for p := range cf.patterns { - newPatterns.Insert(p) + // The set of cluster-wide patterns doesn't need to keep track of all + // the subscription id's, so we just use the cluster-wide pattern uuid + newPatterns.Insert(p, clusterWide) } } f.filters[clusterWide] = &ClusterNodeFilter{patterns: newPatterns} @@ -183,11 +244,11 @@ func (f *Filters) applyChanges(c clusterNodeID, changes []FilterChange) { defer f.notify(c) f.lock.Lock() defer f.lock.Unlock() - var newPatterns sets.Set[pattern] + var newPatterns patternSet if existing, ok := f.filters[c]; ok { newPatterns = existing.patterns } else { - newPatterns = sets.New[pattern]() + newPatterns = newPatternSet() } for _, change := range changes { applyChange(newPatterns, &change) @@ -196,18 +257,18 @@ func (f *Filters) applyChanges(c clusterNodeID, changes []FilterChange) { } // applyChange applies a single filter change to the given set. -func applyChange(s sets.Set[pattern], change *FilterChange) { +func applyChange(s patternSet, change *FilterChange) { switch change.Operation { case FilterChangeAdd: nsPatterns := slices.Clone(change.NamespacePatterns) sort.Strings(nsPatterns) p := pattern{eventTypePattern: change.EventTypePattern, namespacePatterns: cleanJoinNamespaces(nsPatterns)} - s.Insert(p) + s.Insert(p, change.SubscriberID) case FilterChangeRemove: nsPatterns := slices.Clone(change.NamespacePatterns) sort.Strings(nsPatterns) check := pattern{eventTypePattern: change.EventTypePattern, namespacePatterns: cleanJoinNamespaces(nsPatterns)} - s.Delete(check) + s.Delete(check, change.SubscriberID) case FilterChangeClear: s.Clear() } @@ -219,28 +280,32 @@ func cleanJoinNamespaces(nsPatterns []string) string { trimmed[i] = strings.TrimSpace(nsPatterns[i]) } // sort and uniq - trimmed = sets.NewString(trimmed...).List() + sort.Strings(trimmed) + trimmed = slices.Compact(trimmed) return strings.Join(trimmed, " ") } // addPattern adds a pattern to a cluster node's list. -func (f *Filters) addPattern(c clusterNodeID, namespacePatterns []string, eventTypePattern string) { +func (f *Filters) addPattern(c clusterNodeID, namespacePatterns []string, eventTypePattern string, subscriptionID string) { defer f.notify(c) f.lock.Lock() defer f.lock.Unlock() if _, ok := f.filters[c]; !ok { f.filters[c] = &ClusterNodeFilter{ - patterns: sets.New[pattern](), + patterns: newPatternSet(), } } nsPatterns := slices.Clone(namespacePatterns) sort.Strings(nsPatterns) - p := pattern{eventTypePattern: eventTypePattern, namespacePatterns: cleanJoinNamespaces(namespacePatterns)} - f.filters[c].patterns.Insert(p) + p := pattern{ + eventTypePattern: eventTypePattern, + namespacePatterns: cleanJoinNamespaces(nsPatterns), + } + f.filters[c].patterns.Insert(p, subscriptionID) } // removePattern removes a pattern from a cluster node's list. -func (f *Filters) removePattern(c clusterNodeID, namespacePatterns []string, eventTypePattern string) { +func (f *Filters) removePattern(c clusterNodeID, namespacePatterns []string, eventTypePattern, subscriptionID string) { defer f.notify(c) nsPatterns := slices.Clone(namespacePatterns) sort.Strings(nsPatterns) @@ -251,7 +316,7 @@ func (f *Filters) removePattern(c clusterNodeID, namespacePatterns []string, eve if !ok { return } - filters.patterns.Delete(check) + filters.patterns.Delete(check, subscriptionID) } // anyMatch returns true if any cluster node's pattern list matches the arguments. @@ -338,6 +403,10 @@ func (f *Filters) watch(ctx context.Context, clusterNode clusterNodeID) (<-chan return } changes := calculateChanges(current, next) + if len(changes) == 0 { + // If there are no changes, don't notify + continue + } current = next // check if the context is finished before sending select { @@ -357,6 +426,7 @@ type FilterChange struct { Operation int NamespacePatterns []string EventTypePattern string + SubscriberID string } const ( @@ -376,34 +446,43 @@ func calculateChanges(from *ClusterNodeFilter, to *ClusterNodeFilter) []FilterCh changes = append(changes, FilterChange{ Operation: FilterChangeClear, }) - for pattern := range to.patterns { + for pattern, subscriberIDs := range to.patterns { if !pattern.isEmpty() { - changes = append(changes, FilterChange{ - Operation: FilterChangeAdd, - NamespacePatterns: strings.Split(pattern.namespacePatterns, " "), - EventTypePattern: pattern.eventTypePattern, - }) + for _, subscriberID := range subscriberIDs { + changes = append(changes, FilterChange{ + Operation: FilterChangeAdd, + NamespacePatterns: strings.Split(pattern.namespacePatterns, " "), + EventTypePattern: pattern.eventTypePattern, + SubscriberID: subscriberID, + }) + } } } } else { additions := to.patterns.Difference(from.patterns) subtractions := from.patterns.Difference(to.patterns) - for add := range additions { + for add, subscriberIDs := range additions { if !add.isEmpty() { - changes = append(changes, FilterChange{ - Operation: FilterChangeAdd, - NamespacePatterns: strings.Split(add.namespacePatterns, " "), - EventTypePattern: add.eventTypePattern, - }) + for _, subscriberID := range subscriberIDs { + changes = append(changes, FilterChange{ + Operation: FilterChangeAdd, + NamespacePatterns: strings.Split(add.namespacePatterns, " "), + EventTypePattern: add.eventTypePattern, + SubscriberID: subscriberID, + }) + } } } - for sub := range subtractions { + for sub, subscriberIDs := range subtractions { if !sub.isEmpty() { - changes = append(changes, FilterChange{ - Operation: FilterChangeRemove, - NamespacePatterns: strings.Split(sub.namespacePatterns, " "), - EventTypePattern: sub.eventTypePattern, - }) + for _, subscriberID := range subscriberIDs { + changes = append(changes, FilterChange{ + Operation: FilterChangeRemove, + NamespacePatterns: strings.Split(sub.namespacePatterns, " "), + EventTypePattern: sub.eventTypePattern, + SubscriberID: subscriberID, + }) + } } } } diff --git a/vault/eventbus/filter_test.go b/vault/eventbus/filter_test.go index 2621ac0827..c2f0a36591 100644 --- a/vault/eventbus/filter_test.go +++ b/vault/eventbus/filter_test.go @@ -35,12 +35,12 @@ func TestFilters_AddRemoveMatchLocal(t *testing.T) { assert.False(t, f.localMatch(ns, "abc")) assert.False(t, f.anyMatch(ns, "abc")) - f.addPattern("self", []string{ns.Path}, "abc") + f.addPattern("self", []string{ns.Path}, "abc", "uuid") assert.True(t, f.localMatch(ns, "abc")) assert.False(t, f.localMatch(ns, "abcd")) assert.True(t, f.anyMatch(ns, "abc")) assert.False(t, f.anyMatch(ns, "abcd")) - f.removePattern("self", []string{ns.Path}, "abc") + f.removePattern("self", []string{ns.Path}, "abc", "uuid") assert.False(t, f.localMatch(ns, "abc")) assert.False(t, f.anyMatch(ns, "abc")) } @@ -51,7 +51,7 @@ func TestFilters_Watch(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(cancelFunc) f := NewFilters("self") - f.addPattern("self", []string{"ns1"}, "e3") + f.addPattern("self", []string{"ns1"}, "e3", "uuid") ch, cancelFunc2 := f.watch(ctx, "self") t.Cleanup(cancelFunc2) initial := <-ch // we always get one immediately for the current state @@ -60,24 +60,60 @@ func TestFilters_Watch(t *testing.T) { assert.Equal(t, FilterChangeAdd, initial[1].Operation) assert.Equal(t, []string{"ns1"}, initial[1].NamespacePatterns) assert.Equal(t, "e3", initial[1].EventTypePattern) + assert.Equal(t, "uuid", initial[1].SubscriberID) go func() { - f.addPattern("self", []string{"ns1"}, "e2") + f.addPattern("self", []string{"ns1"}, "e2", "uuid1") }() changes := waitForChanges(t, ch) assert.Equal(t, []FilterChange{{ Operation: FilterChangeAdd, NamespacePatterns: []string{"ns1"}, EventTypePattern: "e2", + SubscriberID: "uuid1", }}, changes) go func() { - f.removePattern("self", []string{"ns1"}, "e3") + f.addPattern("self", []string{"ns1"}, "e2", "uuid2") + }() + changes = waitForChanges(t, ch) + assert.Equal(t, []FilterChange{{ + Operation: FilterChangeAdd, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "e2", + SubscriberID: "uuid2", + }}, changes) + go func() { + f.removePattern("self", []string{"ns1"}, "e3", "uuid") }() changes = waitForChanges(t, ch) assert.Equal(t, []FilterChange{{ Operation: FilterChangeRemove, NamespacePatterns: []string{"ns1"}, EventTypePattern: "e3", + SubscriberID: "uuid", + }}, changes) + + // Remove and add one of the e2 patterns to test the copyPatternWithLock + // logic + go func() { + f.removePattern("self", []string{"ns1"}, "e2", "uuid1") + }() + changes = waitForChanges(t, ch) + assert.Equal(t, []FilterChange{{ + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "e2", + SubscriberID: "uuid1", + }}, changes) + go func() { + f.addPattern("self", []string{"ns1"}, "e2", "uuid1") + }() + changes = waitForChanges(t, ch) + assert.Equal(t, []FilterChange{{ + Operation: FilterChangeAdd, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "e2", + SubscriberID: "uuid1", }}, changes) } @@ -96,7 +132,7 @@ func waitForChanges(t *testing.T, ch <-chan []FilterChange) []FilterChange { // TestFilters_WatchCancel checks that calling the cancel function will clean up the channel. func TestFilters_WatchCancel(t *testing.T) { f := NewFilters("self") - f.addPattern("self", []string{"ns1"}, "e3") + f.addPattern("self", []string{"ns1"}, "e3", "uuid") ch, cancelFunc := f.watch(context.Background(), "self") t.Cleanup(cancelFunc) initial := <-ch // we always get one immediately for the current state @@ -128,18 +164,18 @@ func TestFilters_WatchCancel(t *testing.T) { // TestFilters_AddRemoveClear tests that add/remove/clear works as expected. func TestFilters_AddRemoveClear(t *testing.T) { f := NewFilters("self") - f.addPattern("somecluster", []string{"ns1"}, "abc") - f.removePattern("somecluster", []string{"ns1"}, "abcd") - assert.Equal(t, "{ns=ns1,ev=abc}", f.filters["somecluster"].String()) - f.removePattern("somecluster", []string{"ns1"}, "abc") + f.addPattern("somecluster", []string{"ns1"}, "abc", "uuid") + f.removePattern("somecluster", []string{"ns1"}, "abcd", "uuid") + assert.Equal(t, "{ns=ns1,ev=abc}: [uuid]", f.filters["somecluster"].String()) + f.removePattern("somecluster", []string{"ns1"}, "abc", "uuid") assert.Equal(t, "", f.filters["somecluster"].String()) - f.addPattern("somecluster", []string{"ns1"}, "abc") + f.addPattern("somecluster", []string{"ns1"}, "abc", "uuid") f.clearClusterNodePatterns("somecluster") assert.NotContains(t, f.filters, "somecluster") f.addClusterWidePattern([]string{"ns1"}, "abc") f.removeClusterWidePattern([]string{"ns1"}, "abcd") - assert.Equal(t, "{ns=ns1,ev=abc}", f.filters[clusterWide].String()) + assert.Equal(t, "{ns=ns1,ev=abc}: [__cluster_wide__]", f.filters[clusterWide].String()) f.removeClusterWidePattern([]string{"ns1"}, "abc") assert.Equal(t, "", f.filters[clusterWide].String()) f.addClusterWidePattern([]string{"ns1"}, "abc") @@ -151,12 +187,376 @@ func TestFilters_AddRemoveClear(t *testing.T) { // works as expected. func TestFilters_makeClusterWideFilters(t *testing.T) { f := NewFilters("self") - f.addPattern("node1", []string{"ns1"}, "abc") - f.addPattern("node2", []string{"ns1"}, "abc") + f.addPattern("node1", []string{"ns1"}, "abc", "uuid") + f.addPattern("node2", []string{"ns1"}, "abc", "uuid") f.makeClusterWideFilters() - assert.Equal(t, "{ns=ns1,ev=abc}", f.filters[clusterWide].String()) + assert.Equal(t, "{ns=ns1,ev=abc}: [__cluster_wide__]", f.filters[clusterWide].String()) - f.addPattern("node3", []string{"ns3"}, "def") + f.addPattern("node3", []string{"ns3"}, "def", "uuid") f.makeClusterWideFilters() - assert.Equal(t, "{ns=ns1,ev=abc},{ns=ns3,ev=def}", f.filters[clusterWide].String()) + assert.Equal(t, "{ns=ns1,ev=abc}: [__cluster_wide__],{ns=ns3,ev=def}: [__cluster_wide__]", f.filters[clusterWide].String()) +} + +// TestFilters_duplicate_filters tests the underpinnings of the scenario where +// two subscribers subscribe to the same standby node using the same event +// filters, then one disconnects. The filter should still be present for the +// remaining subscriber (so that the active node knows to keep forwarding +// matching events, for example). +func TestFilters_duplicate_filters(t *testing.T) { + f := NewFilters("self") + expectedPattern := pattern{eventTypePattern: "abc", namespacePatterns: "ns1"} + + f.addPattern("node1", []string{expectedPattern.namespacePatterns}, expectedPattern.eventTypePattern, "uuid1") + f.addPattern("node1", []string{expectedPattern.namespacePatterns}, expectedPattern.eventTypePattern, "uuid2") + assert.Equal(t, 1, len(f.filters["node1"].patterns)) + assert.Equal(t, []string{"uuid1", "uuid2"}, f.filters["node1"].patterns[expectedPattern]) + + f.removePattern("node1", []string{"ns1"}, "abc", "uuid1") + assert.Equal(t, 1, len(f.filters["node1"].patterns)) + assert.Equal(t, []string{"uuid2"}, f.filters["node1"].patterns[expectedPattern]) + + f.removePattern("node1", []string{"ns1"}, "abc", "uuid2") + assert.Empty(t, f.filters["node1"].patterns) +} + +// TestPatternSet_basics tests the basic functionality of the patternSet type. +func TestPatternSet_basics(t *testing.T) { + ps := newPatternSet() + ps.Insert(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_def") + ps.Insert(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_abc") + ps.Insert(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_ghi") + // The subscriber id's should be sorted + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_abc", "uuid_def", "uuid_ghi"}, + }, + ps, + ) + + // Duplicate uuids should be ignored + ps.Insert(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_abc") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_abc", "uuid_def", "uuid_ghi"}, + }, + ps, + ) + + ps.Insert(pattern{eventTypePattern: "def", namespacePatterns: "ns2"}, "uuid") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_abc", "uuid_def", "uuid_ghi"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid"}, + }, + ps, + ) + + ps.Delete(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_abc") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_def", "uuid_ghi"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid"}, + }, + ps, + ) + + ps.Delete(pattern{eventTypePattern: "def", namespacePatterns: "ns2"}, "uuid") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_def", "uuid_ghi"}, + }, + ps, + ) + + // subscriber id isn't present, nothing deleted + ps.Delete(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_abc") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_def", "uuid_ghi"}, + }, + ps, + ) + + // Delete all patterns + ps.Delete(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_def") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid_ghi"}, + }, + ps, + ) + ps.Delete(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid_ghi") + assert.Empty(t, ps) + + // Insert again + ps.Insert(pattern{eventTypePattern: "ghi", namespacePatterns: "ns1"}, "uuid") + assert.Len(t, ps, 1) + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "ghi", namespacePatterns: "ns1"}: []string{"uuid"}, + }, + ps, + ) + ps.Insert(pattern{eventTypePattern: "jkl", namespacePatterns: "ns2"}, "uuid") + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "ghi", namespacePatterns: "ns1"}: []string{"uuid"}, + pattern{eventTypePattern: "jkl", namespacePatterns: "ns2"}: []string{"uuid"}, + }, + ps, + ) + ps.Delete(pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, "uuid") + // should have deleted nothing + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "ghi", namespacePatterns: "ns1"}: []string{"uuid"}, + pattern{eventTypePattern: "jkl", namespacePatterns: "ns2"}: []string{"uuid"}, + }, + ps, + ) + ps.Delete(pattern{eventTypePattern: "ghi", namespacePatterns: "ns1"}, "uuid") + assert.Len(t, ps, 1) + assert.Equal(t, + patternSet{ + pattern{eventTypePattern: "jkl", namespacePatterns: "ns2"}: []string{"uuid"}, + }, + ps, + ) + + ps.Clear() + assert.Empty(t, ps) +} + +// TestPatternSetDelete does more in-depth testing of the patternSet Delete() +func TestPatternSetDelete(t *testing.T) { + testCases := map[string]struct { + ps patternSet + pattern pattern + subscriptionID string + expected patternSet + }{ + "empty ps": { + ps: newPatternSet(), + pattern: pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, + subscriptionID: "uuid", + expected: patternSet{}, + }, + "remove one full pattern": { + ps: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1", "uuid2"}, + }, + pattern: pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, + subscriptionID: "uuid1", + expected: patternSet{ + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1", "uuid2"}, + }, + }, + "remove first subscription id": { + ps: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2", "uuid3"}, + }, + pattern: pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, + subscriptionID: "uuid1", + expected: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid2", "uuid3"}, + }, + }, + "remove last subscription id": { + ps: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2"}, + }, + pattern: pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, + subscriptionID: "uuid2", + expected: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + }, + }, + "remove only pattern": { + ps: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + }, + pattern: pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}, + subscriptionID: "uuid1", + expected: patternSet{}, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := tc.ps.Delete(tc.pattern, tc.subscriptionID) + assert.Equal(t, tc.expected, result) + }) + } +} + +// TestPatternSetDifference tests the Difference method of the patternSet type. +func TestPatternSetDifference(t *testing.T) { + testCases := map[string]struct { + ps1 patternSet + ps2 patternSet + expectedDiff patternSet + }{ + "empty ps1": { + ps1: newPatternSet(), + ps2: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1", "uuid2"}, + }, + expectedDiff: newPatternSet(), + }, + "empty ps2": { + ps1: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1", "uuid2"}, + }, + ps2: newPatternSet(), + expectedDiff: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1", "uuid2"}, + }, + }, + "both empty": { + ps1: newPatternSet(), + ps2: newPatternSet(), + expectedDiff: newPatternSet(), + }, + "regular diff": { + ps1: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + ps2: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "xyz", namespacePatterns: ""}: []string{"uuid1"}, + }, + expectedDiff: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid2"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + }, + } + + for _, tc := range testCases { + diff := tc.ps1.Difference(tc.ps2) + assert.Equal(t, tc.expectedDiff, diff) + } +} + +// Test_calculateChanges exercises the logic for calculating changes in the form +// of []FilterChange between two patternSets +func Test_calculateChanges(t *testing.T) { + type testCase struct { + from patternSet + to patternSet + expectedChanges []FilterChange + } + testCases := map[string]testCase{ + "remove one pattern": { + from: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + to: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + }, + expectedChanges: []FilterChange{ + { + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns2"}, + EventTypePattern: "def", + SubscriberID: "uuid1", + }, + }, + }, + "remove one uuid": { + from: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + to: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid2"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + expectedChanges: []FilterChange{ + { + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "abc", + SubscriberID: "uuid1", + }, + }, + }, + "remove all uuids": { + from: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + to: patternSet{}, + expectedChanges: []FilterChange{ + { + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "abc", + SubscriberID: "uuid1", + }, + { + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "abc", + SubscriberID: "uuid2", + }, + { + Operation: FilterChangeRemove, + NamespacePatterns: []string{"ns2"}, + EventTypePattern: "def", + SubscriberID: "uuid1", + }, + }, + }, + "add one pattern": { + from: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + }, + to: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + pattern{eventTypePattern: "def", namespacePatterns: "ns2"}: []string{"uuid1"}, + }, + expectedChanges: []FilterChange{ + { + Operation: FilterChangeAdd, + NamespacePatterns: []string{"ns2"}, + EventTypePattern: "def", + SubscriberID: "uuid1", + }, + }, + }, + "add one uuid": { + from: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1"}, + }, + to: patternSet{ + pattern{eventTypePattern: "abc", namespacePatterns: "ns1"}: []string{"uuid1", "uuid2"}, + }, + expectedChanges: []FilterChange{ + { + Operation: FilterChangeAdd, + NamespacePatterns: []string{"ns1"}, + EventTypePattern: "abc", + SubscriberID: "uuid2", + }, + }, + }, + } + for name, tt := range testCases { + t.Run(name, func(t *testing.T) { + fromCNF := ClusterNodeFilter{ + patterns: tt.from, + } + toCNF := ClusterNodeFilter{ + patterns: tt.to, + } + got := calculateChanges(&fromCNF, &toCNF) + assert.ElementsMatch(t, tt.expectedChanges, got) + }) + } } From 6dba9df96d919cd11298a4843de14ff845c1e62d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 15:34:41 -0400 Subject: [PATCH 087/468] [VAULT-43251]: docker: resolve ALPINE-CVE-2026-22184 Resolve ALPINE-CVE-2026-22184 by updating zlib when creating our Alpine container. Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 32da3d036a..0f5006fba4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,9 @@ ENV NAME=$NAME # Create a non-root user to run the software. RUN addgroup ${NAME} && adduser -S -G ${NAME} ${NAME} -RUN apk add --no-cache libcap su-exec dumb-init tzdata +# NOTE: zlib is only here to resolve ALPINE-CVE-2026-27171, it can be removed +# when when our Alpine release is >= 3.23.4 +RUN apk update && apk add --upgrade --no-cache libcap su-exec dumb-init tzdata zlib COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME /bin/ From 4f71c2cde046739cbd2f5be6d1c10d3efea527ff Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 17:44:36 -0400 Subject: [PATCH 088/468] Copy pki/acme: reject unsafe validation targets during challenge verification into main (#12959) (#12963) * Copy https://github.com/hashicorp/vault/pull/31828 into main * pki/acme: reject unsafe validation targets reject loopback, link-local, unspecified, multicast, and other non-global-unicast targets before HTTP-01 and TLS-ALPN-01 validation connections are attempted. the shared dial helper now filters both direct IP literals and DNS resolution results, and regression tests cover loopback-via-DNS for both validators plus direct loopback literals. * changelog: add entry for ACME validation hardening PR * pki/acme: allow configured validation targets * pki/acme: add docs for validation tests --------- Co-authored-by: 1seal --- builtin/logical/pki/acme_challenges.go | 100 +++++++++++++- builtin/logical/pki/acme_challenges_test.go | 136 +++++++++++++++++++- builtin/logical/pki/path_config_acme.go | 1 + changelog/31828.txt | 3 + 4 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 changelog/31828.txt diff --git a/builtin/logical/pki/acme_challenges.go b/builtin/logical/pki/acme_challenges.go index b5eade5fcc..530625840e 100644 --- a/builtin/logical/pki/acme_challenges.go +++ b/builtin/logical/pki/acme_challenges.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "encoding/asn1" "encoding/base64" + "errors" "fmt" "io" "net" @@ -118,6 +119,91 @@ func buildDialerConfig(config *acmeConfigEntry) (*net.Dialer, error) { }, nil } +func isIPInCIDRList(cidrList []string, ip net.IP) bool { + if len(cidrList) == 0 { + return false + } + + for _, cidr := range cidrList { + _, ipNet, err := net.ParseCIDR(cidr) + if err == nil && ipNet.Contains(ip) { + return true + } + + allowedIP := net.ParseIP(cidr) + if allowedIP != nil && allowedIP.Equal(ip) { + return true + } + } + + return false +} + +func isDisallowedACMEValidationIP(config *acmeConfigEntry, ip net.IP) bool { + if ip == nil { + return true + } + + if isIPInCIDRList(config.AllowedCIDRList, ip) { + return false + } + + // Vault is often deployed on private networks, so avoid a blanket private + // range denylist here and instead reject obviously unsafe targets. + if ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + return true + } + + return !ip.IsGlobalUnicast() +} + +func dialACMEValidationTarget(ctx context.Context, config *acmeConfigEntry, dialer *net.Dialer, network string, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return dialer.DialContext(ctx, network, address) + } + + if ip := net.ParseIP(host); ip != nil { + if isDisallowedACMEValidationIP(config, ip) { + return nil, fmt.Errorf("%w: validation target resolved to a disallowed ip range", ErrRejectedIdentifier) + } + + return dialer.DialContext(ctx, network, address) + } + + if dialer.Resolver == nil { + return dialer.DialContext(ctx, network, address) + } + + ips, err := dialer.Resolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + + var lastErr error + var foundAllowed bool + for _, candidate := range ips { + if isDisallowedACMEValidationIP(config, candidate.IP) { + continue + } + + foundAllowed = true + + conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(candidate.IP.String(), port)) + if err == nil { + return conn, nil + } + + lastErr = err + } + + if !foundAllowed { + return nil, fmt.Errorf("%w: validation target resolved to a disallowed ip range", ErrRejectedIdentifier) + } + + return nil, lastErr +} + // Validates a given ACME http-01 challenge against the specified domain, // per RFC 8555. // @@ -142,7 +228,9 @@ func ValidateHTTP01Challenge(domain string, token string, thumbprint string, con // We'd rather timeout and re-attempt validation later than hang // too many validators waiting for slow hosts. - DialContext: dialer.DialContext, + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return dialACMEValidationTarget(ctx, config, dialer, network, address) + }, ResponseHeaderTimeout: 10 * time.Second, } @@ -167,6 +255,10 @@ func ValidateHTTP01Challenge(domain string, token string, thumbprint string, con resp, err := client.Get(path) if err != nil { + if errors.Is(err, ErrRejectedIdentifier) { + return false, fmt.Errorf("%w: http-01: validation target resolved to a disallowed ip range", ErrRejectedIdentifier) + } + return false, fmt.Errorf("%w: %s", ErrConnection, fmt.Errorf("http-01: failed to fetch path %v: %w", path, err).Error()) } @@ -469,8 +561,12 @@ func ValidateTLSALPN01Challenge(domain string, token string, thumbprint string, // > 3. The ACME server initiates a TLS connection to the chosen IP // > address. This connection MUST use TCP port 443. address := fmt.Sprintf("%v:"+ALPNPort, domain) - conn, err := dialer.Dial("tcp", address) + conn, err := dialACMEValidationTarget(context.Background(), config, dialer, "tcp", address) if err != nil { + if errors.Is(err, ErrRejectedIdentifier) { + return false, fmt.Errorf("%w: tls-alpn-01: validation target resolved to a disallowed ip range", ErrRejectedIdentifier) + } + return false, fmt.Errorf("%w: %s", ErrConnection, fmt.Errorf("tls-alpn-01: failed to dial host: %w", err).Error()) } diff --git a/builtin/logical/pki/acme_challenges_test.go b/builtin/logical/pki/acme_challenges_test.go index ac845e735f..fcba6e85c4 100644 --- a/builtin/logical/pki/acme_challenges_test.go +++ b/builtin/logical/pki/acme_challenges_test.go @@ -17,6 +17,7 @@ import ( "encoding/base64" "fmt" "math/big" + "net" "net/http" "net/http/httptest" "strconv" @@ -26,6 +27,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/sdk/helper/testhelpers/dnstest" + "github.com/miekg/dns" "github.com/stretchr/testify/require" ) @@ -95,6 +97,59 @@ var keyAuthorizationTestCases = []keyAuthorizationTestCase{ }, } +func startLocalDNSServer(t *testing.T, records map[string]net.IP) string { + t.Helper() + + handler := dns.NewServeMux() + handler.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { + msg := new(dns.Msg) + msg.SetReply(r) + + for _, q := range r.Question { + if q.Qtype != dns.TypeA { + continue + } + + name := strings.TrimSuffix(strings.ToLower(q.Name), ".") + ip, ok := records[name] + if !ok { + continue + } + + a := ip.To4() + if a == nil { + continue + } + + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 0, + }, + A: a, + }) + } + + _ = w.WriteMsg(msg) + }) + + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + require.NoError(t, err) + + srv := &dns.Server{PacketConn: pc, Handler: handler} + go func() { + _ = srv.ActivateAndServe() + }() + + t.Cleanup(func() { + _ = srv.Shutdown() + }) + + return pc.LocalAddr().String() +} + func TestAcmeValidateKeyAuthorization(t *testing.T) { t.Parallel() @@ -149,7 +204,9 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { defer ts.Close() host := ts.URL[7:] - isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) + isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{ + AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + }) if !isValid && err == nil { t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", index, handlerIndex, isValid, err) } @@ -198,7 +255,9 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { defer ts.Close() host := ts.URL[7:] - isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{}) + isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{ + AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + }) if isValid || err == nil { t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err) } @@ -206,6 +265,30 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { } } +// TestAcmeValidateHTTP01ChallengeRejectsLoopbackViaDNS verifies that hosts that +// resolve to localhost are rejected. +func TestAcmeValidateHTTP01ChallengeRejectsLoopbackViaDNS(t *testing.T) { + t.Parallel() + + host := "acme-ipfilter-http01.example.test" + dnsAddr := startLocalDNSServer(t, map[string]net.IP{ + host: net.IPv4(127, 0, 0, 1), + }) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("my-token.my-thumbprint")) + })) + defer ts.Close() + + port := strconv.Itoa(ts.Listener.Addr().(*net.TCPAddr).Port) + config := &acmeConfigEntry{DNSResolver: dnsAddr} + + isValid, err := ValidateHTTP01Challenge(net.JoinHostPort(host, port), "my-token", "my-thumbprint", config) + require.False(t, isValid) + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedIdentifier) +} + func TestAcmeValidateDNS01Challenge(t *testing.T) { t.Parallel() @@ -243,7 +326,9 @@ func TestAcmeValidateTLSALPN01Challenge(t *testing.T) { // This test is not parallel because we modify ALPNPort to use a custom // non-standard port _just for testing purposes_. host := "localhost" - config := &acmeConfigEntry{} + config := &acmeConfigEntry{ + AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + } log := hclog.L() @@ -717,6 +802,47 @@ func TestAcmeValidateTLSALPN01Challenge(t *testing.T) { } } +// TestAcmeValidateTLSALPN01ChallengeRejectsLoopbackViaDNS verifies that hosts +// that resolve to localhost are rejected. +func TestAcmeValidateTLSALPN01ChallengeRejectsLoopbackViaDNS(t *testing.T) { + host := "acme-ipfilter-tlsalpn01.example.test" + dnsAddr := startLocalDNSServer(t, map[string]net.IP{ + host: net.IPv4(127, 0, 0, 1), + }) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + go func() { + conn, err := ln.Accept() + if err == nil { + _ = conn.Close() + } + }() + + oldPort := ALPNPort + ALPNPort = strconv.Itoa(ln.Addr().(*net.TCPAddr).Port) + defer func() { + ALPNPort = oldPort + }() + + config := &acmeConfigEntry{DNSResolver: dnsAddr} + isValid, err := ValidateTLSALPN01Challenge(host, "my-token", "my-thumbprint", config) + require.False(t, isValid) + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedIdentifier) +} + +// TestDialACMEValidationTargetRejectsLoopbackLiteral verifies that localhost is +// rejected as a valid validation target. +func TestDialACMEValidationTargetRejectsLoopbackLiteral(t *testing.T) { + conn, err := dialACMEValidationTarget(context.Background(), &acmeConfigEntry{}, &net.Dialer{Timeout: time.Second}, "tcp", "127.0.0.1:1") + require.Nil(t, conn) + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedIdentifier) +} + // TestAcmeValidateHttp01TLSRedirect verify that we allow a http-01 challenge to redirect // to a TLS server and not validate the certificate chain is valid. We don't validate the // TLS chain as we would have accepted the auth over a non-secured channel anyway had @@ -744,7 +870,9 @@ func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { defer ts.Close() host := ts.URL[len("http://"):] - isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) + isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{ + AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + }) if !isValid && err == nil { st.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) } diff --git a/builtin/logical/pki/path_config_acme.go b/builtin/logical/pki/path_config_acme.go index 3ac8981392..20cbcf7e10 100644 --- a/builtin/logical/pki/path_config_acme.go +++ b/builtin/logical/pki/path_config_acme.go @@ -37,6 +37,7 @@ type acmeConfigEntry struct { DNSResolver string `json:"dns_resolver"` EabPolicyName EabPolicyName `json:"eab_policy_name"` MaxTTL time.Duration `json:"max_ttl"` + AllowedCIDRList []string `json:"allowed_cidr_list"` } var defaultAcmeConfig = acmeConfigEntry{ diff --git a/changelog/31828.txt b/changelog/31828.txt new file mode 100644 index 0000000000..8f439d54aa --- /dev/null +++ b/changelog/31828.txt @@ -0,0 +1,3 @@ +```release-note:improvement +pki: Reject obviously unsafe validation targets during ACME HTTP-01 and TLS-ALPN-01 challenge verification +``` From 6208c6a416583b8bb40b2e8f7800cbb7d2c5f2ae Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 12 Mar 2026 18:20:40 -0400 Subject: [PATCH 089/468] UI: add playwright test coverage for policies (#12860) (#12947) * add playwright test coverage for policies * add playwright test coverage for policy generator in kv v2 * only show intro button for acl policies * separate ent tests Co-authored-by: lane-wetmore --- ui/app/components/page/policies.ts | 3 +- ui/e2e/tests/superuser/kv.spec.ts | 17 ++++ ui/e2e/tests/superuser/policies.ent.spec.ts | 99 +++++++++++++++++++++ ui/e2e/tests/superuser/policies.spec.ts | 74 +++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 ui/e2e/tests/superuser/policies.ent.spec.ts create mode 100644 ui/e2e/tests/superuser/policies.spec.ts diff --git a/ui/app/components/page/policies.ts b/ui/app/components/page/policies.ts index 22af985125..a37a1bd3a2 100644 --- a/ui/app/components/page/policies.ts +++ b/ui/app/components/page/policies.ts @@ -9,6 +9,7 @@ import { tracked } from '@glimmer/tracking'; import Component from '@glimmer/component'; import { WIZARD_ID } from 'vault/components/wizard/acl-policies/acl-wizard'; import errorMessage from 'vault/utils/error-message'; +import { PolicyTypes } from 'core/utils/code-generators/policy'; import type ApiService from 'vault/services/api'; import type FlashMessageService from 'vault/services/flash-messages'; @@ -88,7 +89,7 @@ export default class PagePoliciesComponent extends Component { } get showIntroButton() { - return this.showContent && this.hasOnlyDefaultPolicies; + return this.args.policyType === PolicyTypes.ACL && this.showContent && this.hasOnlyDefaultPolicies; } // Show when it is not in a dismissed state and there are no non-default policies and diff --git a/ui/e2e/tests/superuser/kv.spec.ts b/ui/e2e/tests/superuser/kv.spec.ts index 7249ca4b46..15fa331167 100644 --- a/ui/e2e/tests/superuser/kv.spec.ts +++ b/ui/e2e/tests/superuser/kv.spec.ts @@ -126,4 +126,21 @@ test('kvv2 workflow', async ({ page }) => { await expect(page.locator('section')).toContainText( 'Version 1 of this secret has been permanently destroyed A version that has been permanently deleted cannot be restored. You can view other versions of this secret in the Version History tab above. KV v2 API docs' ); + // generate policy + await page.getByRole('button', { name: 'Generate policy' }).click(); + await page.getByRole('textbox', { name: 'Policy name' }).fill('foo-policy'); + await page.getByRole('checkbox', { name: 'sudo' }).nth(0).check(); + await page.getByRole('checkbox', { name: 'read' }).nth(1).check(); + await page.getByRole('checkbox', { name: 'list' }).nth(2).check(); + await page.getByRole('button', { name: 'Delete' }).nth(3).click(); + await page.getByRole('button', { name: 'Add rule' }).click(); + await page.getByRole('textbox', { name: 'Resource path' }).nth(5).fill('kv-test/nomatch'); + await page.getByRole('checkbox', { name: 'create' }).nth(5).check(); + await page.getByRole('button', { name: 'Automation snippets' }).click(); + await expect(page.getByRole('code')).toContainText( + 'resource "vault_policy" "" { name = "foo-policy" policy = < { + await page.goto('dashboard'); + // nav to rgp policies and create a policy + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'Role governing policies' }).click(); + await page.getByRole('heading', { name: 'RGP policies', exact: true }).click(); + await page.getByRole('link', { name: 'Create RGP policy' }).click(); + await page.getByRole('textbox', { name: 'Policy name' }).fill('rgp-policy'); + await page.getByRole('button', { name: 'How to write a policy' }).click(); + await page.getByText('Here is an example policy').click(); + await page.getByRole('button', { name: 'Copy' }).nth(1).click(); + // access the clipboard to get the example policy + const clipboardValue = await page.evaluate(() => navigator.clipboard.readText()); + await page.getByRole('button', { name: 'Close' }).click(); + await page.getByRole('textbox', { name: 'Policy editor' }).fill(clipboardValue); + await page.getByLabel('Enforcement level').selectOption('advisory'); + await page.getByRole('button', { name: 'Create policy' }).click(); + await page.getByRole('heading', { name: 'rgp-policy' }).click(); + await page.getByRole('button', { name: 'Download policy' }).click(); + await expect(page.getByRole('alert', { name: 'Info' })).toBeVisible(); + await expect(page.getByLabel('Enforcement level: advisory')).toBeVisible(); + + // edit + await page.getByRole('link', { name: 'Edit policy' }).click(); + await page.getByLabel('Policy').clear(); + const updatedValue = clipboardValue + '\n# just a comment'; + await page.getByLabel('Policy').fill(updatedValue); + await page.getByLabel('Enforcement level', { exact: true }).selectOption('hard-mandatory'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByLabel('Enforcement level: hard-')).toBeVisible(); + await expect(page.getByRole('code')).toContainText('# just a comment'); + + // delete + await page.getByRole('link', { name: 'Edit policy' }).click(); + await page.getByRole('button', { name: 'Delete policy' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.getByRole('link', { name: 'rgp-policy', exact: true })).not.toBeVisible(); +}); + +test('egp policy workflow', async ({ page }) => { + await page.goto('dashboard'); + // nav to egp policies and create a policy + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'Endpoint governing policies' }).click(); + await page.getByRole('heading', { name: 'EGP policies', exact: true }).click(); + + await page.getByRole('link', { name: 'Create EGP policy' }).click(); + await page.getByRole('textbox', { name: 'Policy name' }).fill('egp-policy'); + await page.getByRole('button', { name: 'How to write a policy' }).click(); + + await page.getByText('Here is an example policy').click(); + await page.getByRole('button', { name: 'Copy' }).nth(1).click(); + // access the clipboard to get the example policy + const clipboardValue = await page.evaluate(() => navigator.clipboard.readText()); + await page.getByRole('button', { name: 'Close' }).click(); + await page.getByRole('textbox', { name: 'Policy editor' }).fill(clipboardValue); + await page.getByLabel('Enforcement level').selectOption('advisory'); + await page.getByRole('textbox', { name: 'Paths list item' }).fill('foo'); + await page.getByRole('button', { name: 'Add' }).click(); + await page.getByRole('textbox', { name: 'Paths list item 1' }).fill('bar'); + await page.getByRole('button', { name: 'Add' }).click(); + await page.getByRole('button', { name: 'Create policy' }).click(); + await page.getByRole('heading', { name: 'egp-policy' }).click(); + await page.getByRole('button', { name: 'Download policy' }).click(); + await expect(page.getByRole('alert', { name: 'Info' })).toBeVisible(); + await expect(page.getByLabel('Enforcement level: advisory')).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'foo' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'bar' })).toBeVisible(); + + // edit + await page.getByRole('link', { name: 'Edit policy' }).click(); + await page.getByLabel('Policy').clear(); + const updatedValue = clipboardValue + '\n# just a comment'; + await page.getByLabel('Policy').fill(updatedValue); + await page.getByLabel('Enforcement level', { exact: true }).selectOption('hard-mandatory'); + await page.getByRole('button', { name: 'delete row' }).nth(1).click(); + await page.getByRole('textbox', { name: 'Paths list item 1' }).fill('baz'); + await page.getByRole('button', { name: 'Add' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByLabel('Enforcement level: hard-')).toBeVisible(); + await page.getByRole('button', { name: 'Show more code' }).click(); + await expect(page.getByText('# just a comment')).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'foo' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'bar' })).not.toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'baz' })).toBeVisible(); + + // delete + await page.getByRole('link', { name: 'Edit policy' }).click(); + await page.getByRole('button', { name: 'Delete policy' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.getByRole('link', { name: 'egp-policy', exact: true })).not.toBeVisible(); +}); diff --git a/ui/e2e/tests/superuser/policies.spec.ts b/ui/e2e/tests/superuser/policies.spec.ts new file mode 100644 index 0000000000..2294bbeb91 --- /dev/null +++ b/ui/e2e/tests/superuser/policies.spec.ts @@ -0,0 +1,74 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('acl policy workflow', async ({ page }) => { + await page.goto('dashboard'); + // nav to acl policies and create a policy + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'Create ACL policy' }).click(); + await page.getByRole('textbox', { name: 'Policy name' }).fill('acl-policy'); + await page.getByRole('textbox', { name: 'Resource path' }).fill('kv'); + await page.getByRole('checkbox', { name: 'read' }).check(); + await page.getByRole('checkbox', { name: 'update' }).check(); + await page.getByRole('switch', { name: 'Show preview' }).click(); + await expect(page.getByRole('code')).toContainText('path "kv" { capabilities = ["read", "update"] }'); + await page.getByRole('switch', { name: 'Hide preview' }).click(); + await page.getByRole('button', { name: 'Add rule' }).click(); + await page.getByRole('textbox', { name: 'Resource path' }).nth(1).fill('pki'); + await page.getByRole('checkbox', { name: 'list' }).nth(1).check(); + await page.getByRole('checkbox', { name: 'patch' }).nth(1).check(); + await page.getByRole('button', { name: 'Add rule' }).click(); + await page.getByRole('textbox', { name: 'Resource path' }).nth(2).fill('totp'); + await page.getByRole('checkbox', { name: 'create' }).nth(2).check(); + // check snippets + await page.getByRole('button', { name: 'Automation snippets' }).click(); + await expect(page.getByRole('code')).toContainText( + 'resource "vault_policy" "" { name = "acl-policy" policy = < Date: Fri, 13 Mar 2026 00:52:39 -0400 Subject: [PATCH 090/468] Add enable engine method to base (#12946) (#12969) Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/e2e/pages/base.ts | 57 ++++++++++++++++++++++++++ ui/e2e/tests/superuser/kv.spec.ts | 14 +------ ui/e2e/tests/superuser/pki.spec.ts | 31 ++++---------- ui/e2e/tests/superuser/transit.spec.ts | 7 +--- 4 files changed, 68 insertions(+), 41 deletions(-) diff --git a/ui/e2e/pages/base.ts b/ui/e2e/pages/base.ts index 6f576e4e2a..01813594b2 100644 --- a/ui/e2e/pages/base.ts +++ b/ui/e2e/pages/base.ts @@ -16,4 +16,61 @@ export class BasePage { await locator.first().click(); } } + + async goToSecrets() { + await this.page.getByRole('link', { name: 'Secrets', exact: true }).click(); + const skipButton = this.page.getByRole('button', { name: 'Skip' }); + if (await skipButton.isVisible()) { + await skipButton.click(); + } + } + + /** + * Enable a secrets engine with a dynamic path + * @param engineType - The type of engine to enable (e.g., 'KV', 'Transit', 'PKI Certificates') + * @param path - The mount path for the engine + * @param options - Optional configuration for the engine with variable key-value pairs + */ + async enableEngine( + engineType: string, + path: string, + options?: { + defaultLeaseTtl?: { unit: number; option: string }; + maxLeaseTtl?: { unit: number; option: string }; + } + ) { + await this.page.goto('dashboard'); + await this.goToSecrets(); + + // Click "Enable new engine" + await this.page.getByRole('link', { name: 'Enable new engine' }).click(); + await this.page.getByRole('heading', { name: engineType }).click(); + + if (options?.defaultLeaseTtl) { + await this.page.locator('label').filter({ hasText: 'Default Lease TTL Vault will' }).click(); + await this.page + .getByLabel('TTL unit for Default Lease TTL') + .selectOption(options.defaultLeaseTtl.option as string); + await this.page + .getByRole('group', { name: 'Default Lease TTL Lease will' }) + .getByLabel('Number of units') + .fill(options.defaultLeaseTtl.unit.toString()); + } + + if (options?.maxLeaseTtl) { + await this.page + .getByLabel('TTL unit for Max Lease TTL') + .selectOption(options.maxLeaseTtl.option as string); + await this.page + .getByRole('group', { name: 'Max Lease TTL Lease will' }) + .getByLabel('Number of units') + .fill(options.maxLeaseTtl.unit.toString()); + } + + // Fill in the path + await this.page.getByRole('textbox', { name: 'Path' }).fill(path); + + // Enable the engine + await this.page.getByRole('button', { name: 'Enable engine' }).click(); + } } diff --git a/ui/e2e/tests/superuser/kv.spec.ts b/ui/e2e/tests/superuser/kv.spec.ts index 15fa331167..17e2fe7d22 100644 --- a/ui/e2e/tests/superuser/kv.spec.ts +++ b/ui/e2e/tests/superuser/kv.spec.ts @@ -8,19 +8,9 @@ import { BasePage } from '../../pages/base'; test('kvv2 workflow', async ({ page }) => { const basePage = new BasePage(page); - await page.goto('dashboard'); // enable kv secrets engine - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - // skip if intro page is shown - const skipButton = page.getByRole('button', { name: 'Skip' }); - if (await skipButton.isVisible()) { - await skipButton.click(); - } - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.locator('div').filter({ hasText: 'KV' }).nth(4).click(); - await page.getByRole('textbox', { name: 'Path' }).click(); - await page.getByRole('textbox', { name: 'Path' }).fill('kv-test'); - await page.getByRole('button', { name: 'Enable engine' }).click(); + await basePage.enableEngine('KV', 'kv-test'); + // once enabled it should navigate to the secrets engine overview page await expect(page.locator('section')).toContainText('kv-test version 2'); await expect(page.locator('section')).toContainText( diff --git a/ui/e2e/tests/superuser/pki.spec.ts b/ui/e2e/tests/superuser/pki.spec.ts index 7601c5ebf0..b3f5ae8473 100644 --- a/ui/e2e/tests/superuser/pki.spec.ts +++ b/ui/e2e/tests/superuser/pki.spec.ts @@ -4,31 +4,16 @@ */ import { test, expect } from '@playwright/test'; +import { BasePage } from '../../pages/base'; test('pki workflow', async ({ page }) => { - await page.goto('dashboard'); - // enable PKI Engine - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - // skip if intro page is shown - const skipButton = page.getByRole('button', { name: 'Skip' }); - if (await skipButton.isVisible()) { - await skipButton.click(); - } - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.getByLabel('PKI Certificates - enabled').click(); - await page.getByRole('textbox', { name: 'Path' }).fill('pki-engine'); - await page.locator('label').filter({ hasText: 'Default Lease TTL Vault will' }).click(); - await page.getByLabel('TTL unit for Default Lease TTL').selectOption('m'); - await page - .getByRole('group', { name: 'Default Lease TTL Lease will' }) - .getByLabel('Number of units') - .fill('5'); - await page.getByLabel('TTL unit for Max Lease TTL').selectOption('m'); - await page - .getByRole('group', { name: 'Max Lease TTL Lease will' }) - .getByLabel('Number of units') - .fill('10'); - await page.getByRole('button', { name: 'Enable engine' }).click(); + const basePage = new BasePage(page); + + // enable PKI secrets engine + await basePage.enableEngine('PKI Certificates', 'pki-engine', { + defaultLeaseTtl: { unit: 5, option: 'm' }, + maxLeaseTtl: { unit: 10, option: 'm' }, + }); // configure PKI Engine await expect(page.getByRole('heading', { name: 'pki-engine' })).toContainText('pki-engine'); diff --git a/ui/e2e/tests/superuser/transit.spec.ts b/ui/e2e/tests/superuser/transit.spec.ts index aba90dfd08..dbce1fa77f 100644 --- a/ui/e2e/tests/superuser/transit.spec.ts +++ b/ui/e2e/tests/superuser/transit.spec.ts @@ -9,13 +9,8 @@ import { BasePage } from '../../pages/base'; test('transit workflow', async ({ page }) => { const basePage = new BasePage(page); - await page.goto('dashboard'); // enable Transit Engine - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.getByRole('heading', { name: 'Transit' }).click(); - await page.getByRole('textbox', { name: 'Path' }).fill('transit-workflow'); - await page.getByRole('button', { name: 'Enable engine' }).click(); + await basePage.enableEngine('Transit', 'transit-workflow'); // create key await page.getByRole('link', { name: 'Create key' }).click(); From 9790b23c388dd37f3b1e8dfe023111014bb930da Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 02:28:09 -0400 Subject: [PATCH 091/468] SECVULN-38932 update rollup override to latest patch (#12774) (#12800) Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/package.json | 2 +- ui/pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/package.json b/ui/package.json index 407fb3a84c..84fc21c755 100644 --- a/ui/package.json +++ b/ui/package.json @@ -198,7 +198,7 @@ "minimatch@>=9.0.0 <9.0.6": "9.0.9", "prismjs": "1.30.0", "qs": "6.14.1", - "rollup": "2.79.2", + "rollup": "2.80.0", "serialize-javascript": "3.1.0", "socket.io": "4.8.1", "underscore": "1.13.7" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index e204c4c5c7..5cbfb3be3f 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -28,7 +28,7 @@ overrides: minimatch@>=9.0.0 <9.0.6: 9.0.9 prismjs: 1.30.0 qs: 6.14.1 - rollup: 2.79.2 + rollup: 2.80.0 serialize-javascript: 3.1.0 socket.io: 4.8.1 underscore: 1.13.7 @@ -313,7 +313,7 @@ importers: version: 5.0.0 ember-service-worker: specifier: meirish/ember-service-worker#configurable-scope - version: https://codeload.github.com/meirish/ember-service-worker/tar.gz/dda14187aace0d73ecdb6a55beac2194a3aec01b(rollup@2.79.2) + version: https://codeload.github.com/meirish/ember-service-worker/tar.gz/dda14187aace0d73ecdb6a55beac2194a3aec01b(rollup@2.80.0) ember-sinon-qunit: specifier: ~7.5.0 version: 7.5.0(@babel/core@7.26.10)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))(qunit@2.24.1)(sinon@20.0.0) @@ -1790,7 +1790,7 @@ packages: '@rollup/plugin-replace@2.3.0': resolution: {integrity: sha512-rzWAMqXAHC1w3eKpK6LxRqiF4f3qVFaa1sGii6Bp3rluKcwHNOpPt+hWRCmAH6SDEPtbPiLFf0pfNQyHs6Btlg==} peerDependencies: - rollup: 2.79.2 + rollup: 2.80.0 '@scalvert/ember-setup-middleware-reporter@0.1.1': resolution: {integrity: sha512-C5DHU6YlKaISB5utGQ+jpsMB57ZtY0uZ8UkD29j855BjqG6eJ98lhA2h/BoJbyPw89RKLP1EEXroy9+5JPoyVw==} @@ -7060,8 +7060,8 @@ packages: rollup-pluginutils@2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - rollup@2.79.2: - resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + rollup@2.80.0: + resolution: {integrity: sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==} engines: {node: '>=10.0.0'} hasBin: true @@ -10861,10 +10861,10 @@ snapshots: '@ro0gr/ceibo@2.2.0': {} - '@rollup/plugin-replace@2.3.0(rollup@2.79.2)': + '@rollup/plugin-replace@2.3.0(rollup@2.80.0)': dependencies: magic-string: 0.25.9 - rollup: 2.79.2 + rollup: 2.80.0 rollup-pluginutils: 2.8.2 '@scalvert/ember-setup-middleware-reporter@0.1.1': @@ -12422,7 +12422,7 @@ snapshots: heimdalljs-logger: 0.1.10 magic-string: 0.25.9 node-modules-path: 1.0.2 - rollup: 2.79.2 + rollup: 2.80.0 symlink-or-copy: 1.3.1 walk-sync: 1.1.4 transitivePeerDependencies: @@ -14423,9 +14423,9 @@ snapshots: transitivePeerDependencies: - supports-color - ember-service-worker@https://codeload.github.com/meirish/ember-service-worker/tar.gz/dda14187aace0d73ecdb6a55beac2194a3aec01b(rollup@2.79.2): + ember-service-worker@https://codeload.github.com/meirish/ember-service-worker/tar.gz/dda14187aace0d73ecdb6a55beac2194a3aec01b(rollup@2.80.0): dependencies: - '@rollup/plugin-replace': 2.3.0(rollup@2.79.2) + '@rollup/plugin-replace': 2.3.0(rollup@2.80.0) broccoli-caching-writer: 3.0.3 broccoli-file-creator: 2.1.1 broccoli-funnel: 2.0.2 @@ -17732,7 +17732,7 @@ snapshots: dependencies: estree-walker: 0.6.1 - rollup@2.79.2: + rollup@2.80.0: optionalDependencies: fsevents: 2.3.3 From 2e2e50b76a3eadd5f9bdb91b9c7259523e4e79f7 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 14:17:43 -0400 Subject: [PATCH 092/468] enos: poll for LDAP server readiness when populating org, groups, and users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enos: poll for LDAP server readiness when populating org, groups, and users The prior implementation had a hard 10 second sleep waiting for the container to start up. That is not enough time as we see regular failures in CI: ``` │ Error: exit status 1 │ │ Error: Execution Error │ │ with module.set_up_external_integration_target.enos_remote_exec.populate_ldap, │ on ../../modules/set_up_external_integration_target/main.tf line 70, in resource "enos_remote_exec" "populate_ldap": │ 70: resource "enos_remote_exec" "populate_ldap" { │ │ failed to execute commands due to: running script: │ [/home/runner/actions-runner/_work/vault-enterprise/vault-enterprise/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh] │ failed, due to: 1 error occurred: │ * executing script: Process exited with status 255: ldap_sasl_bind(SIMPLE): │ Can't contact LDAP server (-1) ``` Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .../main.tf | 10 +++-- .../scripts/populate-ldap.sh | 39 ++++++++++++++----- .../variables.tf | 28 +++++++++---- .../Dynamic-roles/dynamic-roles-deletion.sh | 1 + 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/enos/modules/set_up_external_integration_target/main.tf b/enos/modules/set_up_external_integration_target/main.tf index 1e9281036c..72dc8974ef 100755 --- a/enos/modules/set_up_external_integration_target/main.tf +++ b/enos/modules/set_up_external_integration_target/main.tf @@ -73,10 +73,12 @@ resource "enos_remote_exec" "populate_ldap" { scripts = [abspath("${path.module}/scripts/populate-ldap.sh")] environment = { - LDAP_SERVER = local.ldap_server.host.private_ip - LDAP_PORT = local.ldap_server.port - LDAP_ADMIN_PW = local.ldap_server.admin_pw - LDAP_DOMAIN = local.ldap_server.domain + LDAP_SERVER = local.ldap_server.host.private_ip + LDAP_PORT = local.ldap_server.port + LDAP_ADMIN_PW = local.ldap_server.admin_pw + LDAP_DOMAIN = local.ldap_server.domain + RETRY_INTERVAL = var.retry_interval + TIMEOUT_SECONDS = var.timeout } transport = { diff --git a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh index 146b4823d0..e777e4db80 100644 --- a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh +++ b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh @@ -13,10 +13,8 @@ fail() { [[ -z "$LDAP_PORT" ]] && fail "LDAP_PORT env variable has not been set" [[ -z "$LDAP_ADMIN_PW" ]] && fail "LDAP_ADMIN_PW env variable has not been set" [[ -z "$LDAP_DOMAIN" ]] && fail "LDAP_DOMAIN env variable has not been set" - -echo "OpenLDAP: Checking for OpenLDAP Server Connection: ${LDAP_SERVER}:${LDAP_PORT}" -# Wait for LDAP server to be ready -sleep 10 +[[ -z "$RETRY_INTERVAL" ]] && fail "RETRY_INTERVAL env variable has not been set" +[[ -z "$TIMEOUT_SECONDS" ]] && fail "TIMEOUT_SECONDS env variable has not been set" # Extract domain components from LDAP_DOMAIN (e.g., "enos.com" -> "dc=enos,dc=com") IFS='.' read -ra DOMAIN_PARTS <<< "$LDAP_DOMAIN" @@ -29,16 +27,37 @@ for part in "${DOMAIN_PARTS[@]}"; do fi done +echo "OpenLDAP: Checking for OpenLDAP Server Connection: ${LDAP_SERVER}:${LDAP_PORT}" echo "OpenLDAP: Using domain DN: ${DOMAIN_DN}" echo "OpenLDAP: Testing connection with admin credentials" -# Test connection -ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base +begin_time=$(date +%s) +end_time=$((begin_time + TIMEOUT_SECONDS)) +test_conn_out="" +test_conn_res="" +declare -i tries=0 +while [ "$(date +%s)" -lt "$end_time" ]; do + # Test connection + tries+=1 + test_conn_out=$(ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base 2>&1) + test_conn_res=$? + if [ "$test_conn_res" -eq 0 ]; then + break + fi + + echo "Unable to connect to ldap://${LDAP_SERVER}:${LDAP_PORT} cn=admin,${DOMAIN_DN}, attempt: ${tries}, exit code: ${test_conn_res}, error: ${test_conn_out}, retrying..." + sleep "$RETRY_INTERVAL" +done + +if [ "$test_conn_res" -ne 0 ]; then + echo "Timed out waiting to connect to ldap://${LDAP_SERVER}:${LDAP_PORT} cn=admin,${DOMAIN_DN}, attempt: ${tries}, exit code: ${test_conn_res}, error: ${test_conn_out}" 2>&1 + exit "$test_conn_res" +fi echo "OpenLDAP: Creating organizational units" # Creating Users and Groups Org Units LDIF file OU_LDIF="ou.ldif" -cat << EOF > ${OU_LDIF} +cat << EOF > "${OU_LDIF}" dn: ou=users,${DOMAIN_DN} objectClass: organizationalUnit ou: users @@ -48,11 +67,11 @@ objectClass: organizationalUnit ou: groups EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f ${OU_LDIF} || echo "OUs may already exist" +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f "${OU_LDIF}" || echo "OUs may already exist" echo "OpenLDAP: Creating test users" USER_LDIF="users.ldif" -cat << EOF > ${USER_LDIF} +cat << EOF > "${USER_LDIF}" # User: enos dn: uid=enos,ou=users,${DOMAIN_DN} objectClass: inetOrgPerson @@ -92,6 +111,6 @@ uid: svc-delete userPassword: ${LDAP_ADMIN_PW} EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f ${USER_LDIF} || echo "Users may already exist" +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f "${USER_LDIF}" || echo "Users may already exist" echo "LDAP population completed successfully." diff --git a/enos/modules/set_up_external_integration_target/variables.tf b/enos/modules/set_up_external_integration_target/variables.tf index 1a46fe2f4c..09bd5b9f6d 100644 --- a/enos/modules/set_up_external_integration_target/variables.tf +++ b/enos/modules/set_up_external_integration_target/variables.tf @@ -16,14 +16,6 @@ variable "ip_version" { default = "4" } -variable "ports" { - description = "Port configuration for services" - type = map(object({ - port = string - description = string - })) -} - variable "ldap_version" { type = string description = "OpenLDAP Server Version to use" @@ -35,3 +27,23 @@ variable "packages" { description = "A list of packages to install via the target host package manager" default = [] } + +variable "ports" { + description = "Port configuration for services" + type = map(object({ + port = string + description = string + })) +} + +variable "retry_interval" { + type = number + description = "How many seconds to wait between each retry" + default = 2 +} + +variable "timeout" { + type = number + description = "The max number of seconds to wait before timing out" + default = 60 +} diff --git a/enos/modules/verify_secrets_engines/scripts/ldap/Dynamic-roles/dynamic-roles-deletion.sh b/enos/modules/verify_secrets_engines/scripts/ldap/Dynamic-roles/dynamic-roles-deletion.sh index 6b781279c6..8038aff5ec 100644 --- a/enos/modules/verify_secrets_engines/scripts/ldap/Dynamic-roles/dynamic-roles-deletion.sh +++ b/enos/modules/verify_secrets_engines/scripts/ldap/Dynamic-roles/dynamic-roles-deletion.sh @@ -119,6 +119,7 @@ test_role_deletion_with_active_leases() { "$binpath" lease revoke -prefix "${MOUNT}/creds/${lease_role}" > /dev/null 2>&1 # Define the check function + # shellcheck disable=SC2329 wait_for_user_deletion() { check_user=$(ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" \ -b "dc=${LDAP_USERNAME},dc=com" \ From 5319eb2384f0cb8b08e48bc4610d514e4f0af449 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 14:33:50 -0400 Subject: [PATCH 093/468] VAULT-42996 remove SCIM clien_role (IGAvsIdP) concept (#12889) (#12977) * add resource orphaning to SCIM client delete * add background orphaning handling * delete instead of orphan, add retry and startup tests * revert: undo accidental changes to Makefile and golang instructions * fix tests * stop log flood (try again) * fix linter findings * try to silence spam again * try to silence spam once more * dont allow running outside of active primary * go docs * fix active check and pass client id via context * remove unnecessary change * remove client_role concept * add more tests * remove reserved field from pb * address review * fix merge * fix tests * re-add duplicate canonical id metadata handling * linter * address test copilot feedback Co-authored-by: Bruno Oliveira de Souza --- helper/identity/types.pb.go | 217 +++++++++++++++++------------------- helper/identity/types.proto | 27 ++--- 2 files changed, 115 insertions(+), 129 deletions(-) diff --git a/helper/identity/types.pb.go b/helper/identity/types.pb.go index fad851ea48..75ace54a67 100644 --- a/helper/identity/types.pb.go +++ b/helper/identity/types.pb.go @@ -701,41 +701,39 @@ func (x *Alias) GetIssuer() string { } // ScimClient defines the stored configuration for a single SCIM client. -// This configuration links a client's identity within Vault to its specific -// role and capabilities within the SCIM server. +// This configuration links a client's identity within Vault to its +// capabilities within the SCIM server. type ScimClient struct { state protoimpl.MessageState `protogen:"open.v1"` // ClientID is a unique identifier for this specific SCIM // client configuration. // @inject_tag: sentinel:"-" ClientID string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty" sentinel:"-"` - // ClientRole defines the client's function and authoritative power. - // It must be either "IGA" (authoritative) or "IDP" (standard). - // @inject_tag: sentinel:"-" - ClientRole string `protobuf:"bytes,2,opt,name=client_role,json=clientRole,proto3" json:"client_role,omitempty" sentinel:"-"` // AccessGrantPrincipal is the Vault Entity ID that represents the SCIM // client application itself. This is the principal that will be granted the // necessary permissions to perform SCIM operations. // @inject_tag: sentinel:"-" - AccessGrantPrincipal string `protobuf:"bytes,3,opt,name=access_grant_principal,json=accessGrantPrincipal,proto3" json:"access_grant_principal,omitempty" sentinel:"-"` + AccessGrantPrincipal string `protobuf:"bytes,2,opt,name=access_grant_principal,json=accessGrantPrincipal,proto3" json:"access_grant_principal,omitempty" sentinel:"-"` // AliasMountAccessor is an optional field that specifies the mount accessor - // of an auth method where login aliases should be created for provisioned users. - // This is typically used for clients with the 'IDP' role. + // of an auth method where login aliases should be created for provisioned + // users. When set, the SCIM client will create both a Vault entity and an + // alias on the specified auth mount for each provisioned user. When empty, + // only entities are created. // @inject_tag: sentinel:"-" - AliasMountAccessor string `protobuf:"bytes,4,opt,name=alias_mount_accessor,json=aliasMountAccessor,proto3" json:"alias_mount_accessor,omitempty" sentinel:"-"` - // ClientName is an user defined identifier for this specific SCIM + AliasMountAccessor string `protobuf:"bytes,3,opt,name=alias_mount_accessor,json=aliasMountAccessor,proto3" json:"alias_mount_accessor,omitempty" sentinel:"-"` + // ClientName is a user-defined identifier for this specific SCIM // client configuration. (e.g., 'Okta-Prod', 'SailPoint-Dev'). // @inject_tag: sentinel:"-" - ClientName string `protobuf:"bytes,5,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty" sentinel:"-"` + ClientName string `protobuf:"bytes,4,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty" sentinel:"-"` // NamespaceID is the identifier of the namespace to which this entity // belongs to. Do not return this value over the API when reading the // entity. // @inject_tag: sentinel:"-" - NamespaceID string `protobuf:"bytes,6,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty" sentinel:"-"` + NamespaceID string `protobuf:"bytes,5,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty" sentinel:"-"` // Deleting indicates that the SCIM client is in the process of being deleted. // This allows cascading cleanup to be eventually handled even if there are failures during the deletion process. // @inject_tag: sentinel:"-" - Deleting bool `protobuf:"varint,7,opt,name=deleting,proto3" json:"deleting,omitempty" sentinel:"-"` + Deleting bool `protobuf:"varint,6,opt,name=deleting,proto3" json:"deleting,omitempty" sentinel:"-"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -777,13 +775,6 @@ func (x *ScimClient) GetClientID() string { return "" } -func (x *ScimClient) GetClientRole() string { - if x != nil { - return x.ClientRole - } - return "" -} - func (x *ScimClient) GetAccessGrantPrincipal() string { if x != nil { return x.AccessGrantPrincipal @@ -1202,100 +1193,98 @@ var file_helper_identity_types_proto_rawDesc = string([]byte{ 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x92, 0x02, 0x0a, 0x0a, 0x53, 0x63, 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x38, 0x01, 0x22, 0xf1, 0x01, 0x0a, 0x0a, 0x53, 0x63, 0x69, 0x6d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, - 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x12, - 0x34, 0x0a, 0x16, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, - 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x14, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x50, 0x72, 0x69, 0x6e, - 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x12, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x88, 0x05, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, - 0x0a, 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, - 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, - 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x65, - 0x72, 0x67, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, - 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x62, 0x75, 0x63, - 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0b, 0x6d, 0x66, - 0x61, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x2c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x66, - 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6d, - 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x6d, 0x66, 0x61, - 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xf9, 0x03, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, - 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x16, - 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x6d, 0x65, - 0x72, 0x67, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, - 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2c, - 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, - 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x34, + 0x0a, 0x16, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x70, + 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x50, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x88, 0x05, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, 0x0a, + 0x08, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, + 0x6e, 0x61, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, + 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x64, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, + 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x62, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0b, 0x6d, 0x66, 0x61, + 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, + 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x66, 0x61, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6d, 0x66, + 0x61, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4a, 0x0a, 0x0f, 0x4d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x6d, 0x66, 0x61, 0x2e, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xf9, 0x03, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x16, 0x6d, + 0x65, 0x72, 0x67, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, + 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2c, 0x5a, + 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, + 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, }) var ( diff --git a/helper/identity/types.proto b/helper/identity/types.proto index 3a7bed0943..194f42c3f9 100644 --- a/helper/identity/types.proto +++ b/helper/identity/types.proto @@ -273,46 +273,43 @@ message Alias { } // ScimClient defines the stored configuration for a single SCIM client. -// This configuration links a client's identity within Vault to its specific -// role and capabilities within the SCIM server. +// This configuration links a client's identity within Vault to its +// capabilities within the SCIM server. message ScimClient { // ClientId is a unique identifier for this specific SCIM // client configuration. // @inject_tag: sentinel:"-" string client_id = 1; - // ClientRole defines the client's function and authoritative power. - // It must be either "IGA" (authoritative) or "IdP" (standard). - // @inject_tag: sentinel:"-" - string client_role = 2; - // AccessGrantPrincipal is the Vault Entity ID that represents the SCIM // client application itself. This is the principal that will be granted the // necessary permissions to perform SCIM operations. // @inject_tag: sentinel:"-" - string access_grant_principal = 3; + string access_grant_principal = 2; // AliasMountAccessor is an optional field that specifies the mount accessor - // of an auth method where login aliases should be created for provisioned users. - // This is typically used for clients with the 'IdP' role. + // of an auth method where login aliases should be created for provisioned + // users. When set, the SCIM client will create both a Vault entity and an + // alias on the specified auth mount for each provisioned user. When empty, + // only entities are created. // @inject_tag: sentinel:"-" - string alias_mount_accessor = 4; + string alias_mount_accessor = 3; - // ClientName is an user defined identifier for this specific SCIM + // ClientName is a user-defined identifier for this specific SCIM // client configuration. (e.g., 'Okta-Prod', 'SailPoint-Dev'). // @inject_tag: sentinel:"-" - string client_name = 5; + string client_name = 4; // NamespaceID is the identifier of the namespace to which this entity // belongs to. Do not return this value over the API when reading the // entity. // @inject_tag: sentinel:"-" - string namespace_id = 6; + string namespace_id = 5; // Deleting indicates that the SCIM client is in the process of being deleted. // This allows cascading cleanup to be eventually handled even if there are failures during the deletion process. // @inject_tag: sentinel:"-" - bool deleting = 7; + bool deleting = 6; } // Deprecated. Retained for backwards compatibility. From 346e1386e92243f29434788a412b0afcf98260b0 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 15:38:02 -0400 Subject: [PATCH 094/468] enos: guard LDAP test connection (#12992) (#13000) Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun --- .../scripts/populate-ldap.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh index e777e4db80..5ee641f024 100644 --- a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh +++ b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh @@ -39,12 +39,12 @@ declare -i tries=0 while [ "$(date +%s)" -lt "$end_time" ]; do # Test connection tries+=1 - test_conn_out=$(ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base 2>&1) - test_conn_res=$? - if [ "$test_conn_res" -eq 0 ]; then + if test_conn_out=$(ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base 2>&1); then + test_conn_res=0 break fi + test_conn_res=$? echo "Unable to connect to ldap://${LDAP_SERVER}:${LDAP_PORT} cn=admin,${DOMAIN_DN}, attempt: ${tries}, exit code: ${test_conn_res}, error: ${test_conn_out}, retrying..." sleep "$RETRY_INTERVAL" done From 83b6e12d81dd77962c657f09bd3d25df299d1c91 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 17:08:42 -0400 Subject: [PATCH 095/468] UI: fix dropdown triggered flyout (#12933) (#13011) * fix dropdown triggered flyout * update tests * hide policy generator action on community Co-authored-by: lane-wetmore --- .../code-generator/policy/flyout.ts | 2 +- ui/lib/kv/addon/components/page/list.hbs | 25 ++++++--- ui/lib/kv/addon/components/page/list.js | 2 + .../code-generator/policy/flyout-test.js | 51 +++++++++++-------- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/ui/lib/core/addon/components/code-generator/policy/flyout.ts b/ui/lib/core/addon/components/code-generator/policy/flyout.ts index 796729aa66..90c906cb20 100644 --- a/ui/lib/core/addon/components/code-generator/policy/flyout.ts +++ b/ui/lib/core/addon/components/code-generator/policy/flyout.ts @@ -93,7 +93,7 @@ export default class CodeGeneratorPolicyFlyout extends Component { models: ['acl', this.policyName], }, }); - this.showFlyout = false; + this.closeFlyout(); this.resetFlyoutState(); } catch (e) { const { message } = yield this.api.parseError(e); diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs index d0ccd0be6e..090f4e8698 100644 --- a/ui/lib/kv/addon/components/page/list.hbs +++ b/ui/lib/kv/addon/components/page/list.hbs @@ -12,15 +12,16 @@ <:actions> - - <:customTrigger as |openFlyout|> - - Generate policy - - - + {{#unless this.version.isCommunity}} + + Generate policy + + {{/unless}} - + + <:customTrigger as |openFlyout|> + {{#if this.showPolicyFlyout}} +
    + {{/if}} + + + <:tabs>
  • Secrets
  • diff --git a/ui/lib/kv/addon/components/page/list.js b/ui/lib/kv/addon/components/page/list.js index f5c8fe9aa7..863115c834 100644 --- a/ui/lib/kv/addon/components/page/list.js +++ b/ui/lib/kv/addon/components/page/list.js @@ -28,9 +28,11 @@ export default class KvListPageComponent extends Component { @service flashMessages; @service('app-router') router; @service api; + @service version; @tracked secretPath; @tracked metadataToDelete = null; // set to the metadata intended to delete + @tracked showPolicyFlyout = false; // used for KV list and list-directory view // ex: beep/ diff --git a/ui/tests/integration/components/code-generator/policy/flyout-test.js b/ui/tests/integration/components/code-generator/policy/flyout-test.js index 59e517264e..7dd16b8b09 100644 --- a/ui/tests/integration/components/code-generator/policy/flyout-test.js +++ b/ui/tests/integration/components/code-generator/policy/flyout-test.js @@ -113,21 +113,30 @@ module('Integration | Component | code-generator/policy/flyout', function (hooks // This test is to demonstrate how to implement closing the dropdown when the flyout trigger is a dropdown element test('it closes dropdown if custom trigger is a dropdown item', async function (assert) { + this.showPolicyFlyout = false; await render(hbs` - - <:customTrigger as |openFlyout|> - - Make me a policy! - - - + + Make me a policy! + Magic stuff - `); + + + <:customTrigger as |openFlyout|> + {{#if this.showPolicyFlyout}} +
    + {{/if}} + + + `); + await click(GENERAL.dropdownToggle('Toolbox')); assert.dom(GENERAL.dropdownToggle('Toolbox')).hasAttribute('aria-expanded', 'true'); await click(GENERAL.button('Make me a policy!')); assert.dom(GENERAL.flyout).exists('flyout is open'); + await fillIn(GENERAL.inputByAttr('name'), 'test-policy'); + await click(GENERAL.submitButton); + assert.dom(GENERAL.messageError).exists(); await click(GENERAL.cancelButton); assert.dom(GENERAL.flyout).doesNotExist('flyout is closed'); const dropdown = find(GENERAL.dropdownToggle('Toolbox')); @@ -139,20 +148,18 @@ module('Integration | Component | code-generator/policy/flyout', function (hooks test('it does not render yielded custom trigger component on community', async function (assert) { this.version.type = 'community'; - await this.renderComponent({ open: false }); - await render(hbs` - - - <:customTrigger as |openFlyout|> - - Make me a policy! - - - - Magic stuff - `); - await click(GENERAL.dropdownToggle('Toolbox')); - assert.dom(GENERAL.button('Magic stuff')).exists('dropdown opens'); + await render(hbs` + + <:customTrigger> + + + + `); assert.dom(GENERAL.button('Make me a policy!')).doesNotExist(); }); From 98fe959bc273b05a138be0460b3f14108ab4dae5 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 17:50:07 -0400 Subject: [PATCH 096/468] [VAULT-42685] UI: add playwright coverage for transform binary tests (#12970) (#12983) * [VAULT-42685] UI: add playwright coverage for transform binary tests * use enableEngine() helper from base page Co-authored-by: Shannon Roberts (Beagin) --- ui/e2e/tests/superuser/transform.spec.ts | 93 ++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 ui/e2e/tests/superuser/transform.spec.ts diff --git a/ui/e2e/tests/superuser/transform.spec.ts b/ui/e2e/tests/superuser/transform.spec.ts new file mode 100644 index 0000000000..ce8662eac2 --- /dev/null +++ b/ui/e2e/tests/superuser/transform.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; +import { BasePage } from '../../pages/base'; + +test('transform workflow', async ({ page }) => { + const basePage = new BasePage(page); + + await test.step('enable Transform secrets engine mount', async () => { + await basePage.enableEngine('Transform', 'transform-test'); + }); + + await test.step('Transform secrets engine mount saved successfully', async () => { + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await expect(page.getByText('Successfully mounted the')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('Transformation can be created', async () => { + await page.getByRole('link', { name: 'Create transformation' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('test-transformation'); + await page.getByRole('checkbox', { name: 'Allow deletion' }).check(); + await page.getByRole('button', { name: 'Create transformation' }).click(); + await page.getByLabel('Template').getByText('Search').click(); + await page.getByRole('option', { name: 'builtin/socialsecuritynumber' }).click(); + await page.getByRole('button', { name: 'Create transformation' }).click(); + await expect(page.getByText('test-transformation', { exact: true })).toBeVisible(); + }); + + await test.step('Role can be created', async () => { + await page.getByRole('link', { name: 'transform-test' }).click(); + await page.getByRole('link', { name: 'Roles' }).click(); + await expect(page.getByRole('heading', { name: 'No roles in this backend' })).toBeVisible(); + await expect(page.getByText('Roles in this backend will be')).toBeVisible(); + await page.getByRole('link', { name: 'Create role' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('test-role'); + await page.getByText('Search').click(); + await page.getByRole('option', { name: 'test-transformation' }).click(); + await page.getByRole('button', { name: 'Create role' }).click(); + }); + + await test.step('Template can be created', async () => { + await page.getByRole('link', { name: 'transform-test' }).click(); + await page.getByRole('link', { name: 'Templates' }).click(); + await page.getByRole('link', { name: 'Create template' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('test-template'); + await page.getByRole('textbox', { name: 'Pattern' }).fill('`^(19)'); + await page.getByText('Search').click(); + await page.getByRole('option', { name: 'builtin/alphalower' }).click(); + await page.getByRole('button', { name: 'Create template' }).click(); + }); + + await test.step('Template saved successfully', async () => { + await expect(page.getByText('Success')).toBeVisible(); + await expect(page.getByText('Transform template saved.')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('Alphabet can be created', async () => { + await page.getByRole('link', { name: 'transform-test' }).click(); + await page.getByRole('link', { name: 'Alphabets' }).click(); + await page.getByRole('link', { name: 'Create alphabet' }).click(); + await page.getByRole('textbox', { name: 'Name' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('test-alphabet'); + await page.getByRole('textbox', { name: 'Alphabet' }).click(); + await page.getByRole('textbox', { name: 'Alphabet' }).fill('abc'); + await page.getByRole('button', { name: 'Create alphabet' }).click(); + }); + + await test.step('Alphabet saved successfully', async () => { + await expect(page.getByText('Success')).toBeVisible(); + await expect(page.getByText('Alphabet saved.')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + + await test.step('Transform mount can be configured/updated', async () => { + await page.getByRole('link', { name: 'transform-test' }).click(); + await page.getByRole('button', { name: 'Manage', exact: true }).click(); + await page.getByRole('link', { name: 'Configure' }).click(); + await page.getByRole('textbox', { name: 'Description' }).click(); + await page.getByRole('textbox', { name: 'Description' }).fill('My transform secrets engine. test'); + await page.getByRole('button', { name: 'Save changes' }).click(); + }); + + await test.step('Transform mount updated successfully', async () => { + await expect(page.getByText('Configuration saved')).toBeVisible(); + await expect(page.getByText('Engine settings successfully')).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); +}); From 96aa73f9fa3e9a034071859f34936008ba1c947e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:12:37 -0400 Subject: [PATCH 097/468] VAULT-42410 - Refactored empty state component for kv directory (#12803) (#13017) * VAULT-42410 - refactored empty state component for kv directory * VAULT-42410 - refactored empty state component for ldap directory * Revert "VAULT-42410 - refactored empty state component for ldap directory" This reverts commit be819f7cc4f71912827eb8e7b1740c3a6b111205. e * added data-test attributes Co-authored-by: mohit-hashicorp --- ui/lib/kv/addon/components/page/list.hbs | 16 +-- .../addon/components/page/secret/details.hbs | 22 +++-- .../page/secret/metadata/details.hbs | 98 +++++++++++-------- .../components/page/secret/metadata/edit.hbs | 27 ++--- .../page/secret/metadata/version-diff.hbs | 15 ++- .../page/secret/metadata/version-history.hbs | 8 +- 6 files changed, 107 insertions(+), 79 deletions(-) diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs index 090f4e8698..ccd5f12682 100644 --- a/ui/lib/kv/addon/components/page/list.hbs +++ b/ui/lib/kv/addon/components/page/list.hbs @@ -184,13 +184,17 @@ /> {{else}} {{#if @filterValue}} - + + + {{else}} - + + + + {{/if}} {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/details.hbs b/ui/lib/kv/addon/components/page/secret/details.hbs index d5c5cebca3..7d6e16009f 100644 --- a/ui/lib/kv/addon/components/page/secret/details.hbs +++ b/ui/lib/kv/addon/components/page/secret/details.hbs @@ -172,17 +172,21 @@ {{/if}} {{#if this.emptyState}} - + + + {{#if this.emptyState.link}} - + + + {{/if}} - + {{else}}
    {{#if this.showJsonView}} diff --git a/ui/lib/kv/addon/components/page/secret/metadata/details.hbs b/ui/lib/kv/addon/components/page/secret/metadata/details.hbs index 4bb66c1bf5..805ad26aac 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/details.hbs +++ b/ui/lib/kv/addon/components/page/secret/metadata/details.hbs @@ -68,10 +68,13 @@ {{else}} {{#if (and (not @capabilities.canReadMetadata) (not @capabilities.canReadData))}} - + + + + {{else}} {{! Offer opportunity to manually request /data/ for custom_metadata }} {{#if this.error.isControlGroup}} @@ -80,44 +83,50 @@ {{/if}} {{#if this.canRequestData}} - -
    - - - Sensitive secret data will be retrieved. - - - -
    -
    + + + + +
    + + + Sensitive secret data will be retrieved. + + + +
    +
    +
    {{else}} - + + + {{#if @capabilities.canUpdateMetadata}} - + + + {{/if}} - + {{/if}} {{/if}} {{/each-in}} @@ -144,9 +153,12 @@ />
    {{else}} - + + + + {{/if}}
    \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/metadata/edit.hbs b/ui/lib/kv/addon/components/page/secret/metadata/edit.hbs index 0386f88c16..13453be447 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/edit.hbs +++ b/ui/lib/kv/addon/components/page/secret/metadata/edit.hbs @@ -52,17 +52,18 @@
    {{else}} - - - - + + + + + + + + {{/if}} \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs b/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs index ed668c5586..bb3e4c46ba 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs +++ b/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs @@ -37,10 +37,17 @@ {{#if this.deactivatedState}} - + + + + {{else}}
    {{sanitized-html this.visualDiff}}
    diff --git a/ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs b/ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs index 9d5ed773ea..137e9d88a6 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs +++ b/ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs @@ -140,8 +140,8 @@ {{/each}} {{else}} - + + + + {{/if}} \ No newline at end of file From e5e8399f7b7c3fd6031e2612e6a351e4920c7da9 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:13:10 -0400 Subject: [PATCH 098/468] VAULT-42410 - Refactored empty state component for ldap directory (#12804) (#13018) * VAULT-42410 - refactored empty state component for ldap directory * added data-test attributes * fixed application state body text Co-authored-by: mohit-hashicorp --- .../addon/components/accounts-checked-out.hbs | 9 ++++----- ui/lib/ldap/addon/components/config-cta.hbs | 10 +++++++--- .../ldap/addon/components/page/configure.hbs | 9 ++++----- .../ldap/addon/components/page/libraries.hbs | 19 ++++++++++++++----- ui/lib/ldap/addon/components/page/roles.hbs | 19 ++++++++++++++----- 5 files changed, 43 insertions(+), 23 deletions(-) diff --git a/ui/lib/ldap/addon/components/accounts-checked-out.hbs b/ui/lib/ldap/addon/components/accounts-checked-out.hbs index c483bfc7b8..7c1d63b131 100644 --- a/ui/lib/ldap/addon/components/accounts-checked-out.hbs +++ b/ui/lib/ldap/addon/components/accounts-checked-out.hbs @@ -38,11 +38,10 @@ {{else}} - + + + + {{/if}} diff --git a/ui/lib/ldap/addon/components/config-cta.hbs b/ui/lib/ldap/addon/components/config-cta.hbs index 955558d2e0..62870fa081 100644 --- a/ui/lib/ldap/addon/components/config-cta.hbs +++ b/ui/lib/ldap/addon/components/config-cta.hbs @@ -3,6 +3,10 @@ SPDX-License-Identifier: BUSL-1.1 }} - - - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/ui/lib/ldap/addon/components/page/configure.hbs b/ui/lib/ldap/addon/components/page/configure.hbs index 87e6e74aa5..47141d352c 100644 --- a/ui/lib/ldap/addon/components/page/configure.hbs +++ b/ui/lib/ldap/addon/components/page/configure.hbs @@ -37,11 +37,10 @@
    {{else}} - + + + + {{/if}}
    diff --git a/ui/lib/ldap/addon/components/page/libraries.hbs b/ui/lib/ldap/addon/components/page/libraries.hbs index ab85a03097..d8df02f426 100644 --- a/ui/lib/ldap/addon/components/page/libraries.hbs +++ b/ui/lib/ldap/addon/components/page/libraries.hbs @@ -28,12 +28,21 @@ {{else if (not this.filteredLibraries)}} {{#if this.filterValue}} - + + + {{else}} - + + + + {{/if}} {{else}}
    diff --git a/ui/lib/ldap/addon/components/page/roles.hbs b/ui/lib/ldap/addon/components/page/roles.hbs index e129935275..c25a758411 100644 --- a/ui/lib/ldap/addon/components/page/roles.hbs +++ b/ui/lib/ldap/addon/components/page/roles.hbs @@ -27,12 +27,21 @@ {{else if (not @roles.meta.filteredTotal)}} {{#if @pageFilter}} - + + + {{else}} - + + + + {{/if}} {{else}}
    From 94c5e6b68ea712357e207aa473af900c2d01c224 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:13:42 -0400 Subject: [PATCH 099/468] VAULT-42410 - Refactored empty state component for pki directory (#12805) (#13019) * VAULT-42410 - refactored empty state component for pki directory * added data-test attributes * fixed failing tests Co-authored-by: mohit-hashicorp --- .../page/pki-configuration-edit.hbs | 71 ++++++++++++------- .../components/page/pki-configure-create.hbs | 5 +- .../addon/components/page/pki-issuer-list.hbs | 22 +++--- .../addon/components/page/pki-key-list.hbs | 27 ++++--- .../addon/components/page/pki-tidy-status.hbs | 28 ++++---- .../addon/components/pki-generate-root.hbs | 11 +-- .../addon/templates/certificates/index.hbs | 33 +++++---- ui/lib/pki/addon/templates/overview.hbs | 22 +++--- ui/lib/pki/addon/templates/roles/index.hbs | 33 +++++---- ui/lib/pki/addon/templates/tidy/index.hbs | 22 +++--- .../pki/pki-engine-workflow-test.js | 12 ++-- .../pki/page/pki-configuration-edit-test.js | 25 +++---- 12 files changed, 181 insertions(+), 130 deletions(-) diff --git a/ui/lib/pki/addon/components/page/pki-configuration-edit.hbs b/ui/lib/pki/addon/components/page/pki-configuration-edit.hbs index c734a99376..7a37926669 100644 --- a/ui/lib/pki/addon/components/page/pki-configuration-edit.hbs +++ b/ui/lib/pki/addon/components/page/pki-configuration-edit.hbs @@ -29,13 +29,17 @@ {{/each}} {{else}} - - POST /{{@backend}}/config/cluster - + + + + + POST /{{@backend}}/config/cluster + + {{/if}} @@ -48,13 +52,17 @@ {{/each}} {{else}} - - POST /{{@backend}}/config/acme - + + + + + POST /{{@backend}}/config/acme + + {{/if}} @@ -67,13 +75,17 @@ {{/each}} {{else}} - - POST /{{@backend}}/config/urls - + + + + + POST /{{@backend}}/config/urls + + {{/if}} @@ -114,12 +126,17 @@ {{/each-in}} {{/each}} {{else}} - - POST /{{@backend}}/config/crl - + + + + + POST /{{@backend}}/config/crl + + {{/if}} diff --git a/ui/lib/pki/addon/components/page/pki-configure-create.hbs b/ui/lib/pki/addon/components/page/pki-configure-create.hbs index 050e33c2be..f402bb412c 100644 --- a/ui/lib/pki/addon/components/page/pki-configure-create.hbs +++ b/ui/lib/pki/addon/components/page/pki-configure-create.hbs @@ -58,7 +58,10 @@ @onComplete={{transition-to "vault.cluster.secrets.backend.pki.overview"}} /> {{else}} - + + + +
    diff --git a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs index 26dc628c5e..5e2dbd678c 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs @@ -125,14 +125,18 @@ {{/each}} <:empty> - - - + + + + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-key-list.hbs b/ui/lib/pki/addon/components/page/pki-key-list.hbs index 4d5109a9a8..a22defe240 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-list.hbs @@ -82,18 +82,25 @@ <:empty> - + + + + <:configure> - - - + + + + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-tidy-status.hbs b/ui/lib/pki/addon/components/page/pki-tidy-status.hbs index 3098c4c9c5..83d46ac34f 100644 --- a/ui/lib/pki/addon/components/page/pki-tidy-status.hbs +++ b/ui/lib/pki/addon/components/page/pki-tidy-status.hbs @@ -97,19 +97,23 @@ {{/each}} {{/if}} {{else}} - - + + - + + + + {{/if}} {{! TIDY OPTIONS MODAL }} diff --git a/ui/lib/pki/addon/components/pki-generate-root.hbs b/ui/lib/pki/addon/components/pki-generate-root.hbs index c09cbd348f..93972b6d4a 100644 --- a/ui/lib/pki/addon/components/pki-generate-root.hbs +++ b/ui/lib/pki/addon/components/pki-generate-root.hbs @@ -101,10 +101,13 @@ /> {{/each}} {{else}} - + + + + {{/if}} {{/if}} diff --git a/ui/lib/pki/addon/templates/certificates/index.hbs b/ui/lib/pki/addon/templates/certificates/index.hbs index e45bd4e935..fc61484a9c 100644 --- a/ui/lib/pki/addon/templates/certificates/index.hbs +++ b/ui/lib/pki/addon/templates/certificates/index.hbs @@ -47,20 +47,27 @@ {{/each}} <:empty> - + + + + <:configure> - - - + + + + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/overview.hbs b/ui/lib/pki/addon/templates/overview.hbs index 9f85b82009..2d1535d15f 100644 --- a/ui/lib/pki/addon/templates/overview.hbs +++ b/ui/lib/pki/addon/templates/overview.hbs @@ -17,13 +17,17 @@ @canListRoles={{this.model.canListRoles}} /> {{else}} - - - + + + + + + + {{/if}} \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/index.hbs b/ui/lib/pki/addon/templates/roles/index.hbs index 499b25413f..9d239d6cda 100644 --- a/ui/lib/pki/addon/templates/roles/index.hbs +++ b/ui/lib/pki/addon/templates/roles/index.hbs @@ -57,20 +57,27 @@ {{/each}} <:empty> - + + + + <:configure> - - - + + + + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/tidy/index.hbs b/ui/lib/pki/addon/templates/tidy/index.hbs index ca98c21e15..72267f8bba 100644 --- a/ui/lib/pki/addon/templates/tidy/index.hbs +++ b/ui/lib/pki/addon/templates/tidy/index.hbs @@ -9,13 +9,17 @@ {{else}} - - - + + + + + + + {{/if}} \ No newline at end of file diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index 10ac01c496..42bed21c6f 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -622,16 +622,14 @@ module('Acceptance | pki workflow', function (hooks) { await login(this.mixedConfigCapabilities); await visit(`/vault/secrets-engines/${this.mountPath}/pki/configuration/edit`); assert - .dom(`${PKI_CONFIG_EDIT.configEditSection} [data-test-component="empty-state"]`) - .hasText( - `You do not have permission to set this mount's the cluster config Ask your administrator if you think you should have access to: POST /${this.mountPath}/config/cluster` - ); + .dom(`${PKI_CONFIG_EDIT.configEditSection} ${GENERAL.emptyStateTitle}`) + .hasText("You do not have permission to set this mount's the cluster config"); assert.dom(PKI_CONFIG_EDIT.acmeEditSection).exists(); assert.dom(PKI_CONFIG_EDIT.urlsEditSection).exists(); assert.dom(PKI_CONFIG_EDIT.crlEditSection).exists(); - assert.dom(`${PKI_CONFIG_EDIT.acmeEditSection} [data-test-component="empty-state"]`).doesNotExist(); - assert.dom(`${PKI_CONFIG_EDIT.urlsEditSection} [data-test-component="empty-state"]`).doesNotExist(); - assert.dom(`${PKI_CONFIG_EDIT.crlEditSection} [data-test-component="empty-state"]`).doesNotExist(); + assert.dom(`${PKI_CONFIG_EDIT.acmeEditSection} ${GENERAL.emptyStateTitle}`).doesNotExist(); + assert.dom(`${PKI_CONFIG_EDIT.urlsEditSection} ${GENERAL.emptyStateTitle}`).doesNotExist(); + assert.dom(`${PKI_CONFIG_EDIT.crlEditSection} ${GENERAL.emptyStateTitle}`).doesNotExist(); await click(PKI_CONFIG_EDIT.crlToggleInput('expiry')); await click(PKI_CONFIG_EDIT.saveButton); assert.strictEqual(currentURL(), `/vault/secrets-engines/${this.mountPath}/pki/configuration`); diff --git a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js index eb78b88bf7..9c27907272 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js @@ -9,6 +9,7 @@ import { click, fillIn, render } from '@ember/test-helpers'; import { setupEngine } from 'ember-engines/test-support'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { PKI_CONFIG_EDIT } from 'vault/tests/helpers/pki/pki-selectors'; import { configCapabilities } from 'vault/tests/helpers/pki/pki-helpers'; import PkiConfigClusterForm from 'vault/forms/secrets/pki/config/cluster'; @@ -306,25 +307,17 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks) await this.renderComponent(); assert - .dom(`${PKI_CONFIG_EDIT.configEditSection} [data-test-component="empty-state"]`) - .hasText( - "You do not have permission to set this mount's the cluster config Ask your administrator if you think you should have access to: POST /pki-engine/config/cluster" - ); + .dom(`${PKI_CONFIG_EDIT.configEditSection} ${GENERAL.emptyStateTitle}`) + .hasText("You do not have permission to set this mount's the cluster config"); assert - .dom(`${PKI_CONFIG_EDIT.acmeEditSection} [data-test-component="empty-state"]`) - .hasText( - "You do not have permission to set this mount's ACME config Ask your administrator if you think you should have access to: POST /pki-engine/config/acme" - ); + .dom(`${PKI_CONFIG_EDIT.acmeEditSection} ${GENERAL.emptyStateTitle}`) + .hasText("You do not have permission to set this mount's ACME config"); assert - .dom(`${PKI_CONFIG_EDIT.urlsEditSection} [data-test-component="empty-state"]`) - .hasText( - "You do not have permission to set this mount's URLs Ask your administrator if you think you should have access to: POST /pki-engine/config/urls" - ); + .dom(`${PKI_CONFIG_EDIT.urlsEditSection} ${GENERAL.emptyStateTitle}`) + .hasText("You do not have permission to set this mount's URLs"); assert - .dom(`${PKI_CONFIG_EDIT.crlEditSection} [data-test-component="empty-state"]`) - .hasText( - "You do not have permission to set this mount's revocation configuration Ask your administrator if you think you should have access to: POST /pki-engine/config/crl" - ); + .dom(`${PKI_CONFIG_EDIT.crlEditSection} ${GENERAL.emptyStateTitle}`) + .hasText("You do not have permission to set this mount's revocation configuration"); }); test('it renders alert banner and endpoint respective error', async function (assert) { From b5c6e83db135b64eea22abe21f2ba598bb9373a1 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:14:55 -0400 Subject: [PATCH 100/468] VAULT-42410 - Refactored empty state component for kmip directory (#12806) (#13020) * VAULT-42410 - refactored empty state component for kmip directory * added data-test attributes * fixed failing tests Co-authored-by: mohit-hashicorp --- .../addon/components/page/configuration.hbs | 8 +++---- .../addon/components/page/credentials.hbs | 24 +++++++++++++------ .../addon/components/page/scope/roles.hbs | 24 +++++++++++++------ ui/lib/kmip/addon/components/page/scopes.hbs | 24 +++++++++++++------ ui/tests/pages/components/list-view.js | 3 ++- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/ui/lib/kmip/addon/components/page/configuration.hbs b/ui/lib/kmip/addon/components/page/configuration.hbs index 87ce6b75d8..0b6a12e318 100644 --- a/ui/lib/kmip/addon/components/page/configuration.hbs +++ b/ui/lib/kmip/addon/components/page/configuration.hbs @@ -50,8 +50,8 @@ {{else}} - + + + + {{/if}} \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/page/credentials.hbs b/ui/lib/kmip/addon/components/page/credentials.hbs index 892196341b..f9a19536c8 100644 --- a/ui/lib/kmip/addon/components/page/credentials.hbs +++ b/ui/lib/kmip/addon/components/page/credentials.hbs @@ -95,13 +95,23 @@
    {{else}} {{#if @filterValue}} - + + + {{else}} - - - + + + + + + + {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/page/scope/roles.hbs b/ui/lib/kmip/addon/components/page/scope/roles.hbs index f1a80b6253..7a09fc1614 100644 --- a/ui/lib/kmip/addon/components/page/scope/roles.hbs +++ b/ui/lib/kmip/addon/components/page/scope/roles.hbs @@ -106,13 +106,23 @@
    {{else}} {{#if @filterValue}} - + + + {{else}} - - - + + + + + + + {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/page/scopes.hbs b/ui/lib/kmip/addon/components/page/scopes.hbs index ea9ec1de51..c6a6b95d14 100644 --- a/ui/lib/kmip/addon/components/page/scopes.hbs +++ b/ui/lib/kmip/addon/components/page/scopes.hbs @@ -94,13 +94,23 @@
    {{else}} {{#if @filterValue}} - + + + {{else}} - - - + + + + + + + {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/tests/pages/components/list-view.js b/ui/tests/pages/components/list-view.js index bec4c1844b..39ed59f5c0 100644 --- a/ui/tests/pages/components/list-view.js +++ b/ui/tests/pages/components/list-view.js @@ -4,9 +4,10 @@ */ import { text, isPresent, collection, clickable } from 'ember-cli-page-object'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; export default { - isEmpty: isPresent('[data-test-component="empty-state"]'), + isEmpty: isPresent(GENERAL.emptyStateTitle), listItemLinks: collection('[data-test-list-item-link]', { text: text(), click: clickable(), From 1ea39ba53d6bb0da11756dd702f2b3ca19fdd18e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:21:19 -0400 Subject: [PATCH 101/468] VAULT-42410 - refactored empty state component for replication and recovery directories (#12807) (#13021) * VAULT-42410 - refactored empty state component for replication and recovery directories * added data-test attributes Co-authored-by: mohit-hashicorp --- ui/app/components/recovery/page/snapshots.hbs | 76 +++++++++++-------- ui/lib/replication/addon/templates/index.hbs | 8 +- .../mode/secondaries/config-show.hbs | 5 +- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/ui/app/components/recovery/page/snapshots.hbs b/ui/app/components/recovery/page/snapshots.hbs index ee33f2eb40..5e51de4dca 100644 --- a/ui/app/components/recovery/page/snapshots.hbs +++ b/ui/app/components/recovery/page/snapshots.hbs @@ -10,10 +10,8 @@ /> {{#if @model.snapshots.showRaftStorageMessage}} -
    - - - + + {{else}} {{#if @model.showCommunityMessage}} - - + - + + + + + {{else if (not @model.snapshots)}} {{! Currently, only a single snapshot is supported and the UI automatically redirects users to "recovery.snapshots.snapshot.manage" if one exists. In the future, this may change to support multiple loaded snapshots and a LIST view will be built then. }} {{#let (get this.emptyStateDetails this.state) as |d|}} - + - {{#if (eq this.state this.viewState.ALLOW_UPLOAD)}} - - {{else}} - - {{/if}} - + + + + + {{#if (eq this.state this.viewState.ALLOW_UPLOAD)}} + + {{else}} + + {{/if}} + + {{/let}} {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/lib/replication/addon/templates/index.hbs b/ui/lib/replication/addon/templates/index.hbs index eca78545c0..5c0de37362 100644 --- a/ui/lib/replication/addon/templates/index.hbs +++ b/ui/lib/replication/addon/templates/index.hbs @@ -18,7 +18,13 @@ - + + + {{else if this.model.replicationIsInitializing}} {{else if this.model.allReplicationDisabled}} diff --git a/ui/lib/replication/addon/templates/mode/secondaries/config-show.hbs b/ui/lib/replication/addon/templates/mode/secondaries/config-show.hbs index 4eaeb77483..ad93358f3c 100644 --- a/ui/lib/replication/addon/templates/mode/secondaries/config-show.hbs +++ b/ui/lib/replication/addon/templates/mode/secondaries/config-show.hbs @@ -44,5 +44,8 @@
    {{else}} - + + + + {{/if}} \ No newline at end of file From 9f129c4c8e629458b833f6b2f8c96060104b81fe Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:21:33 -0400 Subject: [PATCH 102/468] VAULT-42410 - Refactored empty state component for sync directory (#12808) (#13022) * VAULT-42410 - refactored empty state component for sync directory * added data-test attributes Co-authored-by: mohit-hashicorp --- .../components/secrets/page/destinations.hbs | 4 ++- .../page/destinations/destination/secrets.hbs | 25 +++++++++++-------- .../components/secrets/page/overview.hbs | 23 +++++++++-------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/ui/lib/sync/addon/components/secrets/page/destinations.hbs b/ui/lib/sync/addon/components/secrets/page/destinations.hbs index 2ba426a26d..c45dadd037 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations.hbs +++ b/ui/lib/sync/addon/components/secrets/page/destinations.hbs @@ -116,7 +116,9 @@ />
    {{else}} - + + + {{/if}} {{#if this.destinationToDelete}} diff --git a/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.hbs b/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.hbs index 28b1a99c48..6a837ce135 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.hbs +++ b/ui/lib/sync/addon/components/secrets/page/destinations/destination/secrets.hbs @@ -91,17 +91,22 @@ />
    {{else}} - - + + - + + + + {{/if}} {{#if this.secretToUnsync}} diff --git a/ui/lib/sync/addon/components/secrets/page/overview.hbs b/ui/lib/sync/addon/components/secrets/page/overview.hbs index aabe4545c4..1eb2680175 100644 --- a/ui/lib/sync/addon/components/secrets/page/overview.hbs +++ b/ui/lib/sync/addon/components/secrets/page/overview.hbs @@ -76,17 +76,18 @@ Loading destinations... {{else if (not this.destinationMetrics)}} - - - + + + + + + + {{else}} <:head as |H|> From 7ed28a9e59d65e08e4bd79d67522c94a250267b4 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:21:49 -0400 Subject: [PATCH 103/468] VAULT-42410 - Refactored empty state component for remaining files (#12809) (#13023) * VAULT-42410 - refactored empty state component for remaining files * added data-test attributes Co-authored-by: mohit-hashicorp --- .../components/modal-form/policy-template.hbs | 5 +- ui/app/components/secret-edit.hbs | 4 +- ui/app/components/secret-engine/list.hbs | 4 +- .../secret-engine/page/plugin-settings.hbs | 53 +++++++++++-------- ui/app/components/secret-form-show.hbs | 12 +++-- ui/app/components/transit-edit.hbs | 4 +- .../components/login-settings/page/list.hbs | 24 +++++---- .../addon/components/messages/page/list.hbs | 11 ++-- ui/lib/core/addon/components/upgrade-page.hbs | 26 +++++---- 9 files changed, 86 insertions(+), 57 deletions(-) diff --git a/ui/app/components/modal-form/policy-template.hbs b/ui/app/components/modal-form/policy-template.hbs index a270b808e3..26a83c76c0 100644 --- a/ui/app/components/modal-form/policy-template.hbs +++ b/ui/app/components/modal-form/policy-template.hbs @@ -29,5 +29,8 @@ @onChange={{this.setPolicyType}} @noDefault={{true}} /> - + + + + {{/if}} \ No newline at end of file diff --git a/ui/app/components/secret-edit.hbs b/ui/app/components/secret-edit.hbs index 08022cdff7..5749aed5d5 100644 --- a/ui/app/components/secret-edit.hbs +++ b/ui/app/components/secret-edit.hbs @@ -62,7 +62,9 @@ @showAdvancedMode={{this.showAdvancedMode}} /> {{else}} - + + + {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 3498c85352..bc11e87381 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -153,6 +153,8 @@
    {{else}} - + + + {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/app/components/secret-engine/page/plugin-settings.hbs b/ui/app/components/secret-engine/page/plugin-settings.hbs index d178bdf3e7..ed77b0d781 100644 --- a/ui/app/components/secret-engine/page/plugin-settings.hbs +++ b/ui/app/components/secret-engine/page/plugin-settings.hbs @@ -57,31 +57,38 @@ {{/each}} {{else if engineDisplayData.isConfigurable}} {{! Prompt user to configure the secret engine }} - - + + - + + + + {{else}} - - + + - + + + + {{/if}} {{/let}} \ No newline at end of file diff --git a/ui/app/components/secret-form-show.hbs b/ui/app/components/secret-form-show.hbs index f4c4e3e21b..8f2075a6fa 100644 --- a/ui/app/components/secret-form-show.hbs +++ b/ui/app/components/secret-form-show.hbs @@ -4,11 +4,13 @@ }} {{#if @isWriteWithoutRead}} - + + + + {{else}} {{#if @showAdvancedMode}}
    diff --git a/ui/app/components/transit-edit.hbs b/ui/app/components/transit-edit.hbs index 0aa4491f57..8b063b8c7c 100644 --- a/ui/app/components/transit-edit.hbs +++ b/ui/app/components/transit-edit.hbs @@ -42,5 +42,7 @@ @backend={{this.backend}} /> {{else}} - + + + {{/if}} \ No newline at end of file diff --git a/ui/lib/config-ui/addon/components/login-settings/page/list.hbs b/ui/lib/config-ui/addon/components/login-settings/page/list.hbs index c942b6473a..ec86b6c1d0 100644 --- a/ui/lib/config-ui/addon/components/login-settings/page/list.hbs +++ b/ui/lib/config-ui/addon/components/login-settings/page/list.hbs @@ -61,17 +61,21 @@ {{/each}} {{else}} - - + + - + + + + {{/if}} {{#if this.ruleToDelete}} diff --git a/ui/lib/config-ui/addon/components/messages/page/list.hbs b/ui/lib/config-ui/addon/components/messages/page/list.hbs index a822d10695..62c16a1e88 100644 --- a/ui/lib/config-ui/addon/components/messages/page/list.hbs +++ b/ui/lib/config-ui/addon/components/messages/page/list.hbs @@ -143,10 +143,13 @@ @queryFunction={{this.paginationQueryParams}} /> {{else}} - + + + + {{/if}} {{#if this.showMaxMessageModal}} diff --git a/ui/lib/core/addon/components/upgrade-page.hbs b/ui/lib/core/addon/components/upgrade-page.hbs index 1e88d1ce21..bb6f505ea0 100644 --- a/ui/lib/core/addon/components/upgrade-page.hbs +++ b/ui/lib/core/addon/components/upgrade-page.hbs @@ -11,15 +11,19 @@ - - + + - \ No newline at end of file + + + + \ No newline at end of file From 17e74fed3a202288ce856321ba86dd9d06cb41e1 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:22:18 -0400 Subject: [PATCH 104/468] VAULT-42410 - Refactored empty state component for kubernetes directory (#12802) (#13024) * VAULT-42410 - refactored empty state component for kubernetes directory * added data-test attributes Co-authored-by: mohit-hashicorp --- .../addon/components/config-cta.hbs | 16 ++++++++++------ .../components/page/role/create-and-edit.hbs | 12 +++++++----- .../addon/components/page/roles.hbs | 19 ++++++++++++++----- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/ui/lib/kubernetes/addon/components/config-cta.hbs b/ui/lib/kubernetes/addon/components/config-cta.hbs index 6693e89624..3c63929277 100644 --- a/ui/lib/kubernetes/addon/components/config-cta.hbs +++ b/ui/lib/kubernetes/addon/components/config-cta.hbs @@ -3,9 +3,13 @@ SPDX-License-Identifier: BUSL-1.1 }} - - - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs index c327a4c553..f4bfcb4c6b 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -121,11 +121,13 @@ {{/if}} {{else}} - + + + + {{/if}}
    diff --git a/ui/lib/kubernetes/addon/components/page/roles.hbs b/ui/lib/kubernetes/addon/components/page/roles.hbs index cdaad1df74..699818a28b 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.hbs +++ b/ui/lib/kubernetes/addon/components/page/roles.hbs @@ -23,12 +23,21 @@ {{else if (not @roles)}} {{#if @filterValue}} - + + + {{else}} - + + + + {{/if}} {{else}}
    From 9d5cf9b20f37c4a8b3ba748cc11d2de6e52da9d8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 13 Mar 2026 23:23:35 -0400 Subject: [PATCH 105/468] VAULT-42410 - Updated alignment for application state components (#12817) (#13025) * VAULT-42410 - updating quick actions card to use arg @isAutoCentered={{false}} * VAULT-42410 - Updated alignment for application state components * fixed review comment Co-authored-by: mohit-hashicorp --- ui/app/components/clients/no-data.hbs | 8 +++---- .../components/clients/page/client-list.hbs | 18 ++++++++------ ui/app/components/clients/page/counts.hbs | 4 ++-- ui/app/components/clients/page/overview.hbs | 8 +++---- .../dashboard/quick-actions-card.hbs | 4 ++-- .../components/dashboard/replication-card.hbs | 2 +- .../dashboard/secrets-engines-card.hbs | 2 +- ui/app/components/database-connection.hbs | 14 ++++------- ui/app/components/database-role-edit.hbs | 2 +- .../components/database-role-setting-form.hbs | 4 ++-- .../identity/item-alias/alias-metadata.hbs | 2 +- ui/app/components/identity/item-aliases.hbs | 2 +- ui/app/components/identity/item-groups.hbs | 2 +- ui/app/components/identity/item-members.hbs | 2 +- ui/app/components/identity/item-metadata.hbs | 2 +- .../identity/item-parent-groups.hbs | 2 +- ui/app/components/identity/item-policies.hbs | 2 +- ui/app/components/keymgmt/key-edit.hbs | 24 +++++++++++-------- ui/app/components/keymgmt/provider-edit.hbs | 8 +++---- ui/app/components/page/methods.hbs | 4 ++-- ui/app/components/page/policies.hbs | 11 +++++---- .../cluster/access/identity/aliases/index.hbs | 2 +- .../vault/cluster/access/identity/index.hbs | 2 +- .../vault/cluster/access/leases/error.hbs | 18 ++++++++------ .../vault/cluster/access/leases/list.hbs | 12 ++++++---- .../mfa/enforcements/enforcement/index.hbs | 6 ++--- .../cluster/access/mfa/enforcements/index.hbs | 6 ++--- .../cluster/access/mfa/methods/index.hbs | 6 ++--- .../access/mfa/methods/method/index.hbs | 5 ++-- .../access/oidc/clients/client/providers.hbs | 7 +++--- .../cluster/access/oidc/keys/key/clients.hbs | 7 +++--- .../oidc/providers/provider/clients.hbs | 7 +++--- .../cluster/access/oidc/scopes/index.hbs | 2 +- ui/app/templates/vault/cluster/auth.hbs | 18 +++++++------- .../cluster/clients/counts/client-list.hbs | 14 ++++------- .../replication-dr-promote/details.hbs | 8 +++---- .../cluster/replication-dr-promote/index.hbs | 8 +++---- .../vault/cluster/secrets/backend/list.hbs | 10 ++++---- .../cluster/secrets/backend/overview.hbs | 2 +- .../templates/vault/cluster/settings/seal.hbs | 8 +++++-- ui/app/templates/vault/cluster/tools/tool.hbs | 2 +- ui/app/templates/vault/cluster/unseal.hbs | 5 ++-- ui/lib/core/addon/components/page/error.hbs | 2 +- .../components/replication-secondary-card.hbs | 7 +++--- .../components/known-secondaries-card.hbs | 3 ++- .../templates/mode/secondaries/index.hbs | 15 ++++++++---- 46 files changed, 167 insertions(+), 142 deletions(-) diff --git a/ui/app/components/clients/no-data.hbs b/ui/app/components/clients/no-data.hbs index 1e71d4cf48..8d8ad8a588 100644 --- a/ui/app/components/clients/no-data.hbs +++ b/ui/app/components/clients/no-data.hbs @@ -4,7 +4,7 @@ }} {{#if (or @config.reporting_enabled (eq @config.enabled "default-enabled") (eq @config.enabled "enable"))}} - + {{else if @config}} - + {{#if @canUpdate}} - + {{else}} - + {{#if (and (eq tabName "Secret sync") (not this.flags.secretsSyncIsActivated))}} - - - - + + + + {{else}} - - + + {{/if}} @@ -90,7 +94,7 @@ {{else}} - + diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index f2956de69b..bfaca0028b 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -77,7 +77,7 @@ {{yield}} {{else}} {{! Empty state for no data in the selected date range }} - + + <:emptyState> - - - - + + + + {{/if}} {{else}} - + {{/if}} {{else}} - +
    {{else}} - + + + @@ -157,7 +157,7 @@

    Statements

    {{#if (eq @model.statementFields null)}} - + @@ -309,14 +309,8 @@
    {{else if (eq @model.isAvailablePlugin false)}} - - + + {{else}} - + diff --git a/ui/app/components/database-role-setting-form.hbs b/ui/app/components/database-role-setting-form.hbs index d8b846f66f..0bcaabe9f0 100644 --- a/ui/app/components/database-role-setting-form.hbs +++ b/ui/app/components/database-role-setting-form.hbs @@ -37,7 +37,7 @@ {{/each}}
    {{else}} - + @@ -54,7 +54,7 @@ {{/each}} {{else}} - + {{else}} - + {{/each-in}} \ No newline at end of file diff --git a/ui/app/components/identity/item-aliases.hbs b/ui/app/components/identity/item-aliases.hbs index b69ec6b0ab..938e0358cb 100644 --- a/ui/app/components/identity/item-aliases.hbs +++ b/ui/app/components/identity/item-aliases.hbs @@ -29,7 +29,7 @@ {{else}} - + {{/each}} {{else}} - + {{/if}} \ No newline at end of file diff --git a/ui/app/components/identity/item-members.hbs b/ui/app/components/identity/item-members.hbs index a67c8f8d80..93f4c5c5c2 100644 --- a/ui/app/components/identity/item-members.hbs +++ b/ui/app/components/identity/item-members.hbs @@ -45,7 +45,7 @@ {{/each}} {{else}} - + {{/if}} \ No newline at end of file diff --git a/ui/app/components/identity/item-metadata.hbs b/ui/app/components/identity/item-metadata.hbs index 514f470614..4099ea8efd 100644 --- a/ui/app/components/identity/item-metadata.hbs +++ b/ui/app/components/identity/item-metadata.hbs @@ -22,7 +22,7 @@ {{else}} - + {{/each}} {{else}} - + {{/if}} \ No newline at end of file diff --git a/ui/app/components/identity/item-policies.hbs b/ui/app/components/identity/item-policies.hbs index ac5376030f..b33eb3f9f5 100644 --- a/ui/app/components/identity/item-policies.hbs +++ b/ui/app/components/identity/item-policies.hbs @@ -23,7 +23,7 @@ {{else}} - + {{/each}} \ No newline at end of file diff --git a/ui/app/components/keymgmt/key-edit.hbs b/ui/app/components/keymgmt/key-edit.hbs index 30ae8bbc3f..d4540b4bad 100644 --- a/ui/app/components/keymgmt/key-edit.hbs +++ b/ui/app/components/keymgmt/key-edit.hbs @@ -170,13 +170,13 @@ Distribution details {{#if @model.provider.permissionsError}} - + {{else if (is-empty-value @model.provider)}} - - + + {{#if @model.canListProviders}} - - + {{/each}} {{else}} - + {{/if}} diff --git a/ui/app/components/keymgmt/provider-edit.hbs b/ui/app/components/keymgmt/provider-edit.hbs index 85899b5212..ef19c56e76 100644 --- a/ui/app/components/keymgmt/provider-edit.hbs +++ b/ui/app/components/keymgmt/provider-edit.hbs @@ -98,9 +98,9 @@ {{! Only show last field if provider selected }} {{else}} - - - + + + {{/if}} {{else}} @@ -170,7 +170,7 @@ /> {{else}} - + diff --git a/ui/app/components/page/methods.hbs b/ui/app/components/page/methods.hbs index bc1c68830d..9a011ef6a9 100644 --- a/ui/app/components/page/methods.hbs +++ b/ui/app/components/page/methods.hbs @@ -133,8 +133,8 @@ {{else}} - - + + {{/each}} diff --git a/ui/app/components/page/policies.hbs b/ui/app/components/page/policies.hbs index 04cac5dbcb..e2f247a369 100644 --- a/ui/app/components/page/policies.hbs +++ b/ui/app/components/page/policies.hbs @@ -163,8 +163,8 @@ {{/if}} {{else}} - - + + {{/each}} {{#unless this.filter}} @@ -178,12 +178,13 @@ /> {{/unless}} {{else}} - - + + - + {{#if (eq @policyType "acl")}} {{else}} - + {{else}} - + {{this.model.keyId}} is not a valid lease ID. {{else if (eq this.model.httpStatus 404)}} - - + + - + {{else if (eq this.model.httpStatus 403)}} - - - - + + + + diff --git a/ui/app/templates/vault/cluster/access/leases/list.hbs b/ui/app/templates/vault/cluster/access/leases/list.hbs index 8d35ff4646..8b17f8464b 100644 --- a/ui/app/templates/vault/cluster/access/leases/list.hbs +++ b/ui/app/templates/vault/cluster/access/leases/list.hbs @@ -96,8 +96,12 @@ {{or item.keyWithoutParent item.id}} {{else}} - - + + {{/each}} {{#unless this.filter}} @@ -111,7 +115,7 @@ /> {{/unless}} {{else}} - - + + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs b/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs index 76c8ec0a82..54bcaa3993 100644 --- a/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs +++ b/ui/app/templates/vault/cluster/access/mfa/enforcements/enforcement/index.hbs @@ -103,9 +103,9 @@ {{/each}} {{else}} - - - + + + {{/if}} {{else if (eq this.tab "methods")}} diff --git a/ui/app/templates/vault/cluster/access/mfa/enforcements/index.hbs b/ui/app/templates/vault/cluster/access/mfa/enforcements/index.hbs index 597e852055..53eb962784 100644 --- a/ui/app/templates/vault/cluster/access/mfa/enforcements/index.hbs +++ b/ui/app/templates/vault/cluster/access/mfa/enforcements/index.hbs @@ -24,8 +24,8 @@ {{/each}} {{else}} - - - + + + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/access/mfa/methods/index.hbs b/ui/app/templates/vault/cluster/access/mfa/methods/index.hbs index 28e76401f1..e7466b261f 100644 --- a/ui/app/templates/vault/cluster/access/mfa/methods/index.hbs +++ b/ui/app/templates/vault/cluster/access/mfa/methods/index.hbs @@ -24,8 +24,8 @@ {{/each}} {{else}} - - - + + + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/access/mfa/methods/method/index.hbs b/ui/app/templates/vault/cluster/access/mfa/methods/method/index.hbs index da87380a29..9230972f21 100644 --- a/ui/app/templates/vault/cluster/access/mfa/methods/method/index.hbs +++ b/ui/app/templates/vault/cluster/access/mfa/methods/method/index.hbs @@ -92,10 +92,11 @@
    {{#if (is-empty this.model.enforcements)}} - - + + {{else}} diff --git a/ui/app/templates/vault/cluster/access/oidc/clients/client/providers.hbs b/ui/app/templates/vault/cluster/access/oidc/clients/client/providers.hbs index f38fe892f4..390e82f1db 100644 --- a/ui/app/templates/vault/cluster/access/oidc/clients/client/providers.hbs +++ b/ui/app/templates/vault/cluster/access/oidc/clients/client/providers.hbs @@ -7,12 +7,13 @@ {{#if (gt this.model.length 0)}} {{else}} - - + + - + {{else}} - - + + - + {{else}} - - + + - + {{/each}} {{else}} - + diff --git a/ui/app/templates/vault/cluster/auth.hbs b/ui/app/templates/vault/cluster/auth.hbs index 38c3dc8945..42b1972dfd 100644 --- a/ui/app/templates/vault/cluster/auth.hbs +++ b/ui/app/templates/vault/cluster/auth.hbs @@ -4,14 +4,16 @@ }} {{#if this.unwrapTokenError}} - - - - - - - - +
    + + + + + + + + +
    {{else}} - + + Viewing export data requires @@ -20,8 +14,8 @@ /sys/internal/counters/activity/export. - - + {{#if Page.isDisabled}} - + - - + + {{#if Page.isDisabled}} - + - - + + + - + {{#if (or this.model.connectionCapabilities.canCreate this.model.connectionCapabilities.canUpdate)}} diff --git a/ui/app/templates/vault/cluster/settings/seal.hbs b/ui/app/templates/vault/cluster/settings/seal.hbs index 813786a66f..848b1ae716 100644 --- a/ui/app/templates/vault/cluster/settings/seal.hbs +++ b/ui/app/templates/vault/cluster/settings/seal.hbs @@ -15,7 +15,11 @@ {{#if this.model.seal.canUpdate}} {{else}} - - + + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/tools/tool.hbs b/ui/app/templates/vault/cluster/tools/tool.hbs index 14c3d754d2..c767af1c96 100644 --- a/ui/app/templates/vault/cluster/tools/tool.hbs +++ b/ui/app/templates/vault/cluster/tools/tool.hbs @@ -19,7 +19,7 @@ {{! This center aligns error message on the page }}
    - +
    {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/unseal.hbs b/ui/app/templates/vault/cluster/unseal.hbs index 1a3eb96360..647f118aa8 100644 --- a/ui/app/templates/vault/cluster/unseal.hbs +++ b/ui/app/templates/vault/cluster/unseal.hbs @@ -7,11 +7,12 @@
    - + - + diff --git a/ui/lib/core/addon/components/replication-secondary-card.hbs b/ui/lib/core/addon/components/replication-secondary-card.hbs index a2c741e565..710d23eca0 100644 --- a/ui/lib/core/addon/components/replication-secondary-card.hbs +++ b/ui/lib/core/addon/components/replication-secondary-card.hbs @@ -100,12 +100,13 @@
    {{#if (is-empty this.knownPrimaryClusterAddrs)}} - - + + - + {{else}} - + {{/if}} diff --git a/ui/lib/replication/addon/templates/mode/secondaries/index.hbs b/ui/lib/replication/addon/templates/mode/secondaries/index.hbs index 856a453147..71f6bd15e4 100644 --- a/ui/lib/replication/addon/templates/mode/secondaries/index.hbs +++ b/ui/lib/replication/addon/templates/mode/secondaries/index.hbs @@ -57,10 +57,17 @@
    {{/each}} {{else}} - - - - + + + + Date: Mon, 16 Mar 2026 07:23:04 -0400 Subject: [PATCH 106/468] VAULT-43029: SCIM guardrail fixes (#12886) (#13026) * unit tests passing, implementation * other tests pass * fix test godocs * remove skip from tests * add comments Co-authored-by: miagilepner --- helper/identity/identity.go | 21 +++++++++ vault/identity_store_aliases.go | 20 +++++++- vault/identity_store_entities_update.go | 17 ++++++- vault/identity_store_groups.go | 27 +++++++++-- vault/identity_store_scim_schema.go | 62 +++++++++++++++++++++++-- 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/helper/identity/identity.go b/helper/identity/identity.go index 9f9f84f2ad..7344f6f4e3 100644 --- a/helper/identity/identity.go +++ b/helper/identity/identity.go @@ -197,10 +197,31 @@ func (g *Group) SCIMClientID() string { return g.ScimClientID } +// SCIMFields are fields that can only be modified via SCIM, if the resource +// is SCIM-owned. `scim_client_id` cannot ever be modified, so it is not +// included in this list. +func (g *Group) SCIMFields() []string { + return []string{"name", "member_entity_ids"} +} + func (e *Entity) SCIMClientID() string { return e.ScimClientID } +// SCIMFields are fields that can only be modified via SCIM, if the resource +// is SCIM-owned. `scim_client_id` cannot ever be modified, so it is not +// included in this list. +func (e *Entity) SCIMFields() []string { + return []string{"name", "disabled", "metadata", "external_id"} +} + func (a *Alias) SCIMClientID() string { return a.ScimClientID } + +// SCIMFields are fields that can only be modified via SCIM, if the resource +// is SCIM-owned. `scim_client_id` cannot ever be modified, so it is not +// included in this list. +func (a *Alias) SCIMFields() []string { + return []string{"name", "mount_accessor", "canonical_id"} +} diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index 38380a4861..c31e6403f2 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -279,7 +279,7 @@ func (i *IdentityStore) handleAliasCreate(ctx context.Context, canonicalID, name return nil, err } - if err := i.scimResourceCheck(ctx, &identity.Alias{ScimClientID: scimClientID}, "", true); err != nil { + if err := i.scimResourceCheck(ctx, &identity.Alias{ScimClientID: scimClientID}, "", true, nil); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied } var entity *identity.Entity @@ -373,7 +373,23 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name return nil, nil } - if err := i.scimResourceCheck(ctx, alias, alias.ScimClientID, false); err != nil { + // Build the list of fields being modified by this request. + var modifiedFields []string + if name != alias.Name { + modifiedFields = append(modifiedFields, "name") + } + if mountAccessor != alias.MountAccessor { + modifiedFields = append(modifiedFields, "mount_accessor") + } + if canonicalID != "" && canonicalID != alias.CanonicalID { + modifiedFields = append(modifiedFields, "canonical_id") + } + if !strutil.EqualStringMaps(customMetadata, alias.CustomMetadata) { + modifiedFields = append(modifiedFields, "custom_metadata") + } + + // Check that only non-SCIM-managed fields are being modified via the API. + if err := i.scimResourceCheck(ctx, alias, alias.ScimClientID, false, modifiedFields); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied } alias.LastUpdateTime = timestamppb.Now() diff --git a/vault/identity_store_entities_update.go b/vault/identity_store_entities_update.go index bee8cbdcc8..b9f98f7a11 100644 --- a/vault/identity_store_entities_update.go +++ b/vault/identity_store_entities_update.go @@ -19,6 +19,7 @@ type EntityBuilder struct { entity *identity.Entity isNew bool originalSCIMID string + modifiedFields []string err error } @@ -83,6 +84,7 @@ func (b *EntityBuilder) WithExternalID(ctx context.Context, externalID string) * } else { // No entity found, so we're just setting the external ID on the current one. b.entity.ExternalID = externalID + b.modifiedFields = append(b.modifiedFields, "external_id") } return b @@ -116,6 +118,9 @@ func (b *EntityBuilder) WithName(ctx context.Context, name string) *EntityBuilde return b } + if b.entity.Name != name { + b.modifiedFields = append(b.modifiedFields, "name") + } b.entity.Name = name return b } @@ -129,6 +134,10 @@ func (b *EntityBuilder) WithPolicies(policies []string) *EntityBuilder { b.err = fmt.Errorf("policies cannot contain root") return b } + dedupedPolicies := strutil.RemoveDuplicates(policies, false) + if !strutil.EquivalentSlices(b.entity.Policies, dedupedPolicies) { + b.modifiedFields = append(b.modifiedFields, "policies") + } b.entity.Policies = strutil.RemoveDuplicates(policies, false) return b } @@ -138,6 +147,9 @@ func (b *EntityBuilder) WithDisabled(disabled bool) *EntityBuilder { if b.err != nil { return b } + if b.entity.Disabled != disabled { + b.modifiedFields = append(b.modifiedFields, "disabled") + } b.entity.Disabled = disabled return b } @@ -151,6 +163,9 @@ func (b *EntityBuilder) WithMetadata(metadata map[string]string) *EntityBuilder if value, ok := b.entity.Metadata[duplicateCanonicalIDMetadataKey]; ok { metadata[duplicateCanonicalIDMetadataKey] = value } + if !strutil.EqualStringMaps(b.entity.Metadata, metadata) { + b.modifiedFields = append(b.modifiedFields, "metadata") + } b.entity.Metadata = metadata return b } @@ -171,7 +186,7 @@ func (b *EntityBuilder) Build(ctx context.Context) (*logical.Response, error) { return logical.ErrorResponse(b.err.Error()), nil } - if err := b.store.scimResourceCheck(ctx, b.entity, b.originalSCIMID, b.isNew); err != nil { + if err := b.store.scimResourceCheck(ctx, b.entity, b.originalSCIMID, b.isNew, b.modifiedFields); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied } diff --git a/vault/identity_store_groups.go b/vault/identity_store_groups.go index 563449fe26..fdb18b5d32 100644 --- a/vault/identity_store_groups.go +++ b/vault/identity_store_groups.go @@ -262,11 +262,15 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica group = new(identity.Group) newGroup = true } - + var modifiedFields []string // Update the policies if supplied policiesRaw, ok := d.GetOk("policies") if ok { - group.Policies = strutil.RemoveDuplicatesStable(policiesRaw.([]string), true) + dedupedPolicies := strutil.RemoveDuplicatesStable(policiesRaw.([]string), true) + if !strutil.EquivalentSlices(dedupedPolicies, group.Policies) { + modifiedFields = append(modifiedFields, "policies") + } + group.Policies = dedupedPolicies } if strutil.StrListContainsCaseInsensitive(group.Policies, "root") { @@ -310,6 +314,9 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica case groupByName.ID != group.ID: return logical.ErrorResponse("group name is already in use"), nil } + if group.Name != groupName { + modifiedFields = append(modifiedFields, "name") + } group.Name = groupName } @@ -319,14 +326,15 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica group.ScimClientID = scimClientID.(string) } - if err := i.scimResourceCheck(ctx, group, originalSCIMID, newGroup); err != nil { - return logical.ErrorResponse(err.Error()), nil - } metadata, ok, err := d.GetOkErr("metadata") if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to parse metadata: %v", err)), nil } if ok { + metadataMap := metadata.(map[string]string) + if !strutil.EqualStringMaps(group.Metadata, metadataMap) { + modifiedFields = append(modifiedFields, "metadata") + } group.Metadata = metadata.(map[string]string) } @@ -335,6 +343,9 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica if group.Type == groupTypeExternal { return logical.ErrorResponse("member entities can't be set manually for external groups"), nil } + if !strutil.EquivalentSlices(group.MemberEntityIDs, memberEntityIDsRaw.([]string)) { + modifiedFields = append(modifiedFields, "member_entity_ids") + } group.MemberEntityIDs = memberEntityIDsRaw.([]string) } @@ -345,8 +356,14 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica return logical.ErrorResponse("member groups can't be set for external groups"), nil } memberGroupIDs = memberGroupIDsRaw.([]string) + modifiedFields = append(modifiedFields, "member_group_ids") } + // Build the list of fields being modified by this request. + + if err := i.scimResourceCheck(ctx, group, originalSCIMID, newGroup, modifiedFields); err != nil { + return logical.ErrorResponse(err.Error()), nil + } err = i.sanitizeAndUpsertGroup(ctx, group, nil, memberGroupIDs) if err != nil { if errStr := err.Error(); strings.HasPrefix(errStr, errCycleDetectedPrefix) { diff --git a/vault/identity_store_scim_schema.go b/vault/identity_store_scim_schema.go index 8c672dc7a4..8a5fd3c531 100644 --- a/vault/identity_store_scim_schema.go +++ b/vault/identity_store_scim_schema.go @@ -6,6 +6,8 @@ package vault import ( "context" "errors" + "fmt" + "strings" "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/identity" @@ -90,7 +92,13 @@ func scimClientIDFromContext(ctx context.Context) string { return val.(string) } -func (i *IdentityStore) scimResourceCheck(ctx context.Context, resource scimManaged, originalSCIMID string, isCreate bool) error { +// scimResourceCheck performs a number of checks on the resource to ensure that the operation is allowed. +// A new resource must only set a SCIM client ID if the request came through SCIM via the same client. +// An updated resource must: +// - not change the SCIM client ID +// - not be modified by a different SCIM client than the one that owns it +// - only update SCIM managed fields if the request came via the same SCIM client +func (i *IdentityStore) scimResourceCheck(ctx context.Context, resource scimManaged, originalSCIMID string, isCreate bool, modifiedFields []string) error { reqSCIMClientID := scimClientIDFromContext(ctx) resourceSCIMClientID := resource.SCIMClientID() @@ -116,16 +124,62 @@ func (i *IdentityStore) scimResourceCheck(ctx context.Context, resource scimMana if originalSCIMID != resourceSCIMClientID { return errors.New("cannot update scim_client_id") } - // if the resource is being updated, this must be via SCIM - if originalSCIMID != reqSCIMClientID { - return errors.New("SCIM-managed resources must be modified through SCIM") + if originalSCIMID != "" && reqSCIMClientID != "" && originalSCIMID != reqSCIMClientID { + return errors.New("cannot update resource via SCIM with a different SCIM client ID") + } + if err := i.scimFieldGuard(ctx, resource, modifiedFields); err != nil { + return err } } return nil } +// scimFieldGuard checks whether an API request is allowed to modify the given +// fields on a SCIM-managed resource. If the resource is not SCIM-managed +// (scimClientID is empty), all modifications are allowed. If the request +// comes from the owning SCIM client, all modifications are allowed. Otherwise, +// only non-SCIM-managed fields may be modified. +func (i *IdentityStore) scimFieldGuard(ctx context.Context, resource scimManaged, modifiedFields []string) error { + // If the resource is not SCIM-managed, no restrictions apply. + scimClientID := resource.SCIMClientID() + if scimClientID == "" { + return nil + } + + // If the request comes from the owning SCIM client, all modifications are allowed. + if scimClientIDFromContext(ctx) == scimClientID { + return nil + } + + // The request is from the API (not SCIM). Check whether any of the + // modified fields are SCIM-managed. + scimManagedFields := resource.SCIMFields() + managedSet := make(map[string]struct{}, len(scimManagedFields)) + for _, f := range scimManagedFields { + managedSet[f] = struct{}{} + } + + var blocked []string + for _, f := range modifiedFields { + if _, ok := managedSet[f]; ok { + blocked = append(blocked, f) + } + } + + if len(blocked) > 0 { + return fmt.Errorf("cannot modify SCIM-managed field(s) %q through the API", strings.Join(blocked, ", ")) + } + + return nil +} + type scimManaged interface { + // SCIMClientID returns the SCIM client ID of the managed resource SCIMClientID() string + // SCIMFields returns the list of fields that are managed by SCIM and cannot + // be modified through the API. This list does not need to include `scim_client_id` + // as that is verified separately + SCIMFields() []string } var ( From a4d77b43c50b8c0910f3388a5c817d02f81e47af Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 10:39:46 -0400 Subject: [PATCH 107/468] Add core plugin testing framework (#12723) (#13027) - Add plugin registry for managing built-in plugin factories - Add plugin configuration and binary management - Add plugin utility functions and session management - Enable blackbox testing infrastructure for Vault plugins Co-authored-by: Luis (LT) Carbonell --- .../testcluster/blackbox/plugin_binary.go | 199 ++++++ .../testcluster/blackbox/plugin_config.go | 174 +++++ .../testcluster/blackbox/plugin_registry.go | 136 ++++ .../testcluster/blackbox/plugin_utils.go | 143 +++++ .../testcluster/blackbox/session_plugin.go | 607 ++++++++++++++++++ 5 files changed, 1259 insertions(+) create mode 100644 sdk/helper/testcluster/blackbox/plugin_binary.go create mode 100644 sdk/helper/testcluster/blackbox/plugin_config.go create mode 100644 sdk/helper/testcluster/blackbox/plugin_registry.go create mode 100644 sdk/helper/testcluster/blackbox/plugin_utils.go diff --git a/sdk/helper/testcluster/blackbox/plugin_binary.go b/sdk/helper/testcluster/blackbox/plugin_binary.go new file mode 100644 index 0000000000..14ab713bf0 --- /dev/null +++ b/sdk/helper/testcluster/blackbox/plugin_binary.go @@ -0,0 +1,199 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// PluginBinaryInfo contains information about a plugin binary +type PluginBinaryInfo struct { + Name string + Path string + SHA256 string + Version string + PluginType string + Environment map[string]string +} + +// BuildPluginBinary builds a plugin binary from source code +func BuildPluginBinary(t *testing.T, sourcePath, outputPath string) error { + t.Helper() + + // Ensure the output directory exists + outputDir := filepath.Dir(outputPath) + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) + } + + // Build the plugin binary + cmd := exec.Command("go", "build", "-o", outputPath, sourcePath) + cmd.Env = append(os.Environ(), "CGO_ENABLED=0") // Ensure static binary + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to build plugin binary: %w\nOutput: %s", err, output) + } + + t.Logf("Successfully built plugin binary: %s", outputPath) + return nil +} + +// DeployPluginBinary deploys a plugin binary to the plugin directory +func DeployPluginBinary(t *testing.T, binaryPath, pluginDir string) error { + t.Helper() + + // Ensure the plugin directory exists + if err := os.MkdirAll(pluginDir, 0o755); err != nil { + return fmt.Errorf("failed to create plugin directory %s: %w", pluginDir, err) + } + + // Copy the binary to the plugin directory + binaryName := filepath.Base(binaryPath) + targetPath := filepath.Join(pluginDir, binaryName) + + sourceFile, err := os.Open(binaryPath) + if err != nil { + return fmt.Errorf("failed to open source binary %s: %w", binaryPath, err) + } + defer sourceFile.Close() + + targetFile, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed to create target binary %s: %w", targetPath, err) + } + defer targetFile.Close() + + if _, err := io.Copy(targetFile, sourceFile); err != nil { + return fmt.Errorf("failed to copy binary: %w", err) + } + + // Make the binary executable + if err := os.Chmod(targetPath, 0o755); err != nil { + return fmt.Errorf("failed to make binary executable: %w", err) + } + + t.Logf("Successfully deployed plugin binary to: %s", targetPath) + return nil +} + +// ValidatePluginBinary validates a plugin binary +func ValidatePluginBinary(t *testing.T, binaryPath string) error { + t.Helper() + + // Check if the file exists + if _, err := os.Stat(binaryPath); err != nil { + return fmt.Errorf("plugin binary not found at %s: %w", binaryPath, err) + } + + // Check if the file is executable + info, err := os.Stat(binaryPath) + if err != nil { + return fmt.Errorf("failed to get binary info: %w", err) + } + + if info.Mode()&0o111 == 0 { + return fmt.Errorf("plugin binary %s is not executable", binaryPath) + } + + // Try to run the binary with --help to see if it's a valid plugin + cmd := exec.Command(binaryPath, "--help") + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Note: Plugin binary %s may not support --help flag: %v", binaryPath, err) + } else { + t.Logf("Plugin binary %s validation output: %s", binaryPath, string(output)) + } + + t.Logf("Plugin binary validation passed for: %s", binaryPath) + return nil +} + +// GetPluginBinaryInfo extracts information about a plugin binary +func GetPluginBinaryInfo(t *testing.T, binaryPath string) (*PluginBinaryInfo, error) { + t.Helper() + + // Calculate SHA256 + f, err := os.Open(binaryPath) + if err != nil { + return nil, fmt.Errorf("failed to open binary for SHA256 calculation: %w", err) + } + defer f.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, f); err != nil { + return nil, fmt.Errorf("failed to calculate SHA256: %w", err) + } + + sha256Sum := hex.EncodeToString(hasher.Sum(nil)) + binaryName := filepath.Base(binaryPath) + + info := &PluginBinaryInfo{ + Name: strings.TrimSuffix(binaryName, filepath.Ext(binaryName)), + Path: binaryPath, + SHA256: sha256Sum, + PluginType: "unknown", // Will be determined by usage context + Environment: make(map[string]string), + } + + return info, nil +} + +// ComparePluginVersions compares two plugin sessions for differences +func ComparePluginVersions(t *testing.T, v1, v2 *PluginSession) (*PluginDiff, error) { + t.Helper() + + diff := &PluginDiff{ + PluginName: v1.PluginName, + V1Info: v1.GetPluginInfo(), + V2Info: v2.GetPluginInfo(), + Changes: make(map[string]interface{}), + } + + // Compare binary paths (for external plugins) + if !v1.IsBuiltin && !v2.IsBuiltin { + if v1.BinaryPath != v2.BinaryPath { + diff.Changes["binary_path"] = map[string]string{ + "old": v1.BinaryPath, + "new": v2.BinaryPath, + } + } + } + + // Compare configuration + for key, v1Val := range v1.Config { + if v2Val, exists := v2.Config[key]; !exists { + diff.Changes["config_removed_"+key] = v1Val + } else if v1Val != v2Val { + diff.Changes["config_changed_"+key] = map[string]interface{}{ + "old": v1Val, + "new": v2Val, + } + } + } + + for key, v2Val := range v2.Config { + if _, exists := v1.Config[key]; !exists { + diff.Changes["config_added_"+key] = v2Val + } + } + + return diff, nil +} + +// PluginDiff represents differences between plugin versions +type PluginDiff struct { + PluginName string + V1Info map[string]interface{} + V2Info map[string]interface{} + Changes map[string]interface{} +} diff --git a/sdk/helper/testcluster/blackbox/plugin_config.go b/sdk/helper/testcluster/blackbox/plugin_config.go new file mode 100644 index 0000000000..ea79acdf94 --- /dev/null +++ b/sdk/helper/testcluster/blackbox/plugin_config.go @@ -0,0 +1,174 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "fmt" + "testing" +) + +// PluginConfig represents a plugin configuration with validation +type PluginConfig struct { + Type string `json:"type"` + Name string `json:"name"` + Config map[string]interface{} `json:"config"` + Policies []string `json:"policies,omitempty"` + Environment map[string]string `json:"environment,omitempty"` + Required []string `json:"required,omitempty"` // Required config fields + Optional []string `json:"optional,omitempty"` // Optional config fields + Validators map[string]ConfigValidator `json:"-"` // Field validators +} + +// ConfigValidator validates a configuration field +type ConfigValidator func(value interface{}) error + +// NewPluginConfig creates a new plugin configuration with validation +func NewPluginConfig(pluginType, pluginName string) *PluginConfig { + return &PluginConfig{ + Type: pluginType, + Name: pluginName, + Config: make(map[string]interface{}), + Environment: make(map[string]string), + Validators: make(map[string]ConfigValidator), + } +} + +// SetRequired sets the required configuration fields +func (pc *PluginConfig) SetRequired(fields ...string) *PluginConfig { + pc.Required = fields + return pc +} + +// SetOptional sets the optional configuration fields +func (pc *PluginConfig) SetOptional(fields ...string) *PluginConfig { + pc.Optional = fields + return pc +} + +// AddValidator adds a validator for a specific field +func (pc *PluginConfig) AddValidator(field string, validator ConfigValidator) *PluginConfig { + pc.Validators[field] = validator + return pc +} + +// SetConfig sets a configuration value +func (pc *PluginConfig) SetConfig(key string, value interface{}) *PluginConfig { + pc.Config[key] = value + return pc +} + +// SetEnvironment sets an environment variable +func (pc *PluginConfig) SetEnvironment(key, value string) *PluginConfig { + pc.Environment[key] = value + return pc +} + +// ValidateConfig validates the plugin configuration +func (pc *PluginConfig) ValidateConfig() error { + // Check required fields + for _, field := range pc.Required { + if _, exists := pc.Config[field]; !exists { + return fmt.Errorf("required configuration field '%s' is missing", field) + } + } + + // Run field validators + for field, validator := range pc.Validators { + if value, exists := pc.Config[field]; exists { + if err := validator(value); err != nil { + return fmt.Errorf("validation failed for field '%s': %w", field, err) + } + } + } + + return nil +} + +// ============================================================================ +// Common Configuration Validators +// ============================================================================ + +// StringValidator validates that a value is a string +func StringValidator(value interface{}) error { + if _, ok := value.(string); !ok { + return fmt.Errorf("expected string, got %T", value) + } + return nil +} + +// IntValidator validates that a value is an integer +func IntValidator(value interface{}) error { + switch value.(type) { + case int, int64, int32: + return nil + case float64: + // JSON numbers are often decoded as float64 + return nil + default: + return fmt.Errorf("expected integer, got %T", value) + } +} + +// BoolValidator validates that a value is a boolean +func BoolValidator(value interface{}) error { + if _, ok := value.(bool); !ok { + return fmt.Errorf("expected boolean, got %T", value) + } + return nil +} + +// NonEmptyStringValidator validates that a value is a non-empty string +func NonEmptyStringValidator(value interface{}) error { + str, ok := value.(string) + if !ok { + return fmt.Errorf("expected string, got %T", value) + } + if str == "" { + return fmt.Errorf("string cannot be empty") + } + return nil +} + +// ============================================================================ +// Configuration Management Functions +// ============================================================================ + +// LoadPluginConfig loads a plugin configuration from a file +func LoadPluginConfig(t *testing.T, configPath string) (*PluginConfig, error) { + t.Helper() + + // Framework teams can implement JSON/YAML/etc. loading here + return &PluginConfig{ + Type: "unknown", + Name: "unknown", + Config: make(map[string]interface{}), + Environment: make(map[string]string), + Validators: make(map[string]ConfigValidator), + }, nil +} + +// ApplyPluginConfig applies a plugin configuration to a session +func ApplyPluginConfig(ps *PluginSession, config *PluginConfig) error { + // Validate the configuration + if err := config.ValidateConfig(); err != nil { + return fmt.Errorf("configuration validation failed: %w", err) + } + + // Apply configuration + for key, value := range config.Config { + ps.Config[key] = value + } + + // Apply environment variables + for key, value := range config.Environment { + ps.Environment[key] = value + } + + // Apply the configuration to the plugin + if len(ps.Config) > 0 { + ps.MustConfigure(ps.Config) + } + + return nil +} diff --git a/sdk/helper/testcluster/blackbox/plugin_registry.go b/sdk/helper/testcluster/blackbox/plugin_registry.go new file mode 100644 index 0000000000..4b8886971f --- /dev/null +++ b/sdk/helper/testcluster/blackbox/plugin_registry.go @@ -0,0 +1,136 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "fmt" + + "github.com/hashicorp/vault/sdk/logical" +) + +// BuiltinPluginRegistry manages built-in plugin factories +type BuiltinPluginRegistry struct { + AuthFactories map[string]logical.Factory + SecretsFactories map[string]logical.Factory + DatabaseFactories map[string]logical.Factory +} + +// NewBuiltinPluginRegistry creates a new built-in plugin registry +func NewBuiltinPluginRegistry() *BuiltinPluginRegistry { + return &BuiltinPluginRegistry{ + AuthFactories: make(map[string]logical.Factory), + SecretsFactories: make(map[string]logical.Factory), + DatabaseFactories: make(map[string]logical.Factory), + } +} + +// RegisterAuthPlugin registers a built-in auth plugin factory +func (r *BuiltinPluginRegistry) RegisterAuthPlugin(name string, factory logical.Factory) { + r.AuthFactories[name] = factory +} + +// RegisterSecretsPlugin registers a built-in secrets plugin factory +func (r *BuiltinPluginRegistry) RegisterSecretsPlugin(name string, factory logical.Factory) { + r.SecretsFactories[name] = factory +} + +// RegisterDatabasePlugin registers a built-in database plugin factory +func (r *BuiltinPluginRegistry) RegisterDatabasePlugin(name string, factory logical.Factory) { + r.DatabaseFactories[name] = factory +} + +// GetBuiltinPluginFactory retrieves a built-in plugin factory by type and name +func (r *BuiltinPluginRegistry) GetBuiltinPluginFactory(pluginType, pluginName string) (logical.Factory, error) { + switch pluginType { + case "auth": + if factory, exists := r.AuthFactories[pluginName]; exists { + return factory, nil + } + return nil, fmt.Errorf("auth plugin %s not found in registry", pluginName) + case "secret": + if factory, exists := r.SecretsFactories[pluginName]; exists { + return factory, nil + } + return nil, fmt.Errorf("secrets plugin %s not found in registry", pluginName) + case "database": + if factory, exists := r.DatabaseFactories[pluginName]; exists { + return factory, nil + } + return nil, fmt.Errorf("database plugin %s not found in registry", pluginName) + default: + return nil, fmt.Errorf("unsupported plugin type: %s", pluginType) + } +} + +// ListBuiltinPlugins lists all built-in plugins of a given type +func (r *BuiltinPluginRegistry) ListBuiltinPlugins(pluginType string) []string { + var plugins []string + + switch pluginType { + case "auth": + for name := range r.AuthFactories { + plugins = append(plugins, name) + } + case "secret": + for name := range r.SecretsFactories { + plugins = append(plugins, name) + } + case "database": + for name := range r.DatabaseFactories { + plugins = append(plugins, name) + } + } + + return plugins +} + +// GetAllBuiltinPlugins returns all registered built-in plugins +func (r *BuiltinPluginRegistry) GetAllBuiltinPlugins() map[string][]string { + return map[string][]string{ + "auth": r.ListBuiltinPlugins("auth"), + "secret": r.ListBuiltinPlugins("secret"), + "database": r.ListBuiltinPlugins("database"), + } +} + +// DefaultBuiltinPluginRegistry returns a registry with common built-in plugins +func DefaultBuiltinPluginRegistry() *BuiltinPluginRegistry { + registry := NewBuiltinPluginRegistry() + + // Teams can register actual plugin factories here using the provided methods: + // registry.RegisterSecretsPlugin("kv", kvFactory) + // registry.RegisterSecretsPlugin("transit", transitFactory) + // registry.RegisterAuthPlugin("userpass", userpassFactory) + // registry.RegisterDatabasePlugin("postgres", postgresFactory) + + return registry +} + +// ============================================================================ +// Global Registry and Convenience Functions +// ============================================================================ + +// Global registry instance for convenience +var DefaultRegistry = DefaultBuiltinPluginRegistry() + +// Convenience functions for global registry access +func GetBuiltinPluginFactory(pluginType, pluginName string) (logical.Factory, error) { + return DefaultRegistry.GetBuiltinPluginFactory(pluginType, pluginName) +} + +func ListBuiltinPlugins(pluginType string) []string { + return DefaultRegistry.ListBuiltinPlugins(pluginType) +} + +func RegisterBuiltinAuthPlugin(name string, factory logical.Factory) { + DefaultRegistry.RegisterAuthPlugin(name, factory) +} + +func RegisterBuiltinSecretsPlugin(name string, factory logical.Factory) { + DefaultRegistry.RegisterSecretsPlugin(name, factory) +} + +func RegisterBuiltinDatabasePlugin(name string, factory logical.Factory) { + DefaultRegistry.RegisterDatabasePlugin(name, factory) +} diff --git a/sdk/helper/testcluster/blackbox/plugin_utils.go b/sdk/helper/testcluster/blackbox/plugin_utils.go new file mode 100644 index 0000000000..0f8c5a32a6 --- /dev/null +++ b/sdk/helper/testcluster/blackbox/plugin_utils.go @@ -0,0 +1,143 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package blackbox + +import ( + "fmt" + + "github.com/hashicorp/vault/sdk/logical" +) + +// ExtendedPluginRegistration provides enhanced registration with metadata +func (s *Session) ExtendedPluginRegistration(pluginName, binaryPath, pluginType string, metadata map[string]interface{}) error { + s.t.Helper() + + // First, validate the binary + if err := ValidatePluginBinary(s.t, binaryPath); err != nil { + return fmt.Errorf("plugin binary validation failed: %w", err) + } + + // Get binary info + binaryInfo, err := GetPluginBinaryInfo(s.t, binaryPath) + if err != nil { + return fmt.Errorf("failed to get plugin binary info: %w", err) + } + + // Perform standard registration + s.MustRegisterPlugin(pluginName, binaryPath, pluginType) + + // Store metadata (this would typically be stored in a database or file) + s.t.Logf("Plugin %s registered with SHA256: %s", pluginName, binaryInfo.SHA256) + if metadata != nil { + s.t.Logf("Plugin %s metadata: %+v", pluginName, metadata) + } + + return nil +} + +// BatchPluginRegistration registers multiple plugins at once +func (s *Session) BatchPluginRegistration(plugins []PluginRegistrationRequest) error { + s.t.Helper() + + for _, plugin := range plugins { + if err := s.ExtendedPluginRegistration(plugin.Name, plugin.BinaryPath, plugin.Type, plugin.Metadata); err != nil { + return fmt.Errorf("failed to register plugin %s: %w", plugin.Name, err) + } + } + + s.t.Logf("Successfully registered %d plugins", len(plugins)) + return nil +} + +// PluginRegistrationRequest represents a plugin registration request +type PluginRegistrationRequest struct { + Name string + BinaryPath string + Type string + Metadata map[string]interface{} +} + +// ============================================================================ +// Built-in Plugin Direct Access Utilities +// ============================================================================ + +// NewBuiltinPluginSessionFromRegistry creates a plugin session from the registry +func (s *Session) NewBuiltinPluginSessionFromRegistry(pluginType, pluginName string) (*PluginSession, error) { + s.t.Helper() + + factory, err := GetBuiltinPluginFactory(pluginType, pluginName) + if err != nil { + return nil, fmt.Errorf("failed to get built-in plugin factory: %w", err) + } + + return s.NewBuiltinPluginSession(pluginType, pluginName, factory), nil +} + +// ============================================================================ +// Setup Helper Functions for Built-in Plugins +// ============================================================================ + +// SetupBuiltinAuthPlugin creates and registers a built-in auth plugin +func SetupBuiltinAuthPlugin(v *Session, pluginName string, factory logical.Factory) *PluginSession { + v.t.Helper() + + ps := v.NewBuiltinPluginSession("auth", pluginName, factory) + ps.MustRegisterAndEnable() + + return ps +} + +// SetupBuiltinSecretsPlugin creates and registers a built-in secrets plugin +func SetupBuiltinSecretsPlugin(v *Session, pluginName string, factory logical.Factory) *PluginSession { + v.t.Helper() + + ps := v.NewBuiltinPluginSession("secret", pluginName, factory) + ps.MustRegisterAndEnable() + + return ps +} + +// SetupBuiltinDatabasePlugin creates and registers a built-in database plugin +func SetupBuiltinDatabasePlugin(v *Session, pluginName string, factory logical.Factory) *PluginSession { + v.t.Helper() + + ps := v.NewBuiltinPluginSession("database", pluginName, factory) + ps.MustRegisterAndEnable() + + return ps +} + +// ============================================================================ +// Setup Helper Functions for External Plugins +// ============================================================================ + +// SetupExternalAuthPlugin creates and registers an external auth plugin +func SetupExternalAuthPlugin(v *Session, pluginName, binaryPath string) *PluginSession { + v.t.Helper() + + ps := v.NewExternalPluginSession("auth", pluginName, binaryPath) + ps.MustRegisterAndEnable() + + return ps +} + +// SetupExternalSecretsPlugin creates and registers an external secrets plugin +func SetupExternalSecretsPlugin(v *Session, pluginName, binaryPath string) *PluginSession { + v.t.Helper() + + ps := v.NewExternalPluginSession("secret", pluginName, binaryPath) + ps.MustRegisterAndEnable() + + return ps +} + +// SetupExternalDatabasePlugin creates and registers an external database plugin +func SetupExternalDatabasePlugin(v *Session, pluginName, binaryPath string) *PluginSession { + v.t.Helper() + + ps := v.NewExternalPluginSession("database", pluginName, binaryPath) + ps.MustRegisterAndEnable() + + return ps +} diff --git a/sdk/helper/testcluster/blackbox/session_plugin.go b/sdk/helper/testcluster/blackbox/session_plugin.go index 35aa2d7cb0..eef2eb06db 100644 --- a/sdk/helper/testcluster/blackbox/session_plugin.go +++ b/sdk/helper/testcluster/blackbox/session_plugin.go @@ -6,11 +6,13 @@ package blackbox import ( "crypto/sha256" "encoding/hex" + "fmt" "io" "os" "path/filepath" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/require" ) @@ -62,3 +64,608 @@ func (s *Session) AssertPluginConfigured(path string) { configPath := filepath.Join(path, "config") s.MustRead(configPath) } + +// PluginSession extends the base Session with plugin-specific functionality +// supporting both built-in and external plugins +type PluginSession struct { + *Session + PluginType string // "auth", "secret", or "database" + PluginName string // Name of the plugin + MountPath string // Mount path for the plugin + IsBuiltin bool // Distinguishes built-in vs external plugins + BinaryPath string // Only for external plugins + Factory logical.Factory // Only for built-in plugins + Environment map[string]string // Environment variables for external plugins + Config map[string]interface{} // Plugin configuration +} + +// PluginSessionOptions provides configuration for creating a PluginSession +type PluginSessionOptions struct { + PluginType string + PluginName string + MountPath string + Config map[string]interface{} + Environment map[string]string +} + +// NewBuiltinPluginSession creates a new plugin session for built-in plugins +func (s *Session) NewBuiltinPluginSession(pluginType, pluginName string, factory logical.Factory) *PluginSession { + s.t.Helper() + + mountPath := fmt.Sprintf("%s-builtin-%s", pluginName, randomString(6)) + + ps := &PluginSession{ + Session: s, + PluginType: pluginType, + PluginName: pluginName, + MountPath: mountPath, + IsBuiltin: true, + Factory: factory, + Environment: make(map[string]string), + Config: make(map[string]interface{}), + } + + s.t.Cleanup(func() { + ps.cleanup() + }) + + return ps +} + +// NewExternalPluginSession creates a new plugin session for external plugins +func (s *Session) NewExternalPluginSession(pluginType, pluginName, binaryPath string) *PluginSession { + s.t.Helper() + + mountPath := fmt.Sprintf("%s-external-%s", pluginName, randomString(6)) + + ps := &PluginSession{ + Session: s, + PluginType: pluginType, + PluginName: pluginName, + MountPath: mountPath, + IsBuiltin: false, + BinaryPath: binaryPath, + Environment: make(map[string]string), + Config: make(map[string]interface{}), + } + + s.t.Cleanup(func() { + ps.cleanup() + }) + + return ps +} + +// NewPluginSessionWithOptions creates a plugin session using the provided options +func (s *Session) NewPluginSessionWithOptions(opts PluginSessionOptions, isBuiltin bool, factoryOrPath interface{}) *PluginSession { + s.t.Helper() + + mountPath := opts.MountPath + if mountPath == "" { + prefix := "builtin" + if !isBuiltin { + prefix = "external" + } + mountPath = fmt.Sprintf("%s-%s-%s", opts.PluginName, prefix, randomString(6)) + } + + ps := &PluginSession{ + Session: s, + PluginType: opts.PluginType, + PluginName: opts.PluginName, + MountPath: mountPath, + IsBuiltin: isBuiltin, + Environment: opts.Environment, + Config: opts.Config, + } + + if ps.Environment == nil { + ps.Environment = make(map[string]string) + } + if ps.Config == nil { + ps.Config = make(map[string]interface{}) + } + + if isBuiltin { + if factory, ok := factoryOrPath.(logical.Factory); ok { + ps.Factory = factory + } else { + s.t.Fatalf("Expected logical.Factory for built-in plugin, got %T", factoryOrPath) + } + } else { + if binaryPath, ok := factoryOrPath.(string); ok { + ps.BinaryPath = binaryPath + } else { + s.t.Fatalf("Expected string binary path for external plugin, got %T", factoryOrPath) + } + } + + s.t.Cleanup(func() { + ps.cleanup() + }) + + return ps +} + +// MustRegisterAndEnable registers and enables the plugin based on its type +func (ps *PluginSession) MustRegisterAndEnable() { + ps.t.Helper() + + if ps.IsBuiltin { + ps.mustEnableBuiltinPlugin() + } else { + ps.mustRegisterAndEnableExternalPlugin() + } + + // Apply any initial configuration + if len(ps.Config) > 0 { + ps.MustConfigure(ps.Config) + } +} + +// mustEnableBuiltinPlugin enables a built-in plugin directly +func (ps *PluginSession) mustEnableBuiltinPlugin() { + ps.t.Helper() + + switch ps.PluginType { + case "auth": + ps.Session.MustEnableAuth(ps.MountPath, &api.EnableAuthOptions{Type: ps.PluginName}) + case "secret": + ps.Session.MustEnableSecretsEngine(ps.MountPath, &api.MountInput{Type: ps.PluginName}) + case "database": + // Database plugins are typically mounted under the database secrets engine + ps.Session.MustEnableSecretsEngine(ps.MountPath, &api.MountInput{Type: "database"}) + default: + ps.t.Fatalf("Unsupported plugin type for built-in plugin: %s", ps.PluginType) + } +} + +// mustRegisterAndEnableExternalPlugin registers and enables an external plugin +func (ps *PluginSession) mustRegisterAndEnableExternalPlugin() { + ps.t.Helper() + + // Register the external plugin + ps.Session.MustRegisterPlugin(ps.PluginName, ps.BinaryPath, ps.PluginType) + + // Enable the plugin + ps.Session.MustEnablePlugin(ps.MountPath, ps.PluginName, ps.PluginType) +} + +// MustConfigure applies configuration to the plugin +func (ps *PluginSession) MustConfigure(config map[string]interface{}) { + ps.t.Helper() + + configPath := ps.getConfigPath() + ps.Session.MustWrite(configPath, config) + + // Store the configuration for reference + for k, v := range config { + ps.Config[k] = v + } +} + +// MustTestPluginHealth performs basic health checks on the plugin +func (ps *PluginSession) MustTestPluginHealth() { + ps.t.Helper() + + switch ps.PluginType { + case "auth": + ps.mustTestAuthPluginHealth() + case "secret": + ps.mustTestSecretsPluginHealth() + case "database": + ps.mustTestDatabasePluginHealth() + default: + ps.t.Fatalf("Unsupported plugin type: %s", ps.PluginType) + } +} + +// mustTestAuthPluginHealth tests basic auth plugin functionality +func (ps *PluginSession) mustTestAuthPluginHealth() { + ps.t.Helper() + + // Test that we can read the plugin configuration + configPath := ps.getConfigPath() + _, err := ps.Client.Logical().Read(configPath) + if err != nil { + ps.t.Logf("Note: Could not read auth plugin config at %s (this may be expected): %v", configPath, err) + } + + // Test that the auth method is listed + auths, err := ps.Client.Sys().ListAuth() + require.NoError(ps.t, err) + + expectedPath := ps.MountPath + "/" + if auths[expectedPath] == nil { + ps.t.Fatalf("Auth method %s not found in auth list", expectedPath) + } + + ps.t.Logf("Auth plugin %s health check passed", ps.PluginName) +} + +// mustTestSecretsPluginHealth tests basic secrets plugin functionality +func (ps *PluginSession) mustTestSecretsPluginHealth() { + ps.t.Helper() + + // Test that we can read the plugin configuration + configPath := ps.getConfigPath() + _, err := ps.Client.Logical().Read(configPath) + if err != nil { + ps.t.Logf("Note: Could not read secrets plugin config at %s (this may be expected): %v", configPath, err) + } + + // Test that the secrets engine is listed + mounts, err := ps.Client.Sys().ListMounts() + require.NoError(ps.t, err) + + expectedPath := ps.MountPath + "/" + if mounts[expectedPath] == nil { + ps.t.Fatalf("Secrets engine %s not found in mounts list", expectedPath) + } + + ps.t.Logf("Secrets plugin %s health check passed", ps.PluginName) +} + +// mustTestDatabasePluginHealth tests basic database plugin functionality +func (ps *PluginSession) mustTestDatabasePluginHealth() { + ps.t.Helper() + + // For database plugins, test that the database secrets engine is mounted + mounts, err := ps.Client.Sys().ListMounts() + require.NoError(ps.t, err) + + expectedPath := ps.MountPath + "/" + mount := mounts[expectedPath] + if mount == nil { + ps.t.Fatalf("Database secrets engine %s not found in mounts list", expectedPath) + } + + if mount.Type != "database" { + ps.t.Fatalf("Expected mount type 'database', got '%s'", mount.Type) + } + + ps.t.Logf("Database plugin %s health check passed", ps.PluginName) +} + +// MustTestPluginReload tests plugin reload functionality +func (ps *PluginSession) MustTestPluginReload() { + ps.t.Helper() + + if ps.IsBuiltin { + ps.t.Logf("Skipping reload test for built-in plugin %s (reload not applicable)", ps.PluginName) + return + } + + // Test plugin reload for external plugins + reloadInput := &api.ReloadPluginInput{ + Plugin: ps.PluginName, + } + + reloadID, err := ps.Client.Sys().ReloadPlugin(reloadInput) + require.NoError(ps.t, err) + require.NotEmpty(ps.t, reloadID) + + ps.t.Logf("Successfully reloaded external plugin %s (reload ID: %s)", ps.PluginName, reloadID) +} + +// MustTestPluginUpgrade tests plugin upgrade functionality (external plugins only) +func (ps *PluginSession) MustTestPluginUpgrade(newBinaryPath string) { + ps.t.Helper() + + if ps.IsBuiltin { + ps.t.Skip("Plugin upgrades not applicable for built-in plugins") + return + } + + // Store original binary path + originalBinaryPath := ps.BinaryPath + + // Register the new plugin version + newPluginName := ps.PluginName + "-v2" + ps.Session.MustRegisterPlugin(newPluginName, newBinaryPath, ps.PluginType) + + // Test that both versions are registered + ps.Session.AssertPluginRegistered(ps.PluginName) + ps.Session.AssertPluginRegistered(newPluginName) + + // Cleanup: restore original state + ps.t.Cleanup(func() { + ps.BinaryPath = originalBinaryPath + }) + + ps.t.Logf("Successfully tested plugin upgrade from %s to %s", ps.PluginName, newPluginName) +} + +// GetMountPath returns the mount path for this plugin session +func (ps *PluginSession) GetMountPath() string { + return ps.MountPath +} + +// GetPluginInfo returns basic information about the plugin +func (ps *PluginSession) GetPluginInfo() map[string]interface{} { + return map[string]interface{}{ + "plugin_type": ps.PluginType, + "plugin_name": ps.PluginName, + "mount_path": ps.MountPath, + "is_builtin": ps.IsBuiltin, + "binary_path": ps.BinaryPath, + "environment": ps.Environment, + "config": ps.Config, + } +} + +// UpdateConfig merges new configuration with existing config +func (ps *PluginSession) UpdateConfig(newConfig map[string]interface{}) { + ps.t.Helper() + + // Merge configurations + for k, v := range newConfig { + ps.Config[k] = v + } + + // Apply the updated configuration + ps.MustConfigure(ps.Config) +} + +// getConfigPath returns the configuration path for the plugin +func (ps *PluginSession) getConfigPath() string { + switch ps.PluginType { + case "auth": + return filepath.Join("auth", ps.MountPath, "config") + case "secret": + return filepath.Join(ps.MountPath, "config") + case "database": + return filepath.Join(ps.MountPath, "config", ps.PluginName) + default: + return filepath.Join(ps.MountPath, "config") + } +} + +// cleanup performs cleanup operations for the plugin session +func (ps *PluginSession) cleanup() { + if ps.Session == nil || ps.Session.Client == nil { + return + } + + ps.t.Logf("Cleaning up plugin session: %s (%s)", ps.PluginName, ps.MountPath) + + // Disable the plugin mount + var err error + switch ps.PluginType { + case "auth": + err = ps.Client.Sys().DisableAuth(ps.MountPath) + case "secret", "database": + err = ps.Client.Sys().Unmount(ps.MountPath) + } + + if err != nil { + ps.t.Logf("Warning: Failed to cleanup plugin mount %s: %v", ps.MountPath, err) + } + + // For external plugins, optionally deregister (commented out to avoid affecting other tests) + // if !ps.IsBuiltin { + // _, err := ps.Client.Logical().Delete(filepath.Join("sys/plugins/catalog", ps.PluginType, ps.PluginName)) + // if err != nil { + // ps.t.Logf("Warning: Failed to deregister plugin %s: %v", ps.PluginName, err) + // } + // } +} + +// ============================================================================ +// Plugin Testing Utilities and Helpers +// ============================================================================ + +// TestEndpointExists checks if a plugin endpoint exists and is accessible +func (ps *PluginSession) TestEndpointExists(path string) bool { + ps.t.Helper() + + fullPath := ps.buildPath(path) + _, err := ps.Client.Logical().Read(fullPath) + + // We consider the endpoint to exist if we get any response (including errors) + // that indicate the endpoint exists but may require different parameters/auth + return err == nil || !isNotFoundError(err) +} + +// WriteAndValidate writes data to a plugin endpoint and returns the response +func (ps *PluginSession) WriteAndValidate(path string, data map[string]interface{}) (*api.Secret, error) { + ps.t.Helper() + + fullPath := ps.buildPath(path) + return ps.Client.Logical().Write(fullPath, data) +} + +// ReadAndValidate reads from a plugin endpoint and returns the response +func (ps *PluginSession) ReadAndValidate(path string) (*api.Secret, error) { + ps.t.Helper() + + fullPath := ps.buildPath(path) + return ps.Client.Logical().Read(fullPath) +} + +// DeleteAndValidate deletes from a plugin endpoint and returns the response +func (ps *PluginSession) DeleteAndValidate(path string) (*api.Secret, error) { + ps.t.Helper() + + fullPath := ps.buildPath(path) + return ps.Client.Logical().Delete(fullPath) +} + +// ListAndValidate lists from a plugin endpoint and returns the response +func (ps *PluginSession) ListAndValidate(path string) (*api.Secret, error) { + ps.t.Helper() + + fullPath := ps.buildPath(path) + return ps.Client.Logical().List(fullPath) +} + +// ExpectError expects an operation to fail and validates the error +func (ps *PluginSession) ExpectError(operation func() (*api.Secret, error)) error { + ps.t.Helper() + + _, err := operation() + if err == nil { + return fmt.Errorf("expected operation to fail, but it succeeded") + } + return nil // Error was expected +} + +// ExpectSuccess expects an operation to succeed and returns the response +func (ps *PluginSession) ExpectSuccess(operation func() (*api.Secret, error)) *api.Secret { + ps.t.Helper() + + resp, err := operation() + require.NoError(ps.t, err, "Expected operation to succeed") + return resp +} + +// ValidateResponse validates that a response has expected properties +func (ps *PluginSession) ValidateResponse(resp *api.Secret, validations ...ResponseValidator) { + ps.t.Helper() + + for _, validation := range validations { + if err := validation(resp); err != nil { + ps.t.Fatalf("Response validation failed: %v", err) + } + } +} + +// ResponseValidator validates aspects of an API response +type ResponseValidator func(*api.Secret) error + +// HasDataField validates that response has a specific data field +func HasDataField(field string) ResponseValidator { + return func(resp *api.Secret) error { + if resp == nil || resp.Data == nil { + return fmt.Errorf("response or data is nil") + } + if _, exists := resp.Data[field]; !exists { + return fmt.Errorf("response missing expected field: %s", field) + } + return nil + } +} + +// HasDataValue validates that response has a specific data field with expected value +func HasDataValue(field string, expectedValue interface{}) ResponseValidator { + return func(resp *api.Secret) error { + if resp == nil || resp.Data == nil { + return fmt.Errorf("response or data is nil") + } + value, exists := resp.Data[field] + if !exists { + return fmt.Errorf("response missing expected field: %s", field) + } + if value != expectedValue { + return fmt.Errorf("field %s has value %v, expected %v", field, value, expectedValue) + } + return nil + } +} + +// IsNotEmpty validates that response data is not empty +func IsNotEmpty() ResponseValidator { + return func(resp *api.Secret) error { + if resp == nil || resp.Data == nil || len(resp.Data) == 0 { + return fmt.Errorf("response data is empty") + } + return nil + } +} + +// HasAuth validates that response has authentication information +func HasAuth() ResponseValidator { + return func(resp *api.Secret) error { + if resp == nil || resp.Auth == nil { + return fmt.Errorf("response has no authentication information") + } + return nil + } +} + +// HasLease validates that response has lease information +func HasLease() ResponseValidator { + return func(resp *api.Secret) error { + if resp == nil || resp.LeaseID == "" { + return fmt.Errorf("response has no lease information") + } + return nil + } +} + +// TestSequence runs a sequence of operations and validates them +func (ps *PluginSession) TestSequence(operations ...SequenceOperation) { + ps.t.Helper() + + for i, op := range operations { + ps.t.Logf("Executing sequence operation %d: %s", i+1, op.Name) + if err := op.Execute(ps); err != nil { + ps.t.Fatalf("Sequence operation %d (%s) failed: %v", i+1, op.Name, err) + } + } +} + +// SequenceOperation represents a single operation in a test sequence +type SequenceOperation struct { + Name string + Execute func(*PluginSession) error +} + +// WriteOp creates a write operation for test sequences +func WriteOp(name, path string, data map[string]interface{}, validators ...ResponseValidator) SequenceOperation { + return SequenceOperation{ + Name: name, + Execute: func(ps *PluginSession) error { + resp, err := ps.WriteAndValidate(path, data) + if err != nil { + return err + } + ps.ValidateResponse(resp, validators...) + return nil + }, + } +} + +// ReadOp creates a read operation for test sequences +func ReadOp(name, path string, validators ...ResponseValidator) SequenceOperation { + return SequenceOperation{ + Name: name, + Execute: func(ps *PluginSession) error { + resp, err := ps.ReadAndValidate(path) + if err != nil { + return err + } + ps.ValidateResponse(resp, validators...) + return nil + }, + } +} + +// DeleteOp creates a delete operation for test sequences +func DeleteOp(name, path string) SequenceOperation { + return SequenceOperation{ + Name: name, + Execute: func(ps *PluginSession) error { + _, err := ps.DeleteAndValidate(path) + return err + }, + } +} + +// buildPath constructs the full path for a plugin endpoint +func (ps *PluginSession) buildPath(path string) string { + if ps.PluginType == "auth" { + return filepath.Join("auth", ps.MountPath, path) + } + return filepath.Join(ps.MountPath, path) +} + +// isNotFoundError checks if an error indicates the endpoint was not found +func isNotFoundError(err error) bool { + if err == nil { + return false + } + // This is a simple check - in practice you might want more sophisticated error detection + return fmt.Sprintf("%v", err) == "404 Not Found" || + fmt.Sprintf("%v", err) == "405 Method Not Allowed" +} From 0aae8acb20c5334f20d3fc5fb946ef1eff9276ab Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 11:52:41 -0400 Subject: [PATCH 108/468] Add IP range filtering for ACME challenge validation. (#13010) (#13036) * Add IP range filtering for ACME challenge validation. * Add challenge_permitted_ip_ranges and challenge_excluded_ip_ranges API fields * Add PermittedIPRanges and ExcludedIPRanges fields to acmeConfigEntry * Implement CIDR/IP validation with precedence: excluded > permitted > default * Add changelog entry. Co-authored-by: Victor Rodriguez Rizo --- builtin/logical/pki/acme_challenges.go | 23 ++- builtin/logical/pki/acme_challenges_test.go | 187 ++++++++++++++++++- builtin/logical/pki/path_config_acme.go | 75 ++++++-- builtin/logical/pki/path_config_acme_test.go | 133 +++++++++++++ changelog/_13010.txt | 3 + 5 files changed, 394 insertions(+), 27 deletions(-) create mode 100644 changelog/_13010.txt diff --git a/builtin/logical/pki/acme_challenges.go b/builtin/logical/pki/acme_challenges.go index 530625840e..7580b34322 100644 --- a/builtin/logical/pki/acme_challenges.go +++ b/builtin/logical/pki/acme_challenges.go @@ -139,22 +139,33 @@ func isIPInCIDRList(cidrList []string, ip net.IP) bool { return false } -func isDisallowedACMEValidationIP(config *acmeConfigEntry, ip net.IP) bool { +func isValidChallengeIP(config *acmeConfigEntry, ip net.IP) bool { if ip == nil { + return false + } + + // Check excluded list first - it takes precedence + if isIPInCIDRList(config.ChallengeExcludedIPRanges, ip) { + return false + } + + // Check permitted list - if IP is in permitted list, allow it + if isIPInCIDRList(config.ChallengePermittedIPRanges, ip) { return true } - if isIPInCIDRList(config.AllowedCIDRList, ip) { + // If permitted list is configured but IP is not in it, reject it + if len(config.ChallengePermittedIPRanges) > 0 { return false } // Vault is often deployed on private networks, so avoid a blanket private // range denylist here and instead reject obviously unsafe targets. if ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { - return true + return false } - return !ip.IsGlobalUnicast() + return ip.IsGlobalUnicast() } func dialACMEValidationTarget(ctx context.Context, config *acmeConfigEntry, dialer *net.Dialer, network string, address string) (net.Conn, error) { @@ -164,7 +175,7 @@ func dialACMEValidationTarget(ctx context.Context, config *acmeConfigEntry, dial } if ip := net.ParseIP(host); ip != nil { - if isDisallowedACMEValidationIP(config, ip) { + if !isValidChallengeIP(config, ip) { return nil, fmt.Errorf("%w: validation target resolved to a disallowed ip range", ErrRejectedIdentifier) } @@ -183,7 +194,7 @@ func dialACMEValidationTarget(ctx context.Context, config *acmeConfigEntry, dial var lastErr error var foundAllowed bool for _, candidate := range ips { - if isDisallowedACMEValidationIP(config, candidate.IP) { + if !isValidChallengeIP(config, candidate.IP) { continue } diff --git a/builtin/logical/pki/acme_challenges_test.go b/builtin/logical/pki/acme_challenges_test.go index fcba6e85c4..c01ec3aa1b 100644 --- a/builtin/logical/pki/acme_challenges_test.go +++ b/builtin/logical/pki/acme_challenges_test.go @@ -205,7 +205,7 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { host := ts.URL[7:] isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{ - AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + ChallengePermittedIPRanges: []string{"127.0.0.1/32", "::1/128"}, }) if !isValid && err == nil { t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", index, handlerIndex, isValid, err) @@ -256,7 +256,7 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { host := ts.URL[7:] isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{ - AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + ChallengePermittedIPRanges: []string{"127.0.0.1/32", "::1/128"}, }) if isValid || err == nil { t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err) @@ -327,7 +327,7 @@ func TestAcmeValidateTLSALPN01Challenge(t *testing.T) { // non-standard port _just for testing purposes_. host := "localhost" config := &acmeConfigEntry{ - AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + ChallengePermittedIPRanges: []string{"127.0.0.1/32", "::1/128"}, } log := hclog.L() @@ -871,7 +871,7 @@ func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { host := ts.URL[len("http://"):] isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{ - AllowedCIDRList: []string{"127.0.0.1/32", "::1/128"}, + ChallengePermittedIPRanges: []string{"127.0.0.1/32", "::1/128"}, }) if !isValid && err == nil { st.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) @@ -884,3 +884,182 @@ func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { }) } } + +// TestIsValidChallengeIP tests the excluded CIDR list functionality +func TestIsValidChallengeIP(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + permittedIPRanges []string + excludedIPRanges []string + testIP string + shouldBeDisallowed bool + }{ + { + name: "nil IP should be disallowed", + permittedIPRanges: []string{}, + excludedIPRanges: []string{}, + testIP: "", + shouldBeDisallowed: true, + }, + { + name: "loopback IP should be disallowed by default", + permittedIPRanges: []string{}, + excludedIPRanges: []string{}, + testIP: "127.0.0.1", + shouldBeDisallowed: true, + }, + { + name: "public IP should be allowed by default", + permittedIPRanges: []string{}, + excludedIPRanges: []string{}, + testIP: "8.8.8.8", + shouldBeDisallowed: false, + }, + { + name: "IP in excluded list should be rejected", + permittedIPRanges: []string{}, + excludedIPRanges: []string{"8.8.8.0/24"}, + testIP: "8.8.8.8", + shouldBeDisallowed: true, + }, + { + name: "IP in excluded list (exact match) should be rejected", + permittedIPRanges: []string{}, + excludedIPRanges: []string{"8.8.8.8"}, + testIP: "8.8.8.8", + shouldBeDisallowed: true, + }, + { + name: "excluded list takes precedence over permitted list", + permittedIPRanges: []string{"8.8.8.0/24"}, + excludedIPRanges: []string{"8.8.8.8"}, + testIP: "8.8.8.8", + shouldBeDisallowed: true, + }, + { + name: "IP in permitted list but not excluded should be allowed", + permittedIPRanges: []string{"8.8.8.0/24"}, + excludedIPRanges: []string{"8.8.4.0/24"}, + testIP: "8.8.8.8", + shouldBeDisallowed: false, + }, + { + name: "loopback in permitted list should be allowed", + permittedIPRanges: []string{"127.0.0.1/32"}, + excludedIPRanges: []string{}, + testIP: "127.0.0.1", + shouldBeDisallowed: false, + }, + { + name: "private IP with permitted list should be rejected if not in list", + permittedIPRanges: []string{"8.8.8.0/24"}, + excludedIPRanges: []string{}, + testIP: "192.168.1.1", + shouldBeDisallowed: true, + }, + { + name: "IPv6 loopback should be disallowed by default", + permittedIPRanges: []string{}, + excludedIPRanges: []string{}, + testIP: "::1", + shouldBeDisallowed: true, + }, + { + name: "IPv6 in excluded list should be rejected", + permittedIPRanges: []string{}, + excludedIPRanges: []string{"2001:db8::/32"}, + testIP: "2001:db8::1", + shouldBeDisallowed: true, + }, + { + name: "IPv6 in permitted list should be allowed", + permittedIPRanges: []string{"2001:db8::/32"}, + excludedIPRanges: []string{}, + testIP: "2001:db8::1", + shouldBeDisallowed: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(st *testing.T) { + config := &acmeConfigEntry{ + ChallengePermittedIPRanges: tc.permittedIPRanges, + ChallengeExcludedIPRanges: tc.excludedIPRanges, + } + + var ip net.IP + if tc.testIP != "" { + ip = net.ParseIP(tc.testIP) + if ip == nil { + st.Fatalf("failed to parse test IP: %s", tc.testIP) + } + } + + result := !isValidChallengeIP(config, ip) + if result != tc.shouldBeDisallowed { + st.Fatalf("expected isDisallowed=%v, got %v for IP %s with permitted=%v, excluded=%v", + tc.shouldBeDisallowed, result, tc.testIP, tc.permittedIPRanges, tc.excludedIPRanges) + } + }) + } +} + +// TestAcmeValidateHTTP01ChallengeWithExcludedIPRange tests HTTP-01 validation with excluded CIDRs +func TestAcmeValidateHTTP01ChallengeWithExcludedIPRange(t *testing.T) { + t.Parallel() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("my-token.my-thumbprint")) + })) + defer ts.Close() + + host := ts.URL[7:] // Remove "http://" + + // Test with excluded localhost + config := &acmeConfigEntry{ + ChallengeExcludedIPRanges: []string{"127.0.0.0/8", "::1/128"}, + } + + isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", config) + require.False(t, isValid) + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedIdentifier) +} + +// TestAcmeValidateTLSALPN01ChallengeWithExcludedIPRange tests TLS-ALPN-01 validation with excluded CIDRs +func TestAcmeValidateTLSALPN01ChallengeWithExcludedIPRange(t *testing.T) { + host := "acme-excluded-cidr.example.test" + dnsAddr := startLocalDNSServer(t, map[string]net.IP{ + host: net.IPv4(127, 0, 0, 1), + }) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + go func() { + conn, err := ln.Accept() + if err == nil { + _ = conn.Close() + } + }() + + oldPort := ALPNPort + ALPNPort = strconv.Itoa(ln.Addr().(*net.TCPAddr).Port) + defer func() { + ALPNPort = oldPort + }() + + // Test with excluded 127.0.0.0/8 range + config := &acmeConfigEntry{ + DNSResolver: dnsAddr, + ChallengeExcludedIPRanges: []string{"127.0.0.0/8"}, + } + + isValid, err := ValidateTLSALPN01Challenge(host, "my-token", "my-thumbprint", config) + require.False(t, isValid) + require.Error(t, err) + require.ErrorIs(t, err, ErrRejectedIdentifier) +} diff --git a/builtin/logical/pki/path_config_acme.go b/builtin/logical/pki/path_config_acme.go index 20cbcf7e10..501db2ba68 100644 --- a/builtin/logical/pki/path_config_acme.go +++ b/builtin/logical/pki/path_config_acme.go @@ -29,15 +29,16 @@ const ( ) type acmeConfigEntry struct { - Enabled bool `json:"enabled"` - AllowedIssuers []string `json:"allowed_issuers="` - AllowedRoles []string `json:"allowed_roles"` - AllowRoleExtKeyUsage bool `json:"allow_role_ext_key_usage"` - DefaultDirectoryPolicy string `json:"default_directory_policy"` - DNSResolver string `json:"dns_resolver"` - EabPolicyName EabPolicyName `json:"eab_policy_name"` - MaxTTL time.Duration `json:"max_ttl"` - AllowedCIDRList []string `json:"allowed_cidr_list"` + Enabled bool `json:"enabled"` + AllowedIssuers []string `json:"allowed_issuers="` + AllowedRoles []string `json:"allowed_roles"` + AllowRoleExtKeyUsage bool `json:"allow_role_ext_key_usage"` + DefaultDirectoryPolicy string `json:"default_directory_policy"` + DNSResolver string `json:"dns_resolver"` + EabPolicyName EabPolicyName `json:"eab_policy_name"` + MaxTTL time.Duration `json:"max_ttl"` + ChallengePermittedIPRanges []string `json:"challenge_permitted_ip_ranges"` + ChallengeExcludedIPRanges []string `json:"challenge_excluded_ip_ranges"` } var defaultAcmeConfig = acmeConfigEntry{ @@ -145,6 +146,16 @@ func pathAcmeConfig(b *backend) *framework.Path { Description: `Specify the maximum TTL for ACME certificates. Role TTL values will be limited to this value`, Default: defaultAcmeMaxTTL.Seconds(), }, + "challenge_permitted_ip_ranges": { + Type: framework.TypeCommaStringSlice, + Description: `List of CIDR blocks that are permitted for ACME challenge validation. If set, only IPs within these ranges will be allowed for validation. Can be individual IPs or CIDR notation.`, + Default: []string{}, + }, + "challenge_excluded_ip_ranges": { + Type: framework.TypeCommaStringSlice, + Description: `List of CIDR blocks that are excluded from ACME challenge validation. IPs within these ranges will be rejected for validation. Can be individual IPs or CIDR notation. This list takes precedence over challenge_permitted_ip_ranges.`, + Default: []string{}, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ @@ -196,14 +207,16 @@ func (b *backend) pathAcmeRead(ctx context.Context, req *logical.Request, _ *fra func genResponseFromAcmeConfig(config *acmeConfigEntry, warnings []string) *logical.Response { response := &logical.Response{ Data: map[string]interface{}{ - "allowed_roles": config.AllowedRoles, - "allow_role_ext_key_usage": config.AllowRoleExtKeyUsage, - "allowed_issuers": config.AllowedIssuers, - "default_directory_policy": config.DefaultDirectoryPolicy, - "enabled": config.Enabled, - "dns_resolver": config.DNSResolver, - "eab_policy": config.EabPolicyName, - "max_ttl": config.MaxTTL.Seconds(), + "allowed_roles": config.AllowedRoles, + "allow_role_ext_key_usage": config.AllowRoleExtKeyUsage, + "allowed_issuers": config.AllowedIssuers, + "default_directory_policy": config.DefaultDirectoryPolicy, + "enabled": config.Enabled, + "dns_resolver": config.DNSResolver, + "eab_policy": config.EabPolicyName, + "max_ttl": config.MaxTTL.Seconds(), + "challenge_permitted_ip_ranges": config.ChallengePermittedIPRanges, + "challenge_excluded_ip_ranges": config.ChallengeExcludedIPRanges, }, Warnings: warnings, } @@ -280,6 +293,34 @@ func (b *backend) pathAcmeWrite(ctx context.Context, req *logical.Request, d *fr config.MaxTTL = maxTTL } + if permittedIPRangesRaw, ok := d.GetOk("challenge_permitted_ip_ranges"); ok { + permittedIPRanges := permittedIPRangesRaw.([]string) + // Validate each CIDR entry + for _, cidr := range permittedIPRanges { + if _, _, err := net.ParseCIDR(cidr); err != nil { + // Try parsing as IP address + if net.ParseIP(cidr) == nil { + return nil, fmt.Errorf("invalid CIDR or IP address in challenge_permitted_ip_ranges: %s", cidr) + } + } + } + config.ChallengePermittedIPRanges = permittedIPRanges + } + + if excludedIPRangesRaw, ok := d.GetOk("challenge_excluded_ip_ranges"); ok { + excludedIPRanges := excludedIPRangesRaw.([]string) + // Validate each CIDR entry + for _, cidr := range excludedIPRanges { + if _, _, err := net.ParseCIDR(cidr); err != nil { + // Try parsing as IP address + if net.ParseIP(cidr) == nil { + return nil, fmt.Errorf("invalid CIDR or IP address in challenge_excluded_ip_ranges: %s", cidr) + } + } + } + config.ChallengeExcludedIPRanges = excludedIPRanges + } + // Validate Default Directory Behavior: defaultDirectoryPolicyType, extraInfo, err := getDefaultDirectoryPolicyType(config.DefaultDirectoryPolicy) if err != nil { diff --git a/builtin/logical/pki/path_config_acme_test.go b/builtin/logical/pki/path_config_acme_test.go index bcdeb3d425..35218201a8 100644 --- a/builtin/logical/pki/path_config_acme_test.go +++ b/builtin/logical/pki/path_config_acme_test.go @@ -153,3 +153,136 @@ func TestAcmeExternalPolicyOss(t *testing.T) { }) } } + +// TestAcmeIPRangeConfiguration tests setting and modifying challenge IP range configuration +func TestAcmeIPRangeConfiguration(t *testing.T) { + t.Parallel() + + cluster, client, _ := setupAcmeBackend(t) + defer cluster.Cleanup() + + testCtx := context.Background() + + _, err := client.Logical().WriteWithContext(testCtx, "pki/config/acme", map[string]interface{}{ + "enabled": true, + }) + require.NoError(t, err) + + cases := []struct { + name string + writeConfig map[string]interface{} + expectErrorContains string + expectedPermittedRanges []string + expectedExcludedRanges []string + }{ + { + name: "set_permitted_ip_ranges", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{"192.168.1.0/24", "10.0.0.0/8"}, + }, + expectedPermittedRanges: []string{"192.168.1.0/24", "10.0.0.0/8"}, + expectedExcludedRanges: []string{}, + }, + { + name: "set_excluded_ip_ranges", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{}, + "challenge_excluded_ip_ranges": []string{"127.0.0.0/8", "::1/128"}, + }, + expectedPermittedRanges: []string{}, + expectedExcludedRanges: []string{"127.0.0.0/8", "::1/128"}, + }, + { + name: "set_both_ranges", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{"192.168.0.0/16"}, + "challenge_excluded_ip_ranges": []string{"192.168.1.0/24"}, + }, + expectedPermittedRanges: []string{"192.168.0.0/16"}, + expectedExcludedRanges: []string{"192.168.1.0/24"}, + }, + { + name: "set_individual_ips", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{"192.168.1.100"}, + "challenge_excluded_ip_ranges": []string{"10.0.0.1"}, + }, + expectedPermittedRanges: []string{"192.168.1.100"}, + expectedExcludedRanges: []string{"10.0.0.1"}, + }, + { + name: "invalid_cidr_notation", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{"invalid-cidr"}, + }, + expectErrorContains: "invalid CIDR or IP address", + }, + { + name: "invalid_excluded_ip", + writeConfig: map[string]interface{}{ + "challenge_excluded_ip_ranges": []string{"999.999.999.999"}, + }, + expectErrorContains: "invalid CIDR or IP address", + }, + { + name: "modify_existing_config", + writeConfig: map[string]interface{}{ + "challenge_excluded_ip_ranges": []string{"10.0.0.0/8"}, + }, + expectedPermittedRanges: []string{"192.168.1.100"}, + expectedExcludedRanges: []string{"10.0.0.0/8"}, + }, + { + name: "clear_ranges", + writeConfig: map[string]interface{}{ + "challenge_permitted_ip_ranges": []string{}, + "challenge_excluded_ip_ranges": []string{}, + }, + expectedPermittedRanges: []string{}, + expectedExcludedRanges: []string{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(st *testing.T) { + deadline := time.Now().Add(1 * time.Minute) + subTestCtx, _ := context.WithDeadline(testCtx, deadline) + + _, err := client.Logical().WriteWithContext(subTestCtx, "pki/config/acme", tc.writeConfig) + + if tc.expectErrorContains != "" { + require.Contains(st, err.Error(), tc.expectErrorContains) + return + } + + require.NoError(st, err) + + // Read back and verify + resp, err := client.Logical().ReadWithContext(subTestCtx, "pki/config/acme") + require.NoError(st, err) + require.NotNil(st, resp) + + var permittedRanges []interface{} + if resp.Data["challenge_permitted_ip_ranges"] != nil { + permittedRanges = resp.Data["challenge_permitted_ip_ranges"].([]interface{}) + } + + var excludedRanges []interface{} + if resp.Data["challenge_excluded_ip_ranges"] != nil { + excludedRanges = resp.Data["challenge_excluded_ip_ranges"].([]interface{}) + } + + // Verify permitted ranges + require.Len(st, permittedRanges, len(tc.expectedPermittedRanges)) + for _, expected := range tc.expectedPermittedRanges { + require.Contains(st, permittedRanges, expected) + } + + // Verify excluded ranges + require.Len(st, excludedRanges, len(tc.expectedExcludedRanges)) + for _, expected := range tc.expectedExcludedRanges { + require.Contains(st, excludedRanges, expected) + } + }) + } +} diff --git a/changelog/_13010.txt b/changelog/_13010.txt new file mode 100644 index 0000000000..e12c152ac6 --- /dev/null +++ b/changelog/_13010.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/pki: Add ACME configuration fields challenge_permitted_ip_ranges and challenge_excluded_ip_ranges configuration to control which IP addresses are allowed or disallowed for challenge validation. +``` From 3b25846e7580ca67e8bebaf776aa1828e89bb6ab Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 12:10:29 -0400 Subject: [PATCH 109/468] Update `ldaputil` to allow plugins to perform their own schema validation (#12897) (#12995) Co-authored-by: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com> --- builtin/credential/ldap/path_config.go | 8 ++++++++ sdk/helper/ldaputil/config.go | 15 ++++++++------- sdk/helper/ldaputil/config_test.go | 9 +++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go index 975fc5ecf5..ea91903617 100644 --- a/builtin/credential/ldap/path_config.go +++ b/builtin/credential/ldap/path_config.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/sdk/helper/automatedrotationutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/ldaputil" + "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/helper/tokenutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/rotation" @@ -96,6 +97,13 @@ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ldapConfig return nil, err } + // perform schema validation, as NewConfigEntry does not check supported schemas + if result.Schema != "" { + if !strutil.StrListContains(ldaputil.SupportedSchemas(), result.Schema) { + return nil, fmt.Errorf("unsupported schema type %q: must be one of %v", result.Schema, ldaputil.SupportedSchemas()) + } + } + // No user overrides, return default configuration result.CaseSensitiveNames = new(bool) *result.CaseSensitiveNames = false diff --git a/sdk/helper/ldaputil/config.go b/sdk/helper/ldaputil/config.go index a4ac61647b..5e438bc890 100644 --- a/sdk/helper/ldaputil/config.go +++ b/sdk/helper/ldaputil/config.go @@ -489,10 +489,6 @@ func NewConfigEntry(existing *ConfigEntry, d *framework.FieldData) (*ConfigEntry if _, ok := d.Raw["schema"]; ok || !hadExisting { rawSchema := d.Get("schema").(string) cfg.Schema = NormalizedSchema(rawSchema) - // Validate the normalized schema, not the raw input string, to allow for case-insensitive input while still enforcing valid schema types. - if !strutil.StrListContains(SupportedSchemas(), cfg.Schema) { - return nil, fmt.Errorf("unsupported schema type %q: must be one of %v", rawSchema, SupportedSchemas()) - } } return cfg, nil @@ -591,7 +587,7 @@ func validateCertificate(pemBlock []byte) error { return nil } -func (c *ConfigEntry) Validate() error { +func (c *ConfigEntry) Validate(schemas ...string) error { if len(c.Url) == 0 { return errors.New("at least one url must be provided") } @@ -622,8 +618,13 @@ func (c *ConfigEntry) Validate() error { } } normalizedSchema := NormalizedSchema(c.Schema) - if !strutil.StrListContains(SupportedSchemas(), normalizedSchema) { - return fmt.Errorf("unsupported schema type %q: must be one of %v", c.Schema, SupportedSchemas()) + // use the default schema list if none provided + supportedSchemas := SupportedSchemas() + if len(schemas) > 0 { + supportedSchemas = schemas + } + if !strutil.StrListContains(supportedSchemas, normalizedSchema) { + return fmt.Errorf("unsupported schema type %q: must be one of %v", c.Schema, supportedSchemas) } return nil } diff --git a/sdk/helper/ldaputil/config_test.go b/sdk/helper/ldaputil/config_test.go index ba6b8fff32..a659f53916 100644 --- a/sdk/helper/ldaputil/config_test.go +++ b/sdk/helper/ldaputil/config_test.go @@ -342,12 +342,13 @@ func TestSchemaValidation(t *testing.T) { Schema: schema, } - // NewConfigEntry should fail because schema validation happens during creation - _, err := NewConfigEntry(nil, data) - if err == nil { - t.Error("expected error from NewConfigEntry for unsupported schema, got nil") + config, err := NewConfigEntry(nil, data) + if err != nil { + t.Errorf("expected config entry to be created: %s", err) } + // should fail schema validation even after normalization + err = config.Validate() // Verify the error message is helpful if err != nil { if !strings.Contains(err.Error(), "unsupported schema") { From cdc616e098c8c18e7616aa16bf4e64e262e72949 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 12:36:27 -0400 Subject: [PATCH 110/468] Permission Ceiling for Agents (#12932) (#13041) --- vault/request_handling.go | 78 ++++++++++++++++++++++++++-- vault/request_handling_ce.go | 4 ++ vault/request_handling_test.go | 93 ++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 3 deletions(-) diff --git a/vault/request_handling.go b/vault/request_handling.go index f1adf8987a..6736b24f52 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -350,11 +350,17 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req if secondEntity != nil { c.logger.Debug("building separate ACL for second entity", "entity_id", secondEntity.ID) secondEntityPolicyNames = make(map[string][]string) - _, secondEntityIdentityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, tokenNS, secondEntity.ID, false) + secondEntityIdentityPolicies, err := c.fetchCeilingPolicies(ctx, secondEntity) + if err != nil { + return nil, nil, nil, nil, err + } + allowOnly, err := c.allPoliciesAllowOnly(ctx, secondEntityIdentityPolicies) if err != nil { - c.logger.Error("failed to fetch second entity policies", "error", err) return nil, nil, nil, nil, ErrInternalError } + if !allowOnly { + return nil, nil, nil, nil, logical.ErrPermissionDenied + } // Store second entity policies separately - do NOT merge with primary entity's policies for nsID, nsPolicies := range secondEntityIdentityPolicies { secondEntityPolicyNames[nsID] = policyutil.SanitizePolicies(nsPolicies, false) @@ -401,7 +407,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, ErrInternalError } - if secondEntity != nil && len(secondEntityPolicyNames) > 0 { + if secondEntity != nil { newAcl, err := c.performSecondaryEntityTokenChecks(tokenCtx, acl, secondEntity, secondEntityPolicyNames) if err != nil { return nil, nil, nil, nil, err @@ -2980,3 +2986,69 @@ func (c *Core) checkSSCTokenInternal(ctx context.Context, token string, isPerfSt // status code. return "", logical.ErrMissingRequiredState } + +// allPoliciesAllowOnly is a helper function that checks if all policies in +// a given set have only "allow" capabilities, and not "deny" or "sudo". +// +// Example of allow-only policy: +// +// path "secret/data/team/public/*" { +// capabilities = ["read"] +// } +// +// Example of a policy that is not allow-only: +// +// path "secret/data/team/*" { +// capabilities = ["read"] +// } +// +// path "secret/data/team/private/*" { +// capabilities = ["deny"] +// } +func (c *Core) allPoliciesAllowOnly(ctx context.Context, policyNamesByNamespace map[string][]string) (bool, error) { + for nsID, policyNames := range policyNamesByNamespace { + policyNS, err := NamespaceByID(ctx, nsID, c) + if err != nil { + return false, err + } + if policyNS == nil { + return false, namespace.ErrNoNamespace + } + + policyCtx := namespace.ContextWithNamespace(ctx, policyNS) + for _, policyName := range policyNames { + policy, err := c.policyStore.GetPolicy(policyCtx, policyName, PolicyTypeACL) + if err != nil { + return false, err + } + if policy == nil { + return false, fmt.Errorf("policy %q not found in namespace %q", policyName, policyNS.Path) + } + if !policyIsAllowOnly(policy) { + return false, nil + } + } + } + + return true, nil +} + +// policyIsAllowOnly is a helper function that checks if a policy has only "allow" capabilities, and not "deny" or "sudo". +func policyIsAllowOnly(policy *Policy) bool { + if policy == nil || policy.Name == "root" { + return false + } + + for _, pathRules := range policy.Paths { + if pathRules == nil || pathRules.Permissions == nil { + continue + } + + capabilities := pathRules.Permissions.CapabilitiesBitmap + if capabilities&DenyCapabilityInt != 0 || capabilities&SudoCapabilityInt != 0 { + return false + } + } + + return true +} diff --git a/vault/request_handling_ce.go b/vault/request_handling_ce.go index 64e0953aea..7cb5b254ab 100644 --- a/vault/request_handling_ce.go +++ b/vault/request_handling_ce.go @@ -40,3 +40,7 @@ func getEnterpriseTokenAuthorizationDetails(_ map[string]interface{}) []logical. func (c *Core) performSecondaryEntityTokenChecks(_ context.Context, _ *ACL, _ *identity.Entity, _ map[string][]string) (*ACL, error) { return nil, errors.New("not implemented") } + +func (c *Core) fetchCeilingPolicies(ctx context.Context, entity *identity.Entity) (map[string][]string, error) { + return nil, errors.New("not implemented") +} diff --git a/vault/request_handling_test.go b/vault/request_handling_test.go index e563121ed3..839e91a152 100644 --- a/vault/request_handling_test.go +++ b/vault/request_handling_test.go @@ -643,6 +643,99 @@ func TestRequestHandling_fetchACLTokenEntryAndEntity_NilRequest(t *testing.T) { require.Equal(t, ErrInternalError, err) } +// Test_allPoliciesAllowOnly tests a helper function that checks if all policies in +// a given set have only "allow" capabilities, and not "deny" or "sudo" +func Test_allPoliciesAllowOnly(t *testing.T) { + t.Parallel() + + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(context.Background()) + + allowPolicy, err := ParseACLPolicy(namespace.RootNamespace, ` +path "secret/data/*" { + capabilities = ["read", "list"] +} +`) + require.NoError(t, err) + allowPolicy.Name = "allow-only" + require.NoError(t, c.policyStore.SetPolicy(ctx, allowPolicy)) + + denyPolicy, err := ParseACLPolicy(namespace.RootNamespace, ` +path "secret/data/*" { + capabilities = ["deny"] +} +`) + require.NoError(t, err) + denyPolicy.Name = "deny-policy" + require.NoError(t, c.policyStore.SetPolicy(ctx, denyPolicy)) + + sudoPolicy, err := ParseACLPolicy(namespace.RootNamespace, ` +path "secret/data/*" { + capabilities = ["read", "sudo"] +} +`) + require.NoError(t, err) + sudoPolicy.Name = "sudo-policy" + require.NoError(t, c.policyStore.SetPolicy(ctx, sudoPolicy)) + + tests := map[string]struct { + policyNamesByNamespace map[string][]string + expected bool + wantErr string + }{ + "all allow only": { + policyNamesByNamespace: map[string][]string{ + namespace.RootNamespaceID: {"allow-only"}, + }, + expected: true, + }, + "deny policy": { + policyNamesByNamespace: map[string][]string{ + namespace.RootNamespaceID: {"deny-policy"}, + }, + expected: false, + }, + "sudo policy": { + policyNamesByNamespace: map[string][]string{ + namespace.RootNamespaceID: {"sudo-policy"}, + }, + expected: false, + }, + "root policy": { + policyNamesByNamespace: map[string][]string{ + namespace.RootNamespaceID: {"root"}, + }, + expected: false, + }, + "missing policy": { + policyNamesByNamespace: map[string][]string{ + namespace.RootNamespaceID: {"missing-policy"}, + }, + expected: false, + wantErr: "policy \"missing-policy\" not found", + }, + "missing namespace": { + policyNamesByNamespace: map[string][]string{ + "missing-namespace": {"allow-only"}, + }, + expected: false, + wantErr: namespace.ErrNoNamespace.Error(), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual, err := c.allPoliciesAllowOnly(ctx, tc.policyNamesByNamespace) + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.expected, actual) + }) + } +} + // TestAuth_AuthorizationDetails_CopiedFromRequest verifies that logical.Auth.AuthorizationDetails // matches the authorization details already carried on the request. func TestAuth_AuthorizationDetails_CopiedFromRequest(t *testing.T) { From 248b9c5a5059ff8aeafa59c212c7bacc210a6da8 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 12:54:03 -0400 Subject: [PATCH 111/468] [UI] Kubernetes Secrets Engine Playwright Test (#12972) (#12982) * adds playwright test for kubernetes secrets engine * updates kubernetes create and edit page tests Co-authored-by: Jordan Reimer Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/forms/secrets/kubernetes/role.ts | 3 +- .../superuser/kubernetes-secrets.spec.ts | 209 ++++++++++++++++++ .../page/role/create-and-edit-test.js | 49 ++-- 3 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 ui/e2e/tests/superuser/kubernetes-secrets.spec.ts diff --git a/ui/app/forms/secrets/kubernetes/role.ts b/ui/app/forms/secrets/kubernetes/role.ts index 501b236e74..24f3169798 100644 --- a/ui/app/forms/secrets/kubernetes/role.ts +++ b/ui/app/forms/secrets/kubernetes/role.ts @@ -44,7 +44,8 @@ export default class KubernetesRoleForm extends Form { full: ['service_account_name', 'kubernetes_role_name'], }[pref]; props.forEach((prop) => { - delete this.data[prop as keyof typeof this.data]; + // if the prop is deleted it will not be serialized in the payload and the previous value will remain unchanged on the backend + (this.data[prop as keyof KubernetesRoleFormData] as string) = ''; }); } this._generationPreference = pref; diff --git a/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts b/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts new file mode 100644 index 0000000000..3038d1fa01 --- /dev/null +++ b/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts @@ -0,0 +1,209 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; +import { BasePage } from '../../pages/base'; + +test('kubernetes secrets workflow', async ({ page }) => { + const basePage = new BasePage(page); + + await test.step('enable kubernetes secrets engine', async () => { + await page.goto('dashboard'); + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await page.getByRole('link', { name: 'Enable new engine' }).click(); + await page.getByLabel('Kubernetes - enabled engine').click(); + await page.getByRole('button', { name: 'Enable engine' }).click(); + await expect(page.locator('section')).toContainText('Kubernetes not configured'); + await basePage.dismissFlashMessages(); + }); + + await test.step('configure kubernetes secrets engine', async () => { + await page.getByRole('link', { name: 'Configure Kubernetes' }).click(); + await page.getByRole('button', { name: 'Get config values' }).click(); + await expect(page.locator('section')).toContainText( + 'Vault could not infer a configuration from your environment variables. Check your configuration file to edit or delete them, or configure manually.' + ); + await page.getByRole('radio', { name: 'Manual configuration Generate' }).check(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.locator('#error-kubernetes_host')).toContainText('Kubernetes host is required'); + await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://192.168.99.100:8443'); + await page.getByRole('textbox', { name: 'Service account JWT' }).fill('test-jwt'); + await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----CERTIFICATE-----'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Kubernetes host https://192.168.99.100:8443', + }) + ).toBeVisible(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Certificate PEM Format -----CERTIFICATE-----', + }) + ).toBeVisible(); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + await expect(page.locator('section')).toContainText( + 'Roles Create Role The number of Vault roles being used to generate Kubernetes credentials. None' + ); + await expect(page.locator('section')).toContainText( + 'Generate credentials Quickly generate credentials by typing the role name. Type to find a role... Generate' + ); + await basePage.dismissFlashMessages(); + }); + + await test.step('edit kubernetes configuration', async () => { + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Configure' }).click(); + await page.getByRole('link', { name: 'Edit configuration' }).click(); + await expect(page.getByRole('textbox', { name: 'Service account JWT' })).toBeEmpty(); + await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://127.0.0.1:8443'); + await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----NEW CERT-----'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Kubernetes host https://127.0.0.1:8443', + }) + ).toBeVisible(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Certificate PEM Format -----NEW CERT-----', + }) + ).toBeVisible(); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + await basePage.dismissFlashMessages(); + }); + + await test.step('create kubernetes role', async () => { + await page.getByRole('link', { name: 'Roles' }).click(); + await expect(page.locator('section')).toContainText( + 'No roles yet When created, roles will be listed here. Create a role to start generating service account tokens.' + ); + await page.getByRole('link', { name: 'Create role' }).click(); + await expect(page.locator('section')).toContainText( + 'Choose an option above To configure a Vault role, choose what should be generated in Kubernetes by Vault.' + ); + await page.getByRole('radio', { name: 'Generate token only using' }).check(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.locator('#error-name')).toContainText('Name is required'); + await page.getByRole('textbox', { name: 'Role name' }).fill('test-role'); + await page.getByRole('textbox', { name: 'Service account name' }).fill('foo'); + await page.getByRole('textbox', { name: 'Allowed Kubernetes namespaces' }).fill('*'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Role name test-role', + }) + ).toBeVisible(); + + await page.getByRole('link', { name: 'Roles' }).click(); + await expect(page.getByRole('link', { name: 'test-role', exact: true })).toBeVisible(); + await basePage.dismissFlashMessages(); + }); + + await test.step('edit kubernetes role', async () => { + await page.getByRole('link', { name: 'test-role', exact: true }).click(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Service account name foo', + }) + ).toBeVisible(); + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Edit role' }).click(); + await page.getByRole('radio', { name: 'Generate token, service' }).check(); + await page.getByRole('textbox', { name: 'Kubernetes role name' }).click(); + await page.getByRole('textbox', { name: 'Kubernetes role name' }).fill('admin'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.locator('.info-table-row').filter({ + hasText: 'Kubernetes role name admin', + }) + ).toBeVisible(); + + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Edit role' }).click(); + await page.getByRole('radio', { name: 'Generate entire Kubernetes' }).check(); + await page.getByLabel('Role rules template').selectOption('5'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.locator('section')).toContainText( + 'Generated role rules Role rules rules: - apiGroups: [""] resources: ["secrets", "services"] verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"]' + ); + await basePage.dismissFlashMessages(); + }); + + await test.step('generate credentials from kubernetes role', async () => { + // mock since we aren't connected to a kubernetes cluster + await page.route('**/v1/kubernetes/creds/test-role', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + lease_duration: 3600, + lease_id: 'kubernetes/creds/test-role/aWczfcfJ7NKUdiirJrPXIs38', + data: { + service_account_name: 'default', + service_account_namespace: 'default', + service_account_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imlr', + }, + }), + }); + }); + await page.getByRole('button', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Generate credentials' }).click(); + await page.getByRole('textbox', { name: 'Kubernetes namespace' }).fill('user'); + await page.getByRole('button', { name: 'Generate credentials' }).click(); + await expect(page.getByLabel('Warning')).toContainText( + "Warning You won't be able to access these credentials later, so please copy them now." + ); + await page.getByRole('button', { name: 'Done' }).click(); + await page.getByRole('link', { name: 'Roles' }).click(); + await basePage.dismissFlashMessages(); + }); + + await test.step('filter kubernetes roles', async () => { + await page.getByRole('link', { name: 'Create role' }).click(); + await page.getByRole('radio', { name: 'Generate token only using' }).check(); + await page.getByRole('textbox', { name: 'Role name' }).fill('foo'); + await page.getByRole('textbox', { name: 'Allowed Kubernetes namespaces' }).fill('*'); + await page.getByRole('textbox', { name: 'Service account name' }).fill('bar'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('link', { name: 'Roles' }).click(); + await page.getByRole('textbox', { name: 'Search by path' }).fill('test'); + await page.getByRole('button', { name: 'Search' }).click(); + await expect(page.getByRole('link', { name: 'test-role', exact: true })).toBeVisible(); + await expect(page.getByRole('link', { name: 'foo', exact: true })).not.toBeVisible(); + await page.getByRole('textbox', { name: 'Search by path' }).fill('foo'); + await page.getByRole('button', { name: 'Search' }).click(); + await expect(page.getByRole('link', { name: 'foo', exact: true })).toBeVisible(); + await expect(page.getByRole('link', { name: 'test-role', exact: true })).not.toBeVisible(); + }); + + await test.step('delete kubernetes role', async () => { + await page.getByRole('button', { name: 'More options' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('section')).toContainText('There are no roles matching "foo"'); + await page.getByRole('textbox', { name: 'Search by path' }).click(); + await page.getByRole('textbox', { name: 'Search by path' }).fill(''); + await page.getByRole('button', { name: 'Search' }).click(); + await expect(page.getByRole('link', { name: 'test-role', exact: true })).toBeVisible(); + await basePage.dismissFlashMessages(); + }); + + await test.step('kubernetes overview', async () => { + await page.getByRole('link', { name: 'Overview' }).click(); + await expect(page.locator('section')).toContainText( + 'Roles View Roles The number of Vault roles being used to generate Kubernetes credentials. 1' + ); + await page.getByRole('link', { name: 'View Roles' }).click(); + await expect(page.getByRole('link', { name: 'test-role', exact: true })).toBeVisible(); + await page.getByRole('link', { name: 'Overview' }).click(); + await page.getByText('Type to find a role...').click(); + await page.getByRole('option', { name: 'test-role' }).click(); + await page.getByRole('button', { name: 'Generate' }).click(); + await expect(page.getByRole('paragraph')).toContainText( + 'This will generate credentials using the role test-role.' + ); + }); +}); diff --git a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js index 3e18e026e7..e9ae3acaf5 100644 --- a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js +++ b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js @@ -34,6 +34,14 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct this.writeStub = sinon.stub(this.owner.lookup('service:api').secrets, 'kubernetesWriteRole').resolves(); + this.basicPayload = { + kubernetes_role_type: '', + kubernetes_role_name: '', + generated_role_rules: '', + name_template: '', + service_account_name: 'default', + }; + this.setupEdit = (trait) => { const role = this.server.create('kubernetes-role', trait); this.form = new KubernetesRoleForm(role); @@ -47,6 +55,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { label: 'Roles', route: 'roles' }, { label: 'Create' }, ]; + setRunOptions({ rules: { // TODO: fix RadioCard component (replace with HDS) @@ -109,7 +118,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click('[data-test-radio-card="expanded"]'); assert.strictEqual( this.form.data.service_account_name, - undefined, + '', 'Service account name cleared when switching from basic to expanded' ); @@ -117,7 +126,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click('[data-test-radio-card="full"]'); assert.strictEqual( this.form.data.kubernetes_role_name, - undefined, + '', 'Kubernetes role name cleared when switching from expanded to full' ); @@ -128,28 +137,44 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click('[data-test-radio-card="expanded"]'); assert.strictEqual( this.form.data.generated_role_rules, - undefined, + '', 'Role rules cleared when switching from full to expanded' ); await click('[data-test-radio-card="basic"]'); assert.strictEqual( this.form.data.kubernetes_role_type, - undefined, + '', 'Kubernetes role type cleared when switching from expanded to basic' ); assert.strictEqual( this.form.data.kubernetes_role_name, - undefined, + '', 'Kubernetes role name cleared when switching from expanded to basic' ); assert.strictEqual( this.form.data.name_template, - undefined, + '', 'Name template cleared when switching from expanded to basic' ); }); + test('it should send cleared fields in payload when editing role and change generation preference', async function (assert) { + this.role = this.setupEdit(); + await this.renderComponent(); + + await click('[data-test-radio-card="expanded"]'); + await fillIn(GENERAL.inputByAttr('kubernetes_role_name'), 'test'); + await click(GENERAL.submitButton); + + const payload = this.writeStub.lastCall.args[2]; + assert.propContains( + payload, + { service_account_name: '', kubernetes_role_name: 'test' }, + 'Payload contains cleared and updated fields when switching generation preference on edit' + ); + }); + test('it should update code editor when template selection changes', async function (assert) { await this.renderComponent(); @@ -204,7 +229,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click(GENERAL.submitButton); assert.true( - this.writeStub.calledWith(name, this.backend, { ...this.payload, service_account_name: 'default' }), + this.writeStub.calledWith(name, this.backend, this.basicPayload), 'Write role request made with correct params' ); assert.true( @@ -314,8 +339,9 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click(GENERAL.submitButton); const { rules } = getRules().find((r) => r.id === '5'); + const payload = { kubernetes_role_name: '', service_account_name: '', generated_role_rules: rules }; assert.true( - this.writeStub.calledWith('role-1', this.backend, { generated_role_rules: rules }), + this.writeStub.calledWith('role-1', this.backend, payload), 'Generated roles rules are passed in save request' ); }); @@ -323,11 +349,6 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct test('it should unset selectedTemplateId when switching from full generation preference', async function (assert) { assert.expect(1); - this.server.post('/kubernetes-test/roles/role-1', (schema, req) => { - const payload = JSON.parse(req.requestBody); - assert.strictEqual(payload.generated_role_rules, null, 'Generated roles rules are not set'); - }); - await this.renderComponent(); await click('[data-test-radio-card="full"]'); await fillIn(GENERAL.inputByAttr('name'), 'role-1'); @@ -336,7 +357,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await fillIn(GENERAL.inputByAttr('service_account_name'), 'default'); await click(GENERAL.submitButton); assert.true( - this.writeStub.calledWith('role-1', this.backend, { service_account_name: 'default' }), + this.writeStub.calledWith('role-1', this.backend, this.basicPayload), 'Save request made successfully without generated role rules' ); }); From 523654c7c877911eb17d30e1da865cca6ac6f863 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 13:21:03 -0400 Subject: [PATCH 112/468] [UI] Update kmip and keymgmt tests to use enableEngine() helper (#12971) (#12984) Co-authored-by: Shannon Roberts (Beagin) --- ui/e2e/pages/base.ts | 13 +++++++++- .../keymgmt-mount-external.ent.spec.ts | 16 +++++++------ ui/e2e/tests/superuser/keymgmt.spec.ts | 24 ++++++++++--------- ui/e2e/tests/superuser/kmip.spec.ts | 20 ++++++---------- 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/ui/e2e/pages/base.ts b/ui/e2e/pages/base.ts index 01813594b2..aa0d3d0492 100644 --- a/ui/e2e/pages/base.ts +++ b/ui/e2e/pages/base.ts @@ -37,6 +37,9 @@ export class BasePage { options?: { defaultLeaseTtl?: { unit: number; option: string }; maxLeaseTtl?: { unit: number; option: string }; + external?: boolean; + pluginVersion?: string; + skipEnable?: boolean; } ) { await this.page.goto('dashboard'); @@ -46,6 +49,12 @@ export class BasePage { await this.page.getByRole('link', { name: 'Enable new engine' }).click(); await this.page.getByRole('heading', { name: engineType }).click(); + if (options?.external) { + // Prerequisite: mock plugin catalog endpoint in the test so the External plugin option is available. + await this.page.locator('label:nth-child(2) > .hds-form-radio-card__control-wrapper').click(); + await this.page.getByLabel('Plugin version Required').selectOption(options.pluginVersion); + } + if (options?.defaultLeaseTtl) { await this.page.locator('label').filter({ hasText: 'Default Lease TTL Vault will' }).click(); await this.page @@ -71,6 +80,8 @@ export class BasePage { await this.page.getByRole('textbox', { name: 'Path' }).fill(path); // Enable the engine - await this.page.getByRole('button', { name: 'Enable engine' }).click(); + if (!options?.skipEnable) { + await this.page.getByRole('button', { name: 'Enable engine' }).click(); + } } } diff --git a/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts b/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts index d7245f0898..b949fc3e4b 100644 --- a/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts +++ b/ui/e2e/tests/superuser/keymgmt-mount-external.ent.spec.ts @@ -4,6 +4,7 @@ */ import { expect, test } from '@playwright/test'; +import { BasePage } from '../../pages/base'; const PINNED_PLUGIN_DATA = { data: { @@ -60,6 +61,8 @@ const PLUGIN_CATALOG_DATA = { }; test('mount external keymgmt workflow', async ({ page }) => { + const basePage = new BasePage(page); + await test.step('mock the keymgmt pinned version response', async () => { await page.route('**v1/sys/plugins/pins/secret/vault-plugin-secrets-keymgmt', async (route) => { if (route.request().method() === 'GET') { @@ -91,21 +94,20 @@ test('mount external keymgmt workflow', async ({ page }) => { await page.goto('dashboard'); await test.step('navigate to enable Key Management engine', async () => { - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.getByLabel('Key Management - enabled').click(); - await page.getByRole('textbox', { name: 'Path' }).fill('keymgmt-external'); + await basePage.enableEngine('Key Management', 'keymgmt-external', { + external: true, + pluginVersion: 'v0.17.0+ent', + skipEnable: true, + }); }); await test.step('verify builtin and external plugin type options are visible', async () => { await expect(page.getByText('Built-in plugin Preregistered')).toBeVisible(); await expect(page.getByText('External plugin External')).toBeVisible(); - await expect(page.getByText('Plugin version Required')).not.toBeVisible(); + await expect(page.getByText('Plugin version Required')).toBeVisible(); }); await test.step('selecting external plugin type shows plugin version dropdown', async () => { - await page.locator('label:nth-child(2) > .hds-form-radio-card__control-wrapper').click(); - await expect(page.getByText('Plugin version Required')).toBeVisible(); await expect(page.getByLabel('Plugin version Required')).toContainText( 'v0.17.0+ent (pinned) v0.16.0+ent v0.18.0+ent' ); diff --git a/ui/e2e/tests/superuser/keymgmt.spec.ts b/ui/e2e/tests/superuser/keymgmt.spec.ts index 4b8371f1bd..866c08f377 100644 --- a/ui/e2e/tests/superuser/keymgmt.spec.ts +++ b/ui/e2e/tests/superuser/keymgmt.spec.ts @@ -4,10 +4,13 @@ */ import { expect, test } from '@playwright/test'; +import { BasePage } from '../../pages/base'; test('keymgmt workflow', async ({ page }) => { + const basePage = new BasePage(page); + await test.step('mock the distribution response', async () => { - await page.route('**/v1/keymgmt-builtin/kms/test-provider/key/test-key', async (route) => { + await page.route('**/v1/keymgmt-test/kms/test-provider/key/test-key', async (route) => { if (route.request().method() === 'PUT') { await route.fulfill({ status: 200, @@ -19,21 +22,20 @@ test('keymgmt workflow', async ({ page }) => { }); }); - await page.goto('dashboard'); + await test.step('enable Key Management secrets engine mount', async () => { + await basePage.enableEngine('Key Management', 'keymgmt-test', { skipEnable: true }); + + await expect(page.getByText('Built-in plugin Preregistered')).toBeVisible(); + await expect(page.getByText('External plugin External')).toBeVisible(); + await expect(page.getByText('Plugin version Required')).not.toBeVisible(); - await test.step('enable Key Management secrets engine', async () => { - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.getByLabel('Key Management - enabled').click(); - await page.getByRole('textbox', { name: 'Path' }).fill('keymgmt-builtin'); - await page.getByRole('button', { name: 'Method Options' }).click(); - await page.getByRole('textbox', { name: 'Description' }).fill('This is a keymgmt mount.'); - await page.getByRole('checkbox', { name: 'Local' }).check(); await page.getByRole('button', { name: 'Enable engine' }).click(); + }); + await test.step('Key Management secrets engine mount saved successfully', async () => { await expect(page.getByText('Success', { exact: true })).toBeVisible(); await expect( - page.getByText('Successfully mounted the keymgmt secrets engine at keymgmt-builtin') + page.getByText('Successfully mounted the keymgmt secrets engine at keymgmt-test') ).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); }); diff --git a/ui/e2e/tests/superuser/kmip.spec.ts b/ui/e2e/tests/superuser/kmip.spec.ts index 488993dcf1..d761da07d1 100644 --- a/ui/e2e/tests/superuser/kmip.spec.ts +++ b/ui/e2e/tests/superuser/kmip.spec.ts @@ -4,24 +4,18 @@ */ import { expect, test } from '@playwright/test'; +import { BasePage } from '../../pages/base'; test('kmip workflow', async ({ page }) => { - await page.goto('dashboard'); + const basePage = new BasePage(page); await test.step('enable KMIP secrets engine', async () => { - await page.getByRole('link', { name: 'Secrets', exact: true }).click(); - await page.getByRole('link', { name: 'Enable new engine' }).click(); - await page.getByLabel('KMIP - enabled').click(); - await page.getByRole('textbox', { name: 'Path' }).fill('kmip-builtin'); - await page.getByRole('button', { name: 'Method Options' }).click(); - await page.getByRole('textbox', { name: 'Description' }).fill('This is a kmip mount.'); - await page.getByRole('checkbox', { name: 'Local' }).check(); - await page.getByRole('button', { name: 'Enable engine' }).click(); + await basePage.enableEngine('KMIP', 'kmip-test'); + }); + await test.step('KMIP secrets engine mount saved successfully', async () => { await expect(page.getByText('Success', { exact: true })).toBeVisible(); - await expect( - page.getByText('Successfully mounted the kmip secrets engine at kmip-builtin') - ).toBeVisible(); + await expect(page.getByText('Successfully mounted the kmip secrets engine at kmip-test')).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); }); @@ -123,7 +117,7 @@ test('kmip workflow', async ({ page }) => { }); await test.step('delete scope', async () => { - await page.getByRole('link', { name: 'kmip-builtin' }).click(); + await page.getByRole('link', { name: 'kmip-test' }).click(); await page.getByRole('button', { name: 'More options' }).click(); await page.getByRole('button', { name: 'Delete scope' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); From f118b381efeccdfc8bcc5bc5e4d76fe8382d7606 Mon Sep 17 00:00:00 2001 From: Kianna <30884335+kiannaquach@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:45 -0700 Subject: [PATCH 113/468] [UI][VAULT-42963] plugin management (#12973) (#13044) * plugin management tests! * Add general settings test * Add configuration settings page * Update configuration page tests.. * Use configuration page for pki tests * Fix double comment * Add configuration page tests * Update keymgmt and pki tune tests * Update secret engines to use the step helper * Add transform configuration workflow test --- ui/e2e/pages/base.ts | 13 +- ui/e2e/pages/configuration-settings.ts | 113 ++++++++++++++++++ .../keymgmt-tune-external.ent.spec.ts | 14 ++- ui/e2e/tests/superuser/keymgmt.spec.ts | 47 ++++++++ .../superuser/kubernetes-secrets.spec.ts | 87 ++++++++++---- ui/e2e/tests/superuser/kv.spec.ts | 55 +++++++++ ui/e2e/tests/superuser/pki.spec.ts | 76 ++++++++++++ ui/e2e/tests/superuser/transform.spec.ts | 47 ++++++++ ui/e2e/tests/superuser/transit.spec.ts | 47 ++++++++ 9 files changed, 469 insertions(+), 30 deletions(-) create mode 100644 ui/e2e/pages/configuration-settings.ts diff --git a/ui/e2e/pages/base.ts b/ui/e2e/pages/base.ts index aa0d3d0492..bfca4aabd7 100644 --- a/ui/e2e/pages/base.ts +++ b/ui/e2e/pages/base.ts @@ -4,6 +4,7 @@ */ import { Page } from '@playwright/test'; +import { findEngineDisplayName } from './configuration-settings'; export class BasePage { constructor(protected page: Page) {} @@ -25,6 +26,16 @@ export class BasePage { } } + async disableEngine(path: string) { + await this.page.goto('secrets-engines'); + await this.page + .getByRole('row', { name: `Type of backend ${path}` }) + .getByLabel('supported secrets engine menu') + .click(); + await this.page.getByRole('button', { name: 'Delete' }).click(); + await this.page.getByRole('button', { name: 'Confirm' }).click(); + } + /** * Enable a secrets engine with a dynamic path * @param engineType - The type of engine to enable (e.g., 'KV', 'Transit', 'PKI Certificates') @@ -47,7 +58,7 @@ export class BasePage { // Click "Enable new engine" await this.page.getByRole('link', { name: 'Enable new engine' }).click(); - await this.page.getByRole('heading', { name: engineType }).click(); + await this.page.getByRole('heading', { name: findEngineDisplayName(engineType) }).click(); if (options?.external) { // Prerequisite: mock plugin catalog endpoint in the test so the External plugin option is available. diff --git a/ui/e2e/pages/configuration-settings.ts b/ui/e2e/pages/configuration-settings.ts new file mode 100644 index 0000000000..a470e0491b --- /dev/null +++ b/ui/e2e/pages/configuration-settings.ts @@ -0,0 +1,113 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, type Page } from '@playwright/test'; +import { ALL_ENGINES } from '../../lib/core/addon/utils/all-engines-metadata'; + +export const findEngineDisplayName = (engineType: string) => { + const engine = ALL_ENGINES.find((e) => e.type === engineType); + return engine ? engine.displayName : engineType; +}; + +const configurableEngines = ALL_ENGINES.filter((engine) => engine.isConfigurable).map( + (engine) => engine.type +); + +export class ConfigurationSettingsPage { + constructor(protected page: Page) {} + + async navigateToConfiguration(path: string) { + await this.page.getByRole('button', { name: 'Manage', exact: true }).click(); + await this.page.getByRole('link', { name: 'Configure' }).click(); + await expect(this.page.getByRole('heading', { name: `${path} configuration` })).toContainText( + `${path} configuration` + ); + } + + async assertPluginSettingsTabActive(engineType: string) { + const engineDisplayName = findEngineDisplayName(engineType); + await expect(this.page.getByRole('link', { name: 'General settings' })).not.toHaveClass(/active/); + await expect(this.page.getByRole('link', { name: `${engineDisplayName} settings` })).toHaveClass( + /active/ + ); + await expect(this.page.getByRole('link', { name: `${engineDisplayName} settings` })).toContainText( + `${engineDisplayName} settings` + ); + } + + async navigateToGeneralSettings(engineType: string) { + const engineDisplayName = findEngineDisplayName(engineType); + await this.page.getByRole('link', { name: 'General settings' }).click(); + await expect(this.page.getByRole('link', { name: 'General settings' })).toHaveClass(/active/); + + if (configurableEngines.includes(engineDisplayName)) { + await expect( + this.page.getByRole('link', { + name: `${engineDisplayName} settings`, + }) + ).not.toHaveClass(/active/); + } + + await expect(this.page.getByRole('form', { name: 'general settings form' })).toBeVisible(); + } + + async editAndVerifyGeneralSettings(path: string, engineType: string, isExternalPlugin = false) { + await expect(this.page.getByText(`Engine type ${engineType}`)).toBeVisible(); + await expect(this.page.getByText('Running version')).toContainText('Running version'); + await expect(this.page.getByRole('group', { name: 'Path' })).toBeVisible(); + await expect(this.page.getByRole('button', { name: `copy ${path}/` })).toContainText(`${path}/`); + await expect(this.page.getByRole('group', { name: 'Accessor' })).toBeVisible(); + await expect( + this.page.getByText('Default time-to-live (TTL) How long secrets in this engine stay valid. seconds') + ).toBeVisible(); + + if (!isExternalPlugin) { + await this.page.getByRole('textbox', { name: 'Description' }).fill('some description'); + await this.page.getByRole('textbox', { name: 'Default time-to-live (TTL)time' }).fill('2'); + await this.page.getByRole('button', { name: 'Save changes' }).click(); + await expect(this.page.getByRole('alert', { name: 'Configuration saved' })).toBeVisible(); + await expect(this.page.getByRole('textbox', { name: 'Description' })).toHaveValue('some description'); + await expect(this.page.getByRole('textbox', { name: 'Default time-to-live (TTL)time' })).toHaveValue( + '2' + ); + } + } + + async verifyUnsavedChangesModalOnNavigateAway(path: string) { + // make a change to trigger the unsaved changes modal + await this.page.getByRole('textbox', { name: 'Description' }).fill('unsaved changes test'); + + // try to navigate away + this.page.getByRole('link', { name: 'Back to main navigation' }).click(); + + // verify the unsaved changes modal appears + const modal = this.page.getByRole('dialog', { name: 'Unsaved changes' }); + await expect(modal).toBeVisible(); + await expect( + this.page.getByText("You've made changes to the following: Description Would you like to apply them?") + ).toBeVisible(); + + // save unsaved changes + await modal.getByRole('button', { name: 'Save changes' }).click(); + + // verify still on the configuration page and description was updated + await this.page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await this.page.getByRole('link', { name: path }).click(); + await this.page.getByRole('button', { name: 'Manage' }).click(); + await this.page.getByRole('link', { name: 'Configure' }).click(); + await this.page.getByRole('link', { name: 'General settings' }).click(); + await expect(this.page.getByRole('textbox', { name: 'Description' })).toHaveValue('unsaved changes test'); + + // make another change to trigger the unsaved changes modal + await this.page.getByRole('textbox', { name: 'Description' }).fill('unsaved changes test 2'); + + // try to navigate away again + this.page.getByRole('link', { name: 'Back to main navigation' }).click(); + + // dismiss the modal + await modal.getByRole('button', { name: 'Discard changes' }).click(); + await expect(this.page.getByRole('link', { name: 'Dashboard' })).toBeVisible(); + } +} diff --git a/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts b/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts index 6acdd0ac4e..1642e67a04 100644 --- a/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts +++ b/ui/e2e/tests/superuser/keymgmt-tune-external.ent.spec.ts @@ -4,6 +4,7 @@ */ import { expect, test } from '@playwright/test'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; const PINNED_PLUGIN_DATA = { data: { @@ -100,6 +101,8 @@ const UPDATED_KEYMGMT_EXTERNAL_MOUNT_DATA = { }; test('tune external keymgmt workflow', async ({ page }) => { + const configurationSettingsPage = new ConfigurationSettingsPage(page); + await test.step('mock the keymgmt pinned version response', async () => { await page.route('**v1/sys/plugins/pins/secret/vault-plugin-secrets-keymgmt', async (route) => { if (route.request().method() === 'GET') { @@ -142,7 +145,7 @@ test('tune external keymgmt workflow', async ({ page }) => { }); }); - await test.step('mock the initial keymgmt external tunde response', async () => { + await test.step('mock the initial keymgmt external tune response', async () => { await page.route('**/v1/sys/mounts/keymgmt-external/tune', async (route) => { if (route.request().method() === 'POST') { await route.fulfill({ @@ -159,11 +162,10 @@ test('tune external keymgmt workflow', async ({ page }) => { await test.step("navigate to the external keymgmt mount's general settings page", async () => { await page.goto('secrets-engines/keymgmt-external/list'); - await page.getByRole('button', { name: 'Manage' }).click(); - await page.getByRole('link', { name: 'Configure' }).click(); - await expect(page.getByRole('heading', { level: 1 })).toContainText('keymgmt-external configuration'); - await expect(page.getByRole('link', { name: 'General settings' })).toBeVisible(); - await expect(page.getByRole('paragraph').nth(2)).toContainText('vault-plugin-secrets-keymgmt'); + const path = 'keymgmt-external'; + const engineType = 'vault-plugin-secrets-keymgmt'; + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType, true); await expect(page.getByRole('paragraph').nth(3)).toContainText('v0.17.0+ent (Pinned)'); }); diff --git a/ui/e2e/tests/superuser/keymgmt.spec.ts b/ui/e2e/tests/superuser/keymgmt.spec.ts index 866c08f377..07e34acc75 100644 --- a/ui/e2e/tests/superuser/keymgmt.spec.ts +++ b/ui/e2e/tests/superuser/keymgmt.spec.ts @@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('keymgmt workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -86,3 +87,49 @@ test('keymgmt workflow', async ({ page }) => { await page.getByRole('button', { name: 'Dismiss' }).click(); }); }); + +test('keymgmt tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'keymgmt-tune'; + const engineType = 'keymgmt'; + + await test.step('enable keymgmt secrets engine mount', async () => { + await basePage.enableEngine(engineType, path); + }); + + await test.step('navigate to configuration page from manage dropdown ', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + }); + + // keymgmt does not have plugin settings, so we only need to test for general settings + + await test.step('navigate and verify general settings form', async () => { + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the keymgmt overview page when Exit configuration is clicked', async () => { + await expect( + page + .locator('div') + .filter({ hasText: `${path} Manage Create provider` }) + .nth(3) + ).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); +}); diff --git a/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts b/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts index 3038d1fa01..57759df65c 100644 --- a/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts +++ b/ui/e2e/tests/superuser/kubernetes-secrets.spec.ts @@ -5,6 +5,7 @@ import { test, expect } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('kubernetes secrets workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -52,29 +53,6 @@ test('kubernetes secrets workflow', async ({ page }) => { await basePage.dismissFlashMessages(); }); - await test.step('edit kubernetes configuration', async () => { - await page.getByRole('button', { name: 'Manage' }).click(); - await page.getByRole('link', { name: 'Configure' }).click(); - await page.getByRole('link', { name: 'Edit configuration' }).click(); - await expect(page.getByRole('textbox', { name: 'Service account JWT' })).toBeEmpty(); - await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://127.0.0.1:8443'); - await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----NEW CERT-----'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByRole('button', { name: 'Confirm' }).click(); - await expect( - page.locator('.info-table-row').filter({ - hasText: 'Kubernetes host https://127.0.0.1:8443', - }) - ).toBeVisible(); - await expect( - page.locator('.info-table-row').filter({ - hasText: 'Certificate PEM Format -----NEW CERT-----', - }) - ).toBeVisible(); - await page.getByRole('link', { name: 'Exit configuration' }).click(); - await basePage.dismissFlashMessages(); - }); - await test.step('create kubernetes role', async () => { await page.getByRole('link', { name: 'Roles' }).click(); await expect(page.locator('section')).toContainText( @@ -206,4 +184,67 @@ test('kubernetes secrets workflow', async ({ page }) => { 'This will generate credentials using the role test-role.' ); }); + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine('kubernetes'); + }); +}); + +test('kubernetes tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'kubernetes-tune'; + const engineType = 'kubernetes'; + + await test.step('enable kubernetes secrets engine mount', async () => { + await basePage.enableEngine(engineType, path); + }); + + await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.assertPluginSettingsTabActive(engineType); + }); + + await test.step('configure kubernetes plugin settings', async () => { + await page.locator('div').filter({ hasText: 'Manual configuration Generate' }).nth(4).click(); + await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://192.168.99.100:8443'); + await page.getByRole('textbox', { name: 'Service account JWT' }).fill('test-jwt'); + await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----CERTIFICATE-----'); + await page.getByRole('button', { name: 'Save' }).click(); + }); + + await test.step('ensure kubernetes plugin settings was saved', async () => { + await expect(page.locator('section')).toContainText('Kubernetes host https://192.168.99.100:8443'); + }); + + await test.step('edit plugin settings configuration', async () => { + await page.getByRole('link', { name: 'Edit configuration' }).click(); + await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://192.168.99.100:8448'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('section')).toContainText('Kubernetes host https://192.168.99.100:8448'); + }); + + await test.step('navigate and verify general settings form', async () => { + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the kubernetes overview page when Exit configuration is clicked', async () => { + await expect(page.getByText(`Vault Secrets engines ${path} ${path} Kubernetes Manage`)).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); }); diff --git a/ui/e2e/tests/superuser/kv.spec.ts b/ui/e2e/tests/superuser/kv.spec.ts index 17e2fe7d22..e5674c8bbb 100644 --- a/ui/e2e/tests/superuser/kv.spec.ts +++ b/ui/e2e/tests/superuser/kv.spec.ts @@ -5,6 +5,7 @@ import { test, expect } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('kvv2 workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -134,3 +135,57 @@ test('kvv2 workflow', async ({ page }) => { await page.getByRole('link', { name: 'View policy' }).click(); await expect(page.getByRole('heading', { name: 'foo-policy' })).toBeVisible(); }); + +test('kvv2 tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'kv-tune'; + const engineType = 'kv'; + + await test.step('enable kvv2 secrets engine mount', async () => { + await basePage.enableEngine(engineType, path); + }); + + await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.assertPluginSettingsTabActive(engineType); + }); + + await test.step('edit plugin settings configuration', async () => { + await page.getByRole('link', { name: 'Edit configuration' }).click(); + await page.getByRole('link', { name: 'Edit configuration' }).click(); + await page.locator('#max_versions').fill('2'); + await page.locator('#label-cas_required').check(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Require check and set Yes')).toBeVisible(); + }); + + await test.step('navigate and verify general settings form', async () => { + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the kv overview page when Exit configuration is clicked', async () => { + await expect( + page + .locator('div') + .filter({ hasText: `${path} version 2 KV Manage` }) + .nth(3) + ).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); +}); diff --git a/ui/e2e/tests/superuser/pki.spec.ts b/ui/e2e/tests/superuser/pki.spec.ts index b3f5ae8473..e5007aea81 100644 --- a/ui/e2e/tests/superuser/pki.spec.ts +++ b/ui/e2e/tests/superuser/pki.spec.ts @@ -5,6 +5,7 @@ import { test, expect } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('pki workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -144,3 +145,78 @@ test('pki workflow', async ({ page }) => { await page.getByText('Type to find an issuer...').click(); await expect(page.getByRole('option').first()).toBeVisible(); }); + +test('pki tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'pki-tune'; + const engineType = 'pki'; + + await test.step('enable pki secrets engine mount', async () => { + await basePage.enableEngine(engineType, path, { + defaultLeaseTtl: { unit: 5, option: 'm' }, + maxLeaseTtl: { unit: 10, option: 'm' }, + }); + }); + + await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.assertPluginSettingsTabActive(engineType); + }); + + await test.step('configure pki plugin settings', async () => { + await page.locator('label').filter({ hasText: 'Generate root Generates a new' }).click(); + await page.getByLabel('Type').selectOption('exported'); + await page.getByRole('textbox', { name: 'Common name' }).click(); + await page.getByRole('textbox', { name: 'Common name' }).fill('common-name-1'); + await page.getByRole('textbox', { name: 'Issuer name' }).click(); + await page.getByRole('textbox', { name: 'Issuer name' }).fill('issue-name-1'); + await page.getByRole('button', { name: 'Done' }).click(); + }); + + await test.step('ensure pki plugin settings was saved', async () => { + await expect(page.getByLabel('Next steps')).toContainText( + 'Next steps The private_key is only available once. Make sure you copy and save it now.' + ); + await expect(page.locator('section')).toContainText('Common name common-name-1'); + await expect(page.locator('section')).toContainText('Issuer name issue-name-1'); + await page.getByRole('button', { name: 'Done' }).click(); + }); + + // Navigate back to plugin settings page + await configurationSettingsPage.navigateToConfiguration(path); + + await test.step('edit plugin settings configuration', async () => { + await page.getByRole('link', { name: 'Edit configuration' }).click(); + await expect(page.locator('form')).toContainText('Cluster Config'); + await expect(page.getByText("Mount's API path Specifies")).toBeVisible(); + await page.getByRole('button', { name: 'Cancel' }).click(); + }); + + await test.step('navigate and verify general settings form', async () => { + // General settings + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the pki overview page when Exit configuration is clicked', async () => { + await expect( + page.getByText(`Vault Secrets engines ${path} ${path} Generate policy Manage`) + ).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); +}); diff --git a/ui/e2e/tests/superuser/transform.spec.ts b/ui/e2e/tests/superuser/transform.spec.ts index ce8662eac2..80629b02f9 100644 --- a/ui/e2e/tests/superuser/transform.spec.ts +++ b/ui/e2e/tests/superuser/transform.spec.ts @@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('transform workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -91,3 +92,49 @@ test('transform workflow', async ({ page }) => { await page.getByRole('button', { name: 'Dismiss' }).click(); }); }); + +test('transform tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'transform-tune'; + const engineType = 'transform'; + + await test.step('enable transform secrets engine mount', async () => { + await basePage.enableEngine(engineType, path); + }); + + await test.step('navigate to configuration page from manage dropdown ', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + }); + + // transform does not have plugin settings, so we only need to test for general settings + + await test.step('navigate and verify general settings form', async () => { + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the transform overview page when Exit configuration is clicked', async () => { + await expect( + page + .locator('div') + .filter({ hasText: `${path} Manage Create transformation` }) + .nth(3) + ).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); +}); diff --git a/ui/e2e/tests/superuser/transit.spec.ts b/ui/e2e/tests/superuser/transit.spec.ts index dbce1fa77f..c9c53add68 100644 --- a/ui/e2e/tests/superuser/transit.spec.ts +++ b/ui/e2e/tests/superuser/transit.spec.ts @@ -5,6 +5,7 @@ import { test, expect } from '@playwright/test'; import { BasePage } from '../../pages/base'; +import { ConfigurationSettingsPage } from '../../pages/configuration-settings'; test('transit workflow', async ({ page }) => { const basePage = new BasePage(page); @@ -82,3 +83,49 @@ test('transit workflow', async ({ page }) => { await expect(page.getByText('Results Valid')).toBeVisible(); await page.getByRole('button', { name: 'Close' }).click(); }); + +test('transit tune workflow', async ({ page }) => { + const basePage = new BasePage(page); + const configurationSettingsPage = new ConfigurationSettingsPage(page); + + const path = 'transit-tune'; + const engineType = 'transit'; + + await test.step('enable Transit secrets engine mount', async () => { + await basePage.enableEngine(engineType, path); + }); + + await test.step('navigate to configuration page from manage dropdown ', async () => { + await configurationSettingsPage.navigateToConfiguration(path); + }); + + // Transit does not have plugin settings, so we only need to test for general settings + + await test.step('navigate and verify general settings form', async () => { + await configurationSettingsPage.navigateToGeneralSettings(engineType); + await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType); + await page.getByRole('link', { name: 'Exit configuration' }).click(); + }); + + await test.step('ensure that we navigate back to the transit overview page when Exit configuration is clicked', async () => { + await expect( + page + .locator('div') + .filter({ hasText: `${path} Manage Create key` }) + .nth(3) + ).toBeVisible(); + }); + + await test.step('verify unsaved changes modal works in general settings', async () => { + // Navigate back to general settings + await configurationSettingsPage.navigateToConfiguration(path); + await configurationSettingsPage.navigateToGeneralSettings(engineType); + + // Test Unsaved changes modal + await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path); + }); + + await test.step('clean up and disable engine', async () => { + await basePage.disableEngine(path); + }); +}); From 39f0a7c54c9d01ad9535cf0f35d6b5a6d3fb7d61 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 16 Mar 2026 16:56:57 -0400 Subject: [PATCH 114/468] adding test for cli console (#13016) (#13042) Co-authored-by: Dan Rivera --- ui/e2e/tests/superuser/console.spec.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 ui/e2e/tests/superuser/console.spec.ts diff --git a/ui/e2e/tests/superuser/console.spec.ts b/ui/e2e/tests/superuser/console.spec.ts new file mode 100644 index 0000000000..66ab59ab85 --- /dev/null +++ b/ui/e2e/tests/superuser/console.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test, expect } from '@playwright/test'; + +test('console workflow', async ({ page }) => { + await page.goto('dashboard'); + + await test.step('open console and verify content', async () => { + await page.getByRole('button', { name: 'Console toggle' }).click(); + // verify console is open and has expected content + await expect(page.getByRole('textbox', { name: 'Web R.E.P.L.' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Close console' })).toBeVisible(); + + // verify clicking maximize button adds expected class to panel and clicking minimize removes it + await page.getByRole('button', { name: 'Maximize window' }).click(); + await expect(page.locator('.console-ui-panel')).toHaveClass(/fullscreen/); + + await page.getByRole('button', { name: 'Minimize window' }).click(); + await expect(page.locator('.console-ui-panel')).not.toHaveClass(/fullscreen/); + }); + + await test.step('execute command in console and verify output', async () => { + await page + .getByRole('textbox', { name: 'Web R.E.P.L.' }) + .fill('write sys/mounts/console-route-test type=kv'); + await page.getByRole('textbox', { name: 'Web R.E.P.L.' }).press('Enter'); + await expect(page.getByText('Success! Data written to: sys')).toBeVisible(); + + // verify clicking close button hides the console + await page.getByRole('button', { name: 'Close console' }).click(); + await expect(page.getByRole('textbox', { name: 'Web R.E.P.L.' })).toBeHidden(); + + // navigate to the secrets page and verify the new mount is visible + await page.getByRole('link', { name: 'Secrets', exact: true }).click(); + await expect(page.getByRole('link', { name: 'console-route-test/' })).toBeVisible(); + }); +}); From 8b04b3b86d433ba792f87ca295f058cf22d12598 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 17 Mar 2026 10:37:12 -0400 Subject: [PATCH 115/468] UI: add playwright test for OIDC provider binary testing (#13052) (#13077) * add playwright test for OIDC provider binary testing * spacing Co-authored-by: lane-wetmore --- ui/e2e/tests/superuser/oidc.spec.ts | 163 ++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 ui/e2e/tests/superuser/oidc.spec.ts diff --git a/ui/e2e/tests/superuser/oidc.spec.ts b/ui/e2e/tests/superuser/oidc.spec.ts new file mode 100644 index 0000000000..82cc44075b --- /dev/null +++ b/ui/e2e/tests/superuser/oidc.spec.ts @@ -0,0 +1,163 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '@playwright/test'; + +test('oidc workflow', async ({ page }) => { + await page.goto('dashboard'); + + await test.step('navigate to OIDC provider page', async () => { + await page.getByRole('link', { name: 'Access control' }).click(); + await page.getByRole('link', { name: 'OIDC provider' }).click(); + await expect(page.getByRole('img', { name: 'Example flow of a user' })).toBeVisible(); + }); + + await test.step('create application', async () => { + await page.getByRole('link', { name: 'Create your first app' }).click(); + await page.getByRole('textbox', { name: 'Application name' }).fill('test-oidc-app'); + await page.getByRole('button', { name: 'More options' }).click(); + await page + .getByRole('group', { name: 'ID Token TTL Lease will' }) + .getByLabel('Number of units') + .fill('30'); + await page.getByLabel('TTL unit for ID Token TTL').selectOption('m'); + await page + .getByRole('group', { name: 'Access Token TTL Lease will' }) + .getByLabel('Number of units') + .fill('30'); + await page.getByLabel('TTL unit for Access Token TTL').selectOption('m'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByRole('heading', { name: 'test-oidc-app' })).toBeVisible(); + await expect(page.getByText('ID Token TTL 30 minutes')).toBeVisible(); + await expect(page.getByText('Access Token TTL 30 minutes')).toBeVisible(); + await page.getByRole('link', { name: 'Available providers' }).click(); + await expect(page.getByRole('link', { name: 'default Issuer: /v1/identity/' })).toBeVisible(); + await page.getByRole('link', { name: 'Applications' }).click(); + await expect(page.getByRole('link', { name: 'test-oidc-app Client ID:' })).toBeVisible(); + }); + + await test.step('create key', async () => { + await page.getByRole('link', { name: 'Keys' }).click(); + await expect(page.getByRole('link', { name: 'default Key nav options' })).toBeVisible(); + await page.getByRole('link', { name: 'Create key' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('test-oidc-key'); + await page.getByLabel('Algorithm').selectOption('ES256'); + await page + .getByRole('group', { name: 'Rotation period Lease will' }) + .getByLabel('Number of units') + .fill('30'); + await page.getByLabel('TTL unit for Rotation period').selectOption('m'); + await page + .getByRole('group', { name: 'Verification TTL Lease will' }) + .getByLabel('Number of units') + .fill('30'); + await page.getByLabel('TTL unit for Verification TTL').selectOption('m'); + await expect(page.locator('.radio-card').nth(1)).toHaveClass(/is-disabled/); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByRole('heading', { name: 'test-oidc-key' })).toBeVisible(); + await expect(page.getByText('Algorithm ES256')).toBeVisible(); + await expect(page.getByText('Rotation period 30 minutes')).toBeVisible(); + await expect(page.getByText('Verification TTL 30 minutes')).toBeVisible(); + }); + + await test.step('rotate key', async () => { + await page.getByRole('button', { name: 'Rotate key' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + }); + + await test.step('edit key', async () => { + await page.getByRole('link', { name: 'Edit key' }).click(); + await page.getByLabel('Algorithm').selectOption('EdDSA'); + await page.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText('Algorithm EdDSA')).toBeVisible(); + await page.getByRole('link', { name: 'Keys' }).click(); + }); + + await test.step('create assignment', async () => { + // create a group and entity for the assignment + await page.getByRole('link', { name: 'Groups' }).click(); + await page.getByRole('link', { name: 'Create group' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('oidc-group'); + await page.getByRole('button', { name: 'Create' }).click(); + await page.getByRole('link', { name: 'Entities' }).click(); + await page.getByRole('link', { name: 'Create entity' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('oidc-entity'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.getByRole('link', { name: 'OIDC provider' }).click(); + await page.getByRole('link', { name: 'Assignments' }).click(); + await expect(page.locator('.list-item-row')).toHaveClass(/is-disabled/); + + await page.getByRole('link', { name: 'Create assignment' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('oidc-assignment'); + await page.getByLabel('Entities').getByText('Search').click(); + await page.locator('div').filter({ hasText: 'Vault Assignments Create' }).nth(1).click(); + await page.getByRole('button', { name: 'Create' }).click(); + await expect(page.getByText('At least one entity or group')).toBeVisible(); + await page.getByLabel('Groups').getByText('Search').click(); + await page.getByRole('option', { name: 'oidc-group' }).click(); + await page.getByRole('button', { name: 'Create' }).click(); + await expect(page.getByRole('link', { name: 'oidc-group' })).toBeVisible(); + }); + + await test.step('edit assignment', async () => { + await page.getByRole('link', { name: 'Edit assignment' }).click(); + await page.getByLabel('Entities').getByText('Search').click(); + await page.getByRole('option', { name: 'oidc-entity' }).click(); + await page.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByRole('link', { name: 'oidc-entity' })).toBeVisible(); + await page.getByRole('link', { name: 'Assignments' }).click(); + await expect(page.getByRole('link', { name: 'oidc-assignment' })).toBeVisible(); + }); + + await test.step('create provider', async () => { + await page.getByRole('link', { name: 'Providers' }).click(); + await page.getByRole('link', { name: 'Create provider' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('oidc-provider'); + await page.getByRole('radio', { name: 'Limit access to selected' }).check(); + await page.getByLabel('Application name').getByText('Search').click(); + await page.getByRole('option', { name: 'test-oidc-app' }).click(); + await page.getByRole('button', { name: 'Create' }).click(); + await expect(page.getByText('/v1/identity/oidc/provider/')).toBeVisible(); + }); + + await test.step('create scope', async () => { + await page.getByRole('link', { name: 'Providers' }).click(); + await page.getByRole('link', { name: 'Scopes' }).click(); + await expect(page.getByRole('heading', { name: 'No scopes yet' })).toBeVisible(); + await page.getByRole('link', { name: 'Create scope' }).click(); + await page.getByRole('textbox', { name: 'Name' }).fill('oidc-scope'); + await page.getByRole('textbox', { name: 'Description' }).fill('oidc scope description'); + await page.getByRole('textbox', { name: 'JSON Template' }).fill(`{ + "username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}}, + "contact": { + "email": {{identity.entity.metadata.email}}, + "phone_number": {{identity.entity.metadata.phone_number}} + }, + "groups": {{identity.entity.groups.names}} + }`); + await page.getByRole('button', { name: 'Create' }).click(); + }); + + await test.step('edit scope', async () => { + await page.getByRole('link', { name: 'Edit scope' }).click(); + await page.getByRole('textbox', { name: 'Description' }).fill('updated description'); + await page.getByRole('textbox', { name: 'JSON Template' }).fill(`{ + "username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}}, + "contact": { + "email": {{identity.entity.metadata.email}} + }, + "groups": {{identity.entity.groups.names}} + }`); + + await page.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText('updated description')).toBeVisible(); + await expect(page.getByText('"phone_number"')).not.toBeVisible(); + await page.getByRole('link', { name: 'Scopes' }).click(); + await expect(page.getByRole('link', { name: 'oidc-scope' })).toBeVisible(); + }); +}); From 417dd0775ba272da82aab9b9692eae329d619cbe Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 17 Mar 2026 17:12:45 -0400 Subject: [PATCH 116/468] Backport Update vault-plugin-auth-gcp to v0.22.1 into ce/main (#13086) * Update vault-plugin-auth-gcp to v0.22.1 (#13051) * go.mod: update go.mod and go.sum for vault-plugin-auth-gcp * add changelog * update changelog file * remove changelog/_13051.txt --------- Co-authored-by: Arjun K S --- changelog/_13086.txt | 3 +++ go.mod | 2 +- go.sum | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/_13086.txt diff --git a/changelog/_13086.txt b/changelog/_13086.txt new file mode 100644 index 0000000000..1ede0b59ee --- /dev/null +++ b/changelog/_13086.txt @@ -0,0 +1,3 @@ +```release-note:bug +auth/gcp: Fix intermittent context canceled failures for Workload Identity Federation (WIF) authentication +``` diff --git a/go.mod b/go.mod index b0dd9de888..11440bd45b 100644 --- a/go.mod +++ b/go.mod @@ -143,7 +143,7 @@ require ( github.com/hashicorp/vault-plugin-auth-alicloud v0.22.0 github.com/hashicorp/vault-plugin-auth-azure v0.22.0 github.com/hashicorp/vault-plugin-auth-cf v0.22.0 - github.com/hashicorp/vault-plugin-auth-gcp v0.22.0 + github.com/hashicorp/vault-plugin-auth-gcp v0.22.1 github.com/hashicorp/vault-plugin-auth-jwt v0.25.0 github.com/hashicorp/vault-plugin-auth-kerberos v0.16.0 github.com/hashicorp/vault-plugin-auth-kubernetes v0.23.1 diff --git a/go.sum b/go.sum index 3ed0af0d1d..fbb5fcf839 100644 --- a/go.sum +++ b/go.sum @@ -1558,6 +1558,8 @@ github.com/hashicorp/vault-plugin-auth-cf v0.22.0 h1:DTK733vsB2lBVwSLxra3/IuhlGd github.com/hashicorp/vault-plugin-auth-cf v0.22.0/go.mod h1:qToMQoW7dX1egtJwEHd21I/7pgzg+DBEwRAytd+Pgtc= github.com/hashicorp/vault-plugin-auth-gcp v0.22.0 h1:c5LEJmHNV6VzbKTM9nn05uGLhu0VRnIsacCshj0AJ8M= github.com/hashicorp/vault-plugin-auth-gcp v0.22.0/go.mod h1:6WhVeAZUu+67H4tkXsnFoUU3+UaBFLlE6ffUHDkVM0o= +github.com/hashicorp/vault-plugin-auth-gcp v0.22.1 h1:ahbC0bbzUXAckxSmPA9V12VH+4CIg1efJqRq1biHSr4= +github.com/hashicorp/vault-plugin-auth-gcp v0.22.1/go.mod h1:6WhVeAZUu+67H4tkXsnFoUU3+UaBFLlE6ffUHDkVM0o= github.com/hashicorp/vault-plugin-auth-jwt v0.25.0 h1:YdrZ+fGutoaaF/Hiw3+xtmcRalmqGH8sXRKIrkr5dsk= github.com/hashicorp/vault-plugin-auth-jwt v0.25.0/go.mod h1:HhfLJxvpYt10mrZoBTa+ToioN9HGOZbWYSR1UHqTMrs= github.com/hashicorp/vault-plugin-auth-kerberos v0.16.0 h1:bZpmQS26TorpeensFerDPM7z+NgZjrRj9IbdGRdjaLM= From b057aac746864bf5ac952ab043f3b19af5770760 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 17 Mar 2026 18:52:40 -0400 Subject: [PATCH 117/468] [VAULT-43339] 1/2 Chore update TS (#13050) (#13105) * Initial ts updgrade * Migrate linked-block to ts to squash ts errors * [VAULT-43339] 2/2 Update vault-reporting and add ember-intl (#13062) * Update vault-reporting and add ember-intl * Add setupIntl for rendering tests Co-authored-by: Jim Wright --- ui/app/routes/application.js | 2 + .../secrets/backend/configuration/index.ts | 3 +- ui/app/services/unsaved-changes.ts | 9 +- ui/app/utils/forms/validate.ts | 6 +- ui/config/ember-intl.js | 13 + ui/lib/core/addon/components/linked-block.js | 65 - ui/lib/core/addon/components/linked-block.ts | 87 ++ .../secrets/sync-activation-modal.ts | 3 +- ui/package.json | 5 +- ui/pnpm-lock.yaml | 1170 ++++++++++++++++- ui/pnpm-workspace.yaml | 3 + ui/tests/helpers/index.js | 3 +- .../components/alert-inline-test.js | 2 +- .../auth-config-form/config-test.js | 2 +- .../auth-config-form/options-test.js | 2 +- .../auth-method/configuration-test.js | 2 +- .../components/auth/fields-test.js | 2 +- .../components/auth/form-template-test.js | 2 +- .../components/auth/form/base-test.js | 50 +- .../components/auth/form/oidc-jwt-test.js | 14 +- .../components/auth/form/okta-test.js | 10 +- .../components/auth/form/saml-test.js | 10 +- .../components/auth/namespace-input-test.js | 2 +- .../auth/page/listing-visibility-test.js | 2 +- .../auth/page/login-settings-test.js | 8 +- .../auth/page/method-authentication-test.js | 2 +- .../components/auth/page/mfa-test.js | 2 +- .../components/auth/page/page-test.js | 4 +- .../integration/components/auth/tabs-test.js | 2 +- .../components/autocomplete-input-test.js | 2 +- .../integration/components/b64-toggle-test.js | 2 +- .../components/checkbox-grid-test.js | 2 +- .../integration/components/chevron-test.js | 2 +- .../components/clients/config-test.js | 2 +- .../components/clients/page/counts-test.js | 2 +- .../components/clients/running-total-test.js | 2 +- .../components/clients/table-test.js | 2 +- .../components/clients/usage-stats-test.js | 2 +- .../automation-snippets-test.js | 10 +- .../code-generator/policy/builder-test.js | 2 +- .../code-generator/policy/flyout-test.js | 2 +- .../code-generator/policy/stanza-test.js | 2 +- .../components/confirm-action-test.js | 2 +- .../components/confirmation-modal-test.js | 2 +- .../components/console/log-command-test.js | 2 +- .../components/console/log-error-test.js | 2 +- .../components/console/log-json-test.js | 2 +- .../components/console/log-list-test.js | 2 +- .../components/console/log-object-test.js | 2 +- .../components/console/log-text-test.js | 2 +- .../components/console/ui-panel-test.js | 2 +- .../components/control-group-success-test.js | 2 +- .../components/control-group-test.js | 2 +- .../components/database-role-edit-test.js | 2 +- .../database-role-setting-form-test.js | 2 +- .../components/disabled-plugin-card-test.js | 22 +- .../components/download-button-test.js | 2 +- .../integration/components/edit-form-test.js | 2 +- .../components/empty-state-test.js | 2 +- .../components/enabled-plugin-card-test.js | 2 +- .../components/filter-input-explicit-test.js | 2 +- .../components/filter-input-test.js | 2 +- .../components/form-field-label-test.js | 2 +- .../integration/components/form-field-test.js | 2 +- .../components/get-credentials-card-test.js | 2 +- ui/tests/integration/components/icon-test.js | 2 +- .../components/identity/item-details-test.js | 2 +- .../components/info-table-item-array-test.js | 2 +- .../components/info-table-row-test.js | 2 +- .../components/json-editor-test.js | 2 +- .../components/keymgmt/distribute-test.js | 2 +- .../components/keymgmt/key-edit-test.js | 2 +- .../components/keymgmt/provider-edit-test.js | 2 +- .../kmip/details-credentials-test.js | 2 +- .../kmip/page/configuration-test.js | 2 +- .../components/kmip/page/configure-test.js | 2 +- .../components/kmip/page/credentials-test.js | 2 +- .../kmip/page/credentials/generate-test.js | 2 +- .../components/kmip/page/role-test.js | 2 +- .../components/kmip/page/scope/roles-test.js | 2 +- .../components/kmip/page/scopes-test.js | 2 +- .../kmip/page/scopes/create-test.js | 2 +- .../components/kmip/role-form-test.js | 2 +- .../components/known-secondaries-card-test.js | 2 +- .../known-secondaries-table-test.js | 2 +- .../components/kubernetes/config-cta-test.js | 2 +- .../kubernetes/kubernetes-header-test.js | 2 +- .../kubernetes/page/configuration-test.js | 2 +- .../kubernetes/page/configure-test.js | 2 +- .../kubernetes/page/credentials-test.js | 2 +- .../kubernetes/page/overview-test.js | 2 +- .../page/role/create-and-edit-test.js | 2 +- .../kubernetes/page/role/details-test.js | 2 +- .../components/kubernetes/page/roles-test.js | 2 +- .../components/kv-object-editor-test.js | 2 +- .../components/kv-suggestion-input-test.js | 2 +- .../components/kv/kv-paths-card-test.js | 2 +- .../kv/page/kv-page-configuration-test.js | 2 +- .../kv/page/kv-page-configure-test.js | 2 +- .../components/kv/page/kv-page-list-test.js | 2 +- .../kv/page/kv-page-metadata-details-test.js | 2 +- .../kv/page/kv-page-metadata-edit-test.js | 2 +- .../kv/page/kv-page-overview-test.js | 2 +- .../components/kv/page/kv-page-patch-test.js | 2 +- .../kv/page/kv-page-secret-details-test.js | 2 +- .../kv/page/kv-page-secret-paths-test.js | 2 +- .../kv/page/kv-page-version-diff-test.js | 4 +- .../kv/page/kv-page-version-history-test.js | 2 +- .../ldap/accounts-checked-out-test.js | 2 +- .../components/ldap/config-cta-test.js | 2 +- .../ldap/page/configuration-test.js | 2 +- .../components/ldap/page/configure-test.js | 2 +- .../components/ldap/page/libraries-test.js | 2 +- .../ldap/page/library/check-out-test.js | 2 +- .../ldap/page/library/create-and-edit-test.js | 2 +- .../ldap/page/library/details-test.js | 2 +- .../page/library/details/accounts-test.js | 2 +- .../library/details/configuration-test.js | 2 +- .../components/ldap/page/overview-test.js | 2 +- .../ldap/page/role/create-and-edit-test.js | 2 +- .../ldap/page/role/credentials-test.js | 2 +- .../components/ldap/page/role/details-test.js | 2 +- .../components/ldap/page/roles-test.js | 2 +- .../components/ldap/tab-page-header-test.js | 2 +- .../components/license-banners-test.js | 2 +- .../components/license-info-test.js | 2 +- .../components/link-status-test.js | 2 +- .../integration/components/list-table-test.js | 4 +- .../components/manage-dropdown-test.js | 2 +- .../components/masked-input-test.js | 2 +- .../components/message-error-test.js | 12 +- .../mfa-login-enforcement-form-test.js | 2 +- .../mfa-login-enforcement-header-test.js | 2 +- .../components/mfa-method-list-item-test.js | 2 +- .../components/mfa/method-form-test.js | 2 +- .../components/mfa/mfa-form-test.js | 2 +- .../components/mount-accessor-select-test.js | 2 +- .../components/mount-backend-form-test.js | 2 +- ...unt-backend-type-form-enhancements-test.js | 8 +- .../components/mount/configure-tabs-test.js | 2 +- .../mount/secrets-engine-form-test.js | 128 +- .../components/namespace-picker-test.js | 2 +- .../components/oidc-consent-block-test.js | 2 +- .../components/oidc/assignment-form-test.js | 2 +- .../components/oidc/client-form-test.js | 2 +- .../components/oidc/key-form-test.js | 2 +- .../components/oidc/provider-form-test.js | 2 +- .../components/oidc/scope-form-test.js | 2 +- .../components/okta-number-challenge-test.js | 2 +- .../components/overview-card-test.js | 2 +- .../path-filter-config-list-test.js | 2 +- .../integration/components/pgp-file-test.js | 2 +- .../integration/components/pgp-list-test.js | 2 +- .../pki/page/pki-certificate-details-test.js | 2 +- .../page/pki-configuration-details-test.js | 2 +- .../pki/page/pki-issuer-edit-test.js | 2 +- .../pki/page/pki-key-details-test.js | 2 +- .../components/pki/page/pki-key-list-test.js | 2 +- .../components/pki/page/pki-overview-test.js | 2 +- .../pki/page/pki-role-details-test.js | 2 +- .../pki/page/pki-tidy-status-test.js | 2 +- .../pki/pki-import-pem-bundle-test.js | 2 +- .../components/pki/pki-key-form-test.js | 2 +- .../components/pki/pki-key-parameters-test.js | 2 +- .../components/pki/pki-key-usage-test.js | 2 +- .../pki/pki-not-valid-after-form-test.js | 2 +- .../components/pki/pki-page-header-test.js | 2 +- .../components/pki/pki-role-form-test.js | 2 +- .../components/pki/pki-role-generate-test.js | 2 +- .../components/pki/pki-tidy-form-test.js | 2 +- .../plugin-documentation-flyout-test.js | 16 +- .../components/policy-form-test.js | 4 +- .../components/radial-progress-test.js | 2 +- .../components/radio-button-test.js | 2 +- .../integration/components/raft-join-test.js | 2 +- .../components/raft-storage-overview-test.js | 2 +- .../components/raft-storage-restore-test.js | 2 +- .../integration/components/read-more-test.js | 2 +- .../components/readonly-form-field-test.js | 2 +- .../components/regex-validator-test.js | 2 +- .../replication-action-generate-token-test.js | 2 +- .../components/replication-actions-test.js | 2 +- .../components/replication-dashboard-test.js | 2 +- .../components/replication-header-test.js | 2 +- .../components/replication-page-test.js | 2 +- .../replication-primary-card-test.js | 2 +- .../replication-secondary-card-test.js | 2 +- .../replication-summary-card-test.js | 2 +- .../components/replication-table-rows-test.js | 2 +- .../components/search-select-test.js | 2 +- .../search-select-with-modal-test.js | 2 +- .../components/secret-edit-test.js | 2 +- .../components/secret-engines/catalog-test.js | 34 +- .../components/secret-list-header-test.js | 2 +- .../integration/components/select-test.js | 2 +- .../components/selectable-card-test.js | 2 +- .../components/sidebar/frame-test.js | 2 +- .../components/sidebar/nav/access-test.js | 2 +- .../components/sidebar/nav/cluster-test.js | 2 +- .../components/sidebar/nav/reporting-test.js | 2 +- .../nav/resilience-and-recovery-test.js | 2 +- .../components/sidebar/nav/secrets-test.js | 2 +- .../components/sidebar/nav/tools-test.js | 2 +- .../components/sidebar/user-menu-test.js | 2 +- .../components/splash-page-test.js | 2 +- .../integration/components/stat-text-test.js | 2 +- .../components/string-list-test.js | 2 +- .../components/sync-status-badge-test.js | 2 +- .../sync/secrets/destination-header-test.js | 2 +- .../sync/secrets/landing-cta-test.js | 2 +- .../sync/secrets/page/destinations-test.js | 2 +- .../page/destinations/create-and-edit-test.js | 2 +- .../destinations/destination/details-test.js | 2 +- .../destinations/destination/secrets-test.js | 2 +- .../destinations/destination/sync-test.js | 2 +- .../page/destinations/select-type-test.js | 2 +- .../sync/secrets/page/overview-test.js | 2 +- .../components/sync/sync-header-test.js | 2 +- .../components/toggle-button-test.js | 2 +- .../integration/components/toggle-test.js | 2 +- .../components/token-expire-warning-test.js | 2 +- .../components/toolbar-actions-test.js | 2 +- .../components/toolbar-filters-test.js | 2 +- .../components/toolbar-link-test.js | 2 +- .../integration/components/toolbar-test.js | 2 +- .../components/totp/key-form-test.js | 2 +- .../transform-advanced-templating-test.js | 2 +- .../components/transform-edit-base-test.js | 2 +- .../components/transform-list-item-test.js | 2 +- .../components/transform-role-edit-test.js | 2 +- .../components/transit-key-actions-test.js | 2 +- .../integration/components/ttl-picker-test.js | 2 +- .../components/unsaved-changes-modal-test.js | 2 +- .../components/upgrade-page-test.js | 2 +- .../integration/components/wizard-test.js | 2 +- .../integration/components/wrap-ttl-test.js | 2 +- .../integration/helpers/add-to-array-test.js | 2 +- .../helpers/changelog-url-for-test.js | 2 +- .../integration/helpers/date-format-test.js | 2 +- .../integration/helpers/date-from-now-test.js | 2 +- .../helpers/display-nav-item-test.js | 2 +- .../helpers/format-duration-test.js | 2 +- .../integration/helpers/has-feature-test.js | 2 +- .../helpers/has-permission-test.js | 2 +- .../helpers/is-empty-value-test.js | 2 +- .../helpers/remove-from-array-test.js | 2 +- ui/vault-reporting/0.21.0.tgz | Bin 0 -> 124532 bytes ui/vault-reporting/0.8.0.tgz | Bin 76732 -> 0 bytes 248 files changed, 1622 insertions(+), 521 deletions(-) create mode 100644 ui/config/ember-intl.js delete mode 100644 ui/lib/core/addon/components/linked-block.js create mode 100644 ui/lib/core/addon/components/linked-block.ts create mode 100644 ui/vault-reporting/0.21.0.tgz delete mode 100644 ui/vault-reporting/0.8.0.tgz diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index e20aad6f13..c732460881 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -14,6 +14,7 @@ import ControlGroupError from 'vault/lib/control-group-error'; export default class ApplicationRoute extends Route { @service analytics; @service controlGroup; + @service intl; @service('router') routing; @service('namespace') namespaceService; @service('flags') flagsService; @@ -72,6 +73,7 @@ export default class ApplicationRoute extends Route { } beforeModel() { + this.intl.setLocale(['en-us']); return this.flagsService.fetchFeatureFlags(); } diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.ts index 15521a2ec9..5a9b73e23a 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.ts @@ -6,11 +6,12 @@ import Route from '@ember/routing/route'; import RouterService from '@ember/routing/router-service'; import { service } from '@ember/service'; +import type Transition from '@ember/routing/transition'; export default class BackendConfigurationIndexRoute extends Route { @service declare readonly router: RouterService; - beforeModel() { + beforeModel(): Transition { return this.router.replaceWith('vault.cluster.secrets.backend.configuration.general-settings'); } } diff --git a/ui/app/services/unsaved-changes.ts b/ui/app/services/unsaved-changes.ts index 97d7196eaf..8fc581ccb7 100644 --- a/ui/app/services/unsaved-changes.ts +++ b/ui/app/services/unsaved-changes.ts @@ -13,6 +13,11 @@ import type Transition from '@ember/routing/transition'; import type RouterService from '@ember/routing/router-service'; import FlagsService from 'vault/services/flags'; +type TransitionInfo = { + routeName?: string; + params?: Record; +}; + // this service tracks the unsaved changes modal state. export default class UnsavedChangesService extends Service { @service declare readonly router: RouterService; @@ -42,10 +47,10 @@ export default class UnsavedChangesService extends Service { return this.changedFields.length > 0; } - get transitionInfo() { + get transitionInfo(): TransitionInfo { return { routeName: this.intendedTransition?.to?.name, - params: this.intendedTransition?.to?.params, + params: this.intendedTransition?.to?.params as Record | undefined, }; } diff --git a/ui/app/utils/forms/validate.ts b/ui/app/utils/forms/validate.ts index cb4dd344be..0045a4c7b9 100644 --- a/ui/app/utils/forms/validate.ts +++ b/ui/app/utils/forms/validate.ts @@ -59,10 +59,8 @@ export const validate = ( } // dot notation may be used to define key for nested property const passedValidation = useCustomValidator - ? // @ts-expect-error - options may or may not be defined - validator(data, options) - : // @ts-expect-error - options may or may not be defined - validator(get(data, key), options); + ? validator(data, options) + : validator(get(data, key), options); if (!passedValidation) { // message can also be a function diff --git a/ui/config/ember-intl.js b/ui/config/ember-intl.js new file mode 100644 index 0000000000..e27f3da0c4 --- /dev/null +++ b/ui/config/ember-intl.js @@ -0,0 +1,13 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +module.exports = function () { + return { + fallbackLocale: null, + inputPath: 'translations', + publicOnly: false, + wrapTranslationsWithNamespace: true, + }; +}; diff --git a/ui/lib/core/addon/components/linked-block.js b/ui/lib/core/addon/components/linked-block.js deleted file mode 100644 index 4d6b0e4542..0000000000 --- a/ui/lib/core/addon/components/linked-block.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { encodePath } from 'vault/utils/path-encoding-helpers'; -import routerLookup from 'core/utils/router-lookup'; - -/** - * @module LinkedBlock - * LinkedBlock components are linkable divs that yield any content nested within them. They are often used in list views such as when listing the secret engines. - * - * @example - * - * My wrapped content - * - * - * - * @param {Array} params=null - These are values sent to the router's transitionTo method. First item is route, second is the optional path. - * @param {Object} [queryParams=null] - queryParams can be passed via this property. It needs to be an object. - * @param {String} [linkPrefix=null] - Overwrite the params with custom route. Needed for use in engines (KMIP and PKI). ex: vault.cluster.secrets.backend.kmip - * @param {Boolean} [encode=false] - Encode the path. - * @param {boolean} [disabled] - disable the link -- prevents on click and removes linked-block hover styling - */ - -export default class LinkedBlockComponent extends Component { - get router() { - return routerLookup(this); - } - - @action - onClick(event) { - if (!this.args.disabled) { - const $target = event.target; - const isAnchorOrButton = - $target.tagName === 'A' || - $target.tagName === 'BUTTON' || - $target.closest('button') || - $target.closest('a'); - if (!isAnchorOrButton) { - let params = this.args.params; - if (this.args.encode) { - params = params.map((param, index) => { - if (index === 0 || typeof param !== 'string') { - return param; - } - return encodePath(param); - }); - } - const queryParams = this.args.queryParams; - if (queryParams) { - params.push({ queryParams }); - } - if (this.args.linkPrefix) { - let targetRoute = this.args.params[0]; - targetRoute = `${this.args.linkPrefix}.${targetRoute}`; - this.args.params[0] = targetRoute; - } - this.router.transitionTo(...params); - } - } - } -} diff --git a/ui/lib/core/addon/components/linked-block.ts b/ui/lib/core/addon/components/linked-block.ts new file mode 100644 index 0000000000..5c5dbf81b0 --- /dev/null +++ b/ui/lib/core/addon/components/linked-block.ts @@ -0,0 +1,87 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { encodePath } from 'vault/utils/path-encoding-helpers'; +import routerLookup from 'core/utils/router-lookup'; + +import type RouterService from '@ember/routing/router-service'; + +type TransitionArgs = [string, ...unknown[]]; + +interface Args { + params: TransitionArgs; + queryParams?: Record; + linkPrefix?: string; + encode?: boolean; + disabled?: boolean; +} + +/** + * @module LinkedBlock + * LinkedBlock components are linkable divs that yield any content nested within them. They are often used in list views such as when listing the secret engines. + * + * @example + * + * My wrapped content + * + * + * + * @param {Array} params=null - These are values sent to the router's transitionTo method. First item is route, second is the optional path. + * @param {Object} [queryParams=null] - queryParams can be passed via this property. It needs to be an object. + * @param {String} [linkPrefix=null] - Overwrite the params with custom route. Needed for use in engines (KMIP and PKI). ex: vault.cluster.secrets.backend.kmip + * @param {Boolean} [encode=false] - Encode the path. + * @param {boolean} [disabled] - disable the link -- prevents on click and removes linked-block hover styling + */ + +export default class LinkedBlockComponent extends Component { + get router(): RouterService { + return routerLookup(this); + } + + @action + onClick(event: MouseEvent): void { + if (this.args.disabled) { + return; + } + + const target = event.target; + if (!(target instanceof Element)) { + return; + } + + const isAnchorOrButton = + target.tagName === 'A' || + target.tagName === 'BUTTON' || + !!target.closest('button') || + !!target.closest('a'); + if (isAnchorOrButton) { + return; + } + + let params = [...this.args.params] as TransitionArgs; + if (this.args.encode) { + params = params.map((param, index) => { + if (index === 0 || typeof param !== 'string') { + return param; + } + return encodePath(param); + }) as TransitionArgs; + } + + const queryParams = this.args.queryParams; + if (queryParams) { + params.push({ queryParams }); + } + + if (this.args.linkPrefix) { + const [targetRoute, ...rest] = params; + params = [`${this.args.linkPrefix}.${targetRoute}`, ...rest] as TransitionArgs; + } + + this.router.transitionTo(...params); + } +} diff --git a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts index b1c392480a..dfebe11772 100644 --- a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts +++ b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts @@ -9,6 +9,7 @@ import { service } from '@ember/service'; import { task } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; +import type { TaskGenerator } from 'ember-concurrency'; import type FlagsService from 'vault/services/flags'; import type FlashMessageService from 'vault/services/flash-messages'; import type RouterService from '@ember/routing/router-service'; @@ -30,7 +31,7 @@ export default class SyncActivationModal extends Component { @task @waitFor - *onFeatureConfirm() { + *onFeatureConfirm(): TaskGenerator { // clear any previous errors in the parent component this.args.onConfirm(); diff --git a/ui/package.json b/ui/package.json index 84fc21c755..72bf4ab359 100644 --- a/ui/package.json +++ b/ui/package.json @@ -121,6 +121,7 @@ "ember-engines": "0.8.23", "ember-exam": "~9.1.0", "ember-inflector": "4.0.2", + "ember-intl": "^7.4.1", "ember-load-initializers": "~3.0.1", "ember-modifier": "~4.2.0", "ember-power-select": "~8.12.0", @@ -169,7 +170,7 @@ "swagger-ui-dist": "~5.21.0", "text-encoder-lite": "2.0.0", "tracked-built-ins": "~3.4.0", - "typescript": "~5.5.4", + "typescript": "~5.6.0", "webpack": "5.94.0", "webpack-cli": "^6.0.1" }, @@ -231,7 +232,7 @@ "@babel/core": "7.26.10", "@babel/eslint-parser": "^7.28.5", "@carbon/charts": "^1.27.2", - "@hashicorp-internal/vault-reporting": "file:vault-reporting/0.8.0.tgz", + "@hashicorp-internal/vault-reporting": "file:vault-reporting/0.21.0.tgz", "@hashicorp/design-system-components": "4.24.1", "@hashicorp/design-system-tokens": "3.0.0", "@hashicorp/vault-client-typescript": "github:hashicorp/vault-client-typescript", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 5cbfb3be3f..b0745d6f7e 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -47,11 +47,11 @@ importers: specifier: ^1.27.2 version: 1.27.2 '@hashicorp-internal/vault-reporting': - specifier: file:vault-reporting/0.8.0.tgz - version: file:vault-reporting/0.8.0.tgz(@babel/core@7.26.10)(@glint/template@1.7.3)(@hashicorp/design-system-components@4.24.1(9e1e7970f02a7597d41eecd5c69ae0c6))(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)) + specifier: file:vault-reporting/0.21.0.tgz + version: file:vault-reporting/0.21.0.tgz(@babel/core@7.26.10)(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(@hashicorp/design-system-components@4.24.1(080995f06707449af57cc37e52df9a24))(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))(typescript@5.6.3)(webpack@5.94.0) '@hashicorp/design-system-components': specifier: 4.24.1 - version: 4.24.1(9e1e7970f02a7597d41eecd5c69ae0c6) + version: 4.24.1(080995f06707449af57cc37e52df9a24) '@hashicorp/design-system-tokens': specifier: 3.0.0 version: 3.0.0 @@ -160,10 +160,10 @@ importers: version: 1.7.5 '@typescript-eslint/eslint-plugin': specifier: ~5.62.0 - version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4) + version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ~5.62.0 - version: 5.62.0(eslint@8.57.1)(typescript@5.5.4) + version: 5.62.0(eslint@8.57.1)(typescript@5.6.3) asn1js: specifier: ~3.0.6 version: 3.0.6 @@ -290,6 +290,9 @@ importers: ember-inflector: specifier: 4.0.2 version: 4.0.2 + ember-intl: + specifier: ^7.4.1 + version: 7.4.1(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(typescript@5.6.3)(webpack@5.94.0) ember-load-initializers: specifier: ~3.0.1 version: 3.0.1(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)) @@ -418,13 +421,13 @@ importers: version: 20.0.0 stylelint: specifier: ~16.19.1 - version: 16.19.1(typescript@5.5.4) + version: 16.19.1(typescript@5.6.3) stylelint-config-standard: specifier: ~38.0.0 - version: 38.0.0(stylelint@16.19.1(typescript@5.5.4)) + version: 38.0.0(stylelint@16.19.1(typescript@5.6.3)) stylelint-prettier: specifier: ~5.0.3 - version: 5.0.3(prettier@3.0.3)(stylelint@16.19.1(typescript@5.5.4)) + version: 5.0.3(prettier@3.0.3)(stylelint@16.19.1(typescript@5.6.3)) swagger-ui-dist: specifier: ~5.21.0 version: 5.21.0 @@ -435,8 +438,8 @@ importers: specifier: ~3.4.0 version: 3.4.0(@babel/core@7.26.10) typescript: - specifier: ~5.5.4 - version: 5.5.4 + specifier: ~5.6.0 + version: 5.6.3 webpack: specifier: 5.94.0 version: 5.94.0(webpack-cli@6.0.1) @@ -461,10 +464,18 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.27.2': resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.26.10': resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} @@ -473,6 +484,10 @@ packages: resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/eslint-parser@7.28.5': resolution: {integrity: sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} @@ -484,6 +499,10 @@ packages: resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.1': resolution: {integrity: sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==} engines: {node: '>=6.9.0'} @@ -492,6 +511,10 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.27.1': resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} engines: {node: '>=6.9.0'} @@ -509,6 +532,10 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.27.1': resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} @@ -517,12 +544,22 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.27.1': resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} @@ -555,6 +592,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -567,11 +608,20 @@ packages: resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.27.2': resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -1041,14 +1091,26 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.27.1': resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.27.1': resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@carbon/charts@1.27.2': resolution: {integrity: sha512-0eYS1bgwP/z+lCBQrDT8vOJSMJQVKzT3h51lyXwxn9rx3/GLuseEr1+t65elyjUaBxMAs9wT1kDat2PU2d32lA==} @@ -1336,6 +1398,10 @@ packages: '@glint/template': optional: true + '@embroider/reverse-exports@0.2.0': + resolution: {integrity: sha512-WFsw8nQpHZiWGEDYpa/A79KEFfTisqteXbY+jg9eZiww1r1G+LZvsmdszDp86TkotUSCqrMbK/ewn0jR1CJmqg==} + engines: {node: 12.* || 14.* || >= 16} + '@embroider/shared-internals@2.5.2': resolution: {integrity: sha512-jNDJ9YlV6Qp9Na9v17qirUewVuq6T0t32nn+bbnFlCRTvmllKluZdYPSC5RuRnEZKTloVYRSF0+f1rgkTIEvxQ==} engines: {node: 12.* || 14.* || >= 16} @@ -1379,6 +1445,10 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@faker-js/faker@8.4.1': + resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + '@floating-ui/core@1.7.0': resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} @@ -1388,6 +1458,29 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@formatjs/intl@3.1.8': + resolution: {integrity: sha512-LWXgwI5zTMatvR8w8kCNh/priDTOF/ZssokMBHJ7ZWXFoYLVOYo0EJERD9Eajv+xsfQO1QkuAt77KWQ1OI4mOQ==} + peerDependencies: + typescript: ^5.6.0 + peerDependenciesMeta: + typescript: + optional: true + '@glimmer/compiler@0.87.1': resolution: {integrity: sha512-7qXrOv55cH/YW+Vs4dFkNJsNXAW/jP+7kZLhKcH8wCduPfBCQxb9HNh1lBESuFej2rCks6h9I1qXeZHkc/oWxQ==} engines: {node: '>= 16.0.0'} @@ -1502,11 +1595,11 @@ packages: '@handlebars/parser@2.0.0': resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} - '@hashicorp-internal/vault-reporting@file:vault-reporting/0.8.0.tgz': - resolution: {integrity: sha512-3WDH1gPWjCNNQQlZvO1wDCq8gT7ce7Aawpn0zP/8OttzyZDe5uRrKsKw6RtYJbgxpNcz+rMAYR2HN5eXLQeq0g==, tarball: file:vault-reporting/0.8.0.tgz} - version: 0.8.0 + '@hashicorp-internal/vault-reporting@file:vault-reporting/0.21.0.tgz': + resolution: {integrity: sha512-PxIc7KyBEeRguh/wysC+OClVIKI34omonikdtDwt5gj8JJY/0UOwGMNB88ri7th3LfAkoWXGyycHtZa0hGxBZA==, tarball: file:vault-reporting/0.21.0.tgz} + version: 0.21.0 peerDependencies: - '@hashicorp/design-system-components': ^4.22.1 + '@hashicorp/design-system-components': 4.23.0 - 6 '@hashicorp/design-system-components@4.24.1': resolution: {integrity: sha512-rcRbBKc9MWngtVTRKr1i0ixbQypj4tEF8ZnsFrUVZLm7CMS8R5/+cfSu3pgbPriOOqZDCx7qAhNqAeTim4AYag==} @@ -1559,10 +1652,16 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1580,6 +1679,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsdoc/salty@0.2.9': resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==} engines: {node: '>=v12.0.0'} @@ -3045,6 +3147,9 @@ packages: resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} engines: {node: '>=8'} + cldr-core@47.0.0: + resolution: {integrity: sha512-tdYRy66DMgpjEwVOWCTN0zhNr+zh1+d4A6MCNgJKU7voFDGsrwcWHor6jcqudHDmElCgyVNqWBKAB1JeNdSOKg==} + clean-base-url@1.0.0: resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} @@ -3757,6 +3862,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decorator-transforms@1.2.1: resolution: {integrity: sha512-UUtmyfdlHvYoX3VSG1w5rbvBQ2r5TX1JsE4hmKU9snleFymadA3VACjl6SRfi9YgBCSjBbfQvR1bs9PRW9yBKw==} @@ -3920,6 +4028,10 @@ packages: resolution: {integrity: sha512-bcBFDYVTFHyqyq8BNvsj6UO3pE6Uqou/cNmee0WaqBgZ+1nQqFz0UE26usrtnFAT+YaFZSkqF2H36QW84k0/cg==} engines: {node: 12.* || 14.* || >= 16} + ember-auto-import@2.12.1: + resolution: {integrity: sha512-wyvl+aJJKOKbRSLqq6CyMsNrvurmX4SIWHHqZdC5giZ7P8ECGmcn9W9HFoVLpwXkFJoXhNV4L7mqqcU6881t0w==} + engines: {node: 12.* || 14.* || >= 16} + ember-basic-dropdown@8.7.0: resolution: {integrity: sha512-M++bfghpFXVRs3A3VdEtHcrv41STf2Far8JWJf3Sukh41oWVm76P0R6ZXBPJ0MyCJwR2qlHgCn1QIesP3fSOpA==} peerDependencies: @@ -4178,6 +4290,15 @@ packages: resolution: {integrity: sha512-+oRstEa52mm0jAFzhr51/xtEWpCEykB3SEBr7vUg8YnXUZJ5hKNBppP938q8Zzr9XfJEbzrtDSGjhKwJCJv6FQ==} engines: {node: 10.* || 12.* || >= 14} + ember-intl@7.4.1: + resolution: {integrity: sha512-qFzlCbxeKAT6DPmx+iJTB0Aah1G7VbZfBhbkBgXYeDi9JkrjtertiPjFjHnDImhEG4mX7Znya9qTYaKBnLFmmg==} + engines: {node: 18.* || >= 20} + peerDependencies: + '@ember/test-helpers': ^2.9.4 || ^3.2.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + ember-load-initializers@3.0.1: resolution: {integrity: sha512-qV3vxJKw5+7TVDdtdLPy8PhVsh58MlK8jwzqh5xeOwJPNP7o0+BlhvwoIlLYTPzGaHdfjEIFCgVSyMRGd74E1g==} engines: {node: '>= 18.*'} @@ -5349,6 +5470,9 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + invert-kv@3.0.1: resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} engines: {node: '>=8'} @@ -6050,6 +6174,10 @@ packages: resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} engines: {node: '>=8'} + mem@8.1.1: + resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} + engines: {node: '>=10'} + memory-streams@0.1.3: resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} @@ -6130,6 +6258,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -7818,8 +7950,8 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -8260,8 +8392,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.27.2': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -8302,6 +8442,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 1.0.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/eslint-parser@7.28.5(@babel/core@7.26.10)(eslint@8.57.1)': dependencies: '@babel/core': 7.26.10 @@ -8318,6 +8478,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.1': dependencies: '@babel/types': 7.27.1 @@ -8330,6 +8498,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.5 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8356,6 +8532,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8370,6 +8559,13 @@ snapshots: regexpu-core: 6.2.0 semver: 6.3.1 + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + regexpu-core: 6.2.0 + semver: 6.3.1 + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8392,6 +8588,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.27.1 @@ -8406,6 +8615,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8424,6 +8640,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.27.1 @@ -8448,6 +8682,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8466,6 +8709,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.27.1 @@ -8477,6 +8729,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.27.1': @@ -8492,10 +8746,19 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.27.1 + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.27.2': dependencies: '@babel/types': 7.27.1 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8512,6 +8775,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8522,6 +8793,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8532,6 +8808,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8550,6 +8831,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8566,6 +8856,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8582,6 +8880,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8600,6 +8906,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.26.10)': dependencies: '@babel/compat-data': 7.27.2 @@ -8625,6 +8940,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8633,6 +8956,10 @@ snapshots: dependencies: '@babel/core': 7.27.1 + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8653,6 +8980,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8663,6 +9000,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8673,6 +9015,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8683,6 +9030,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8703,6 +9055,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8713,6 +9070,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8725,6 +9087,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8735,6 +9103,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-async-generator-functions@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8753,6 +9126,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-generator-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8771,6 +9153,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8781,6 +9172,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8791,6 +9187,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8807,6 +9208,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8823,6 +9232,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8847,6 +9264,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.27.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8859,6 +9288,12 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + '@babel/plugin-transform-destructuring@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8869,6 +9304,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8881,6 +9321,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8891,6 +9337,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8903,6 +9354,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8913,6 +9370,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8923,6 +9385,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8933,6 +9400,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8949,6 +9421,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8967,6 +9447,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8977,6 +9466,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8987,6 +9481,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -8997,6 +9496,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9007,6 +9511,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9023,6 +9532,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9039,6 +9556,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9059,6 +9584,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9075,6 +9610,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9087,6 +9630,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9097,6 +9646,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9107,6 +9661,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9117,6 +9676,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-object-rest-spread@7.27.2(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9133,6 +9697,14 @@ snapshots: '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-object-rest-spread@7.27.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9149,6 +9721,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9159,6 +9739,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9175,6 +9760,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9185,6 +9778,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9201,6 +9799,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9219,6 +9825,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9229,6 +9844,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9239,6 +9859,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9251,6 +9876,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9261,6 +9892,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-runtime@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9285,6 +9921,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-runtime@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9295,6 +9943,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9311,6 +9964,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9321,6 +9982,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9331,6 +9997,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9341,6 +10012,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9363,6 +10039,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-typescript@7.5.5(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9382,6 +10069,11 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9394,6 +10086,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9406,6 +10104,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9418,6 +10122,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/polyfill@7.12.1': dependencies: core-js: 2.6.12 @@ -9648,6 +10358,81 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-env@7.27.2(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.27.2(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.29.0) + core-js-compat: 3.42.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9662,6 +10447,13 @@ snapshots: '@babel/types': 7.27.1 esutils: 2.0.3 + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.27.1 + esutils: 2.0.3 + '@babel/preset-typescript@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -9683,6 +10475,12 @@ snapshots: '@babel/parser': 7.27.2 '@babel/types': 7.27.1 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.27.1': dependencies: '@babel/code-frame': 7.27.1 @@ -9695,11 +10493,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.1': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@carbon/charts@1.27.2': dependencies: '@carbon/colors': 11.45.0 @@ -10184,6 +10999,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@embroider/reverse-exports@0.2.0': + dependencies: + mem: 8.1.1 + resolve.exports: 2.0.3 + '@embroider/shared-internals@2.5.2': dependencies: babel-import-util: 2.1.1 @@ -10267,6 +11087,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@faker-js/faker@8.4.1': {} + '@floating-ui/core@1.7.0': dependencies: '@floating-ui/utils': 0.2.9 @@ -10278,6 +11100,42 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl@3.1.8(typescript@5.6.3)': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + intl-messageformat: 10.7.18 + tslib: 2.8.1 + optionalDependencies: + typescript: 5.6.3 + '@glimmer/compiler@0.87.1': dependencies: '@glimmer/interfaces': 0.87.1 @@ -10508,20 +11366,27 @@ snapshots: '@handlebars/parser@2.0.0': {} - '@hashicorp-internal/vault-reporting@file:vault-reporting/0.8.0.tgz(@babel/core@7.26.10)(@glint/template@1.7.3)(@hashicorp/design-system-components@4.24.1(9e1e7970f02a7597d41eecd5c69ae0c6))(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))': + '@hashicorp-internal/vault-reporting@file:vault-reporting/0.21.0.tgz(@babel/core@7.26.10)(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(@hashicorp/design-system-components@4.24.1(080995f06707449af57cc37e52df9a24))(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))(typescript@5.6.3)(webpack@5.94.0)': dependencies: '@embroider/macros': 1.15.0(@glint/template@1.7.3) - '@hashicorp/design-system-components': 4.24.1(9e1e7970f02a7597d41eecd5c69ae0c6) + '@faker-js/faker': 8.4.1 + '@hashicorp/design-system-components': 4.24.1(080995f06707449af57cc37e52df9a24) '@lineal-viz/lineal': 0.5.1(@babel/core@7.26.10)(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)) decorator-transforms: 2.3.0(@babel/core@7.26.10) + ember-intl: 7.4.1(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(typescript@5.6.3)(webpack@5.94.0) ember-modifier: 4.2.2(@babel/core@7.26.10) + ember-truth-helpers: 4.0.3(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)) + miragejs: 0.1.48 transitivePeerDependencies: - '@babel/core' + - '@ember/test-helpers' - '@glint/template' - ember-source - supports-color + - typescript + - webpack - '@hashicorp/design-system-components@4.24.1(9e1e7970f02a7597d41eecd5c69ae0c6)': + '@hashicorp/design-system-components@4.24.1(080995f06707449af57cc37e52df9a24)': dependencies: '@codemirror/commands': 6.8.1 '@codemirror/lang-go': 6.0.1 @@ -10568,6 +11433,7 @@ snapshots: tracked-built-ins: 4.0.0(@babel/core@7.26.10) optionalDependencies: ember-engines: 0.8.23(@ember/legacy-built-in-components@0.4.2(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)))(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)) + ember-intl: 7.4.1(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(typescript@5.6.3)(webpack@5.94.0) transitivePeerDependencies: - '@babel/core' - '@ember/test-helpers' @@ -10612,12 +11478,22 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} @@ -10634,6 +11510,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jsdoc/salty@0.2.9': dependencies: lodash: 4.17.23 @@ -11356,22 +12237,22 @@ snapshots: '@types/unist@2.0.11': {} - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.5.4) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3) debug: 4.4.1 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare-lite: 1.4.0 semver: 7.7.2 - tsutils: 3.21.0(typescript@5.5.4) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11387,15 +12268,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) debug: 4.4.1 eslint: 8.57.1 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11404,15 +12285,15 @@ snapshots: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3) debug: 4.4.1 eslint: 8.57.1 - tsutils: 3.21.0(typescript@5.5.4) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -11432,7 +12313,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 @@ -11440,20 +12321,20 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 - tsutils: 3.21.0(typescript@5.5.4) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) '@types/json-schema': 7.0.15 '@types/semver': 7.7.0 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) eslint: 8.57.1 eslint-scope: 5.1.1 semver: 7.7.2 @@ -11835,6 +12716,15 @@ snapshots: babel-import-util@3.0.1: {} + babel-loader@8.4.1(@babel/core@7.26.10)(webpack@5.94.0): + dependencies: + '@babel/core': 7.26.10 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.94.0(webpack-cli@6.0.1) + babel-loader@8.4.1(@babel/core@7.27.1)(webpack@5.94.0): dependencies: '@babel/core': 7.27.1 @@ -11866,6 +12756,11 @@ snapshots: '@babel/core': 7.27.1 semver: 5.7.2 + babel-plugin-debug-macros@0.3.4(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + semver: 5.7.2 + babel-plugin-ember-data-packages-polyfill@0.1.2: dependencies: '@ember-data/rfc395-data': 0.0.4 @@ -11930,6 +12825,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -11946,6 +12850,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.29.0) + core-js-compat: 3.42.0 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -11960,6 +12872,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + babel-plugin-syntax-dynamic-import@6.18.0: {} backbone@1.6.1: @@ -12108,6 +13027,20 @@ snapshots: transitivePeerDependencies: - supports-color + broccoli-babel-transpiler@8.0.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + broccoli-persistent-filter: 3.1.3 + clone: 2.1.2 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + rsvp: 4.8.5 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + broccoli-bridge@1.0.0: dependencies: broccoli-plugin: 1.3.1 @@ -12749,6 +13682,8 @@ snapshots: ci-info@4.2.0: {} + cldr-core@47.0.0: {} + clean-base-url@1.0.0: {} clean-css-promise@0.1.1: @@ -13025,14 +13960,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@9.0.0(typescript@5.5.4): + cosmiconfig@9.0.0(typescript@5.6.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 crelt@1.0.6: {} @@ -13313,6 +14248,8 @@ snapshots: decamelize@1.2.0: {} + decimal.js@10.6.0: {} + decorator-transforms@1.2.1(@babel/core@7.26.10): dependencies: '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.26.10) @@ -13558,6 +14495,50 @@ snapshots: - supports-color - webpack + ember-auto-import@2.12.1(@glint/template@1.7.3)(webpack@5.94.0): + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.10) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.26.10) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.10) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.26.10) + '@babel/preset-env': 7.26.9(@babel/core@7.26.10) + '@embroider/macros': 1.15.0(@glint/template@1.7.3) + '@embroider/reverse-exports': 0.2.0 + '@embroider/shared-internals': 2.9.0 + babel-loader: 8.4.1(@babel/core@7.26.10)(webpack@5.94.0) + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-ember-template-compilation: 2.4.1 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + babel-plugin-syntax-dynamic-import: 6.18.0 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + css-loader: 5.2.7(webpack@5.94.0) + debug: 4.4.3 + fs-extra: 10.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + mini-css-extract-plugin: 2.9.2(webpack@5.94.0) + minimatch: 3.1.5 + parse5: 6.0.1 + pkg-entry-points: 1.1.1 + resolve: 1.22.10 + resolve-package-path: 4.0.3 + semver: 7.7.4 + style-loader: 2.0.0(webpack@5.94.0) + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + - webpack + ember-basic-dropdown@8.7.0(@babel/core@7.26.10)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)): dependencies: '@ember/test-helpers': 5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3) @@ -13705,6 +14686,39 @@ snapshots: transitivePeerDependencies: - supports-color + ember-cli-babel@8.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.29.0) + '@babel/preset-env': 7.27.2(@babel/core@7.29.0) + '@babel/runtime': 7.27.0 + amd-name-resolver: 1.3.1 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-ember-data-packages-polyfill: 0.1.2 + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-module-resolver: 5.0.2 + broccoli-babel-transpiler: 8.0.2(@babel/core@7.29.0) + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-source: 3.0.1 + calculate-cache-key-for-tree: 2.0.0 + clone: 2.1.2 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 + ensure-posix-path: 1.1.1 + resolve-package-path: 4.0.3 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + ember-cli-clean-css@3.0.0: dependencies: broccoli-persistent-filter: 3.1.3 @@ -13948,12 +14962,12 @@ snapshots: dependencies: ansi-to-html: 0.6.15 broccoli-stew: 3.0.0 - debug: 4.4.1 + debug: 4.4.3 execa: 4.1.0 fs-extra: 9.1.0 resolve: 1.22.10 rsvp: 4.8.5 - semver: 7.7.2 + semver: 7.7.4 stagehand: 1.0.1 walk-sync: 2.2.0 transitivePeerDependencies: @@ -14294,6 +15308,32 @@ snapshots: transitivePeerDependencies: - supports-color + ember-intl@7.4.1(@ember/test-helpers@5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3))(@glint/template@1.7.3)(typescript@5.6.3)(webpack@5.94.0): + dependencies: + '@babel/core': 7.29.0 + '@formatjs/icu-messageformat-parser': 2.11.4 + '@formatjs/intl': 3.1.8(typescript@5.6.3) + broccoli-caching-writer: 3.0.3 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-source: 3.0.1 + calculate-cache-key-for-tree: 2.0.0 + cldr-core: 47.0.0 + ember-auto-import: 2.12.1(@glint/template@1.7.3)(webpack@5.94.0) + ember-cli-babel: 8.2.0(@babel/core@7.29.0) + ember-cli-typescript: 5.3.0 + extend: 3.0.2 + intl-messageformat: 10.7.18 + js-yaml: 4.1.1 + json-stable-stringify: 1.3.0 + optionalDependencies: + '@ember/test-helpers': 5.2.2(@babel/core@7.26.10)(@glint/template@1.7.3) + transitivePeerDependencies: + - '@glint/template' + - supports-color + - typescript + - webpack + ember-load-initializers@3.0.1(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0)): dependencies: ember-source: 5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0) @@ -14796,7 +15836,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@8.57.1): dependencies: eslint: 8.57.1 - semver: 7.7.2 + semver: 7.7.4 eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: @@ -15959,6 +16999,13 @@ snapshots: interpret@3.1.1: {} + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + invert-kv@3.0.1: {} ipaddr.js@1.9.1: {} @@ -16716,6 +17763,11 @@ snapshots: mimic-fn: 2.1.0 p-is-promise: 2.1.0 + mem@8.1.1: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 3.1.0 + memory-streams@0.1.3: dependencies: readable-stream: 1.0.34 @@ -16786,7 +17838,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.4.1 + debug: 4.4.3 parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -16810,6 +17862,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@3.1.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -16932,7 +17986,7 @@ snapshots: dependencies: growly: 1.3.0 is-wsl: 2.2.0 - semver: 7.7.2 + semver: 7.7.4 shellwords: 0.1.1 uuid: 8.3.2 which: 2.0.2 @@ -18239,22 +19293,22 @@ snapshots: styled_string@0.0.1: {} - stylelint-config-recommended@16.0.0(stylelint@16.19.1(typescript@5.5.4)): + stylelint-config-recommended@16.0.0(stylelint@16.19.1(typescript@5.6.3)): dependencies: - stylelint: 16.19.1(typescript@5.5.4) + stylelint: 16.19.1(typescript@5.6.3) - stylelint-config-standard@38.0.0(stylelint@16.19.1(typescript@5.5.4)): + stylelint-config-standard@38.0.0(stylelint@16.19.1(typescript@5.6.3)): dependencies: - stylelint: 16.19.1(typescript@5.5.4) - stylelint-config-recommended: 16.0.0(stylelint@16.19.1(typescript@5.5.4)) + stylelint: 16.19.1(typescript@5.6.3) + stylelint-config-recommended: 16.0.0(stylelint@16.19.1(typescript@5.6.3)) - stylelint-prettier@5.0.3(prettier@3.0.3)(stylelint@16.19.1(typescript@5.5.4)): + stylelint-prettier@5.0.3(prettier@3.0.3)(stylelint@16.19.1(typescript@5.6.3)): dependencies: prettier: 3.0.3 prettier-linter-helpers: 1.0.0 - stylelint: 16.19.1(typescript@5.5.4) + stylelint: 16.19.1(typescript@5.6.3) - stylelint@16.19.1(typescript@5.5.4): + stylelint@16.19.1(typescript@5.6.3): dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 @@ -18263,7 +19317,7 @@ snapshots: '@dual-bundle/import-meta-resolve': 4.1.0 balanced-match: 2.0.0 colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.5.4) + cosmiconfig: 9.0.0(typescript@5.6.3) css-functions-list: 3.2.3 css-tree: 3.1.0 debug: 4.4.1 @@ -18639,10 +19693,10 @@ snapshots: tslib: 1.14.1 typescript: 4.9.5 - tsutils@3.21.0(typescript@5.5.4): + tsutils@3.21.0(typescript@5.6.3): dependencies: tslib: 1.14.1 - typescript: 5.5.4 + typescript: 5.6.3 type-check@0.4.0: dependencies: @@ -18719,7 +19773,7 @@ snapshots: typescript@4.9.5: {} - typescript@5.5.4: {} + typescript@5.6.3: {} typical@2.6.1: {} @@ -18842,7 +19896,7 @@ snapshots: validate-peer-dependencies@1.2.0: dependencies: resolve-package-path: 3.1.0 - semver: 7.7.2 + semver: 7.7.4 validate-peer-dependencies@2.2.0: dependencies: diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml index 254d1e2ea8..cc3228bd3c 100644 --- a/ui/pnpm-workspace.yaml +++ b/ui/pnpm-workspace.yaml @@ -6,6 +6,9 @@ peerDependencyRules: allowedVersions: # for @typescript-eslint/parser eslint: '*' + # for ember-engines, which has a peer dep on ember-source + 'ember-engines>ember-source': '5.8.0' + '@hashicorp/design-system-components>ember-engines': '0.8.23' dedupeInjectedDeps: true publicHoistPattern: diff --git a/ui/tests/helpers/index.js b/ui/tests/helpers/index.js index bf5daaccd4..0f002e79e8 100644 --- a/ui/tests/helpers/index.js +++ b/ui/tests/helpers/index.js @@ -8,6 +8,7 @@ import { setupRenderingTest as upstreamSetupRenderingTest, setupTest as upstreamSetupTest, } from 'ember-qunit'; +import { setupIntl } from 'ember-intl/test-support'; // This file exists to provide wrappers around ember-qunit's // test setup functions. This way, you can easily extend the setup that is @@ -34,7 +35,7 @@ function setupApplicationTest(hooks, options) { function setupRenderingTest(hooks, options) { upstreamSetupRenderingTest(hooks, options); - + setupIntl(hooks, 'en-us'); // Additional setup for rendering tests can be done here. } diff --git a/ui/tests/integration/components/alert-inline-test.js b/ui/tests/integration/components/alert-inline-test.js index e8724cddc7..bcb33e9cde 100644 --- a/ui/tests/integration/components/alert-inline-test.js +++ b/ui/tests/integration/components/alert-inline-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled, find, waitUntil } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/auth-config-form/config-test.js b/ui/tests/integration/components/auth-config-form/config-test.js index b1322c0087..0c405d5091 100644 --- a/ui/tests/integration/components/auth-config-form/config-test.js +++ b/ui/tests/integration/components/auth-config-form/config-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/auth-config-form/options-test.js b/ui/tests/integration/components/auth-config-form/options-test.js index c422513100..17dc50518a 100644 --- a/ui/tests/integration/components/auth-config-form/options-test.js +++ b/ui/tests/integration/components/auth-config-form/options-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/auth-method/configuration-test.js b/ui/tests/integration/components/auth-method/configuration-test.js index e14338ce12..198daeb629 100644 --- a/ui/tests/integration/components/auth-method/configuration-test.js +++ b/ui/tests/integration/components/auth-method/configuration-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/auth/fields-test.js b/ui/tests/integration/components/auth/fields-test.js index 44d1a084e9..98db3d7c2e 100644 --- a/ui/tests/integration/components/auth/fields-test.js +++ b/ui/tests/integration/components/auth/fields-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import hbs from 'htmlbars-inline-precompile'; import { find, render } from '@ember/test-helpers'; import { capitalize } from '@ember/string'; diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js index 1c80437161..2903414855 100644 --- a/ui/tests/integration/components/auth/form-template-test.js +++ b/ui/tests/integration/components/auth/form-template-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, fillIn, find, findAll, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/auth/form/base-test.js b/ui/tests/integration/components/auth/form/base-test.js index 33768d890d..79ba7c7bf4 100644 --- a/ui/tests/integration/components/auth/form/base-test.js +++ b/ui/tests/integration/components/auth/form/base-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import hbs from 'htmlbars-inline-precompile'; import { find, render } from '@ember/test-helpers'; import sinon from 'sinon'; @@ -45,20 +45,20 @@ module('Integration | Component | auth | form | base', function (hooks) { this.renderComponent = ({ yieldBlock = false } = {}) => { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - { if (yieldBlock) { return render(hbs` - <:advancedSettings> - + `); } return render(hbs` - " { - path = "my-mount" - type = "kv-v2" + path = "my-mount" + type = "kv-v2" }`; const expectedCli = 'vault kv delete -mount=secret creds'; await click(GENERAL.hdsTab('cli')); @@ -79,8 +79,8 @@ module('Integration | Component | code-generator/automation-snippets', function await this.renderComponent(); const expectedSnippet = `resource "vault_mount" "" { namespace = "admin" - path = "my-mount" - type = "kv-v2" + path = "my-mount" + type = "kv-v2" }`; assert .dom(GENERAL.fieldByAttr('terraform')) diff --git a/ui/tests/integration/components/code-generator/policy/builder-test.js b/ui/tests/integration/components/code-generator/policy/builder-test.js index 506e5058a4..6c6caa47cf 100644 --- a/ui/tests/integration/components/code-generator/policy/builder-test.js +++ b/ui/tests/integration/components/code-generator/policy/builder-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, setupOnerror } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/code-generator/policy/flyout-test.js b/ui/tests/integration/components/code-generator/policy/flyout-test.js index 7dd16b8b09..662c712736 100644 --- a/ui/tests/integration/components/code-generator/policy/flyout-test.js +++ b/ui/tests/integration/components/code-generator/policy/flyout-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, typeIn, waitUntil, find } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/code-generator/policy/stanza-test.js b/ui/tests/integration/components/code-generator/policy/stanza-test.js index c5e871a269..247b76342c 100644 --- a/ui/tests/integration/components/code-generator/policy/stanza-test.js +++ b/ui/tests/integration/components/code-generator/policy/stanza-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, typeIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import Sinon from 'sinon'; diff --git a/ui/tests/integration/components/confirm-action-test.js b/ui/tests/integration/components/confirm-action-test.js index a5897d8dce..25668a3d19 100644 --- a/ui/tests/integration/components/confirm-action-test.js +++ b/ui/tests/integration/components/confirm-action-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/confirmation-modal-test.js b/ui/tests/integration/components/confirmation-modal-test.js index 7bd94ba04a..50ac740a60 100644 --- a/ui/tests/integration/components/confirmation-modal-test.js +++ b/ui/tests/integration/components/confirmation-modal-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import sinon from 'sinon'; import { click, fillIn, render } from '@ember/test-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/console/log-command-test.js b/ui/tests/integration/components/console/log-command-test.js index a8bfeaa23a..70ffb57482 100644 --- a/ui/tests/integration/components/console/log-command-test.js +++ b/ui/tests/integration/components/console/log-command-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/console/log-error-test.js b/ui/tests/integration/components/console/log-error-test.js index 070a702c4a..58f37546ca 100644 --- a/ui/tests/integration/components/console/log-error-test.js +++ b/ui/tests/integration/components/console/log-error-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/console/log-json-test.js b/ui/tests/integration/components/console/log-json-test.js index 4f195ec897..bd3e59b726 100644 --- a/ui/tests/integration/components/console/log-json-test.js +++ b/ui/tests/integration/components/console/log-json-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { assertCodeBlockValue } from 'vault/tests/helpers/codemirror'; diff --git a/ui/tests/integration/components/console/log-list-test.js b/ui/tests/integration/components/console/log-list-test.js index 51d1c66262..ba6a420fc6 100644 --- a/ui/tests/integration/components/console/log-list-test.js +++ b/ui/tests/integration/components/console/log-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/console/log-object-test.js b/ui/tests/integration/components/console/log-object-test.js index eedf528d6b..4a70f21e70 100644 --- a/ui/tests/integration/components/console/log-object-test.js +++ b/ui/tests/integration/components/console/log-object-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stringifyObjectValues } from 'vault/components/console/log-object'; diff --git a/ui/tests/integration/components/console/log-text-test.js b/ui/tests/integration/components/console/log-text-test.js index 0f926f4baf..ce9b34836e 100644 --- a/ui/tests/integration/components/console/log-text-test.js +++ b/ui/tests/integration/components/console/log-text-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/console/ui-panel-test.js b/ui/tests/integration/components/console/ui-panel-test.js index a2b2acd1c3..c11f83cf85 100644 --- a/ui/tests/integration/components/console/ui-panel-test.js +++ b/ui/tests/integration/components/console/ui-panel-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled } from '@ember/test-helpers'; import { create } from 'ember-cli-page-object'; import uiPanel from 'vault/tests/pages/components/console/ui-panel'; diff --git a/ui/tests/integration/components/control-group-success-test.js b/ui/tests/integration/components/control-group-success-test.js index 874f159b39..cf20e7c3fd 100644 --- a/ui/tests/integration/components/control-group-success-test.js +++ b/ui/tests/integration/components/control-group-success-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, fillIn, find, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/control-group-test.js b/ui/tests/integration/components/control-group-test.js index 0eb64d4b00..e849a7a8f7 100644 --- a/ui/tests/integration/components/control-group-test.js +++ b/ui/tests/integration/components/control-group-test.js @@ -5,7 +5,7 @@ import Service from '@ember/service'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/database-role-edit-test.js b/ui/tests/integration/components/database-role-edit-test.js index 83961ce02b..113d8d6ffa 100644 --- a/ui/tests/integration/components/database-role-edit-test.js +++ b/ui/tests/integration/components/database-role-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/database-role-setting-form-test.js b/ui/tests/integration/components/database-role-setting-form-test.js index fac274e1cd..6e3d606490 100644 --- a/ui/tests/integration/components/database-role-setting-form-test.js +++ b/ui/tests/integration/components/database-role-setting-form-test.js @@ -5,7 +5,7 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { setRunOptions } from 'ember-a11y-testing/test-support'; diff --git a/ui/tests/integration/components/disabled-plugin-card-test.js b/ui/tests/integration/components/disabled-plugin-card-test.js index 882ca78c3d..bd1c214768 100644 --- a/ui/tests/integration/components/disabled-plugin-card-test.js +++ b/ui/tests/integration/components/disabled-plugin-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, triggerKeyEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; @@ -25,8 +25,8 @@ module('Integration | Component | disabled-plugin-card', function (hooks) { test('it renders disabled plugin card', async function (assert) { await render(hbs` - @@ -44,8 +44,8 @@ module('Integration | Component | disabled-plugin-card', function (hooks) { test('it handles click events', async function (assert) { await render(hbs` - @@ -59,8 +59,8 @@ module('Integration | Component | disabled-plugin-card', function (hooks) { test('it handles keyboard events', async function (assert) { await render(hbs` - @@ -79,8 +79,8 @@ module('Integration | Component | disabled-plugin-card', function (hooks) { }; await render(hbs` - @@ -94,8 +94,8 @@ module('Integration | Component | disabled-plugin-card', function (hooks) { test('it renders and displays plugin information correctly', async function (assert) { await render(hbs` - diff --git a/ui/tests/integration/components/download-button-test.js b/ui/tests/integration/components/download-button-test.js index e689dd45a9..db78fc9b43 100644 --- a/ui/tests/integration/components/download-button-test.js +++ b/ui/tests/integration/components/download-button-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/edit-form-test.js b/ui/tests/integration/components/edit-form-test.js index 49967895d6..92b5fb6760 100644 --- a/ui/tests/integration/components/edit-form-test.js +++ b/ui/tests/integration/components/edit-form-test.js @@ -5,7 +5,7 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/empty-state-test.js b/ui/tests/integration/components/empty-state-test.js index 546f80df39..d04122ec46 100644 --- a/ui/tests/integration/components/empty-state-test.js +++ b/ui/tests/integration/components/empty-state-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/enabled-plugin-card-test.js b/ui/tests/integration/components/enabled-plugin-card-test.js index 801505f225..eb211f0e82 100644 --- a/ui/tests/integration/components/enabled-plugin-card-test.js +++ b/ui/tests/integration/components/enabled-plugin-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, triggerKeyEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/filter-input-explicit-test.js b/ui/tests/integration/components/filter-input-explicit-test.js index bb4767a400..2f25b36961 100644 --- a/ui/tests/integration/components/filter-input-explicit-test.js +++ b/ui/tests/integration/components/filter-input-explicit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, typeIn, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/filter-input-test.js b/ui/tests/integration/components/filter-input-test.js index 2614a9dc06..57b26f381c 100644 --- a/ui/tests/integration/components/filter-input-test.js +++ b/ui/tests/integration/components/filter-input-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/form-field-label-test.js b/ui/tests/integration/components/form-field-label-test.js index 7fde80e63d..f37462fca3 100644 --- a/ui/tests/integration/components/form-field-label-test.js +++ b/ui/tests/integration/components/form-field-label-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { click } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js index 93506a1b10..e2f564c603 100644 --- a/ui/tests/integration/components/form-field-test.js +++ b/ui/tests/integration/components/form-field-test.js @@ -5,7 +5,7 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, findAll, setupOnerror, waitFor } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { create } from 'ember-cli-page-object'; diff --git a/ui/tests/integration/components/get-credentials-card-test.js b/ui/tests/integration/components/get-credentials-card-test.js index efd216a735..44bcce322d 100644 --- a/ui/tests/integration/components/get-credentials-card-test.js +++ b/ui/tests/integration/components/get-credentials-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import Service from '@ember/service'; import { click, render } from '@ember/test-helpers'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; diff --git a/ui/tests/integration/components/icon-test.js b/ui/tests/integration/components/icon-test.js index 986fd242d5..dfa6d422c6 100644 --- a/ui/tests/integration/components/icon-test.js +++ b/ui/tests/integration/components/icon-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import waitForError from 'vault/tests/helpers/wait-for-error'; diff --git a/ui/tests/integration/components/identity/item-details-test.js b/ui/tests/integration/components/identity/item-details-test.js index 0696dbae64..547da7b263 100644 --- a/ui/tests/integration/components/identity/item-details-test.js +++ b/ui/tests/integration/components/identity/item-details-test.js @@ -6,7 +6,7 @@ import { resolve } from 'rsvp'; import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/info-table-item-array-test.js b/ui/tests/integration/components/info-table-item-array-test.js index 8a3a2a270f..6cf4fd0881 100644 --- a/ui/tests/integration/components/info-table-item-array-test.js +++ b/ui/tests/integration/components/info-table-item-array-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import Service from '@ember/service'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { findAll, render } from '@ember/test-helpers'; import { run } from '@ember/runloop'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/info-table-row-test.js b/ui/tests/integration/components/info-table-row-test.js index 72c4fdfecb..dcf2bfe195 100644 --- a/ui/tests/integration/components/info-table-row-test.js +++ b/ui/tests/integration/components/info-table-row-test.js @@ -6,7 +6,7 @@ import { module, test } from 'qunit'; import { resolve } from 'rsvp'; import Service from '@ember/service'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js index 0cbec9d452..7d85f2d7b3 100644 --- a/ui/tests/integration/components/json-editor-test.js +++ b/ui/tests/integration/components/json-editor-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, triggerKeyEvent, waitFor, setupOnerror } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { SELECTORS } from '../../pages/components/json-editor'; diff --git a/ui/tests/integration/components/keymgmt/distribute-test.js b/ui/tests/integration/components/keymgmt/distribute-test.js index a97da2a778..e80f946fc5 100644 --- a/ui/tests/integration/components/keymgmt/distribute-test.js +++ b/ui/tests/integration/components/keymgmt/distribute-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled, select } from '@ember/test-helpers'; import { create } from 'ember-cli-page-object'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/keymgmt/key-edit-test.js b/ui/tests/integration/components/keymgmt/key-edit-test.js index 7bc8b5364d..ad9aa260d6 100644 --- a/ui/tests/integration/components/keymgmt/key-edit-test.js +++ b/ui/tests/integration/components/keymgmt/key-edit-test.js @@ -6,7 +6,7 @@ import { module, test } from 'qunit'; import sinon from 'sinon'; import EmberObject from '@ember/object'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/keymgmt/provider-edit-test.js b/ui/tests/integration/components/keymgmt/provider-edit-test.js index 9eceaf1676..2cd3646bd0 100644 --- a/ui/tests/integration/components/keymgmt/provider-edit-test.js +++ b/ui/tests/integration/components/keymgmt/provider-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/kmip/details-credentials-test.js b/ui/tests/integration/components/kmip/details-credentials-test.js index 12cd4c25a4..83ff4c178d 100644 --- a/ui/tests/integration/components/kmip/details-credentials-test.js +++ b/ui/tests/integration/components/kmip/details-credentials-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { click, render, findAll } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/kmip/page/configuration-test.js b/ui/tests/integration/components/kmip/page/configuration-test.js index 8ac885c314..eb391e2908 100644 --- a/ui/tests/integration/components/kmip/page/configuration-test.js +++ b/ui/tests/integration/components/kmip/page/configuration-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/page/configure-test.js b/ui/tests/integration/components/kmip/page/configure-test.js index 0214211c87..8d083f3aab 100644 --- a/ui/tests/integration/components/kmip/page/configure-test.js +++ b/ui/tests/integration/components/kmip/page/configure-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/page/credentials-test.js b/ui/tests/integration/components/kmip/page/credentials-test.js index 7528824f04..d269009098 100644 --- a/ui/tests/integration/components/kmip/page/credentials-test.js +++ b/ui/tests/integration/components/kmip/page/credentials-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/page/credentials/generate-test.js b/ui/tests/integration/components/kmip/page/credentials/generate-test.js index b26682f15c..dd5046a4a0 100644 --- a/ui/tests/integration/components/kmip/page/credentials/generate-test.js +++ b/ui/tests/integration/components/kmip/page/credentials/generate-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/page/role-test.js b/ui/tests/integration/components/kmip/page/role-test.js index a1bc009a17..db0b9ac82a 100644 --- a/ui/tests/integration/components/kmip/page/role-test.js +++ b/ui/tests/integration/components/kmip/page/role-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { click, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/kmip/page/scope/roles-test.js b/ui/tests/integration/components/kmip/page/scope/roles-test.js index a601ae5fc9..9af007d016 100644 --- a/ui/tests/integration/components/kmip/page/scope/roles-test.js +++ b/ui/tests/integration/components/kmip/page/scope/roles-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/page/scopes-test.js b/ui/tests/integration/components/kmip/page/scopes-test.js index e16de02ea6..b214494e8c 100644 --- a/ui/tests/integration/components/kmip/page/scopes-test.js +++ b/ui/tests/integration/components/kmip/page/scopes-test.js @@ -6,7 +6,7 @@ import { click, fillIn, render } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupEngine } from 'ember-engines/test-support'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/kmip/page/scopes/create-test.js b/ui/tests/integration/components/kmip/page/scopes/create-test.js index 14e12a5c8c..14af48b368 100644 --- a/ui/tests/integration/components/kmip/page/scopes/create-test.js +++ b/ui/tests/integration/components/kmip/page/scopes/create-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kmip/role-form-test.js b/ui/tests/integration/components/kmip/role-form-test.js index 71b3add69f..719a38d332 100644 --- a/ui/tests/integration/components/kmip/role-form-test.js +++ b/ui/tests/integration/components/kmip/role-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/known-secondaries-card-test.js b/ui/tests/integration/components/known-secondaries-card-test.js index 682ac2f590..47e3ebf0f9 100644 --- a/ui/tests/integration/components/known-secondaries-card-test.js +++ b/ui/tests/integration/components/known-secondaries-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { setupEngine } from 'ember-engines/test-support'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/known-secondaries-table-test.js b/ui/tests/integration/components/known-secondaries-table-test.js index ea23aaad2d..7c602b5f54 100644 --- a/ui/tests/integration/components/known-secondaries-table-test.js +++ b/ui/tests/integration/components/known-secondaries-table-test.js @@ -5,7 +5,7 @@ /* eslint qunit/no-conditional-assertions: "warn" */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { setupEngine } from 'ember-engines/test-support'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/kubernetes/config-cta-test.js b/ui/tests/integration/components/kubernetes/config-cta-test.js index b2267ac8b4..7f7814c810 100644 --- a/ui/tests/integration/components/kubernetes/config-cta-test.js +++ b/ui/tests/integration/components/kubernetes/config-cta-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/kubernetes-header-test.js b/ui/tests/integration/components/kubernetes/kubernetes-header-test.js index a15e09789f..f72b387080 100644 --- a/ui/tests/integration/components/kubernetes/kubernetes-header-test.js +++ b/ui/tests/integration/components/kubernetes/kubernetes-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/configuration-test.js b/ui/tests/integration/components/kubernetes/page/configuration-test.js index 228452c203..433f906582 100644 --- a/ui/tests/integration/components/kubernetes/page/configuration-test.js +++ b/ui/tests/integration/components/kubernetes/page/configuration-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/configure-test.js b/ui/tests/integration/components/kubernetes/page/configure-test.js index 605c0328ba..6ce86f8a56 100644 --- a/ui/tests/integration/components/kubernetes/page/configure-test.js +++ b/ui/tests/integration/components/kubernetes/page/configure-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, fillIn } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/credentials-test.js b/ui/tests/integration/components/kubernetes/page/credentials-test.js index c56f9f63f1..ad63bde138 100644 --- a/ui/tests/integration/components/kubernetes/page/credentials-test.js +++ b/ui/tests/integration/components/kubernetes/page/credentials-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, fillIn } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/overview-test.js b/ui/tests/integration/components/kubernetes/page/overview-test.js index e4f4f68357..05b3531d4f 100644 --- a/ui/tests/integration/components/kubernetes/page/overview-test.js +++ b/ui/tests/integration/components/kubernetes/page/overview-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js index e9ae3acaf5..1289f1fd91 100644 --- a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js +++ b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, fillIn, waitFor, settled } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/role/details-test.js b/ui/tests/integration/components/kubernetes/page/role/details-test.js index 3930ea75ca..3004ef8deb 100644 --- a/ui/tests/integration/components/kubernetes/page/role/details-test.js +++ b/ui/tests/integration/components/kubernetes/page/role/details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kubernetes/page/roles-test.js b/ui/tests/integration/components/kubernetes/page/roles-test.js index 2ef2419ae4..999c6b35b6 100644 --- a/ui/tests/integration/components/kubernetes/page/roles-test.js +++ b/ui/tests/integration/components/kubernetes/page/roles-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kv-object-editor-test.js b/ui/tests/integration/components/kv-object-editor-test.js index 598d2acf38..fb8fa15e41 100644 --- a/ui/tests/integration/components/kv-object-editor-test.js +++ b/ui/tests/integration/components/kv-object-editor-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/kv-suggestion-input-test.js b/ui/tests/integration/components/kv-suggestion-input-test.js index adc3c309ee..70f919c361 100644 --- a/ui/tests/integration/components/kv-suggestion-input-test.js +++ b/ui/tests/integration/components/kv-suggestion-input-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, typeIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/kv/kv-paths-card-test.js b/ui/tests/integration/components/kv/kv-paths-card-test.js index d79063bf35..d305438063 100644 --- a/ui/tests/integration/components/kv/kv-paths-card-test.js +++ b/ui/tests/integration/components/kv/kv-paths-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-configuration-test.js b/ui/tests/integration/components/kv/page/kv-page-configuration-test.js index ff317490c0..721df11106 100644 --- a/ui/tests/integration/components/kv/page/kv-page-configuration-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-configuration-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-configure-test.js b/ui/tests/integration/components/kv/page/kv-page-configure-test.js index 36c273ff09..1f96b16b67 100644 --- a/ui/tests/integration/components/kv/page/kv-page-configure-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-configure-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-list-test.js b/ui/tests/integration/components/kv/page/kv-page-list-test.js index 07d1c6c95d..b42b3c876f 100644 --- a/ui/tests/integration/components/kv/page/kv-page-list-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render, click, fillIn, typeIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js b/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js index 7fdf72342b..fe8e8de350 100644 --- a/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js b/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js index 1dd59e06a6..69c7c408a9 100644 --- a/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, fillIn, click } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/kv/page/kv-page-overview-test.js b/ui/tests/integration/components/kv/page/kv-page-overview-test.js index 79f594788e..42fd70cf9a 100644 --- a/ui/tests/integration/components/kv/page/kv-page-overview-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-overview-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-patch-test.js b/ui/tests/integration/components/kv/page/kv-page-patch-test.js index 5c1acb661e..74a7a532e7 100644 --- a/ui/tests/integration/components/kv/page/kv-page-patch-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-patch-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { blur, click, fillIn, find, render, waitUntil } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js index 0cebeb195d..a796c63358 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js index 84be1d74d4..63d0778320 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js b/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js index 888c318ae5..8353527513 100644 --- a/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, findAll, render } from '@ember/test-helpers'; @@ -99,7 +99,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::VersionDiff', await render( hbs` <:customTableItem as |itemData|> - + <:popupMenu as |rowData|> diff --git a/ui/tests/integration/components/manage-dropdown-test.js b/ui/tests/integration/components/manage-dropdown-test.js index 66401971bf..f73c87ca96 100644 --- a/ui/tests/integration/components/manage-dropdown-test.js +++ b/ui/tests/integration/components/manage-dropdown-test.js @@ -4,7 +4,7 @@ */ import { click, render } from '@ember/test-helpers'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/masked-input-test.js b/ui/tests/integration/components/masked-input-test.js index 3f906e6727..9f53137ba7 100644 --- a/ui/tests/integration/components/masked-input-test.js +++ b/ui/tests/integration/components/masked-input-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, focus, triggerKeyEvent, typeIn, fillIn, click } from '@ember/test-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/message-error-test.js b/ui/tests/integration/components/message-error-test.js index a4063a62c5..3ef56b2d91 100644 --- a/ui/tests/integration/components/message-error-test.js +++ b/ui/tests/integration/components/message-error-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, findAll, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; @@ -21,11 +21,11 @@ module('Integration | Component | message-error', function (hooks) { this.renderComponent = () => { return render(hbs` - `); }; }); diff --git a/ui/tests/integration/components/mfa-login-enforcement-form-test.js b/ui/tests/integration/components/mfa-login-enforcement-form-test.js index 865022d18d..b09cf03218 100644 --- a/ui/tests/integration/components/mfa-login-enforcement-form-test.js +++ b/ui/tests/integration/components/mfa-login-enforcement-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/mfa-login-enforcement-header-test.js b/ui/tests/integration/components/mfa-login-enforcement-header-test.js index a289c36e2a..a50127cdce 100644 --- a/ui/tests/integration/components/mfa-login-enforcement-header-test.js +++ b/ui/tests/integration/components/mfa-login-enforcement-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/mfa-method-list-item-test.js b/ui/tests/integration/components/mfa-method-list-item-test.js index 2bba6e3476..4757a0e0be 100644 --- a/ui/tests/integration/components/mfa-method-list-item-test.js +++ b/ui/tests/integration/components/mfa-method-list-item-test.js @@ -4,7 +4,7 @@ */ import { module, skip } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/mfa/method-form-test.js b/ui/tests/integration/components/mfa/method-form-test.js index 4f319cac89..09e5bc4559 100644 --- a/ui/tests/integration/components/mfa/method-form-test.js +++ b/ui/tests/integration/components/mfa/method-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/mfa/mfa-form-test.js b/ui/tests/integration/components/mfa/mfa-form-test.js index 301a217770..b31fde2819 100644 --- a/ui/tests/integration/components/mfa/mfa-form-test.js +++ b/ui/tests/integration/components/mfa/mfa-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled, fillIn, click, waitUntil, waitFor } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/mount-accessor-select-test.js b/ui/tests/integration/components/mount-accessor-select-test.js index a7f66912f1..28fb03d5f4 100644 --- a/ui/tests/integration/components/mount-accessor-select-test.js +++ b/ui/tests/integration/components/mount-accessor-select-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js index 5bc05e1b8e..925fea26b0 100644 --- a/ui/tests/integration/components/mount-backend-form-test.js +++ b/ui/tests/integration/components/mount-backend-form-test.js @@ -5,7 +5,7 @@ import { click, fillIn, render } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { module, test } from 'qunit'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/mount-backend-type-form-enhancements-test.js b/ui/tests/integration/components/mount-backend-type-form-enhancements-test.js index a3f55ec4af..ce5134281c 100644 --- a/ui/tests/integration/components/mount-backend-type-form-enhancements-test.js +++ b/ui/tests/integration/components/mount-backend-type-form-enhancements-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -23,9 +23,9 @@ module('Integration | Component | mount-backend/type-form | plugin catalog enhan test('it renders basic form elements', async function (assert) { await render(hbs` - `); diff --git a/ui/tests/integration/components/mount/configure-tabs-test.js b/ui/tests/integration/components/mount/configure-tabs-test.js index 9a00db8a97..e9e5960658 100644 --- a/ui/tests/integration/components/mount/configure-tabs-test.js +++ b/ui/tests/integration/components/mount/configure-tabs-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/mount/secrets-engine-form-test.js b/ui/tests/integration/components/mount/secrets-engine-form-test.js index 1db4473256..5f99f67829 100644 --- a/ui/tests/integration/components/mount/secrets-engine-form-test.js +++ b/ui/tests/integration/components/mount/secrets-engine-form-test.js @@ -5,7 +5,7 @@ import { click, fillIn, render, typeIn } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { module, test } from 'qunit'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { @@ -272,9 +272,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it defaults to built-in plugin type', async function (assert) { await render( - hbs`` ); @@ -286,9 +286,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it shows plugin version field when external plugin is selected', async function (assert) { await render( - hbs`` ); @@ -307,9 +307,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it populates version dropdown with sorted options', async function (assert) { await render( - hbs`` ); @@ -325,9 +325,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.versionService.isEnterprise = false; await render( - hbs`` ); @@ -341,9 +341,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.model.availableVersions = [{ version: '', pluginName: 'keymgmt', isBuiltin: true }]; await render( - hbs`` ); @@ -354,9 +354,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it updates plugin version when selection changes', async function (assert) { await render( - hbs`` ); @@ -372,9 +372,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it clears plugin version when switching back to built-in', async function (assert) { await render( - hbs`` ); @@ -394,9 +394,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.model.hasUnversionedPlugins = true; await render( - hbs`` ); @@ -414,9 +414,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.model.hasUnversionedPlugins = false; await render( - hbs`` ); @@ -429,9 +429,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it hides unversioned plugins warning when hasUnversionedPlugins is not provided', async function (assert) { await render( - hbs`` ); @@ -467,9 +467,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it shows pinned version first in dropdown', async function (assert) { await render( - hbs`` ); @@ -488,9 +488,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it shows pinned version in helper text', async function (assert) { await render( - hbs`` ); @@ -503,9 +503,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it shows warning when selecting non-pinned version', async function (assert) { await render( - hbs`` ); @@ -529,9 +529,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { test('it does not show warning when using pinned version', async function (assert) { await render( - hbs`` ); @@ -546,9 +546,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.model.pinnedVersion = null; await render( - hbs`` ); @@ -599,9 +599,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { }); await render( - hbs`` ); @@ -624,9 +624,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { }); await render( - hbs`` ); @@ -653,9 +653,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { }); await render( - hbs`` ); @@ -680,9 +680,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { }); await render( - hbs`` ); @@ -706,9 +706,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { this.model.availableVersions = []; await render( - hbs`` ); @@ -742,9 +742,9 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { ]; await render( - hbs`` ); diff --git a/ui/tests/integration/components/namespace-picker-test.js b/ui/tests/integration/components/namespace-picker-test.js index 020e0443fb..70102cef26 100644 --- a/ui/tests/integration/components/namespace-picker-test.js +++ b/ui/tests/integration/components/namespace-picker-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, findAll, click, find } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/oidc-consent-block-test.js b/ui/tests/integration/components/oidc-consent-block-test.js index d4f8c455bd..83a1aec8df 100644 --- a/ui/tests/integration/components/oidc-consent-block-test.js +++ b/ui/tests/integration/components/oidc-consent-block-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/oidc/assignment-form-test.js b/ui/tests/integration/components/oidc/assignment-form-test.js index 196fa983c5..e90fc7460f 100644 --- a/ui/tests/integration/components/oidc/assignment-form-test.js +++ b/ui/tests/integration/components/oidc/assignment-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/oidc/client-form-test.js b/ui/tests/integration/components/oidc/client-form-test.js index ee7fe62c26..3c3ebefe71 100644 --- a/ui/tests/integration/components/oidc/client-form-test.js +++ b/ui/tests/integration/components/oidc/client-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { create } from 'ember-cli-page-object'; diff --git a/ui/tests/integration/components/oidc/key-form-test.js b/ui/tests/integration/components/oidc/key-form-test.js index e7392fa3c7..ce010d8388 100644 --- a/ui/tests/integration/components/oidc/key-form-test.js +++ b/ui/tests/integration/components/oidc/key-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/oidc/provider-form-test.js b/ui/tests/integration/components/oidc/provider-form-test.js index 61b1bbe88d..fd8ba45085 100644 --- a/ui/tests/integration/components/oidc/provider-form-test.js +++ b/ui/tests/integration/components/oidc/provider-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/oidc/scope-form-test.js b/ui/tests/integration/components/oidc/scope-form-test.js index 5e984df0ec..e735e8d30e 100644 --- a/ui/tests/integration/components/oidc/scope-form-test.js +++ b/ui/tests/integration/components/oidc/scope-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/okta-number-challenge-test.js b/ui/tests/integration/components/okta-number-challenge-test.js index dc9c08d00e..19d9036d5a 100644 --- a/ui/tests/integration/components/okta-number-challenge-test.js +++ b/ui/tests/integration/components/okta-number-challenge-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/overview-card-test.js b/ui/tests/integration/components/overview-card-test.js index 70b93c1e5c..135a08374a 100644 --- a/ui/tests/integration/components/overview-card-test.js +++ b/ui/tests/integration/components/overview-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/path-filter-config-list-test.js b/ui/tests/integration/components/path-filter-config-list-test.js index 04afa02970..1a3a3d5db0 100644 --- a/ui/tests/integration/components/path-filter-config-list-test.js +++ b/ui/tests/integration/components/path-filter-config-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, findAll } from '@ember/test-helpers'; import { typeInSearch, clickTrigger } from 'ember-power-select/test-support/helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/pgp-file-test.js b/ui/tests/integration/components/pgp-file-test.js index ce10f1c18d..4ecdf25ccd 100644 --- a/ui/tests/integration/components/pgp-file-test.js +++ b/ui/tests/integration/components/pgp-file-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, triggerEvent, waitUntil, find } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/pgp-list-test.js b/ui/tests/integration/components/pgp-list-test.js index b1b1179a86..323cf4c636 100644 --- a/ui/tests/integration/components/pgp-list-test.js +++ b/ui/tests/integration/components/pgp-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, triggerEvent, waitUntil } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js index c174ab767d..4c2c05b8fc 100644 --- a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js index 475f481469..fbe1c1ea52 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-issuer-edit-test.js b/ui/tests/integration/components/pki/page/pki-issuer-edit-test.js index 7ee33089b8..529ddb7205 100644 --- a/ui/tests/integration/components/pki/page/pki-issuer-edit-test.js +++ b/ui/tests/integration/components/pki/page/pki-issuer-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, fillIn, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-key-details-test.js b/ui/tests/integration/components/pki/page/pki-key-details-test.js index cc57ff72fa..9c2a08d047 100644 --- a/ui/tests/integration/components/pki/page/pki-key-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-key-list-test.js b/ui/tests/integration/components/pki/page/pki-key-list-test.js index bfa3e91325..35ab876716 100644 --- a/ui/tests/integration/components/pki/page/pki-key-list-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-overview-test.js b/ui/tests/integration/components/pki/page/pki-overview-test.js index 46896fd6b4..49014791f9 100644 --- a/ui/tests/integration/components/pki/page/pki-overview-test.js +++ b/ui/tests/integration/components/pki/page/pki-overview-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-role-details-test.js b/ui/tests/integration/components/pki/page/pki-role-details-test.js index f25711b870..25ea9a764d 100644 --- a/ui/tests/integration/components/pki/page/pki-role-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-role-details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/page/pki-tidy-status-test.js b/ui/tests/integration/components/pki/page/pki-tidy-status-test.js index c4b1126803..bd65c8da36 100644 --- a/ui/tests/integration/components/pki/page/pki-tidy-status-test.js +++ b/ui/tests/integration/components/pki/page/pki-tidy-status-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-import-pem-bundle-test.js b/ui/tests/integration/components/pki/pki-import-pem-bundle-test.js index d82aef4e05..5aed8c70ff 100644 --- a/ui/tests/integration/components/pki/pki-import-pem-bundle-test.js +++ b/ui/tests/integration/components/pki/pki-import-pem-bundle-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-key-form-test.js b/ui/tests/integration/components/pki/pki-key-form-test.js index 5c62247c72..4843bf4b3c 100644 --- a/ui/tests/integration/components/pki/pki-key-form-test.js +++ b/ui/tests/integration/components/pki/pki-key-form-test.js @@ -5,7 +5,7 @@ import sinon from 'sinon'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-key-parameters-test.js b/ui/tests/integration/components/pki/pki-key-parameters-test.js index 93ffff9247..19613d8cb7 100644 --- a/ui/tests/integration/components/pki/pki-key-parameters-test.js +++ b/ui/tests/integration/components/pki/pki-key-parameters-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-key-usage-test.js b/ui/tests/integration/components/pki/pki-key-usage-test.js index e03d7afa26..e3deffd71f 100644 --- a/ui/tests/integration/components/pki/pki-key-usage-test.js +++ b/ui/tests/integration/components/pki/pki-key-usage-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js index dbc846d866..bf59993a0a 100644 --- a/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js +++ b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-page-header-test.js b/ui/tests/integration/components/pki/pki-page-header-test.js index ba91a59854..1672467632 100644 --- a/ui/tests/integration/components/pki/pki-page-header-test.js +++ b/ui/tests/integration/components/pki/pki-page-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/pki/pki-role-form-test.js b/ui/tests/integration/components/pki/pki-role-form-test.js index b67f651542..67e180200e 100644 --- a/ui/tests/integration/components/pki/pki-role-form-test.js +++ b/ui/tests/integration/components/pki/pki-role-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-role-generate-test.js b/ui/tests/integration/components/pki/pki-role-generate-test.js index 2e7108b5cd..b8c1f422b0 100644 --- a/ui/tests/integration/components/pki/pki-role-generate-test.js +++ b/ui/tests/integration/components/pki/pki-role-generate-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/pki/pki-tidy-form-test.js b/ui/tests/integration/components/pki/pki-tidy-form-test.js index 274419ba83..a0906ac6c0 100644 --- a/ui/tests/integration/components/pki/pki-tidy-form-test.js +++ b/ui/tests/integration/components/pki/pki-tidy-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; diff --git a/ui/tests/integration/components/plugin-documentation-flyout-test.js b/ui/tests/integration/components/plugin-documentation-flyout-test.js index 772df85376..479e8f2f24 100644 --- a/ui/tests/integration/components/plugin-documentation-flyout-test.js +++ b/ui/tests/integration/components/plugin-documentation-flyout-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; @@ -21,7 +21,7 @@ module('Integration | Component | plugin-documentation-flyout', function (hooks) this.isOpen = false; await render(hbs` - @@ -41,7 +41,7 @@ module('Integration | Component | plugin-documentation-flyout', function (hooks) this.displayName = 'AWS'; await render(hbs` - @@ -91,7 +91,7 @@ module('Integration | Component | plugin-documentation-flyout', function (hooks) this.displayName = 'LDAP'; await render(hbs` - @@ -124,7 +124,7 @@ module('Integration | Component | plugin-documentation-flyout', function (hooks) this.pluginName = 'aws'; await render(hbs` - " { name = "" - + policy = <` ); @@ -49,10 +49,10 @@ module('Integration | Component | secret-engines/catalog', function (hooks) { test('it calls setMountType when engine is selected', async function (assert) { await render( - hbs`` ); @@ -66,10 +66,10 @@ module('Integration | Component | secret-engines/catalog', function (hooks) { this.pluginCatalogError = true; await render( - hbs`` ); @@ -100,10 +100,10 @@ module('Integration | Component | secret-engines/catalog', function (hooks) { }; await render( - hbs`` ); diff --git a/ui/tests/integration/components/secret-list-header-test.js b/ui/tests/integration/components/secret-list-header-test.js index f031a75265..2888c3e3d5 100644 --- a/ui/tests/integration/components/secret-list-header-test.js +++ b/ui/tests/integration/components/secret-list-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { getContext, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; diff --git a/ui/tests/integration/components/select-test.js b/ui/tests/integration/components/select-test.js index db3aaf0e73..7968b79844 100644 --- a/ui/tests/integration/components/select-test.js +++ b/ui/tests/integration/components/select-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/selectable-card-test.js b/ui/tests/integration/components/selectable-card-test.js index 9afb78fa26..0a691a6851 100644 --- a/ui/tests/integration/components/selectable-card-test.js +++ b/ui/tests/integration/components/selectable-card-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/sidebar/frame-test.js b/ui/tests/integration/components/sidebar/frame-test.js index c780e22358..03ceb33b64 100644 --- a/ui/tests/integration/components/sidebar/frame-test.js +++ b/ui/tests/integration/components/sidebar/frame-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/sidebar/nav/access-test.js b/ui/tests/integration/components/sidebar/nav/access-test.js index c1cdd9da40..82d58ebd88 100644 --- a/ui/tests/integration/components/sidebar/nav/access-test.js +++ b/ui/tests/integration/components/sidebar/nav/access-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index f43224bb70..2d6c55ac65 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/nav/reporting-test.js b/ui/tests/integration/components/sidebar/nav/reporting-test.js index f61a2131b8..228b243f87 100644 --- a/ui/tests/integration/components/sidebar/nav/reporting-test.js +++ b/ui/tests/integration/components/sidebar/nav/reporting-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/nav/resilience-and-recovery-test.js b/ui/tests/integration/components/sidebar/nav/resilience-and-recovery-test.js index 9e0b4840d2..f4ca5e52f3 100644 --- a/ui/tests/integration/components/sidebar/nav/resilience-and-recovery-test.js +++ b/ui/tests/integration/components/sidebar/nav/resilience-and-recovery-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/nav/secrets-test.js b/ui/tests/integration/components/sidebar/nav/secrets-test.js index d1fdabc08d..f1b346d19f 100644 --- a/ui/tests/integration/components/sidebar/nav/secrets-test.js +++ b/ui/tests/integration/components/sidebar/nav/secrets-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/nav/tools-test.js b/ui/tests/integration/components/sidebar/nav/tools-test.js index 7b11a3f156..01bb060610 100644 --- a/ui/tests/integration/components/sidebar/nav/tools-test.js +++ b/ui/tests/integration/components/sidebar/nav/tools-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; diff --git a/ui/tests/integration/components/sidebar/user-menu-test.js b/ui/tests/integration/components/sidebar/user-menu-test.js index e7f62257f1..c06eb4f4fa 100644 --- a/ui/tests/integration/components/sidebar/user-menu-test.js +++ b/ui/tests/integration/components/sidebar/user-menu-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/splash-page-test.js b/ui/tests/integration/components/splash-page-test.js index 916f60d06d..e0ea1ca449 100644 --- a/ui/tests/integration/components/splash-page-test.js +++ b/ui/tests/integration/components/splash-page-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/stat-text-test.js b/ui/tests/integration/components/stat-text-test.js index a3f0a6035b..9a8efe76d3 100644 --- a/ui/tests/integration/components/stat-text-test.js +++ b/ui/tests/integration/components/stat-text-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/string-list-test.js b/ui/tests/integration/components/string-list-test.js index 2abd7a31bb..46cd5e2e4e 100644 --- a/ui/tests/integration/components/string-list-test.js +++ b/ui/tests/integration/components/string-list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn, triggerKeyEvent } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/sync-status-badge-test.js b/ui/tests/integration/components/sync-status-badge-test.js index c11bc05a5a..0114a7c9a8 100644 --- a/ui/tests/integration/components/sync-status-badge-test.js +++ b/ui/tests/integration/components/sync-status-badge-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import hbs from 'htmlbars-inline-precompile'; import { render } from '@ember/test-helpers'; import { PAGE } from 'vault/tests/helpers/sync/sync-selectors'; diff --git a/ui/tests/integration/components/sync/secrets/destination-header-test.js b/ui/tests/integration/components/sync/secrets/destination-header-test.js index 278ef35de6..9d3519cad6 100644 --- a/ui/tests/integration/components/sync/secrets/destination-header-test.js +++ b/ui/tests/integration/components/sync/secrets/destination-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupDataStubs } from 'vault/tests/helpers/sync/setup-hooks'; diff --git a/ui/tests/integration/components/sync/secrets/landing-cta-test.js b/ui/tests/integration/components/sync/secrets/landing-cta-test.js index 411a96787f..8f54974348 100644 --- a/ui/tests/integration/components/sync/secrets/landing-cta-test.js +++ b/ui/tests/integration/components/sync/secrets/landing-cta-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations-test.js b/ui/tests/integration/components/sync/secrets/page/destinations-test.js index 6b9e28421d..48ca5c749a 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, fillIn } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js index fc7a104c66..fecf6d3c37 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/create-and-edit-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupDataStubs } from 'vault/tests/helpers/sync/setup-hooks'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/destination/details-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/destination/details-test.js index 347bccca2d..6cce45b4f3 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/destination/details-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/destination/details-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/destination/secrets-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/destination/secrets-test.js index c0595078d1..e7c5dd07d3 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/destination/secrets-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/destination/secrets-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import syncHandler from 'vault/mirage/handlers/sync'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js index 22c3810a36..7599873e1e 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/destination/sync-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupDataStubs } from 'vault/tests/helpers/sync/setup-hooks'; diff --git a/ui/tests/integration/components/sync/secrets/page/destinations/select-type-test.js b/ui/tests/integration/components/sync/secrets/page/destinations/select-type-test.js index 8c4af7abd2..8aabab107d 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations/select-type-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations/select-type-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; diff --git a/ui/tests/integration/components/sync/secrets/page/overview-test.js b/ui/tests/integration/components/sync/secrets/page/overview-test.js index e81e3e6c20..80a1001c85 100644 --- a/ui/tests/integration/components/sync/secrets/page/overview-test.js +++ b/ui/tests/integration/components/sync/secrets/page/overview-test.js @@ -5,7 +5,7 @@ /* eslint-disable ember/no-settled-after-test-helper */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, settled, findAll } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/sync/sync-header-test.js b/ui/tests/integration/components/sync/sync-header-test.js index 8427bf6e52..8a1b657a34 100644 --- a/ui/tests/integration/components/sync/sync-header-test.js +++ b/ui/tests/integration/components/sync/sync-header-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import hbs from 'htmlbars-inline-precompile'; import { render } from '@ember/test-helpers'; diff --git a/ui/tests/integration/components/toggle-button-test.js b/ui/tests/integration/components/toggle-button-test.js index ebf8917ed0..17153c1955 100644 --- a/ui/tests/integration/components/toggle-button-test.js +++ b/ui/tests/integration/components/toggle-button-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/toggle-test.js b/ui/tests/integration/components/toggle-test.js index 3392a34fb6..5f47790dfa 100644 --- a/ui/tests/integration/components/toggle-test.js +++ b/ui/tests/integration/components/toggle-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, findAll } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/token-expire-warning-test.js b/ui/tests/integration/components/token-expire-warning-test.js index 5780328ad1..cfaa98237e 100644 --- a/ui/tests/integration/components/token-expire-warning-test.js +++ b/ui/tests/integration/components/token-expire-warning-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import Service from '@ember/service'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/toolbar-actions-test.js b/ui/tests/integration/components/toolbar-actions-test.js index e19cc2bba4..d31b16fb61 100644 --- a/ui/tests/integration/components/toolbar-actions-test.js +++ b/ui/tests/integration/components/toolbar-actions-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/toolbar-filters-test.js b/ui/tests/integration/components/toolbar-filters-test.js index 508237b23e..027d9d9c2b 100644 --- a/ui/tests/integration/components/toolbar-filters-test.js +++ b/ui/tests/integration/components/toolbar-filters-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { isPresent } from 'ember-cli-page-object'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/toolbar-link-test.js b/ui/tests/integration/components/toolbar-link-test.js index fda7ff08f9..b3d4900ad5 100644 --- a/ui/tests/integration/components/toolbar-link-test.js +++ b/ui/tests/integration/components/toolbar-link-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/toolbar-test.js b/ui/tests/integration/components/toolbar-test.js index 87be585658..8f27298d7c 100644 --- a/ui/tests/integration/components/toolbar-test.js +++ b/ui/tests/integration/components/toolbar-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { isPresent } from 'ember-cli-page-object'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/totp/key-form-test.js b/ui/tests/integration/components/totp/key-form-test.js index 0ac788caf5..69618b84ab 100644 --- a/ui/tests/integration/components/totp/key-form-test.js +++ b/ui/tests/integration/components/totp/key-form-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, fillIn, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; diff --git a/ui/tests/integration/components/transform-advanced-templating-test.js b/ui/tests/integration/components/transform-advanced-templating-test.js index 2b4ce86d98..7f406da514 100644 --- a/ui/tests/integration/components/transform-advanced-templating-test.js +++ b/ui/tests/integration/components/transform-advanced-templating-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { click, fillIn, render, triggerEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/components/transform-edit-base-test.js b/ui/tests/integration/components/transform-edit-base-test.js index 980da32610..d5bdb52388 100644 --- a/ui/tests/integration/components/transform-edit-base-test.js +++ b/ui/tests/integration/components/transform-edit-base-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/transform-list-item-test.js b/ui/tests/integration/components/transform-list-item-test.js index 516dd0eef4..f306dc900b 100644 --- a/ui/tests/integration/components/transform-list-item-test.js +++ b/ui/tests/integration/components/transform-list-item-test.js @@ -5,7 +5,7 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/transform-role-edit-test.js b/ui/tests/integration/components/transform-role-edit-test.js index ac1d6cea11..b9e85e99ce 100644 --- a/ui/tests/integration/components/transform-role-edit-test.js +++ b/ui/tests/integration/components/transform-role-edit-test.js @@ -4,7 +4,7 @@ */ import { module, skip } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/transit-key-actions-test.js b/ui/tests/integration/components/transit-key-actions-test.js index 3e839e2867..d27278d3f7 100644 --- a/ui/tests/integration/components/transit-key-actions-test.js +++ b/ui/tests/integration/components/transit-key-actions-test.js @@ -7,7 +7,7 @@ import { run } from '@ember/runloop'; import { resolve } from 'rsvp'; import Service from '@ember/service'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, find, fillIn, blur, triggerEvent, waitFor } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { encodeString } from 'vault/utils/b64'; diff --git a/ui/tests/integration/components/ttl-picker-test.js b/ui/tests/integration/components/ttl-picker-test.js index 8f19308b07..df7e19d24f 100644 --- a/ui/tests/integration/components/ttl-picker-test.js +++ b/ui/tests/integration/components/ttl-picker-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/unsaved-changes-modal-test.js b/ui/tests/integration/components/unsaved-changes-modal-test.js index 150334a270..d52ab44497 100644 --- a/ui/tests/integration/components/unsaved-changes-modal-test.js +++ b/ui/tests/integration/components/unsaved-changes-modal-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/upgrade-page-test.js b/ui/tests/integration/components/upgrade-page-test.js index 290483e513..ef1837cbbf 100644 --- a/ui/tests/integration/components/upgrade-page-test.js +++ b/ui/tests/integration/components/upgrade-page-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; diff --git a/ui/tests/integration/components/wizard-test.js b/ui/tests/integration/components/wizard-test.js index 298a1af7ac..a1edfa5a37 100644 --- a/ui/tests/integration/components/wizard-test.js +++ b/ui/tests/integration/components/wizard-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, waitFor } from '@ember/test-helpers'; import sinon from 'sinon'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/components/wrap-ttl-test.js b/ui/tests/integration/components/wrap-ttl-test.js index f957b73d68..8996871c59 100644 --- a/ui/tests/integration/components/wrap-ttl-test.js +++ b/ui/tests/integration/components/wrap-ttl-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import Sinon from 'sinon'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, click, fillIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import waitForError from 'vault/tests/helpers/wait-for-error'; diff --git a/ui/tests/integration/helpers/add-to-array-test.js b/ui/tests/integration/helpers/add-to-array-test.js index c1cbc6f0f4..c8e766e673 100644 --- a/ui/tests/integration/helpers/add-to-array-test.js +++ b/ui/tests/integration/helpers/add-to-array-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { addToArray } from '../../../helpers/add-to-array'; module('Integration | Helper | add-to-array', function (hooks) { diff --git a/ui/tests/integration/helpers/changelog-url-for-test.js b/ui/tests/integration/helpers/changelog-url-for-test.js index 2c131d0400..7ecd7324d4 100644 --- a/ui/tests/integration/helpers/changelog-url-for-test.js +++ b/ui/tests/integration/helpers/changelog-url-for-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { changelogUrlFor } from '../../../helpers/changelog-url-for'; const CHANGELOG_URL = 'https://www.github.com/hashicorp/vault/blob/main/CHANGELOG.md#'; diff --git a/ui/tests/integration/helpers/date-format-test.js b/ui/tests/integration/helpers/date-format-test.js index ab38b24e8d..1ef5a7a26e 100644 --- a/ui/tests/integration/helpers/date-format-test.js +++ b/ui/tests/integration/helpers/date-format-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { find, render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { formatTimeZone } from 'core/helpers/date-format'; diff --git a/ui/tests/integration/helpers/date-from-now-test.js b/ui/tests/integration/helpers/date-from-now-test.js index f7a417c06f..89b868948b 100644 --- a/ui/tests/integration/helpers/date-from-now-test.js +++ b/ui/tests/integration/helpers/date-from-now-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import { subMinutes } from 'date-fns'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { dateFromNow } from 'core/helpers/date-from-now'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/helpers/display-nav-item-test.js b/ui/tests/integration/helpers/display-nav-item-test.js index f45279bade..fc3fb5bb89 100644 --- a/ui/tests/integration/helpers/display-nav-item-test.js +++ b/ui/tests/integration/helpers/display-nav-item-test.js @@ -4,7 +4,7 @@ */ import { computeNavBar, NavSection, RouteName } from 'core/helpers/display-nav-item'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { module, test } from 'qunit'; import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; diff --git a/ui/tests/integration/helpers/format-duration-test.js b/ui/tests/integration/helpers/format-duration-test.js index 7a5031f8b3..c4980e1485 100644 --- a/ui/tests/integration/helpers/format-duration-test.js +++ b/ui/tests/integration/helpers/format-duration-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { duration } from 'core/helpers/format-duration'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/helpers/has-feature-test.js b/ui/tests/integration/helpers/has-feature-test.js index 2a0cc428d1..9b78b741b0 100644 --- a/ui/tests/integration/helpers/has-feature-test.js +++ b/ui/tests/integration/helpers/has-feature-test.js @@ -5,7 +5,7 @@ import Service from '@ember/service'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import waitForError from 'vault/tests/helpers/wait-for-error'; diff --git a/ui/tests/integration/helpers/has-permission-test.js b/ui/tests/integration/helpers/has-permission-test.js index 2dbfc9e768..3b6853808a 100644 --- a/ui/tests/integration/helpers/has-permission-test.js +++ b/ui/tests/integration/helpers/has-permission-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render, settled } from '@ember/test-helpers'; import { tracked } from '@glimmer/tracking'; import hbs from 'htmlbars-inline-precompile'; diff --git a/ui/tests/integration/helpers/is-empty-value-test.js b/ui/tests/integration/helpers/is-empty-value-test.js index e6352f955d..48909f1d43 100644 --- a/ui/tests/integration/helpers/is-empty-value-test.js +++ b/ui/tests/integration/helpers/is-empty-value-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; diff --git a/ui/tests/integration/helpers/remove-from-array-test.js b/ui/tests/integration/helpers/remove-from-array-test.js index 0e2ef26ded..656d0f993c 100644 --- a/ui/tests/integration/helpers/remove-from-array-test.js +++ b/ui/tests/integration/helpers/remove-from-array-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; import { removeManyFromArray, removeFromArray } from 'vault/helpers/remove-from-array'; module('Integration | Helper | remove-from-array', function (hooks) { diff --git a/ui/vault-reporting/0.21.0.tgz b/ui/vault-reporting/0.21.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..9c087d11a6b85f4d88f363d40a33084fd2247796 GIT binary patch literal 124532 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
  • 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%0D<}@(loJ1z#FMcV5Z1~Cxj2sO8<=Rf5Fs{>Y~{9= z$>Mr}j6pqV!Q_Ud|BCTuxjz0-tmhAWIdkG)FG?(z?W;#Gg^c9+2&TeBPLC!k0IwFW zG+?h|*AJpWWrS3SKm#8I$3Q8Az8Y3Tm%Xq9sa^vq3C6%l!Co<_u9yKIldvSVZdSky zv8z>FnF@A{-Rlt{7lv0t&ZiO$7_gZjno!7Inah#+gK!6xgZ=Z*;+24?-%!YogTcQI z=ZpN<688~&^3HXgJK%EkN8xpmoc&<8?Xe4GYRN;(@Y@*`a#Ra7s#}A@mlpq1k0&bE z760d`RCWNvi{@hH%Fx9|L7i+_ z;MPLwlRa&AJh9gg#4v<=m^KXY4xG{%P4^~g-}$xqa7H69_UXqDuhiaD2k0$s^BnZ2 z8NnN3(ZPT4DqHuofl}N-1|l(lm(9V^A6Ud8FF;-8(J+K7B5=CKuyoorQA968j9vhK zDPBApxeVQ^z|kXh1r%_-rl7M;he3o9J}C3tF+}(X(jnNM#}lyY#s;vCjD>M|dD%+( zo2Lc$AcVRSHNaw_xW!tCN@ zKtrFI*Q550D394IZ#(5>r_3RI$+T9@oA9ERMA%@}*g2hQO+Am}kh)R*oK71J6DzZz z!WE*f(lFd-M>s)D9+kM42QbWgz?G%tz&V{_isUSmce_n~NB&FPXqp8LmuCASMHe}n zq}U$%(ecGKmEY=}CaKk${B47z!z7*zh8lxYsO)KB$pH`j1GJ_yBE4s1PsM{bxfyo~ zl3;$&J%S)fcEM-vK2}@MlC|VSZJU{vR_A}@s~LkMR*M)Q9Rh}WfIy_}wf3M%YHMq? zoZf zS0DDOasbuhaN3={qsg>%IswNZQJmtup**G+P8*O-653BF8}e|(XRvueq_5mH@uC(= zrd0mUxo9MIb80~zV-QZ)b!Cx*--9;JS6=}ENm=wcU9f+f#T z;@dk}gX2pT{s`7+u>+-vH>9YHlspg*`jQFeRRHsOS)@LQ(Fam_!GwY3ke>v_<)O=2 z*Q#~fV;9wX4T9FU?CinOAGgp~Tm|M#{!X_SxkWNOZ-&7r5NB6(s20c&mn&n?weGt6 zi+pKuP!RtNVQ=?MX;8^s{Su)p_`IdIE3y+2GLN7smolnMK{h!X1*wv0G#*eSgBS|3 zoB&c1T>iSKc@W|*ED_@*zE~!Y?aP)Y33>pvZDWIK2c?bHkRjq-mvpOKYqM*fB$;EpEYukSuid-FcW~$N8mCAcb z-lqkw;)H4E4f?*b0{=Lm>08Rciz>ckbsC3|z2Ms=b=X5&%B8%H52wHAWPRZkVFhD28+@0y-w5B!AQ%_T@pUXC?weU{*F2-X$ zU{j@5zKB+M)i0T7&tK@B+tIvl_nDM}Ar2Mj55)aVaTY_LUqWg*3gL=Rzg|Ht3=b~% zJdDrkpRDU7fY8Ltt|qBLwOZq|mOXC@$BmjhR#AHKrnsh|ug3D@hu7j(o==I7A70m? zb;IE_-^YWaKMK2;U6OvPy`d>akR&yXP27H-Q1=}8VePZ>eSG6a6TE`w_j&v_)MX@$ z<1yFr;x(X*5C~Vol%YN^q!8T{-j8B1rl5mB#2K7;5UekM#rjDg*Cd}rvd6btvd3&7 zp5_xYe*H>1pP?T2to{2RGO%&2X}elKZRuGPp<;vlQdnr+D1_ku{s;M^tHE9dT2)$K zTxmU53DP8f;F%S373`ddqEf5X!jcxZv1^;D8oqwTbh|Z?hk?JYH7Y~X(%vXokTHYs zmjLNgjfb8lJd8X}wt`-*5=%a#lK~n_+yJx)GM_;g@wCaIi26@t*#DyWSQ_{rFXNrs zVu<8Dzw4_CSro^--4^WkS(1Tj(8lF>YOq$ra9o6;oaPX3ykMNNp!iSo;qoYUm z;I1y$Kf?%)T%`GLbmCO^z@W=LG<3O#EnZM)Jj*@wxD4WONk=I&xo}gWdMy#NYz^mO z>lNrJT%NvaDF%4sV%~`_5O%DKKI>_&KR_`CygYF$1{ZZCCR6NVM_e9Q7rB%O~B3i;NRwMVL z>iS^oXpwv z&_5@Xr3Z018AY0cXo>!3^Rey!v(a7OxYz&Q$@8H7h@=dRpn8y<3AXA-ZFouXrDMEA zRfJMLkOP<| zrWbHJ@IR;`YQBAU{GvyWc@8sBX#|)nFD3(S_(6ThcqoAhYmhi0b!lT8k7?wN17{#V zwNmO08>mWITn0)As|zsDMXWgvBXQho@yr&2To~1vGXef*G#QZ@Kkd%3Bp8iWSWsk- zv>*L=diwt3hZm=(kJ_2TM1{)yAX}l}r?ro7kKVsHc=htti^H|gWRDmxcB(=-`0!%? z_{HJr^Zz}4CE%E^szZAB_T{VJPG9dI9iP5=cZhfxV9UQ>tBw7GgBM3f0J{pc&*}6t+QDm`GR|ze*U&ukYk6QhlFc(} zR%rKS5|2PGuY;K0(38`wQ2BT|rap&o-S#sOO%e>EIH89DSn5Eer8G_q%+Pmc#deNk z?4?Fr0Ze#`T?ZP+nFrKcinMAclea6)rWI(oQei~CQVrhO6^aVbE3M_y6Gk;*yyWt6o9ljmF6e`GV|-tyyneyZAkTyN#`XYu-9-(K(7 z_Fvtt&VBr!J9!M74)FNlVLIYCOoOo=xWgSZjU=;HU=K`3i`PL-L*ph*GNIn9Xgo>L zz8YU2(E%Lssn4AQZfZxengvTWn-iWPwu0%&OI#jc+M2{6?+-8E(pHZA)8oY)@K_sL z@xZxv!am$?Q=dasZMRzOR!byl<)D~Vd!=wDQJgjj{j(4}@1G6+KiO!8kHvF0xu&@c z*LNRDOKuLXatI8thR8+0C|ak^fi% z(m=heSrd6Y&w^1zoYdn>j3ETqm=Xy#8B_Da4hDqgzCryU;pQknwzH`cf#OJfN+=|y z)gm915S)7mn6D4P<|Dvs)2FRZozHN0Dn}77?`XPT0#0CLMO|1}*s>V!_$YXeJmz!H^}49pux*kKLh2@21381I0z9}P!+>%auu;HRHgo%YcMykAD@efv zq4^?l72z@Sc~5kH$@p~B?_`b8?ma@U@U%4eBMVzk_9XaEh{I4$Z;R48ffv*3Y>^lZ(Q7nTPgtVU$w-4AU3^c?OI*KnS z0VbMQNaYEqBi0IOG)RX?zK*We@o7j?YIVtuqhf>_SWsiWcy5X+xoIvA?b5E#!{2`g z6G93`b-6hjumj=6x5rC}P&_T++^2=)Dd&Rr^e!nWuP{F;tUmWL1hQ42^#>ODl*6%? zEhc@``GP3M{qHash4z^@$z(Ryo%}LIJS7FK$RhlmoPl0kV7a0C1koSjuyszSX7rAL zi57SPQ8kD;MENuPI2@6bBkfsmeW-%~}evs$_!mYgP^iR3taYY40lyDPoh*r6|2q}dc`vum<8BH7xb`O)L zsCj{b!*~b`*(?eG`C@34Ka4{N)ONCVBnyk+GFeh2E~(jV$V@{UozD2m&b%cH%e*Ug z*csc5*^-`(?YtnLQduaf-J{=H%A&p!3B_b_d{L`_m>>baj~UJbh$|AOX*_a3JtOAg zVNtp*+eUvubdlLCH{B%NiV{if9=6cLGVlu5iR_Vi`@zc6Hx*o^Jd<_yW;48!k>df> zFgcP+vSz%+j9ig%V37q2&N5})Y^#12q+8MkIyX0R!C?v#`{Z7sMZC#|o)kN3ESwK& zS!`|bWl?bDtF;j7yu!g&E=hSjP>C@82XVIoA}jur_>yg7!3>wB?rbwDiWX)C zR4m$Sid>{t+6pQyMm$Hd71+MXhzoR_THsh7<-3ffYXaNJnz(Zjn&68}PtAD*v*xw% zXB1k~Vi{U~6(TeIozFaY*uwjhv-paLXqBQ%3U_gnqpjGSE}a91gEaWnaTgk>fP%RE zgkm`O(SoNw=cZE7HR>-BRgEjEtYQ)GQ-BLOAIv$~_ho4akx(MC9~kT$1{XnLBH?V3 z%IPAe0^UUNK*KL5VQ6`n+H6y!A$Z- zP`1;#P!s!cz+OeL6Kiu%b~B|(8VG4q$fQ>beUx>h2xG|2Vbeyr>ey(5vTWFGu9x4z zKpl!a(0KbbAHVY09aLKr65G@rz!A;3J;3#%_5k~@+M<#Z4G>6R7M|1>nAq>MwswGH5f8Fp==VStYvS^Zy->45OG~}01K;t z*~4)_g?om=IWd$kdc9Y6xY8U)P%Fpgg*WpKH_zDFts(t07g*G?Vl8ne&;HH}rP()b zTe{dy-88HA9}| zPnj)qQ5#GBe;(WUf48^q^Z(w>^X>hA3TMi_Q_y$vtnUA_%F5@@lKj7$oo?68|GoA2 zKL78XJlFA_F{B#X&qZEVnVut-vycqL{(a{8{g-odQA3AmU%+PpEJa;{OA{={g_N)E zj77G>O!#l{$S!cCVsOzuKmd`i3{2(uq{+jzO~!yQBmOjUM{q?VCPSuQ0f1*NgFJZ= z@hO-}(#Q)aYl#>x&vd^UT|^cUQ!FPNd<)8RpZ3X&=UtwY{ba!K%UDDqAhVa5O@@H_ z>GM46xbU<@caYBYgrI^^#5HRv!y98eloW#YKTJ)CP_l0G@L4{3hAb0L6mM4 zJyHxCPbuK$GAEX%8Cr3}5E7n|%OQ=jb7P|H-dj1hB||NKJ^v+`6soVqnSzZXhBJ>@ z*>vHNxjAJ$UKaduV2Vx{O~^&yqV1E^*)=iQA}9iT`TS zdBX9MYs(M%eVR~pt%Fvrj9}W*CPaokhoNl>-w%hOr&5!g@ywSEgav4iXfhD#MR^FK z1yheaGLnYEL6#>C{nQtK0HQ!$zkVhTc_|UjI%BU8D9*d{jN9t4^2Om`cqibbkr#v+ zK7wM3x~haeMrE~0#*;I91?&!f|({=VTcwopt9f&6q1Z^S+O66m_Supsfnbjrty{u zVVQ>xSZ|&yLw-_Vw3!)swcDAWoiP38TS_n*^AN|d+hk_wJc+9+x(8tAjgQ5$?(*?98>688h?ZJ^H`Hz~cSX6e;eLmgVhjWNA%n{AA$LHU*{@ z8g5EzQ*Ek^Fo-VVbKs3bnv<10B`L{mQro)o8zY9vpL3@dv43T~N`*6w+_(7s!x8@^ zJY}-og_j-kZXWVZ%53*yu9WO-y337OLCIAj)_;|Kv%vYxblZ{sy~;~oF#3$|J)wv@`>@zEQ1m4hQ!*G@u zCzMgJu%9qK>qqACBRdiA@YL{^0i(Qd&TIV^H@pU#fMxzrRC|S zXnD4Ur-6K_yb8FpH{hR8=vP$Z@~1eB$&rQg)+Hru7*9f9EJmQPMA$kKBmU(R#H8le zucU0-F}!^WWPRl$shkybuP!Ov^ATn5B5==_v#Q8H!76Uez-;#|1^z=hbPal>uu^VnOAwdc{1;~-EqJ@)mAj$V ztA6)x-`w2a!vE(!w{-u1gZsZur@OtGyZ>9izyG_F=g#l{?{EIUtLMh|{}sZ4F1i1Q z|6A*Sb93X~{^w4f>qXDZMU7k-IP#ER62AepybQf3j0U(vFqq#vsI?ag;=J{Mw2g$z z+gB+aNk@f60Y>dQ!l9<{8>Wme{Bj(mA-x4dg}b*ZD7peJx&0MU!_MNXEPR=W7gQXJ zuQ+f`{^YJlugTwQl!vm(;@$ipE9AmUX-Da~DsD!-QFU{b)pdtb?Ca6*GI(%Ciu)SY zd)fU%DbOrUK|w?ug*NJ|0`^+m0ewTtBOQ&;3yEn4)iauvL-*yOWymitHRcazBGq1b z#G&~YJGhGhJWsn7LNw(GQ8iA}yX*$-n();foC*6zDxAC@vDkP|PcIX9JVp=cRnFg_ z%@7W-JfMK(Spv?%vVe#dA#W+IcoD+quH2h;x0IbqXArBhtyMAIB$XduUrxEq3!493 z;KNeL=rd0{UQhXjh`1CcfozYz{F2qn6BFbU)dmM_EmA0PhZIsgJU~lpXo`Bi z$emreOaUx8Y1I76seKCKgGk3_j!X$5OCot#rC>Z?+951dZ~2DI&6Wgj%G@-c;_D7o z$uYrZdPl05e%5#ioCpbgBv!PR(Ay z(fE2-0IO^N@AdxokFEc+Q;XbE8%y;6j~{PX`oHexC0c&GMV3iQ~h$a_-NB748*Zkdb z2A2oI`XqGIlt!A(RA!IzDGTdrgb-#+0^OQnZu|PqV4HcGp|v?AT`|Tj(ZBEy4`&Fg z8N}F;rM^J);XJH@8XqXAXD{e+{5HPCYMOqlP>UvQv$=%vLjvMG#@V7>@zB9|_^!4aJ)@20Jx9^GIg( zSz{vTF-VOZm^4n?kA&j`SlnS_X@+U)4wMI2-Y14@=Flak2PGULyc2rlkatfQ^dWu_ z1_eY$@B|uy*FezcYRnd|dnl$$j~v9&1qFY#VLVB=9|ajsMsDOJ)b%mJImeXLX=~7G zlC_Nv@u4+$5U(`^Jg(PlgOI1;g9kT`xH2<#$6)N51fZao6R%~kqJlJEiXSzJ2)ZY@ zjLu^;4rB5c$z4p|qPLb(9~B{FPtfn{R}OCH!2>h^6tosDN&lF@!Wzbxd5WXqwN3N~ z#cS5KzR${BC~YrhF|ZZVpE98z8}C`&u)_Q2jOl9twI;>s&jl;((`PPKKM)qPkw*!m z7c_B0(>n_ci_y`;^7|1nQjiK!_7UbdSV>+6VMroC8rTWWIr-2mB}YK+m6xLOs;awkU9FQKXX^^b1$y6EnVQ+%qlWd4O_lmznf=yIP&6qy{_!y=rP^arcZ#9NxI;u;reKJ0{V9g~%jAM*>yrC$hfD zJ+7b;U`E&oaxlB4k;oCiVIcXi{L>6v8^HC{Z(Q?482%!2UfflC#jm=`Tk%S_}-F*@`XH{<=&eJTb@6HSANmz zA-kLx0n(u(q2^yCA2pb&V5wgo&dfD9e>l5v!}*s?aI-`f)TN+5h`d8m3C@Iy6DFEV z2Ehf@#0HqXx`cvT@xK0*Tgr3b8Eg$`iwL(HEc(fL+$7FMr%9Y`_|M-upDm$90x!c8 z_jXi)!sf?$vG8cooZ4T2Fu<$S7%v_$O$u@uS^J+%vM`sK*bD%^D8vhlF`%a^fQX}^ zKTiy2!FSbimClqwBvzgjZg#udzYaNU=P@?a5I*C`P6jlU7hGI4c_(PFN9u_BUJ*U* z^AnCCY(~C2&E2KS*$NcuK~dng@pug6O(OQd_G5sKnCD7tOlj$qYpT-E zHLdc^B4MyuJ_w7|du&C$-fH%rgD^gGLnovzqt*38%k#fKw(P$)I$MwL^*?v?6>c&=yv`Mb{p&w40l9HGxHz6@!S zsyy*RMS{N%+yp%pG1tzGtts)E??Zg4>_{QfP&>&hO2UTNi;^?dove|IBU?def4!!ay^{HPgIFbny zK!K1F(#>>UriLyN{?)IdG;F<{z#wz2Z@oq%=i^Z=yE%{IRA@JAFb!NU%Ze#}i}lwd zH8zZsw5EBM-h6;8&7#UuC_B>PfAP`EMLxhf$8idJs(c-6+)VMOpJ#bi0n*k_uLz4D zjOV#oDGXArm_c72&a7!L|H%LVaqZE^z03}j6Y1xli7*njzo4EX!cjD)f=`*31Srqv zXQgO0Hy0)MsnZ=) z_1bM8^3nmzYbk!*^5PK}0@{A;0T+%Swhn7^iH`696?t4-;)Dbd6cbUx5mHMj({ncR`A+?9t3sb5I!O~Z zEbY@iT%))iUNdN2YLO2Mk;T7w=RxO=vF5O!00+1$pL9k2xv|H6PSmNkFffV6^ElCS zXedKndlNzz=)72SvgRyaeAx&|uW6e_&gU36C{2^#Y?4xzIZG>v>lsQ}dnaqggyZ`M zjuCimYr`nb&9qlMhh21j%zeKPV{ZZh*hMsX{!QeQ0^Wm+Lcaf)gc73osi1L=bRt-i z;jV7xOu*oG1+o3beyN<$H>|W4x#SZC(t#qVBMhM{GDfT7lUZ(Wn*XGt;tNDAgUT#r z5^$Zoy(kLjRq;U>3p$G5wR~1Wn?K9V|S-mdEl|&A|EyT{})u$8ydSWM*%BPwlWS5Oy zJ85Crny^#7Fggm(;?S>-7T+;(lIKnNvDcQMmow}+ z@0u>PsfZn~GeLt93h6*RvFS31_$y>mp5Tg!NUP^GVlDD2tuc)7HX1>IB@F9DBQ(N5 z@ed=i#wnO?v(y#Ynv`&ehGWDFCkze@g3IT6DpFGwdXjKok{~)iO5MnJA>Q3}hyr_# z$_r%dQ}9kp!X{HBY5`Uj?VTu^pH!61Z$)rDQQ~~z<|!A_em-MWfhz-N#MXK$PvK(A zOSh2E#ekQImN|hb`y_>%Tm}`X!P4Jz9hdRO>TP8cZe6=l!GPP+!QP7bc7gqG=E8kD zD#m5^Ki$oZjhy}O`sTg<_b#3X?OS-@ZuR)x;X4RtK8h~{AMXbtg>ZVVmrmR;oT4G2 z8v-jNsJ?X4WReaYG2d3=ajC`Sge&K4ifp%s5HDi4L zIi4}A-Tx2cB=`%q){uLY58WhPzE3Q@|KGCj|2Ma`H}3iWT|D3F{=aCZ+}i z|Ld)M{w%rw-+sKYW!wL3b?@*0@8r4N`+t>nlIPU~<4tf-_~#k`ybdDjhA+cO;LBD+ zeC1pOf3^9a`qUS1`cR=AL_WPL0QdWIR|ci825t!Jh84hLo*PmGnv}y6qDdPIJNPom zLwSxFM5@6!b0gn`kh=AT+*>yv@$t5`lwt% ze^(pLb2mAFZRVN@tB}YEWU3N_q-Cn&-;5MhqPUrxYFQgsA0UV|pRAa{UQdkGB^R>)=3p7<W~AzcbbsX3d}tIDBz*z)j)fOEB9Yd@o>-w+VbLM(#PNNx_YDlAxkE=#vQTZpu3`3KBt)M9ax*2Otq3m-2ap9$wHhq#8{reSfEf>}l(@ev*AN1RL|xX4E% zj!e&KBWDMe(<+f)jPPhpF$0v%LAN_aj}Obl2scM?Dd9lkJ`wgWi1%FnLi%CZdCTxA z4pKT|+Vz_>3Bt~Soxe$FsUbywl=6;!K@XA%h4y)j5$W90X#FD&qI#`KYB^INjvq3F zB)}Yl&>I^=(E&-A>0u1MOXVYEbi&4)yJYI{u8QG(>NL68+Q_}CcbagazKBltglsfE z>y0F^?0?7GwsOS9`g}yzw~Y=X&nUu=r{~B(8%^RR2g-CPW!d#>Z{fm(U)cDy;);g_ z22Oky!9D;EGsj{!T64}GXruU?`&I?qv4(qIp^qh8)w&6R+!f}KXBfQ}_68;BF1JfzE2h}YhKFXQ1g4YLpIjMO5Rb)g!VJZc8|pR zPA?lPfo7Dxow$;@Bbj0NW87vJo&9<83ap8jmO1OCa?_#NjQ;_KJJk$@%8h<-6!mZT z!Z0wJ&D7tPsgV4FzvMSGJQ82vZ!1vOB0Hkl><7mY7a=b}R6#FLG6;hxB{WIEdo+#+ zKmCyGJVYEP59S$GbLiEWdZ~l8t#Z5i!M4J9!9T+O04MKP%LwFRK!)6`L(o&(Am(#E z;rC9~x}DBXXo04k)3m%U+8qCNK3{OLUWBE?E(T9*g9o6-e^Z{|7Gf&O)M9wjyPyeX zvl5pD{8;WXk;sTqIzoejGfadPFa~!e+(9>rBgcjUGN_Dra3%aMxt*lvh8`x=@>|3j z2Qe|gMZT^d=8=4RD(0c!FG$7)@lkfzIfPmQ~r;j#8|MY*rNIWSz;Vv<4x&m}qx7hn1^r35lnQc5u%;o^W%s$*47*&&idd zwe|4|m?;cm_i<}$3?uHt(ErN+Tx~)Mw*YQ<}Y03$PdGZ!4St2+trMDg}C`WtXNMunoKK^_H2z=KStk|IgoVhAV_}`l3s~=RBhRG`=Z!sctsfd9x{ZUNu`t+d z<&JwrulZ>#zsi@Ia;|2~xwP|!QRg_EFlDrvBf@L#P=Pj^g(bvUCDG3*1zj)PMN3RB z3%T5N?J&t5TVdu+Ej7NIz0A%Q**i_PwqAn7ti3{!*lqyiQu`eWUWO@Wc4ujug5tI2 zBCAg=zqiV-z0?A9$<0RvqtKFz`KlI7=KX)Q*q!pquIe5068oR^t#$kUe|>xV{{H_? z9xa|UMDRQeSQ$FK|@V@Yu8!fjaYiT`Sr z!z=Fpt8V@HEV2LF+RVlO>fXnHzoRGH|DW$4y*PcpfBbusJa-x0fB&jUo`><-{y6wR z**K0EZIXj`uV247IDYl+?di*RAKvUAA2rE=G|T{=)RbWt`CGA2Ux4q7WseDKx!JGIGZl&XP6BpA(u)MIgj% zg$siwzM$_#pnDPMUIe-qf$l}1j6qgN(xKSqK|C78(R+6gL^vqG)hSC~Ch-V3esfuY z)|N&X~^0o1($Y_Z$XRtXqG7LBrjIpOZ|I= zZt~()`pHWccJzpkc*#H(e>FO27BM?l^sS_0X3?@vnWZZ2>7p#(+M87hWkgt^u;^BW z8j?hni{>a+p=O?d6)IWud=KW&XOsSZG|fm-@KWHXeUV+vy~=e(E0IT8`)Hx|VeEk> zcEU2ma<4>N>b*qDdXYm(ea|DLK3V7-2P67Ep*}joN(S64ew|^w@UuXxu#D`oXpbsZ3rX4kKqBk6TOV!qxGlvl4l40#wU3`JSx>kLO8*0qKt z52pIC6rRjh8ktHEmJiNCFcrq9Lj6^SXd$?jMrk3W>krh8#^~n5bAyq&y+OH^@mOgn zkozqoxlw2#X|d#D5^6;%k_u~7B?%U;)OE#WmUTZ@a3Kj)>BVHQid7_qs#TR7szN1_ zybmXXAX-Kbv(!Rj@S=+;p_i#h241Bq4S4BF1SnR-EF;L`S_=tOTy-(Q7S*Xpz+w {{/if}} + + View the number and source of active Vault clients on the cluster to track license compliance. + + Learn more + + + {{#if @activityTimestamp}} + + Dashboard last updated: + {{date-format @activityTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}} + + + {{/if}} <:actions> {{#if this.showExportButton}} diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index bfaca0028b..24ffd4efdd 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -2,7 +2,7 @@ Copyright IBM Corp. 2016, 2025 SPDX-License-Identifier: BUSL-1.1 }} -
    +
    - - This is the dashboard for your overall client count usage. Review Vault's - - client counting documentation - for more information. - - {{#if this.trackingDisabled}} Tracking is disabled diff --git a/ui/app/components/dashboard/vault-version-title.hbs b/ui/app/components/dashboard/vault-version-title.hbs index fe32f375af..cddc05cff7 100644 --- a/ui/app/components/dashboard/vault-version-title.hbs +++ b/ui/app/components/dashboard/vault-version-title.hbs @@ -3,12 +3,16 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:badges> {{#if @version.isEnterprise}} {{/if}} +
    \ No newline at end of file diff --git a/ui/app/components/identity/entity-nav.hbs b/ui/app/components/identity/entity-nav.hbs index 47e855fec9..5b355469d2 100644 --- a/ui/app/components/identity/entity-nav.hbs +++ b/ui/app/components/identity/entity-nav.hbs @@ -3,12 +3,12 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:breadcrumbs> @@ -17,12 +17,12 @@ - {{#if this.model.meta.total}} + {{#if @model.meta.total}} - + {{/if}} - {{#if (eq this.identityType "entity")}} + {{#if (eq @identityType "entity")}} Merge - {{pluralize this.identityType}} + {{pluralize @identityType}} {{/if}} Create - {{this.identityType}} + {{@identityType}} \ No newline at end of file diff --git a/ui/app/components/identity/entity-nav.js b/ui/app/components/identity/entity-nav.js deleted file mode 100644 index 2b674eb095..0000000000 --- a/ui/app/components/identity/entity-nav.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; - -export default Component.extend({}); diff --git a/ui/app/components/identity/entity-nav.ts b/ui/app/components/identity/entity-nav.ts new file mode 100644 index 0000000000..62a487bd1c --- /dev/null +++ b/ui/app/components/identity/entity-nav.ts @@ -0,0 +1,23 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; + +interface Args { + identityType: 'entity' | 'group'; + model: { + meta: { + total: number; + }; + }; +} + +export default class EntityNavComponent extends Component { + get description() { + return this.args.identityType === 'entity' + ? 'Create and manage unique identities for human and non-human identities to serve as the canonical reference ID for policies and metadata.' + : 'Create and name logical collections of entities to simplify policy management and permission scaling across your organization.'; + } +} diff --git a/ui/app/components/license-info.hbs b/ui/app/components/license-info.hbs index 3780ff3850..6026a8ff7d 100644 --- a/ui/app/components/license-info.hbs +++ b/ui/app/components/license-info.hbs @@ -9,6 +9,12 @@ @breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="License")}} /> + <:description> + View your Vault Enterprise license ID, status, expiration date, and feature entitlements. + + Learn more + +
    diff --git a/ui/app/components/page/methods.hbs b/ui/app/components/page/methods.hbs index 9a011ef6a9..4db2769443 100644 --- a/ui/app/components/page/methods.hbs +++ b/ui/app/components/page/methods.hbs @@ -8,6 +8,12 @@ <:breadcrumbs> + <:description> + Configure authentication methods for accessing Vault. + + Learn more + + <:actions> {{#if this.showIntroButton}} + <:description> + Create logically separated, multi-tenant environments so teams can manage secrets, policies, and authentication + methods independently. + + Learn more + + <:actions> {{#if this.showIntroButton}} + + <:description> + {{this.description}} + + Learn more + + <:actions> {{#if this.showIntroButton}} { this.filter = this.args.filter || ''; } + get description() { + const policyType = this.args.policyType; + if (policyType === PolicyTypes.ACL) { + return 'Define fine-grained rules to explicitly grant or forbid access to specific paths and operations within your cluster. Because Vault is a “default deny” system, if a permission is not granted in a policy, an entity would not have permission.'; + } else if (policyType === PolicyTypes.EGP) { + return 'Use Sentinel to specify policies as code that apply to discrete API paths and enforce organizational compliance standards.'; + } else if (policyType === PolicyTypes.RGP) { + return 'Use Sentinel to specify policies as code that apply to tokens, entities, groups and enforce organizational compliance standards.'; + } + return ''; + } + // Check if the filter exactly matches a policy ID get filterMatchesKey(): boolean { const filter = this.filter; @@ -94,7 +106,7 @@ export default class PagePoliciesComponent extends Component { // Show when it is not in a dismissed state and there are no non-default policies and get showWizard() { - if (this.args.policyType !== 'acl') return false; + if (this.args.policyType !== PolicyTypes.ACL) return false; // Use total instead of filtered total to avoid flashing wizard when filtering with no results return !this.wizard.isDismissed(WIZARD_ID) && this.hasOnlyDefaultPolicies; } @@ -106,9 +118,9 @@ export default class PagePoliciesComponent extends Component { const policyType = this.args.policyType; // Use the appropriate sys endpoint based on policy type - if (policyType === 'egp') { + if (policyType === PolicyTypes.EGP) { await this.api.sys.systemDeletePoliciesEgpName(policyName); - } else if (policyType === 'rgp') { + } else if (policyType === PolicyTypes.RGP) { await this.api.sys.systemDeletePoliciesRgpName(policyName); } else { await this.api.sys.policiesDeleteAclPolicy(policyName); diff --git a/ui/app/components/recovery/page/header.hbs b/ui/app/components/recovery/page/header.hbs deleted file mode 100644 index 5b78b4b196..0000000000 --- a/ui/app/components/recovery/page/header.hbs +++ /dev/null @@ -1,29 +0,0 @@ -{{! - Copyright IBM Corp. 2016, 2025 - SPDX-License-Identifier: BUSL-1.1 -}} - - - <:breadcrumbs> - {{#if @breadcrumbs}} - - {{/if}} - - <:badges> - {{#if this.version.isCommunity}} - - {{/if}} - - <:actions> - {{#if @action}} - - {{/if}} - - \ No newline at end of file diff --git a/ui/app/components/recovery/page/header.ts b/ui/app/components/recovery/page/header.ts deleted file mode 100644 index 277bf9f213..0000000000 --- a/ui/app/components/recovery/page/header.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@glimmer/component'; -import { service } from '@ember/service'; -import type VersionService from 'vault/services/version'; - -interface Args { - title: string; - subtitle?: string; - action?: unknown; -} - -export default class Header extends Component { - @service declare readonly version: VersionService; -} diff --git a/ui/app/components/recovery/page/snapshots.hbs b/ui/app/components/recovery/page/snapshots.hbs index 5e51de4dca..751d44cc8c 100644 --- a/ui/app/components/recovery/page/snapshots.hbs +++ b/ui/app/components/recovery/page/snapshots.hbs @@ -3,11 +3,22 @@ SPDX-License-Identifier: BUSL-1.1 }} - + + <:breadcrumbs> + + + <:badges> + {{#if @model.showCommunityMessage}} + + {{/if}} + + <:description> + Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster. + + Learn more + + + {{#if @model.snapshots.showRaftStorageMessage}} diff --git a/ui/app/components/recovery/page/snapshots/load.hbs b/ui/app/components/recovery/page/snapshots/load.hbs index f8d530c6f4..9fa82dcacc 100644 --- a/ui/app/components/recovery/page/snapshots/load.hbs +++ b/ui/app/components/recovery/page/snapshots/load.hbs @@ -3,7 +3,11 @@ SPDX-License-Identifier: BUSL-1.1 }} - + + <:breadcrumbs> + + + {{#if this.bannerError}}
    diff --git a/ui/app/components/recovery/page/snapshots/snapshot-details.hbs b/ui/app/components/recovery/page/snapshots/snapshot-details.hbs index a713e94212..8ab08d0ed5 100644 --- a/ui/app/components/recovery/page/snapshots/snapshot-details.hbs +++ b/ui/app/components/recovery/page/snapshots/snapshot-details.hbs @@ -3,21 +3,29 @@ SPDX-License-Identifier: BUSL-1.1 }} - + + <:breadcrumbs> + + + <:description> + Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster. + + Learn more + + + <:actions> + + + - + <:head as |H|> {{#each this.tableColumns as |col|}} diff --git a/ui/app/components/recovery/page/snapshots/snapshot-manage.hbs b/ui/app/components/recovery/page/snapshots/snapshot-manage.hbs index 8a0489b571..a9a76d4e6c 100644 --- a/ui/app/components/recovery/page/snapshots/snapshot-manage.hbs +++ b/ui/app/components/recovery/page/snapshots/snapshot-manage.hbs @@ -3,11 +3,17 @@ SPDX-License-Identifier: BUSL-1.1 }} - + + <:breadcrumbs> + + + <:description> + Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster. + + Learn more + + + {{#let @model.snapshot as |snapshot|}} {{/let}} -
    - Recover or read data {{#if this.recoveryData}} diff --git a/ui/app/components/seal-action.hbs b/ui/app/components/seal-action.hbs index 1910487459..2494358eca 100644 --- a/ui/app/components/seal-action.hbs +++ b/ui/app/components/seal-action.hbs @@ -3,28 +3,10 @@ SPDX-License-Identifier: BUSL-1.1 }} -
    - {{#if this.error}} - - Error - - {{this.error}} - - - {{/if}} -

    - Sealing a vault tells the Vault server to stop responding to any access operations until it is unsealed again. A sealed - vault throws away its root key to unlock the data, so it physically is blocked from responding to operations again until - the Vault is unsealed again with the "unseal" command or via the API. -

    -
    - -
    - -
    \ No newline at end of file + \ No newline at end of file diff --git a/ui/app/components/seal-action.js b/ui/app/components/seal-action.js deleted file mode 100644 index b59e81c989..0000000000 --- a/ui/app/components/seal-action.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import errorMessage from 'vault/utils/error-message'; - -export default class SealActionComponent extends Component { - @tracked error; - - @action - async handleSeal() { - try { - await this.args.onSeal(); - } catch (e) { - this.error = errorMessage(e, 'Seal attempt failed. Check Vault logs for details.'); - } - } -} diff --git a/ui/app/components/seal-action.ts b/ui/app/components/seal-action.ts new file mode 100644 index 0000000000..bdb2520ba1 --- /dev/null +++ b/ui/app/components/seal-action.ts @@ -0,0 +1,33 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import type ApiService from 'vault/services/api'; +import type FlashMessageService from 'vault/services/flash-messages'; + +interface Args { + onSeal: CallableFunction; +} + +export default class SealActionComponent extends Component { + @service declare readonly api: ApiService; + @service declare readonly flashMessages: FlashMessageService; + + @action + async handleSeal() { + try { + await this.args.onSeal(); + } catch (error) { + const message = await this.api.parseError(error, 'Check Vault logs for details.'); + + this.flashMessages.danger(message.message, { + title: 'Seal attempt failed', + }); + } + } +} diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index bc11e87381..7ec23e98d5 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -3,14 +3,17 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:breadcrumbs> + <:description> + View and manage your configured secrets engines in the current cluster, ranging from key value store (kv) to dynamic + database credentials and more. + + Learn more + + <:actions> {{#if this.showIntroButton}} { @service declare readonly api: ApiService; @service declare readonly flashMessages: FlashMessageService; - @service declare readonly namespace: NamespaceService; @service declare readonly router: RouterService; - @service declare readonly version: VersionService; @service declare readonly wizard: WizardService; @tracked engineTypeFilters: Array = []; diff --git a/ui/app/components/tools/hash.hbs b/ui/app/components/tools/hash.hbs index dcb484b7ad..469fdc01f9 100644 --- a/ui/app/components/tools/hash.hbs +++ b/ui/app/components/tools/hash.hbs @@ -3,14 +3,17 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:breadcrumbs> {{#if this.sum}} -
    +
    {{else}}
    -
    +

    M{=dEY{@+V) zf3L8=uEPFbNmKv*sObNVb@TU1`8!j}-)rOlY1;UEHT?IZhQHUpf7kl=d&T>AqIkd8 zxG$r{f1axSKU&9ruUx+^<@&u={noVV_v-Yj>hya(`oHgE=>H+A?dtB`OP{6rf7WyP ze>NW9=l{8r=a%C+KWIOCKnTV|U>Ix5510#?+A!hU1x==y3T{jjO!60b5dXCPCQdK{ z8B`h}5)iM=homo%c9_Tf3>X>4F+^*dj3J~O#ArK%=ueT~c<`wG0OGHGc=7)AtAl;? zPkZrp|M}||ho?u!`^PWH9%(=N@$~fl#}6+~Pan0j$jqfxcu>jG&!5)be|Ys~|HJ>T zeI|QkZJY!n$gI4xxX#gwgLiKaRjn)|^OAbc-@Q9NI{vW#{{5@Bzwx?fah$R=;fxmR zi%6BFV0IqN>obU1%wwPGB7rv+#Y`YMA@L(C8b4D$K16k_+O@=O8GK#T+LoF-T~QzVxn-&`;Y%7SS8 z=k8IG|Br$M{^`3OBow^eU0@0S-`d*lSp0wMKL5uZJ-5vMRZfAIIFYhTAwIv%lm~tH zTu%jpKb?QDHvWNpr%jG1BnX8xXjnK5&Js77@|2+rqBa6A3s?$S0gyKhXe1J0LiK&$ z$IJ@U$K2l&NP>U?L3v_Q⪼>WPKo)K{~|fkU=zffDN`7rGCA;zOlL0*ugD^S40IG zW)SG?JP`cU4a1KN!0%y_aUNX1yVZa|^kF@kgkgin|Hf!ZUNRl0F~nH)t zh5|L9>3f%F08l~GW$@|t!x=|y2%BHhisDIBZ_G{mD<`1iTd%1_-?;g8IN#c^b@4u$Un z6K3H`vEiN3^pes@3zr8QU{B1_R>sJ(xst{`+*1x=-e=RMq2^5y0&3ac1p=5cKw^=m zVbQ>(X}Dx&Xp5t^vEAvgS`#wGbjdKDB>Kl+w>Pz~zB|=FZg)0+)xKU*dai$7-}>8n zM}6Pu;K*tPV5iP{u1!w8uZo;5OwHC=9Ea3}X);dYGm*5sM>;KFT}8AI10@{vD-2PE zK`I+2qRrS+43M8M9v;QYa}<#<3wOi2od1Pe=v^Qn>T*2l0-c6T(enU^{xB!-(5Vnt^R8l#XXnjk4NfCfiD;41oEDx_Wdo5 z|A@z0|BNPq8wP*fDhpV){&CG4>wmkmeZT(i_Iw{L?{u|S3{r<2A-RNF8N&R#w#Q2d5GXs$=X}JYSMB?f30BMhuGqDc!sS3k=CC~f z`?~G_v%Pt*|GksvTjhVZX3Bl)_wV$nn*aTJE1y4$*Z=zFdbeZ8f8Tn1AOH1E9wVoF z=4A${1YIx9Cv}IEc=*S@e*xF`{xM7kd}C(eP*o2DHV)lsJ_$Sr<9Tu7!%h56Q^K#9 z&>LE5lD^h1&ydYa8(?!~WGeEk6t}p0u2Q(S+C2L(OsHg)4TtAGzT)4LnxTUap+kb{>q02WB8wA721<004zQz~s0ITFNm^Aa$*l zl$i_ULT~;>&j;;FSnm_s53b19ucTI+Kb$FuGHWx>8=1qeg%}|s^~grRPLetvhr)st zy4{z5nOI_^=O7yJf`6w|B$0+O$o$loaQ=Vx-nPAM8(A1WpYtnFZLeBtB}($^)`^=q zj}}17HRi%nWA! z{1d(Vz|5}~)Z*k^Kys&+n&3Lb*BUh5^7Km7ODV-xgCR1If5JCV{hKV#4o`4MpiD~x z9OF@RrGmE9HsI@x+d!w|6NRkkE*qyN@2pzRLPgCLDF>WUIpB=d!`@1oEJz)awmbo; zy?K=Cq?L(>Tfy`U7{$r+n>K=f4ag4A0XhL$35uX==P=<5T32Ub4 znbds5LCTqxZR8TOUd2w&dYPsG+OiHFTS@YUFdYa1MelgoPG<<3-9{&@G}x4`eBz&r z_iS~|8w0(reGK$6%?$KbX5vb5^nihdM9BbGVeL!{<^l$Z$$j#IyG*`O^UQPI&o?=o z0u+~i^2w{n1y?0C<9Q1BD8^S)0B5Ma1aKpTF_R8~069Da8uu+`{%sNp!4-gh>FB zWSPO+ z$T;wm<_cPADNWHLK%C#aqoBZ{4U@sjhg8U-MOqn^Z%Rqfmi&>y%h+wQz0!J=b|q!w zYa$0zUsEO9o2vmMX7eTFrp$EX-R<263w~)uXrBq$Dpo_6yvBsMm-f|VR`k>+_@ezX ziscd9AS%9ibD=&3mfe)Jwm4ua{STsmx@<;)1H!Z}rW4Sb?o6*KT@7fS^cVt9-(IgrWzv$pm2Ao-y!6TjVCnmx=#+rIFv8U%)i@7ba@%zOcvRH11jP$Yt-qc zl}u=SQ_Ci$_57=!_5y(EvaNu_M&~$)uECcY7VR4}ngoeI4zN7Iy9vF`lg;1@H-dbo z;<@3DGz5j1`E0{(9fGXsJn2HyCZ>6lYs`T)gSkisugx=^A>~C*V)!SfI; z$#c4br_)-HEdE)5uV`lWk?&2-^KB^TJ8IKAx3}tcOjn)^0aY2!&XDG}o{|>+iLU(> zxypyDN*41qmc{%UgCY>Rh&_KYf>(nmh+_54Z2e|Qw5O)7w~R}E5_`Q*7E%-i?Sxuk ze3c~J?;AhqXnJb)?Si)KvoOv~!i?j~q`#uhK>qA0q{8!s(_KP}sY-_}X~C5qlCfL3 z0(Yp?of$}qJq#hhm_jApX$15vi4r$>8wy3(9s`=+SzE?<_35B4>f~3?Wd2shqBMbK ztTi&{dAe4?i=&A6KxaGe8v&+Ln#_m(zCjhWD6JrvR$gj^ms;X;dDw)7jV9(3U(zFx zGC_BDg%b<;+TSTqsmUao&BG5t=l)`lLtXVdC9WG3-KhI9$vh7k{Y<43Dq&- zdQ~xpS@k_kv#yBWxxF>sKp8enFG0Ef0?wI~=4e^_7pTKUNzu6F8~~+EWs#1n{_XCL zPLb@gY2?hbsms*R91V|~Qu{SwIKc5$ZyDaeH+Bpb-`Fn@d&RdyAuK4LsQ>{5hAu?e z^8avCBvQG8an9|ne}Tlx%Fkj*V<$$gouNxQBli7$qY9;~#I$`Lc{fykdag^j(}f(3 zMxQ&UaRok$qicg-xacm=l@_LX?wooSF3a1QgNQGEb_V8Far6uQ93`GR1+wRD+nMTiX+J#EiFRCH3y4wM_D?lfq9*=?wqB;{?)Qxjtk=1cg+SYi09z_KU@5l!f-MAHWgsLZb13@kz$b|G z!u7-H2ZQ2~QL)av+f&D?P->nsOgTcgmEv=yH47LDAlBqDx`)a)Og>Ykko1$yVM|U* z0qLKerA9pPQG#hvZG5`4lD=npDAgV%O6qU*a$pe4mV>uc&KXA6V0}En0fwvRiptBh zcV^K_zj5&mY3oxXHR3@Ia0nLbf#o-zV4-FugGgR#N6MWrRWNR=Z*?a&^^5B6roJ~k z<5Dw#0{BAtoBF27Us9meVCw#N>KE#wXQD+N_!RqtRv<%0(qVyXm)mrm=Bn5`B@Bz< zdgYSTk2p8fis?xD_Y1o61wGaL|7Dbh4vWcC&JDQO|9{Q&|KD1x<9{vXxi9~JlTzvn z0AJ=)*8hJt&Sy^n|F3VYoBY4JxmoA`S;{k;U;57;Xxc^~SR91oNkSEL)Sv%F9F4u` zdJ*pYRC$ZN6p}!Ret_s76`k~NOS${w-z(fA_&dAZpPamv@vDzmh`!le?+F$Bu_vK{ zS3IBn4{emjYRvehC{A@UfZ)5{htxk!dg*~%eWo4Ft3HcGHRpbU-FPr8jk*;-fM6g+ zf$ErmKZh_kr3n`__aEjJ$ZEHyjaH7MMp{7lYJ}8jGbH+j+tgn}X;3ecdT_8)o5Yx% z_n2F1Q#RN}gCHU}4SjZ}cn$JKvePK5XZ_q|G48!8iwZv;Xs(%YMyEZYiyQdfvnv$3 zKc26CbP_kd#K~h7O+K5noI_2JvXQx*S=41VU)g2u*itn>$RzWTIwuG_{93O!{tRy* zI;Ua+Jdctj8g&AEk#I+r7pa>6$qVO{`jR}KBnc=ht>(Z)(FA1XQ8~d=ct2AogQ<9* zoe56_Y7TkfL!i;v<|FgOx@VOdKR%PsfYE7(;OF}WqLl_XHu#-&8`A;T0){JPM^GR_J(-yY2?PAN_BXI!%4?a z@M!1umKnChTJtQk z0Y&sz?4k1;3G83&C74=WZk8RKmFCsn3gfYhQekye8)aUjLH=+eK!@{*T78nZxbgf^87Hgy#GB@IIu$f|C$m1Yqj62ThkWuh#s(j7QNYdj=v`l+P zK#uZxR1xB!1NzI0FbP-qTfl;6rjdd;gLNKb_Y;8%7j6&`nv@7##_j-LOoD*ss_^K6 z0UQz?MS*^t4f7%t=wg&XRO@!pTLQj}w3E+x*FGYt^GDZy5TIlT*RI`=-9bh^J5Y&% zpq*i-md`Zjj(uPp)*c;hHwfTX=Y;0RILEL<;E?!Nn2z4iy~5~Y8rS6`&Dn&)s1uFN z#(mcEIK~82rCfzAr^~z?Dkzcalv__7nB>X0xx&G1%Ze*fwXDRKp%bbGm}#SkUX~$Q z1-XRuREbbys_M090?Ms&ek+?r#M$8qHO}PHDMQYy<*+BB@qGODmc6<=<8gFE3>~4q zwgu18px8SpB0-2N?P+(qT{b-mUU0Yl+KoeTwOyFj7+cUA?P`n%I0|&T=*1zNz|o8S zSNm`dPt$W@xWv0bK?jsLI)lj32kir(0Zst-=mjq5U{?%iIi{mRNYUHzi>ixuUtQ% zZuz{!yvI5Y)?hx$pgeWd@=qP#%LEKdyHE^_7d7D&3>z@VKwS~+RO8r(B}#@kzV->$ zXPyDH1X>#V83!*&7o;sovMA3|)RZe!%O1Wj7f|Y;0D&KmXi~(F-`+yOx5uxV*M8_l z*G>@eE)HXS(Q=~Lzx2a9SW-VqPXzUB2MzLtiKnp4TX>uN$~EBB((OaGwXNDYj;`zV z32sTxZ2lj|;{oLOo|MxH7s-FUUf=Zp=&jcGKbG>O;!}}jml|z8-&Ae?NSv0jGZHGEY2VrHPU>C z@4G*I+ijypqxG2U3^fG+-SA{ihzJ7ds$X~Lr77jcl=)fvE#vE^`VH-Sbx5Eo5sC?` zdPl3}h@$8lxzSE5wVjTs+8~Yiw3}5!T0n`SYW0!tc>%t5V|%D-9Y9(*8}pimHDs|2 zPm_XrQTuhsqNt{Qa|yYQV!QH9uP%zJ)!niG3pRtd zvzH=%v<`%QOCzeY-`VS&H)9-KNJx;ELe7eRFN|N$&#DuP0A%-d{Qb#e?@fxcy4Ac~;kT6FpNzYf~7;885#xb_W zzo(s>J|-HH5lIXzwKvnz)Qd7a!OEZ^8j)l^OjUD-ROChM%@1h{&uDTIH1bCnbr^2V*EC%GBp$=%ZyEqU#0h%s`T^Pp+bE&VZM;4oywiAs z?Zd#1%T%pkv*q~Y|Bxtr?T>I1kkA|h)faM_C?v~j%-11DEI=&t*zuhkyXC?PF8ma(>46GgI0Sz+m|P3t=G#8rYNPSX#($6wr;bp)FDx} z+eRzm0|$e>)Y+BldW-=tJu*s0qDdUsMF1&%{vZsk&e%G+7ZR9?&G%sHfbSBT?C z^r9^%z~Lnoes_>ERNXF$!r%sy=~41l6?7{P+g+NQjPY39NdmT=MZ zAl5geXrSRFCUHOz4r!hRSc8y@8c!RP+1&x8bhsY+gCRPfTwdOwYkUq?Y>~c?f@OZq zJY)hIL^0%)3vLjJMi}%25(OC8A9m2n(-rhXS@G8xD3w|m8FN_%?TF!Z2?UsFLgJ}> z(WfnEkiX!N)hQBRoQo*lcLzg}Vv)ax#6chQv{dLR@^F9?te4q_qnOHiD(StX@+OlN zHIk&D8$w!apr^)XU0&3_nQh6$Qi8BXI2lIXwj2cZTP!nTjQCH8MVS^oS7Hb%V&Hax zn8@Z$MJ!_2dN@1?{X{{|ahw$CcKWi(mMMMk6f-z{*hbl^NZ>9F-YpdyV0q|N%Emsu z+W*2$+@_8s?R*PPWR~|*0~=icSF(5xeB}T96ZzzM5S?2ArVLNv?AV}dz5&wZK-t0s z#jWrVshv}Lc{2+fc14Fs{<0xtbwnt{%^*5`M)hhcX{mEzq`b0jePV$ZI~D9-j1kf; zOHVO(kuLuVQn|npJ%Kb$F^_4dG?_ddTbL0=Mjor*kbwZFW4*XS$GrTuz9F}j?~dar zAmxX<7Nt$)lPw1wjEJ)LYez}_ZbL>wXim|i$d+481p$Z7iTXkjDQwphstB|HAShBgPqvp`8`8=1Y;O0YG_?7v=yN-x_8>u&|Z+bQZ#Q_+f_+0b@6e@bmm z2)c-sQb78zCe#@j7WMJP>hfk8-&Ay?!WJ7(5ZFM!%f()F;ran)-fQ#<6$tnPJWisR zAkX)x7{QkSH$-kgJtAr;16&!PyM0T!}1`^1m2S3D$2xeOi`EM{gA9<&(EMvnte*SPK-zIwI42l|(@mxsr%cTZ2aj58ot z+o;iQz{)`A--!O5(7)d&^vBpAW3r7Je>NW7Jx)nVIcPRn4%xPH$8klYF!jj9%C@Nb zi=UPhP$zzPNi#e)Y0gzvm>!V?^j1qm8XUVZRi44W;&l8I>b0ETB0p@de7B-|o8-5` zT~0pXnJBaz@TbyZO~Xg7}C8$(|R zs;ysn24Y%)juN5R^7lU>9D)|(?eT$(1=wUqVYOhZ#fRbJci-MxYVaPl?sh)v3)9Df z;@{q;Sh9<#cX>TL!HM2KT4I;bo}(Q$0uFcy@+Gg*0KSZljA(ZA<3Fy`_gLMd*TsL5>_-Hvm8@9YmpN9ZVeqk zVTDS_ytKn}tHCKdYx;{Pie6Jem}`*59s)=4-LWrqnx;iRcCXt=#`1+s-E`{YY9W7U zwx!sG#Otwpt!yAV5apMyE~Qg$wf`wqnu@wOcCV@0yD0*^imv?{`CdWS@{uk;$gqBj zCP6T|uO^KypMxl8^cX@xE~$BT=_ngAQu;)+g-xPuj!N4iFtp8EbDG_P#ebQ=&#>$W8|HmSo6ZZo0>P?c0ZhpB> z!Tpc^>T1up|GTkP-~V0CBOEf0>1qS^V>~R2snPU#vPtxN2Vr3eRCM^b#;2tJi$V)3QkP!Drtp8n_F)ytrKRAYq zAV!cQmarH(F%BW3^!bGP?cJd32;%#=gM>Qnq3bCAL_6rSt&d)abA+=BK=nhOjob~` z?NK*DEUiH|aBreX0*e|%!ntw--=iKZ01rOIlaMMm{4j}_;*S`YEMSQtlMO&*JWTqtMn3fC0a!j&gm<N7a#tAl=5Lx2in)OAKRoxU_OswZuVkA$P4ltS0B0ve7@dm4>X)kcu@NjIpeA6UZ_R3-sW zb0u{lRo_c5BQa<{Dca(1)KpU^Jl~e+vM^}!X%{qM4J^uhy3$cd70Ie!jsa0o4r(Yg zdFC(SY;)m<^nj~@QClijgriBK7SN)c z(pOnuBxO)U34#*=tf_c;<<6!)1Z`t+T714Q>u@z}ol|%wLD#NhCllMYJ+W=u&cwED zYvN>rH^#)at%+?L@6PxA`@gPzuurv2bpY^027(P)3^l>o}Cux=`pLvVs zypc1CUWO2NC&x3t^TnU`=(%j9?r&U@-i;BFB@~L~`Sxvqfuk#x3Go?_c11XcE6bBx zB1b9?mMH>gI<3T@+h+2Y4I7L>%E;1u&iXo$ue%;} z=-0T#u(XE*q1ksN(ZC4z?R-7Mzs;%j$=Zj?ck{jU?54*}gh^1^hfmfKt;rd-B&oSg z?C*34Wu4;rQTP$?z4$KVGcBT_iyM2Pr68a+Ge&>71b*d-5Zj#7uccrs_sGffj zqQh~FTfnK=&jkju&_T3)k`oQm@kcIVn1w_>S(BsIaQZf_=ekba;0V77OWbadWilVwrtW03KjOq91CSL(4|MYXjPVT;uvJwCa1WgS-S7tdpN~U;UsX@H`7rQeaG{5%aWQLRno&A_4e@I6m)l^unZrqV2EE+sV^Z$V!ZM3b*>W|S66`6tbY@Z1I^l7Z8UTK4 zpa@I24zAxs*PvyA(LaqjWEtGvZ<0h>BlcmPur$e5>QQmH(B;NOA~`(7V|k4^Vex*hao~ z2)LxA^R|MLYK1k~VqT**I)}(zf!ZPxDd-@G_CoiG;nygLBaqX9{?B@*{_a`#=tS=|gEAj&2 z!^nxA^JM-)3$Tm`Yk0NxuLc|r7>XJ?jD%PX&btWrDC5_wjjN8@C5sVg$Z3#8B80Ez zK+t2bf6A~4KxZnMAk67p8(_kHcwLNS;txCA93Nzo0W*>}SA^Yacw;7ryDvcHH&>c2 zRW@!Ox{{m483dV(CZ1^kWd|hByb@DJTYIL+J9Ka6`0r#~-MbFEUyk^~JM7Yiuf(CE zVEF*9E2%67fwRfQ#{M_B0ow9^fiVNJf`da=7gdMkQ2R#CrxET5C+hycOp=MbWX|l< z61b}k{V(+%F=Tex8}3fwwl*+-lSR;is}^ z<+I7$+wvrrD{3SlaTp8kW!24!oAh9MMl*_>Kn`B_l5`FS`U7nw5A5$9q{m33&AT>SgN z^YepECt5n(p4uo)3E^|(Es+6=Zv)oz`)9lWDLW3$m8Ht~{e$VX4gjaldGnk_ZJhsw zpOt(Hqv7ny2F2)2U`KPKvX)S z0%84IaUVB=>*uVNj8NNrcazcH5~=1prA@3Y%@KnBkdqzv`HlWV6bTZcEPzZyK`k+DM@IiX%-4OCQmxt z&Ae%a4C;MCsT3bf)X#odQ(~0uYna9%YMo~~|LaI~#k3oH|9raMhw>1anW0+xEV8+> zAWP2@%GET?EXesA3z#* zHOvI7&vURTp}1-+19kbLB(wlWYb1?Pp!G6mPEZ*iR{rnq`7$=?V3q1|2~ue=7G$Nl z3w~dJo4=HtDdcqzfAO)9ifz%w*elzNL^1X44&F%MKhw5?@Wm5H(M^5}x2V|5rufz+ zaWS^PRMdPUU|MOF^3?F8IOoabk^U5#9U-&Dn$obk7yhpp`he*ACs^EhS5o$PJZ@iu zU2LI46}(Hlo^7e?pUnA3SoEy`et3&B{^#TTyKKBieN}(eZI;`d%4FX=&Ou{*$e`ba zNeIQ#JD{(*a(i^X;5CaD9oy(3ZFf-tgggASQ(s`WE|3WB6k6{Js}v4|=?SPM$)wRu z)_j7$zr+mo%BS#*y5Erw{ZzqSElqZh7L{wcJUU1Zs8=1CJ~GLzWM$4>JK)I>FDK=0 z`}qh1YU5f~)whMwLEsUJdS6RI4J{Pp@YDFaO|Q%(mr-{DeXvj6;v9c0e`9N|_l$lP z&iBqv4c*SAMAxLBsn(=jC5+)h@t{j%Uq72Y;UYN8P`Y=X?;j`LO_K)0{oG4P^u_RJ z(}tb0FS4ql*>H85amlr0ry9Uu%sS?BU%WECB=Iq=zn5WrUDJPLYRV)SHAos6HzdqK zz!@Y%X=V`Bc~;(~1b^{z$(#E&?H(aq*+O2htQy1?P@rzACXxjh!+y{32?U>9(+`3<2&O^0c!K3r5pUh7+T9gI6rV! znF)vHVD9wWg=Z&Ii)@R>_TFes^Q;Xdp^%sdvU(pej~uM%$=m2E+Q+hTBHAS|BJ7};BjdeM6{&uz#H{jBK{LeCA}NbF@jJAR!LwxtJ`DhM~E1fpKT! zRm)4hzDr9IngaG7^qrvsP*T+2+0RkUV{lzznf+$SbL7Go7?Wg~W;vTjKU?odA~fzde;ot; ztlXHsBZ!T!wbm*+2C66n@fGeZ7ua94?qVq*1pU;(dpGzsw4LuSmw>eg0+Va2ijD-8 zY|RDUv{)8%+yLun=vTVDn+Jm8b$w?~5*>z)Hjb&Sv$?_xU4_QO*^@!}^lzPSTRhl> zUWe1F9#3@fJxIdpGNfM_d?B*qNw-*w<~lvTiw(t6(kQPsUPMvTyO8Q#6C8g$x?RKHK}_hE)1`D*7lzAy#6GH zS?snbHHn;>-WR`D#cV^i<)0r=z^AL26smH-*{4BmIb$UsJYdeja-7$&VJnB;`jib^ zO;^6j9v5&*k_=H!?Rqz_XdU)@9h(ZbRiU93guvU+m?+!cIS+y-CBth*3mHPGadeuw zH=0*K=G&lHo)D z$&*Iges%bOY*@dg_he1SPlnJGXFq(CYy8iTa$K5|6U*a8MKC?kQl#qEJP4c5!Ba-a z$a%ai89M#B*tv(8MT}1>jBvtUsc?E$Qyj!1;^C?yAzU={RE9=KB{aq-z6S~2dtB)<1&-`KjBrW?n_y(sJlrt;NLd*=r56b%d zf5aKcaxf>nQKqDf-@9O4_4aj2^LF_> zL7%%w*{ONb1 zxABTjdF9|F(^M#eRi7m7kCv;r(#)Ji6DwOuMEw)8+bwKy3E7SsUQv`?e$7@#z}fjh z%|OPJr7QoI*ETn1-7LB5_inF|6K7H8>3f^hLw8{uVwjl{TDCcV$N^jL@Iy`Z1!g}< z9*@n}gEeIa#^2#^y&s{7&%wWOxGiw`Y?gvXElwO!iy=D5W;;bWaX@7659fO3%N_(e zgbTn+gn0|`9pbr93^RwX%?xZRa9+SV#>mUsoXF)QEk!3X`PbYGB$WHPHZlUT!QbY0 ztJ%>IU8#R_1smF3jAm53;6uQ9RQfg18yT<@Pn)CbLnVed3}QCbtPE{_O>BnT_88{v ztb}}J`~2I82C}_s-t1Qq0Uaz~j_%)f1HNmno!vaI{JMTnwRA-L=$X*0ZG8*)aLEYg z0Ap@}oGsutYIWcxaP@xKbHsBhgyQUHA$-C50s-}AUqjCM?HUdF#fl%lPuUyRjmJx( z(0=UXD)0DwY^eAXsCQ{!q%iW&<0ubVh=Gk@UaedX<~wb4AkLoI8-I1@7?On(6vN@D zJt_ST#m0sodumtDtg?w(3~x3rrJ{?#K$qF@b_z;>;%zc`S;4Heh^dVnagQVTObajg zabUQETd-~e)1jgjKfnG%FZYark0!I{HZ_>%&(4nu;H^M z{Y2d6*bF*}dP0>ke}E`5I|65znyF|&2IlRhNT4E{+bKg8bbjLg{$mb=dsI7Yr3Q3| zu>Px|6u8f4FDkRvKr?40(#m%o$ls#+#7l*q)*OM_T3h@oioL|k^KzOu$HJKZ{G8lj zXRxz9aniz*ix+GwQnkzpqW%|R+WL{q6M{#YN)lru znD5FDJS*qf4=`xb_86KAi_i%(@qOJtzpfHDtRbTG8M-|4@|mI*JAA zM5~8j` zEfyz&l>)Y8ONs-*Z9X4bbpIYvL z8wEk@Wf5erS(a5UG_)oO$w0@+(FOM)>%6V@bMk*y!oRwRx;9h zg!!@k1bUHJ(MvXU3Y%7h-n6Fy73Ueze6j`59ud6XTr-+*D;_(B%>L2G=`!e3a#O8` zas+CkOJfmaS|fhnJC8N3ZETIF)2_j>P%NBdQ2hYvP~Q>q!|#Y<)6lfLXS~CgRYx|^ zG-9WuVtlXIT$Yv;|&qQq z5u9g@Xl0mEBwc6V`8yQ!JA&ZIxWu4WQjLZT58&X89evp-?7&F z?Ff->g7H3I-$Ohf5Y+Y` zZ(`~!@>!OI!W)Hxf%6RdmGgFJCJ9+t zV#XwS?+88FYt6B=qN@-O9E%>s^C894Hf87&@;~mr0eIe+EK4o#^dHQ!Zs~td!$^Zo zw-G-CnXmghFl;^(-vMNM!+r%TcDoH3@fHv7dl`8b5i=2$h|RgO>x6ZP+M^cHNOnJQSl@ z9^}@Rmo1#H0DqV~HKB9U!2xpEgL26)wUuqHpT*&S-g5-{p1~%<6OH zteyFL5C_t|Lu56OVjIz6a<;(=bv~6Hs%jt)QK@%y(=frk@;+l{^rGPh)}SWHbaZN> zn^1IMmg3iTuVH+>Z98m60*_Jr%q)dU;jWTx@7k+;`^k;D$U6L@fthUuR}_wEbJFoh zrc5f4ic1J)ImrT1;D%khy=x5%8!P|k6WJZOZz8-QAtj_({-?5G7M)Vksks6liNZQOe%x{AEV2Xb8$_)hoEF? zL8MCNv-KjQNZM3+0HiZPPQ^8y%ikASbI^pvj12!Y=)Q<^jG9Vt7z^K8LRXH`<+Yfn z^_;)c3v}x+R8dF@NQ$U`t2XTpH>l+7TlH3}r3$HRj8M%pe4{;8pSr0j-2B?rb832D zV85yUXIjh2LQCrrP6%h&HiGn4be3;=b5wXE`;&b6yV*&~Bxh3ys=2VFV-xAjB z!A`DmsjsE<#GYf*F5r+TOj7JXPaT{{Y>iJBaA{i$a2YV+dDID1{|=30iN8ame?Tqf z|Kh7$@3vX*L|uUOubxz@E?{5zR_7dvwMf^zaYu~8b#2EM#k=g5cLb5ouZy7l%<~9t z1*W)BjIwj%J;QJUXHitXGitNUuPjbQ(Ix`P75vzRu+>$4Tg7|c*TbRE1F92y+vYSQr@RUt$uFR%NKVY**nGS@c6wc%l)TmWE~2W$tNo8z%!|8* zb>NiuJqMCKXp!w%24N_JjuWB_KlH(Dpl#*6ach&I$~=RF6V0^Eb{-g~Z9s&h>{itB zAiT6--~?Pagyald4$*gt6QPF7M%bDwDBT$egeaeQO}P|)A=_zZ&@Vt+lV~*&+n_?! zqw&6COqIs(!u0w4dDrto$kGfP;KwAevZjFNX5&?dx|({AN~qx=8jLIs-Uk% zT&luoWF9a2gY=2+!G>xk-Oh}P(S3A-q8M+IM}4~7q)3OFi&Ip6HSMtSm=L)sfV==J6%=lnL3*3NG^?COmTMuafVV+s5r zn%!Ev+b@`5bpZz$`qm(Bi7_X79C}QJWFO!<~mY> zxS~i`(s|^+PTD}Z$ETS6b$XR?)fFJ{!aO8V3XNOcx44p5AHfAX)n5AbIAn*DO-T-{ z*m(>ZxmEHxvU4e9td#?K4D<%$S)U8Nz@@Wp=3}5tW@sYG!vaFC^0qu%JwEiJaPor9?nez?|M=9W%J2hcs}VAlfG(OjZc7YcsGiEUxxjrmX>%V zi+rERMEXFi7rAp(F|-+UxOQ?vS4qd8iLNBQZ_2US@x!85>KPqc=tBY8Q~!XQdb%6c zuK_qS#aGYNBtb-!8+a>(n=;hi$sgsdevCp8K>h+-v7N0|6G_A(FZhr4 zqboh>zuFJ%+l$`4D_97UFd4!he4TL`K2>;mgdfoAo>N>A0mZ>PX`8VU?$4!H^fl-{waemNpE+pDugis$`E#`WF6GCvq2Qhr zqj~&GG)rwgRWAM*Xh%QuAutFx_nkJCR|y52aF#XuM5 zm$Fy9sK1SyP4$>ViVnl8rf%b{Ng6g}K-O53kNya7Aj|`!NvzkoQYH4CwV0D8lyujN za*Mz~j0TN_oQQ&=DUS=k8$FzWYO3=%b4j`X!4mun@pES;mC&NdQF6Y`7ZHxEZUraY zry%EB8E{000O-Lp>$-Ffz zOtOsLFUYFy)`M%H-GG7T%IR#c>!3r~8S-rcXEp>Wqh$7vJHF;;Y9*Uho{FwX-``nQ z^hKP$zA%XXnwtUN3*mS{-N>2KHY-6SVjx-5ElV?L|4$AK=1sRS^&jJR2`1x%R2G=( zg7a8sLkmL|u@oussW37}fY9#JG!>#8y}y>0;Kl??s6_%SEtxiP=iO}KahYweTKdeM zw%NCN9eg*7Vex$jcctT90K2pC`7*C_EtN*!xtF{=SY3XOQM!1kS&kc(B22Hdt(0wy z=|g%i3>Pbv@v1H{97(Dp?gDlMdT$RZgg07=hqyl`&vbRAE9~6P-WJqJ2;)-a6>Yx` zUknaoo71~nKghj#V8b08T$1W3HT8Wf>GH192GzXd=|sO|W5P6`!{-fPR064Yg9`{1 zY&cZbMv8za$sDH*VwMEnb1q1Vfg+zXXW=+N2m7vRVBL4h;xi#l#YTT-<>CLzi%@_L zHIE2i+j^Uo*Fc6t>1IfiyVBu68%*Bl-{y0my*%9M2kEo2ud!D!&T~d0$yT9H3JSBC z8~h~bL=emH<}80tnUd#}2u2~~fap$yS#d3{hr=@S3Qc71>#4&X4=(oz(Ad5|)=vnq zb!ImE%3C6zMno5*p5?!A@o-c&jYh~Npb3KAEFefzGp@at}BZTuZqpP&6JRvXDo#P>@O$r>1 zi8`b((|QAA`azhio0YoAIoN0N9KhU+d#>Oe*(QFTN50@1x2sw)i0RI;a>%mD%ZIq) zu_|3ldW?#r-N2`h4>mlQU1^SEwU%Q&a2JXhoYHoko+qW>HvT}0(J;uac?A&hJD_*K zLMrfkN&56LqxWTqko^2ya@XVc>(mvKp}0xu2%WjKdY4R@i2tlu(*5#pxl6>>s0k7Q z?<8a%w`+k50oO&r5w|TmXlFtQ)DLXxc({+_hy8GBG4RY(tz8|%p*b<4Po8NCPN91u znY%*9T`|&%&~L@V=pePa7*#c!2Ceoo$;h;KX*{;-U-$4gjA_m`bmdzk)9Ou^TpL!C zv+W0|MyWYLM6zl;kmv(ooV@Cn;Wa<{YtJ&!GV~@GEuy5Ts8k-4-s>*$@Vv2C(-Cu& zk@DGH=$5;$jJ?Nc$dvMFe zX<%a=X(Rcu`e7Mynwa6}KT916e@P)Vs?vaf=m&Zn<*UI!v5w{V7RyYV%TT5wH*_Cn z&XlMT(sWrcHvDQjS!1qMxUe`(2-UaYtpXJ%B8S}R;0BlKIqdNxkv zlm;;gAD}l5i1V7B$iljmTG>@{tN)g$c-? z*&=5Wy6Ry{0x~5(@d!H`2hfV!m{%s1)MzbJOZTgfHi3XQmoT)wICHFs!^@O{4}VDJ z@+fe{{9L8Kle@{{a zm5@%4sIhy}blEn&%Sm~J2JY99DH?$P{Z`85l94XNCasdd!?|)RhwURN2f;1irIDqR zb{B01z6HaoBLtL^JQ^{w##0~4M(3~nUsbbDr}6gv5)YZv{-odDt22Dm=F5#`NYYY` z+@azl8}jm$A5&R54qPlq4mOD3SNA#KOyIBoN$J3a4^r#i`Id3lXzm5^OzUR7O*ns8 zngKcfm&vwG$@$()2YGBjadpD7929MrE&6y zwczQziZre&MN(t;C>zd|c_E=EZ>>MC=~cco396UA|J4D@2*CU$22b?~psc42=m-2g zFiEb78%HlVR~sn<*&<_z?bm|2d6L|a4eM*_*)vH-f_hSCD05BJZg;uZJPkq#SrwuQ z0(os@)rUN!lR98-e%EHNl3G(jk7GO?T@!KpWZaB3V$~+B9%4=%OQ(^XWdA%j@HXyrK>yuNHrf)8@B6&y#F7eb`TRu|lFuztjQLp5K3d2VNPyEg> zU|`|1Fz$N=;+z9TX%8iEZ#*qTfxsuJi}wVZ2BTb+E=S%T>9Y=3s!R8qA| zpS2$6$8Ufi8-Eyi7_Ww@oqUy8#_A~#Ni3BaH_5)Loc}jP5@rrcf4ZJubHH{S6)>I! z`NhH3D}JYp@vcKNCPmQ2YA!P_=8IE7i7XwoW1AHI)k!o z!`4rgDq-8qjW=a`uN{%)_&&g(x%;Arle+}&U*5qUOSQ9Q)k584ZB)q3o~~kb4B&gu z#5ppVsc|x4{j}jInQU~zj}h35vP=7v|I}VH*VMrSNYYfX~Ce_D=FaYnH(1MnHGZ+?>v6e0(ocfGc_6cyUTaMf3E^@;hjOaSlDJm zp(z2ha@9CYrdyL6Z-m1Ot#5*F%7zFXmY|s|2uHS*zV+J2OM438%MNN$?SuB0|Yr6CzLuTy2QtlEEuFI>Ha zQpTQvL1Q62Azg$}Q? z6jy4~<9to$#x<&VH)D0FBhYi)T+pwVCOg}nde$%AhXi9DfQ=Cjv@O6I`(HRu+h0c( zen3;2??1|C{|As>&NaY&dH=-QSmHlNCrk~BKl9ZizL0~yE$Vay@66R@5l%U$jh%-J zCNE8E{1Z9+*N2?XE6ySOOM{e*(;>7kpo1$^@au@fthIV;axLki~`IJ9& z1k<#JQg~R1UV&ZyeVR^ZomXg{n>H%auM1n=YMeh>0XsjJ8(x9jFt|UeDKT#Dt>I14 z=1B9q9A2EqR4aDC%=z=zAsOPdxqzIy9Rq^GqN!}0yU-=K9@9#aqky3=vXU^Ho>MtT zdeH&}fXQ?s0ESUIb6Dx+feoCUM0SA<@?%-xJb837k zR2KuDUkzd5`j=rfOqP>yl@Cw2hn z`QZ)SqDLFCvYD~vxWVPccD1HMKGA$SMpBH%lD};T7&pR1Y`&f4F|{(vaWn4ahRtsn zi!iGOGac+1nK_Sh5+`;l)ic8ZG(h=AL9ERD`?&E#3C#*-KVLVmU)kb1Xio@qO{+7A zDbB(IO2thW%9J-z??aV*noDy0y6)*&2#YFOReHt}3+$`3M~$*<-=!*3HS)#R1E#8S zi{gr2w!3~?1H&={n?F5p6up6(Ib8!RY+hwmQI9T1@wLrrepSyCol8~n%X_htxT{%H z6S%2sh!d}bXB3MB%i~M2W01sG1F`r{Eh#eOucV4vS>tc=XpzXe|6EWZQ^=c5#<3{W zjjUhU73Fs*zc2*iw!~IsuE?LB=%c3lPuhh@(rNTncr5Oza>Epklp+uNwS#Hp3bScs zyKd@oWe_{%NO;V~wM$K11ky^xXzNqcvn&&=lw}lJCXK_2D9la$q5zZ_IVw5~KY)4p z(RV#RpX5)05F?sjAlLDJvS&nZa%sSKTiq(%DvPiDxFvwjm*H^AIB-Tk*dqaH{RA5* zaBF&TIpT!gib5~jVJaxkOd0A|j;Gi^NMuYVS>?A@m^EzhLVi0S+(1O>55@&Dd0|WY zu$Mc~wo_>;h6X|pLqMS6re}t~2DzIOrb$??V%=9&_-!BLBQc2?qn3y0xn?lGDOoMF zbrpPb3=u(y0*`^AXYB@&hT=RX&n<-Y6xP@EYxw0EmU1i!uY)XiCo?ejkSaoxJKpaH z8VOwpavsh5FN*(B&{sPDKsN+aMl3YLmRxyAEM_Q_Z?wS!M1on->j)usse$Wvgi22- zS4U2*h~gIo;X(L&W-Y8)OlVv4mq~5PpU-i}@2DBpvsnXj{YSxMPx}80vSM^3E{~rE z)7OnMA`~T(pFG3HJIpf{5V-n0d)ipp-rUm^BW5RP5o-V{mayn=tj>I!_i7agxn0u5vfFKYp^$8het!r6 zUF}$5+{AGX2-6e$00r2F$fqgNh{R)Ty{K>(lafOA7F^5JD#! z9`0YZwjvg14R3CaaQ5#K-`#BNhjdx7$BW=o%Gc1))N42EFmy$sZ{ift;YmU{uJ7Z3 z&`o$%4Pgku#5uCYcG@@H)qXCdwMZD7{S>vaPb+MZ?=~qhx%x;@r@*np9xg1WfvJ>F zi|t{>*idvKaLYzjsTxvq#yH5D@1JD%&lFXghl&kCx93*lU!mWm^4EF8gR!bEH>`El zKm;-=vQP8{NVLgyy=rvk*ghZF;kBw14DQGT%oSrBzU0dmCcfqUjuU5~qs7^29@LVJ zZ617ql!{Gdv$ek6m5FiZc%UyL-M+wN{I*70D#(0#I>*4QdfuHt!!Ca? zHYltwx}ZEyOWLlETV&N^7+7NchebS43aVnubJaKQ^h)`mZN_6xr1O_gQ@KGWP%eX| zS?rYG!IZgh?`+?xHWY;U6TTiRG0tk|&olR(Q&W=$ z!NTUEM*yar`U(&=^Xljy$e;M~*n2jnukS7e8ddjhL_+3@gbPH(f>Zca{BzTxE_eyZCBzz@L4ibdpLss!yQ$wQZ5BBPB zEOn8QKds!36oF%wSa`#U8!ZM*N+?`Ofx>N~%tMn&%@OtiIJZj~ss>02+O zS7#_<%=a$TO>(`=Sit-7FI2QKC+xi$39}3_C!Q>!hBI|B7kVehk<`stbVD%e)kP?$ z`yg%5M`V1B?ERk@C;Q-auqquxnqS*qgoC)5B(1u#gQ)J5vziqf@4^@++^sJxH;K#~ zr`y~k*$u3Q`1|KSF7z{P^Of3_?91!e1&<9#NTz#qsk^eRdqzdgmj$M}EzQ_!Glsiq z9WFK&lBfGgR8h;)4^l(pE)rc z#__Qc^%48WT&2i|>bWpyxp~_Mg%-gzen~wLhGs3VN%2g2dp3$@P%offq*!4im3tAn zw*`8cSa)*Wz0W%#lO~CuarjBr5k3A62Qr&4&icuhn}0r&U1sc?4V^DD^xi2p@cwhn z0{Bd}?J?_+)&1|y75}mS?bjUWev+;EOqMNBey7Ob5&D0+{y)v!jjDB~Hd?Cx?W)$S z{m;M$to&V;JqPPV4>7mUmq@+uMNe?%fkbth-$n)Jd^LrWDVJLxq7(1d>F%gq`_fW# z&nGyt1T#P+Dl%7DYIP8*fNekY9f-9QpUb&_(eFy;&~81zk+rqCjs4HY#_JBO6i1pPpWIz~CC4^_Y=Tp$mfR>)%&B+&*7DmeBZ-UzOyU16)i$ zzTBQ{o(~=_9(UoRgP_=eK95lD&reT^3U_$S-I_K#-%3h97d|_~aZvuX?VnqMQ$*=B z+f`gfc;6>F4kH}Q^9&iG?d~yXTmP!a%dykjUEtA9RRDOeuRV1z@MTW_{;B5i?;EW# zqXE;Uzaxc499&|mdWi_2p-P4#vG#kSX@!6-zX6ak!D9#ycTe9I;B{+9EyCZ^_ch!X zEPb#9jMQeXdvFfq)&N%NMiEG}-GH?i1#1LzZ*qZtDsFNEmo0ttGHw1|Jaeb6hxcm>fXt}GX66(D^07=!u@JP&~3 zydUK*I)rH^-vHdOgztF*kI9}%3*d_$axE~04=99~a=QWcjM+`)uW?gy+u}m-QJpZo zzNrQXej3iTAm2e9(=S_r{#vMjphd&Qo6q`QFvwie0t~Gk0f=0all#WrPk8PjdxmJp zbow214qrj~I&M%)xX$FzfFEGfYQ6VK?_KcRsyW&aNlco(S@TDqi7i7^whDXGL0ZY3 zE;dZTy)MEtl6yl&SYiilhz!A#vY>|Cg~v_ahE<-!LpE1#|KRE$WPSm6H*){>3U)w!N+v zI-|jxMWfm?#dG|z9o>_w7T+mj7ExZ(KJCH&<v=+*Xq)C7A;4|^T zM7|z_ID`Dbhjm=T#zf%GFxsm{B_zfu`Vh~CuE~F z2BZz2?Oz7VUllZ$`XT4Z{xn=YkdO~Zs69BqHd__FPF?nZfgb3%i1wiYMAczt0v*R+ zC5>0Q@3wXdTO6ZSUG`Ls9#U_==e__$l@AwN#(!qG!Gh6{t>?8|*Nr`GqhF=9t1}{V z^?HFf3iIKR%y6(bzNq6Ha9u0AcGJ`fjcTV~wfQ+961Q}?%Vg&9SF+LVbs6)oDE&mB ze#@zkanw*wLlr2~2;sBUV$J%vUUdWez55a;Rxf8jTww<;SaKL4%d&7Q!ddG1$fXy% zg*n}Eete}+T@|T}P@T4NOjR28wF$9m$=+QWV8VRu>^{~n3HJ|uC_V{w`CGLa7uL{} zxZTpJodIf2TDt{Rb(@t9qcg3zw6{_=8ouExc<0Zjxy^ma%_~9VD9T%q9`_Mb!Om~^ z%7&^o_#V$m;_77%xHtp$yZ-0Pfx#Xla3yF~Fy>eB%DH__E!j)!%6S&>X=UXt=TB=( z{o4667njfZpB2ON=apID17=+0Wme)~9I%|rL)YTCI~lrU8;V@O)~5*gGUkHdRfFptpZ+?X};)2Y8lYD-)i9TGxcjHPIHmOH+4 zjF<$e>RZ~XCs{9vyKzDg_YFi8yWo51&BXK%lZ6V*ngoL18ux*oR9DJ^Hc?oO+-`xj{7L0^vxeO?L(`FedN-_I@YL^gY#LP9JIAwEYba&Rq3_rq}=wR;&CWhVg20j^(1qk4?YUp zujR^01eh8&OKU2L3o`ZESm*sVW$Fg8h~@zk+Lz^tjNe9(h}=VVl~vK<2ic8T^U*KK zY+g}5V@}G$xhROoMUQ~gm{n*{8{{|#&k#HdbLH?G{==SgA;ltuO`Szy$|yYVUl*l^ z?|3$ler|6-&Rn7e!m@4N@zk#c?dvSH!D)8%{<6j_fAe<;Lwlp}l|yvF4oAV=zxQ~j z^Mc&XA14ph8kwL4kmR+n=#POKE3 z_}|Ggw6{_(K|0!ys(O%vU6Jt;B!5rvg&L4X5FGZQYi%Xgq0F?wjyt>NNsA-tkSpRE zc!iJ7t7XuP;pAx?wdRwp(LsCK@goEoZDu= zr9lpI7${;ucaHmlh3|qQ5iO8?vZP$g$TC5BoKDo`7~OwqP1bD>+I%_a1J=AZ#j>#s z6Q+FhY%;q^h9CNXP9h{#1!_O3!oKzH3sL1TOnUA=M9@R>p11-8TiOl5s_Y{QN{q{` zL@iTKrNe#~#1ex3(x6?wNI})5^x*frBp7vzBpqmf8x+L~QoVZEUnY~qIMqx``Wnfk z-z_t$u1=%NvT@Fj6_)6vX76FHz_q_A+=C;))5E|HeW$l9rCkx8(-uc$`m>KwT;d-6 zJ)`srV@~it<-G<`fiure+SN`plx(_V12lU#p)c7F@MYyHX-1bT3=vuOawiuo(L&LD zEUur!Cm4m{jCOD^LR+~|eXBWsv==N;hkq~{UV7sx8-_u~n%r+}ulV>xX$t&~(%%S0 zyj$R3Npk>BZlCP*Lth;$Hdo94!=gE;pN{Kg>WP!CVyctde3T-z`3`=j2K|#V{j!X| zF)bYM%5V0;cV@|f+cxxNhAIYNVX20&VT8PZ9WH?5CtFa%e@J~T=;W;0!bwk%k1Mof zN8?xRJpJVxIz;lgGb3(NsqJ{5qcE73k}gkm7D0X-z`FoTnCopG&%^jQhate@tbPp~ z_CH;G;Cjq^0VyvHSX6{1)GPLZA|d)mVq5mv`JX@?r=HS+Vs5X- zjz9dG{xjMgI(bQ6y7&|2K4~3j_$9JW6twdS>fZvInpU7KSuo4mMYYFou^*-%KnV|9 zpJmT&9)6SC8{L+00{6Vl-ejKvj@^-MAPL?U3Yt=0el-T0s@m(8{sS7?+hLUYFarbu zvPGL?f~{T=%STZ_TIy0y#TUwZ3OdmzYl1p5DPsZpXXA5GJrcfh=fJXlc2P(G%!GL% zU=b=R?_K#)W3X1$1~nCJSO!kTS=x%y2CFp)!@oc`x%Yskd83+U7}aIn6{zi~iYN@_ z0_H|j8VW(v&%tM)*b8dBUb+i|B-CAj-)$9qyAFHSI0QPw-#cYGZXjtYbS>d?VxP#&Czu(vb zFbYof2e8&NfSFVO41PCQJp)nc1TwFH;~{T90K^oV3~qg$G^2@|QJ^ z^M|k@)q*NB<$Zfafx^uF@pB|akEZ+tSkUIReS|g32CivQ7NcO+Fs}gXb-4%EOe~c6 zFsy$xFsWw`7(>kW0d7PZm!7uBNcgo9V~>Yehq9sr;h_QE1Vzuq2MOn zXMlab>=8JBwSH0VpO|bOR`y|rF@*jG8?6GVS934Gy+7m_s7K2tBnHgaqk$%vJ8Va= z0sVeg?M?3)r>qNYvEbHUdj*fZZZ_RE=>gy**rZsval3`Tz{y>n0A!q#Bo7m#)@v1X zdX6t)I3gIeM$La7E|#>wJbbZ!F^*$#)(YvtxD@5pvU}ZBqhtXNiy)PYfhhO*SE7n` z0=dk!X@KJE=7Q}h(2`XW4(MvF!utG}EAs^;M43hBz^SY9Qls#)c{?ruJ#>M!ub9=p zh6edFUx9?sDrAtWa{%)y?O{M?!h93hXI)$adZ$d!z%uP%7`3(0 zjQKt&2L70Si$;`d0H9uqL6P`~;US$!eJO~D;`vbLXBed_^97`Xqv|f8K+-ng zzX=0))Igy%igoS8(L_&UunEXtvlviwmL3Kur=>rLc}D!}%`wr1k9^#`01Q`kRz1Hy z0|+}n0e?+uK+TD6@QdS2a2q22D4>4;1>eEepikZvkkkh2WftFW4Ztc|a1I*V!B4Yi zFbQC1_Zw&$1&l}31A)Pt1z%`Dj?@)c2#?fGj0SKdy$3CB0k;1wwm@M1sA@3)-CiY&|f$2e{-iTd(F7I0W3lONwaR>+|$oucZuzn5(1$WE% zOh0_M;|N2u#nSt0m5TA&#YovQEzakZ*E2F51+04QV>^9yI_&8;{la8HCJgeqDxdXu zik-S&mmy!dCfnP7t0SqxIe;3Dy8R~B}zs`7p(Fw)ChMKN?EU^CaAb9TcVs^*$J9 zC5B^!#|+f}1OFWrq`jBNd(ooG@wXHFM}6x<25vObkZHFi>5n^=d+*3zN(#uI@D#sb z8$Zz8i3p|S^$zUq=P#c|n*$IyZwn_~YCEezuuS^~;_{S!&A?oc`+v4Nx z^lBE)gJ+shT8`+I-!CYkZzQ=o_E{g3=$9CXV!mYeRh9;XCnUSQpVa zzF+9CiC!?y?0;V~qSwOWJDb}y4yxf!um@OnYqMl_IG<)HU;IAhhT(||ykS0#dxf@6oxx^axs@3;h=gzQiix3$3}%w;52j= zyY}g(vv=omR@9talAR(Jxfb=+9LMWlY+WYC5tUF2)hij~dAsPz110Vf1!Id2W6%oP z^0Z75i;xAKCy^8%_~?uPtOBa)p#Q2vi z>N`T=6M}RRX7{**oW2=zSmaqX}2r~a&x~fPev=YY67k< zF+Qjmvy$Uiw!9oHEi=`(r6qU>UtCnN(Fzr;6(ofR6i1w~OP1UO>ktw!s`}ANY+`rA zBi8QunV|E#q!LA=G5O^BVPqnZ@ftjDLsv9q+3F5e?`&=~m7wF(&GJq8WTV8i;+)}F zUcq>mC+9)?BNuaGGQBW75jr{!&f%k>>2R)I@3!-|Mb`U>c7>(EUS;YKxm2?L8Ih=t z>eUFvkA8NHH|NYX0GfaEgrQMl?2W-SBYx9pUn~!fvH;ib5p_72fJA$dp zfpqDzjPC0CQ#neTZ2F&PgZBxSe3#@N9o$sVZLciPz_19n*6m0Go1*U;w;g4;v+^*L zW-gmx4|UZF7k|x0jLL9Nn^$M~XVG9MHQ-u>e>$xmZg%j?P252)Xj#oUEQnw40Ne>=ygH?$n|F23i(asLZHhKTq z!Sfd;h}>(QiC)REZC+Tv->=VJMeDaq^+vqZaT8^;*jF=zvPSLYpBxN_^yPQw)sg3B zoXK}HM6=5E97&eqx)+dnTQ|(;gVnKZ+W9q{*6!;2|4 z%XP*0P8Au;MlXCOlfL?VL0~*ku74^i`!-7F`Q3!eLf~zbOJ+8K*B5n7kzt!kH1i48R>{!ECgj8*L(+!t zO$91Q2U$CFS-sK2%Q&9-pG+*Ij~c%5`wfv8AlbzonFk7G4}MS5qfEjG{2c(VBmVi7 z!_aadfUaC8<1WpO&xAIuq^M$yF%sj!6+Iib?+20MdO$rZ!7*(Jk(%P)zRkB~A8t&J zQ21MI4wGEls@xEmNjew2Y3763>%IQFYVINOF}f(u$Cc{(obcQ>FO2EeM6}!<)Ko{r=`x%S~e4YtzHhZ=2kU=*;ZD8PVg8+&u@rkQ=dp{$B2j z^q&I(33TpRZ2Z*@V*GgmJ(G5*e(qKLT2i(w-8AIYie|#St_TnUjdp|D4misw%~C@f zc}L+247Z-jFJ$oKW^+k~Z1b1TWx_Xo5(SU%AU5LorsEsoxF5c*Mb=t?D?Sa-qTG$4 zUh80&TPAJsk=1HYaUD%%rG$n|p`2n6{Y+v9k9dBka6lZc6FGC{EFg_dyE34yWs_uao$Px_LylP>BD)&x}xIMOpuy?D(T5(n|#n|7lEiT=2~r`Bk;bxEjXc za@~nTVCk35HR?BjbNFM^q0Kg5HB#%LEQKESw#6TZuZ&}gS%==g$8?K_QBQOg#tI;z z5)Fe=1`JVk#IETidueo%90D7JGG?|A$YZM}ZqHr{O#hfcXi&Dz>&PmJ!eW}H`P8qP zUtp_tm)C?eAF7GbP~VE!e`LsSPdu;wJ>2mq&i_Nsa0(xtjVa+9hV;9n5n_faxZNAB zuUmAMOPH0Ac+5b420;gFCE_@_ZVannnTQRGfQVeK6k=sbyy2Qpg68jk_#aqqXIUVU z4G>PPp%y1V`bZ2anhN3%qcF(OjCd9*n&^|Q722PCOV+CbY4jBWh>4Z(x4;fLu9@#} zBJzR4Q9~T0U9Y!x_3GYRXR42Myd#x#yWI6y9*1xo4AkXiXuOJP2bI_Zq1o9h#!8b} znbP}vJdQD~9}Xh%CkWWuFgoDI)*A5BrS2ztLgs~29A?b}icI_aTT{4(pXMB7ZSz^l zw0}B`yVhR+?D>*#Ck$TpF5)eAe-0}tKjmTWa(|mBLSLfy1V>s_Q>kwiZ^b^WJE45) z!TbZ>b_61w1l6O}$($QXaZk5JpP2g=5S7)mm7&Q|i=>DlYfbYzOT>^#Lg*)UN!bBD zq+)z#|BdEjSCJBu>;bq7Z^3kI)Y60X=tr##(%SK7UPW08-tw8%n6SYjY!OG+q*p#B7{U>dcaDd_f(*cO=b0TVd@Dx@HdhkAx$F z5?Cmge_JABXW+U~av;J;&;{X}&SZ#gm>P|4m{VK%Plvt9a95OrZkK@wl37#+y3YnJ zf24R6JMcPW>Lt?}BkPPFOfgE>M4TZAc-w^qZRU_DK`f4{@#wQk!)9b>%^I!(ey*2^gp$mZ)I>W%A_XH9=0=|It)6ckY~hr zi#-Po+{xi5e5&DnXEgIAOUat%r}{EMXE`Q(=rOycyyKMq#DUSs>;AUIbpO?6!!aCH z!**OzYOAt(`mJ2N4nf2P(~~HQ4~pjVrr>05)eS(oH+!|IwL?bHpZ0qc@ldRahRG?q zlAXFxxLqjGmi?Boh*2U=i;4S~gNQ_yMt!W{%LrMJoUS~aM&)RkG0>bD zha9AmTX+Fa6QSzyee_8ciSt(u#+49W*y^g7{8n`H(+67?hUu*`eB<0KWVNi1WrT>U zi3{vV8||+=Vh@!KmECwa4@XzeD#MIH z<|ftnocE-MfR3&b<0c@cP8V4nkW=ENn}Znq=9I%LD~LQ7&B|Ib5mBm#)_oPF$u;7g zC&Oh5(_fUhQfW~61gI`wf{Av*d%T|>&tcTKv{+FTuj1v#(9Ls&Bx1`*j4>K6yU{M; z%kRAGHVuN^q%|BW4E<6z8VRJr5tM->ZImV?Cy!`OB?m^9;IM=Ov?=(gg$N1~X(&1y zgB1)e}@5=2Y;P2gf z(k9c*emE>Sg5TYbzInx7 zeVrNmrr0JpT`89lW~;@2Km%y}A~`zfAOv|pyrv1qBn3CCE^Ou+hN6lRCP(vt6=vG8 zm+V03YRw90WpjDea2ezs2XlE`8F+ly7<)kyukqps`!6S2cgV5Wx*Cok2Mvu#*XHcq z--#)Ze4@mww$_NfW@)RQg3D)KVvfKm;LOiYy2h`a{y{DM2EM(4d)}tqTr)O~=M#oh zQJ+GDMBEUyS;N|y<}CTO9zT8QC|b8xakXn^7f-SwC$jwa#Wx|6k~2#qyWi&7SOt~C z`e{+5eCi`2lc1_`ip&1Gv>!Xz)-CjGBN6sK@zX8SL@=Z<|CzIcuiHqN{w~MgH7G_w zPD_XfEfa)NerIxkS44pi%6!N0@tw#v`I9Bn*&Hzzo&}!0SPG^TJ;~M7n{WMJ%q85) zBYBKNG;-iL9A|;**(qFq_NiXunT-1tGbg=A?YWQNtmMR;aQRyh4 z^xieGt=0WGh~7C?bH(Ov$08nT$>GaCZffPBE5w;rM^~~eNZD>;sq_4fa7!K_kfh80Z&tq-gJr?leL!d~F}|F!=rsWAuk17ON18Od?oi zcjXVOzVEGbmezLD1oZs^IT!(ZBLkwq~=z&8y4;pym2SY7pAL zu>pbhBlwLb0}DkImb$d397Ug)>xmC%2ow9>yRJFB50XdyQ%_ts_hFQzkMYpS6vh*O z3Ev1Q)AuGMe#sZO~Y<64x{Gyjd zUJz?Ly)qZI=vXws{l{!r@v}qbtG7O}>^`i%+TO#La*APE8$rBJWer!~X`39FI1^gGVEQ46!mzLAB6<|mYSEq&)ta^j!BdS4#Sk2+3`R z<|@L$LYlv1(oGXssG)R)2!&xtLq{>>RSXHrNL^fByqIVb5uWxM5xnmbwSQZF{Xua0 za}y`xD(tW9$8@BFIt$FnnGlhYO%s2NXr}cOnhBG*_d~m>#rw8A;1$U|9=XU?*3(_b zdLgS%pW6-9a?5`1I2Bwwk>eU}HYP9W4(n?`W{ssXBb_cK%kfj+ChM1+*69aivw-|{ znO-+;-_C%fl<=spTxK9gabuvJYp~7D=2^p`$ZN~9nTM4d)F#WoiX!|ld(ZqwT{oTcfn_S5DP|zF zB6-U0=K?@hVtv0{%~lPPqiHlx zg`dUjF(myFb*V#D{H&ci%*Z~s##2=Vr&6rlQ$d7VM#@ZGnj))?$jkeM=vPi$6?EZ) z9;l92C-r?T$6MbOjJM%uwNze&*?;Tlg@*d#@_Ns9>6mc`&A*x(w$;%A?UT0I(LGc5yhD1L|H0o6{S!nkuBo}-~ z%<%T9pR>={*a{v}KP(CgTR18OF+`w1m<1|iU*Ve7f3Uap`)cDq3aJm2Id%Uk{k_wf zc2hJ|dnHy|u+ZJ@XaqGLo1xYA*yKtKQU42GKSY@p=EFzEWI0)qCW{vE z=OT!^?C17ZLmth5iwRxQ#8m-nQfk701)}>42id(uJ^S_n<~)YaW_tGgqt3WHTwCkw zm;4r3l14Y)KlhZSxhGJHA&T78AMyT$>W#Xk6bBn`rxBXjYPFE5&=41;bDeY<#ZPuX zv2~*ZdGnC4^bM|31_ruJAWvYAT6HhL?)>o#3blph!zT^vSR~q`EuSTc%5~y)C`=|@ zt6x8b^MR|mq%|v1t?CG)Q#rNmyVd8+kn=rpZIOi;6J6h6q+a80PO7=;oDw3_#Jzf|mi~e#bdMs0pwL z2r*M%T>7Iy&c$a;gvjbvB(BxKMEJ0m@0J_6dpFsX71>ff(VkMLgcZMcqm69LoX2fS zMmHNyAXY9X3vpl}?hOP9zyG%_ zoovsaBPyiPWEjRRDPV7`&;l``CNhY&>3eWpAJt(i>)+fs?Yws|fz{-1ponlMWbBq{ z-#3*AT0M0J3#RoXtE0cCt)uP>2*brWy9Yf%nfJ60pxS3+!G(oNGs+jQYVm6%5^68^ zZv>LDtD>8jc_5$5hPFx2!HT^;n{Wd;d`UKd<({j(FPbB2dL=)>CFbc==SsS_Px9NcZ28`yFhXNRIK3S>*hVjFZ_6Ou&8Xs89`)%YuE|$x!Wzd z{_TD^+YyDAzBw{YO!mj$)z^2$f!Ffh{PiD0$ze&hm^6ohl}#?CakhH~=^8oNA0tPn zACUk#R|!3wu+|{^Mf_L?VV|E9*d5?f6b6@IM0nrwaNdLwU+%Ok;CYA z|0K~}q#*nw2XtOZS32(*JeJRGXq2@gmXr@tyubq|_xDxbcqp+mKdrfhP2Yu?sDvnUV z_~pIh!Tk&GYQL04M{N%g7Yd~UD0ZdFh=+$#DHss4h<{=B${L$~FglML`m(6_dJq;&qv zJI_q33w@508+n@5pL!lOF4vkPvva*LA{qa=Lh<(I3dcSmKMnJSXk|(;+x0E8{R-Yy#f zlT~kEkJE>}P`VoIY3ZROoa;JDj5t!nTumZ4ZoDw@H6Nrr>+c6-Lbq&GUM&ctu`@Qw zM4`+*8l{f-FM<-4YA2mFqm7mcFiI4EUWgO#Z_cd!U1qe#)7-L8FZ!9cq%$?WkFV+6Maizj9g=x!BJ} ziYB4gsJQkrA%}`Q&%>!aEA|i%ZfdUxv{g?4yJrp?sp4_tru9xnZV>YbE+~U|YBgjobk*1gynYWw3q= zQPTz=v<7-i41k`zCyCYsn{(U&x^AHx*E^0Jp`TgXl zga%yp9SsCF$!O11EM)e`b`->)@H*739B;pxqh;02A*lOla1_{OLg5E@@v&_zdcCy!i4YYK0hUk&1I{ zE?%<&?Dx*k=&vaGba>_k-;;2;Nmb$uxe*Wz=eX+!`m*w)@jl8~46DtXN3QWCWcl55 zi`e)JzIr1x`xF^fu^AI*tF(EF^ESPcF7jfQQvB{Q_8tEBaPa6+87UKedQu~8DLG5? z?1w8)jXT80IeZj~6ls@a#NzhioB&5oZOv+2N#kUL{ox#UXsDjrvwB4A$}dd@6uK6} z2Ilk_bqOZCppX1ZXdGn4aDTDm6b}lrY>x3}RknUx9~Nk|bG3hS-D`~~cq^m9_pu+B z^e_1Ii9QvD+4L}(HBTP-A(XRQ_fBewT|Q0kWyuvKshIa)2BY8pv1IH&Zr=V=YO~ol zqvjh#ZhvQkRPH`%Z;M>aGu5;0#?&QVIM<fAv zbMAPnoi~qi%A5h>oc5C|bE@u0#hvXZL!qpV%VjE*>!rzefOdiWQ{@-s^tY|YW6l$ug>pJK$}gEG~H--V2o@p znG<6e&jq%T#sjEh(zx;;NO;VH<4W|FqK_-0z3DSq$aE*l7_Ca4qvk;pv#Gn=iYNU( zrV#|ZX=j7bg^^MLCgfMO{8Q-EtcqoCB{PzuvOQYJy>`7MuM89N+I8GdE?qb2uH?rR zK6lbWzv9z=1&5#u$Lon0+wK&v9IL_;lBspPmlIpnGa@OTKZ(4MC~=TtuKGx~SN5?O z-soo4W!#}`Ny*Eo@os(1;_4WKqDYSunL$93%o9P}`ltDC=RCO;9CWloozWD>ujq~1 z{FI{z$)ESXkp?`cNaUnRNpQ`D)b;Ls)Q>}xmBX``!E3`dlM&tQ<}Utm81^3XnaAlJS8&| z=|fLdHg~n1nFA|sC&kB&j$P%doeWN=x@}iUi`k@gjeRKZQjEWa%8$y)BKDUmJBD+$ zzM&?HrR({%rn6U?mSiay;*gIwY_K)F5T!T=3zbP$9bOy)XD?dfZ0ersg8Qg%OU8mIpYCq2Jw%739wO6Ig%vTS!TOe*849? zbApX*{62%g49?v9a_Z#-E~6qH>=tPejDPY7(yZ}^WA6^i_=SoK|L`ki&E_M-MzU_H zQL1J303>mAKB&;iiKs)4@|WC04y_jes9?Mo_5k2M=#hX^a zcq7L{0bCb!5=nHcathQ+cb3Opv}TTg?*-Lv9ctx)TAU(n$!GI6*!YEU3Y-g86n;{e zlZk1w%GmA-UPS09HenX3Zsh{W9cNp)9 zyF-&d$1CP6^(E~~6Y#xJ5rkgP0Q-rtY|n4b8wt!HFQT6QDorp7=B?#O;F)^UZyCVc z8G$FHuS)r=To@*qqiu%fe(t~Ubl%`^4b|kH?5H89blQK?15OqAHmxC4zu>eIP_6la z`Co3H2d^CSO_uzA zHLT(Xn1$^T3vTrEC)SH8!DpDz>6I)zhOt(~YfoZmj;w23@}KEs43`X}C|fj6LkSX! z+h0;eMU*NcXbCq9b~vdd;_BDxyuD>=dC9W8-FNB^6er;Ieupj&Xn^f3l~uH0Jg zdE>UksEvHENICMRQJ{5B{o`wtov+V~?0gF?$~(;S-?|8U^PCJM)H1tx$dj{4st)lj zmW8I#sqy3j*NNPul;v)$f@BlSVa8Z315+v;5IP2EU?=}}n~_ft;lq~@1>qKD`47XM zdSAeSTuJ||(u#foZ8My?IkFxyrqgeXPqH`E@@sLTDHf)u<59Xirc*z@iq2zEPn70I z7dN03X9FHNU8T#Qb)V(}fNEzJcFusb&qH7Dwt=O_NHmtSoN;T|;p|xdss#q*bvqt) zTzT27S^?HpLC_bI&#)F1;~=b>Y0QX>vnCy@b!>(yWV39#*!BK--Xnb{s87F;JxQ87 zfSrR5V5bDnzkmX2cmiXWo&Lw++iW;I% zGN&Q}OXxnqvdj3~Ys6HeEIa!IEh@6OB0u(P7TvEcBb>$83)9*rHnsNe%EB~9wIui z9>>3V_GlLCS(8PgoWS41x2D{3dwWX?Eqadt;g0KY52XE#Rw7yYmToXwL-}dJ_Oejh z?n7jjKFOi<@uUi+22pACGHR;yVT}^U$(#@~a`b_%)!*g+A$o z=U}h$wE4iNky0Iw^NiY81L5nIGqL?Z%^#HWuOcSShCa%0qr`8KK2cnL^R?hoPy+e= zXsFQEr~0=DJG|M8svEQh4=^Extm*&qXPVJ0F3oU^b_#uOW5IsIZBzI!Go+wTS`f^q z?c9?tQq#mi9h#ebGna)t&M2@j)>uzfb;aK{~Rds}a(SFxiI{sm&M zEuUJa&05BzojMrd(W3gmtNfy7>f3W;IY$3{@6`A5D8JEHzkyiEK05kgE}5;an&Zqm zyV%cjri$b^FnJhPVuHNmj@3tCc~5PHp)Dty-`~zcxm&wDyPnRNWFRf8YVxo4rsn7^ z>2Att@?H`1TU`?0%+pM5Xc*~ALvp{1FrF9RJk&=K3T!Py<)G){-9GWaG|xIHQVK=i zuGk}G@v-6C;98xF9lgiT6cnCj+i)Ku-9jnW1rF9muE!?9nmbv?H7^t?DI~J+)}R|d zw>hXs8|H%~awo%+R#aHf;}yeYp$D&B${SYI$JiB?cAn!02hBgLON(I>UNjxAhEkX| zPx5?sQP_nb!a2qYU5eEJcJl@u_1wMGJTGg^R}DptxvHQO`_6J{-1?UD*zL-pF?s!i zCzDUT32kPJ$^3}BUb0w-Tn0P-Hpom3oxRMxnF*^e>Y>kj>J!znU&$4wW%ferzl_D< z;-WtBUue9h4!JiUS*xiOv)mSm$PzP5-{L?nX)Zx7)-|$)cl)w=q zTO`hCDCW+c&XSRJY z1;3w|K)zxMSp6Q#cN^Vsm5o!ZNarCk#Hp9D@UXHXxzvh(bhjUdhYrBo?S!D8_!87@ zbx^PdM;ejMy@9ct{i`@BA@uf@37#Si!3)#m*l;9i!PXkH*b!NJ7gLj*Jm27a4i0;x z`T3NT<-O`#zv~=;&A23a`ftYA4f>kKGidVo9DH822ir%UNpAo;F`OktlIVA9&$HFMM%mH zoLjG}l6m+YZr2dB0jyspWn$y=4n?6mrOf%F|%1n@Ps|9h)|kE7m@ASw7*kBH)UjNM-W?UUZq5e@k7dC@ZcoN`CYz$WNPC( zCOh{++MLMv@bXX)GWMMH(0Tj!u=jjsZiQvDri=4N9@LuxcF$yYmY)Q+o45#i@{!9S!>V>}5$*F{-B*c!(~IAv)?F#F7wo?8 zBdS}SCTX&2_2w5%jX!<1aE^~#*&++tcbDm;^<#DsaeorTrg|JbR&i=^Jh&D8{Wc0- zc^2?=SBVGKSK$)3DsBO|?qMCEid-tOy3C5nm&b5##I~Y&$UNoFdQ+z6!g}75;>{I~a3Bhl-T4g6dLG#3yqJ!K^cYMo^znU6uaF3F zp73xvzX%`k`EHUw7KDGQXSMs6tHThtJ=(Q2F$t#@_jdebh}sYuRnl_XxHghOkh#~;OokA;BjXV0=*9MWr1#z zzld%^Ge^B2XDa;!Vr;ArRnjc2f7=yGgiw`+5TCq=mB3p$N9h<6K7P_%Ysv=yiArG0S1sUqSO4QoKWU3*c?Y)bF2N#wqM`kp{-^&=ksVBdB^ zT_)f+Kgc~dQ}GG1*qJ#A--=hzuUFC1 zg{Om3zo(*bfR0_+0W=%inK&nOM3P@}5}d39;g_wr(Ok#J z4td~8r5t}%%-4_{ZebK%Um8rI3?^t4~JH6Dg;kV-g~&WytoqFg1sM$iMd32!_W4b2B&L1dw;p z`njAiu&BCE$llNWP1p~@oi=u7HP^o1n#bE>qAZyBu!PNShX;KL(1wS=bMU8kjg2X? zjztVJ`M%$^*lp2HW={KaSlTa7Q5_tyRxxNgw)}(8Buv1jOVG!I*~0~KuKPyMr~p%Z zkFZ}=fjon8&*ik)R3X#6Tf)T9DDLGMJG%wYEEa5UT)A3R(QFOQbgxn#%M z)r1;4uB??(<@AvD7gm?gTn8COkJs!wh=-C647OYszsGabW$4qfP3e#!eI9i7&<0qH z7U#|*59yQ~`J+ivLcDN%rdgpvPe7s3r#NzM>9`&CaOMHG(fqz4;SE>q7!vxYN*S|j76<7M7yl^gjV=rM|kJq`}q!EYHgk-gZMuaPo#4>Fo)0z{J% zTie(I8N}E;vpNIsJo&Ax85!*QnYsqqf}5oX zV&~nT67WR%`eimPhW&vf+8D1o=iU|};=);?Kr~I$qoI#mW|L(ilnl!vpx!X3TZT+D z$GtH9bu_Bk-FFHslf4U5J>R=|Y5c*QO_Vxy((DSjdb`%Ea;&Hj;~C>t-_EKzH-N<| zQ!CUazuI%o;=+$_et1+k`d;z#G`aX5tnCDq@77@*&wfEV=VlalZP3vC(autEbRLX1 zprTL8EaR|{7$QVi?^!7yLb%v|g>`1ap6omEAWX?Ds73aUGGUMS?Ja84*UQh3_$24_ zgzwnRNlNmvv*%b5=2E>scJ{^GN_F`Vn^I;_Y4w_0eYekjbU2N&>KPolVRmwLZ^kk- z?qxy$Rt{&h54Xyrgon{~WT3B1`B8W%kS1j~YcY&{C}g-go`zXoC8jhtM(oY#EUCZV zE+>o=D8XZ!eReTDp51;*qX)-Ibd=1nN}Fl)Z{SZ9n0Di(w<#jQBro5|{*ETdVomGa z_Dxz*0WofxzgTR5{?`>Ug9No|p1Iu;G3EwO7Q>lezL8zuaH>M#Nd3kM5g#=7Ygwbs zY)g#PFcmc<^S-qyve)S`F_{`;UBl!KG0aL z15ozgU>;jpm8qR^$UEhK{^XPAAw%-{$jH4iM~G5$M98b?a)7A|kNtnuc2_}heBs~V zarXeh-Q8V6kf4JGg6rV!PH=Y#GPry2Fleyg8r&J2AVGo-v;5xrU+l%!?q0m_%~Va- zsne&bt9z!N{(jCgvw!Le!Z7P3Fv&VIZtx^S=M?!gxyB{CbqEZC-&>gKIH~YL*>?H6 zpbtJ5ezsAttX!RPa&|P(E??ZO<)I{4>M8Uu-aqu;b<23J)@w;4#uLi9dadl2oKN$A z@ex1}S9=Zd>9c?Z+z)TvEJA0(1AW>6KDay zrpjoH>JE>I49k*@u_FTM&2#JHYLwdJ4ycG;Ycf!9*MppS`qLRj=n%7p(jppj2$hMp zF^7->*YT)oD=hlBzeux9WiAgbOTQkH2lO)2lX=aeCfKTy6oT*gQA2QKSFCVHuQcoz zUlseWG%2N)PBljbqT7u1x>n|FL@}6Ko>pb(j*367tCbb770zBJlYdPk?~}Y!@!|CR zAtVk^{6=Iax7Y6Mj^;(ocdM!PipwWuUfMc&h0B)}RVqtbowA%_?9Coh4s8A1!g>@c z?U=xBG02r7qGzud1jOwS_=dY_E9$V8&HijZKV5WxeT-OzGuZ{(*5E@xmif-J)hy)2 zOpRCh2R2i1uDcx)(0gM6gh6C_pVwjRB<|AxvO3z^nOBTnTUKZPT5onYYP%1Ax$PFJ z8dRvdREUc(DRR*gv-ee3x;8$81F@ZpFQVO1Q1a>v-D-2xWM+O!oxLX;;C_E%eoS1g zH8l{G7CvR1w7JjrxUBJ+Q#L(x%VikHV{xfkc86C8K)*ZXW!VSTfOS|91;!Rn1oC{YPf=MS%ni$f>9JtPUJBmxrRyk{9qHrf0qeDc-2jv@x$uiJ3EiYt@LK z1(Y^-V3jv8`xQ55U)0<7ZkBIx;GfY_|9itlwa@&kxXXlXAYzv|37hIB6N$y4dXldV zz5)FTZ!xMAUI_$P8Jm_~iyG!(R-Pl{)||%8A0gM&&VODCqa^eff-AH_HUZ2pixFvm z_)#)z+nYORR7&xx=PtZGOe9rmG6lFWO)ffGIb;U(S7V$-l9eYiwpu;wbxvy`31#on zRwD1$gPf{%U%nl>|JuVjuTb*mJ*#efspy4unYO^X0zpl1%F^dD~ws|&NIvocS zNCzIs6+ktHnl3Z3WQNHr1q5b z&%(R%c~hI+a(%mhz5f(4;?CXZP_#^>Q-<_o&zW4MhlO1TCu)kpdYkH;i$T7RZGmoz z1q7sp8EC#D_zuF1H=&1ImE*i=Pb=wLkB2=l{&cm31zY{2K4|zUbo=M@>nZdb>LV2t z`abCOnU(lc&b?8`P40mO_b+E$i>C9cr}ht@L5>RoOdVU#{8RwpUdd8|pXA32&dC=L z<6BZWV9I=vd>@`ukv>LwQt17u{$z$3oS#uSK4#7Hq%D+gmKvd=6CnQ-eQ^3%h*nVG z_RvmDS0w&At`;*pA^RDJ__(pOk1h4>#lV`Dzs_+q=n(xe82aNa!&!p!ur%k*60NiG z4Bsl`=zg@j=wI<|y$0XpYT`T!;9K3w#}g=LOs6_9n@nyWC=~qsqxcsPGA%5O(X7@X zB*m1QUKTu!?fYG(pbe2LM$cns49qm&UuPZW(fq#(`DKzn3s4m0*-!Q=qiu~Z2n=&Y z*24T+sit-F$Wk`*DZr^hub{YY{ihMp!6k0v-nxRwyjSG=kZ@GahWeImHF&G>SKRgB z&oId9PvzGmncjm7Sn_7xqVT1U@N=K5cHo#2RvumFXsc4&sReK1>whKF&F$5IrnegF zzTqmY>*m>%fio&8aRaQJrHTh)%dLC-e+tF~uQ`XDzIfhJmDE>~J)Z2HE}Te9Q{)F! zJ5~W+A&V!PN4Mj|M^A`K-5${2S3%)lg;XvE$G z{;qdABkTHMzOR|3o`AoBJnoZlxmE3u+!IsZ8`74F^w?8)z_oYM0*MN{OZ?wwi2{?@lHR?aT8t}~H4>LCA1JdX-uJsX`&DnI857s1sYZ<(=EVv5r&Jj@ z7XK2zAcDX%X>C3Pg+EW)9bAfNOCJ&VxHbOiBPDXLbADOZ`xZ*2nX`rB8*Jh*uVEA^ zHr^O+SWl$fo8fRuRs0WiFPH3jEf7AMReE;v(whfEXLSipyDscGT(J~Hl2Jm7lnn^Z zJ1>+S!=g7y^tv+zEVkXiZiSctWj(=)NNkOS#;lUO@6Iaw2@okew1$J&G3*IB&Z4Xf zYnH&m^b(aDXME%Ijr&ekk;G@$GyWRid;xqSe0F1Jtav2_t>yFKik>nWJ2|$k54r#8 z?S2ij$)~pkmIviR%7*E}7W`xK(&U$iyufbA{Cxlx_+cWlwlD7wSuW-y4k3pm%r=)f zuWo~`a^M8x04m{vf6+fYMR}nt{C&l?(X)AL2W;DCvOpU z9}ufWdT86e`iq&t#~FN(%qPK1i}zQg0U|6<{zuw>NrEhv&xddn`C4(9K^2lo^i1yC zM`aIs$qrbVT*6WAZ+7&7MYs=e>@}Ae)#oLw+?dV;@2_&+`MK{KTMrYjnq)Gyi#OfD zEhKJz{$PYh=xv@rMMV>ST~zDfyC6UuIq1;(*OIevR@skFl$Gy)(Wi+3 zgq_h)R4FUpp+>F6ak*_?SWm?Xj0FDiL-<=yrIzNe$Ws1L%9B_f@tD`jLhwsJp54Lo z3ZJ-Tl8IpG{)oZ6Z9dy7K;W-t2e( zIq#aOFz-iH-@nM=Z;r z-d#{JA#gERgQs5GPjFVPQRyJ`6I*y=9ed3(!Q;WF(6q$~)!(8>w`F53#1hQq|KF}0 ze;;_`Ac2I?cT&FmNcqmG={SkvYPLd%z)}RS4Y3j3;EO@{ z*iWuR{dNP*Dg+OJoBg!e2*eSsdzOTUuk=>G1tbNj8}=ttK1^X!w`tPl;g;QDDT340 zP=l%Hk@C{SvSn3BT2ta=qaE6vu<^&yAyyQ9tcyaQj|f2={5 z{PHA&EAUY zdr+0JfSW%|YI%bz*es|zs1JVF#kgdB_ID~y!@?1R<(75K+0Gw2!<4^adJ7jhHes}} zJDt?qWESIOf3}tVDw~koTw5*{EZr7u;;Sei{ZW)NhOTaveBN9B&6E=dew$=VMGUZx z@T)^zC){XS}a4k)fBhn^sqfWrU5JGMxHoS3WCY2Es$8 zX^$SwLyiT@h&v7@Yfa!_@#-F3L;C8o`2jupTzpc;aA-_E7yA)JR|ziNFgL)&D_+Oh zWUZzkoI`A=y=O-76Do!G3yLcJ@4h&5UE)gB2KGwqQvW#YznqlwGeb8k1kntflL$Ku z3k7_nkTO9tP_VUKQaaxs6y1Z-sD4m7rLX`qmLj8R6#qb}oC{z7&G-UxM3Y)PA0g9P_GZt zZ{=8}4pR73k|%ZW1~Ptz@|(lYpGWlhJI>sxWX(9|&pf|-A+d8{CMd)0-pQouv4mR`!8U%WcaKoY+Q)JG{u>A z7h`o~v!f9@Sa9LzG$sP3(%Sut^;zat_S!jho#(ofj{i9^Q&1R@gu60=ZANdibH02* zCxwlublZ0IBzhGiJGthQL#&yT7P;XXu!i+*2WR*g+>Za9oV>iQ;{T14Kb;GcqamQ$ zyA9K2B_1LKU7WHQDaI3o-7AqaCFXGYf501+6HWwk__4L5@~b_EFlv zJiCq`cv7iC57Zz^d=wFG<+SH#K({B?NZAR`Uw1T6Cwp35PHfarQ$+sAPq!J`*+}7L zF(NR4u7thJ>7J^#a4~c-N5)zPbP_Bgbng0m{FSJ?kc6zJdp}x`@;GDTeifddpBKT) z7dWZ480~>YS4QUH`^QLr;D=ab)>c+;n{qZvFR{TQ$*%QNDNQ65a-b#PgdtIdOv}$J zc3!N?oM^WjSv<|%P^(P^OJ6Rntb8(7_y0!8XCwYkQgXbZ=`SbEonVn3=SqINlSXF#cX9zA(QEFAamE!ssVqHeGwtEs^ zG(oKARAK1tmfOeDvzobyUs?L5K@-nmPt>x+Dks5B#)`5HX{TR|Py=(Jjrpg~Apzn6 z?9ZmSH)<^d`hq_@je_uvbDPt+%jQD|L}s;OGLWEdUD($L*O?ATt(I5{%Bp-kmI#;W z1FZU|VT462Y>Q04)7h2be}X1$>cH8DD`@%xY74ZmPd@#dAFVRV|ye%x@icQaVp=`6DD60NX&byCn#=* z!o!}~+czQHQq$+sLP_|;r{G*7op@cHWyK-(THDVfm>BcMsD6vDq7L@_rrk}SX9_NZ z&My0+gq99keXF;XVIerskdPJ$>%rncEBSS{=1Z~lbB`|8SpP|8M?jHInxWNrcztMzFKU3Nt5nb#nsS)7dltF1oKpbgnYO~=kM0& z!P({U8B30hW~uXGLNvVv)>95Ldw&fZ&ea2rcj`#%t9ml{A%U7v{9%JvYm}&VdXOfc z<7a4=609d3cSt9u@8k#uwVA2Cl$Ks!-_)NA!Tno*+!uM$ca@Nm-cJk9T!tZe@*s$p z`6^+OU2+?rlL!LA)g#+Y+S^bNIJ1;;B#KsFpfXA@3q1a>lZ# z->Kv&=7~O0+WTNGiZIT~VHN1x zS<@~<9MVG*E1EA5{CeBFb}K}KZRWj5U;5!*4)?Kyd}e5!WH7BUKJolcW9|O=4m2}{ z3aW0h$CoB}-?JQzcIt~qe4wfp;EMvJpWi$xHm^R4EGLMc5M$hMD82aW=*oTGyB@nk zmDhSDYUYLXa1Xoq_j<&z+rIkPH)HZgcN-Si)Z!1jk?AFZ_E|l__*kC=@|CY^t$b)F z4mk3nUv$%|CxAF-3ObQ&+-_1;-P4{kf>cTqeeTl!*#`9~{dIzXfpGCw`gCLg=RY+s zKq{G2H5%40!g0yhs{k?|l%9smsRs^|C;9MP1{E>l(Sbuo7_h=`>_d(nj{ECHCP%b{ zX*VT*`npfO1@X1M`BaA@(>5wpPq}2O0@l-ISPt{XEoeOML;dd(^=!k|Pk3;fbjO3Z zzL|NDLEQE)5=}7m6e0SxdGSK%dKFt$J={-L7WwxdOEZ2nF^CNli*VOl@K1O3ttEKO za!k}ZindN{uJiL_x75{G7Xy|5JQ1JdEDCS?P~R3FI!)HVdhTC#^fO=AMOTdf=N<{D zg#Gr+j(%8>7&_1jt{^3%T9Ql+3SBD#Mc1l9cIIQbC52QpYm&3t8nDojz)aN(s z3n2d6Q~f^}<2KCeIK?O|$j{f`@>HQs!VV&$6^{;B#@Sw>T5D;;{!LZ`oo6CfpQ9<1)|BvaZ)-DxX3$5W% z612lt%{7-Xw3anyy1@D@O)Np#d%29G@fLc3QN$bN3B#Vu4ITapBU>|;Tyuu5wm#&+ zw^We!Vxygn1S9r;-XG~`)%k7sq89pSsLyuF`D)$&lE^`1V%bN!6RZevLGNzw+{XH< z9y2OR9>NaSJ_SXAQ+4JakNvT8Ip!3Mj7y`PHH~w4u)!z5W0~c7hQ}4OWfo4y6TIW~ zAU~W3W-o<3FWP4}{fi48MEoZe7;uO770pChl229Q^{?f?5D-(EPSwcz(lT^#cUKC7 zfsD(Iii%>lE9XiH4`pLTZ}C_&88%^vq@81sH__b<<2r@Akp zZQ0^PXf&#P0kg`oGa4`X#mW3`T;hd4#C2G~2WVv4G)H+smbL!%xfA#WK>ohh^o#Xf z{00XcN@D{NSs(EFt=s3n)$_jdbBV?)*jvMw!uki;+vENLhItm^dmNm4a+pCz{qv#g z*w+_%+9Pu3tW3320!=8ux}!M4F= z&v5n?b=o!)!|`3PEWLZiT-?(o;nSs=&;3r3J?wtxaatGv3!>kSd(A6+eO8Y^-jmN= zpymC&wbYK24Yf19gdbfHs1!x}MOJ#2*Rp?INK?qF0xchlU1jRU7Sx-5a$qz(6j55p z-eBujE4H1GuR!+?A(SIw|CMm>rYt@2`JCh5Y6PjxhjE?o(lR{J-8Qo)yLxkH%F|9Fqp?eb3Jt~x~8>{O|Gwycy+>$k`V*<5l} z|K>K>8p-7uf%wKVkS$CvP3`=G0__5r-ISj6XGmfb%L>!yw~|O(EnrPwTs+>S3YUVN zf_J-WsaY_wR!wAZbgi`|`P`FJE%Q~Nj$YKTveK%-yNC7>2~J=?8z~Bvv9g>Zo({zA ztNM`VM!Dp}*m)-!B<~Xf^z5Mc79zd3e!jGwfq)yf<02ci><_zCix=O7b`k8$;Jr9i zT^n4&5|wTYOkpUuerAC%COPoJO=F@KObY|BxNq<~$oO4i!l)Bqe#KM2Aeg%!!rNc zVW9%55|!yFos@4(tC|W^P=mpV+jJLl)5U}{ZIB%`VG;4c`09 zI2$t8%!As~mlw+X;=<|3Ey@wFH|N6-h;iR>q21(!qbVf~@!4&J)ba)EdlG0=*Yq#a zXQrI^SbEc`+ssh2)Qjmk!(kS~f&MOvjz-H{N&WmzjSq(}WVFq$mz;l($2U}b46ux4 zF2#VX+N$Z#a}oQ|_vaG}kd_XcO6KE$)(hWdVPF`fcA<3oWUNxSFIX&HsT(f+cn`YX zLM@$7-6meRWegg90VIvAeE3%ifh>4;v360P*+`2@N}nU~`R|IUJl{T?wl4hMIeZ)8 zAD;K-cH2;*2cR6(Tf)GaAsq*Mp9ZdA?4^U@I&Q?XL%hvwBFq`}^>g+7!lW(P);k?~ zX=9@xG3ZfrsRwr^_TpGb23jC6-UAxSU{d@u<{4>?aO)N$xMhHQ^4RdF3-JT8ekA*> z8p@`C(DQ$XT?D<}M{nDoa$$j^RKkg_tOvVx&Id7k(?f*^kw-8h)M!9VHO*lg94UtR zB3bA|MCb0GXd2M6<##*8zH$)7)aC@feYd{e-02WY-k?O(i%yfLeee;>k!VkhUtE46 zeda#0`BizjUTHY!`{Z5*;#tY$B~(tJ`c zsWdACqV^1e_R@vX90z%T)M%7m!gHEwMeK@fvqbn2+bO;dPEa*kTjhukPP$hyG{#Th z)`KwjBul=b7c#8D2+S=5L-t|pr6rx&S^$8!#oB$%JRv$>032@p3Sh4`|O85xv3WKipWqS$w<3njW`7Zk;(RW$@Ux& z;y$|-CpZy>k;IFb4CkdBM~yHzSN+q|bGi8+ld0Tl;+@YBvA zJrrd`Xd*NhFhiI;<4Nl8HKqu!rsSylARvz=@Vlrr2Aeh1@J9HMN?9(lar2-QB@c1fPIoQ3$|xK-ZjUhNDTUB&+r!C5RDkMC73~G zS!G7gRg*-)cmNSWL*t0#nW{sh_IQp&&e`LG^J+N5{%Nn3;sb815>~Kx(WfKAT3Y2I z_&3VYGpB4(=3c%_pRxY7XIX$tU;_=ZAQ=gerRbCdCcCGymsKWi>Un=lvr4$au0X$F zvU-=6Y@0S}0A{d|o!8KpEp}HZ{XxT%RnEu`AP7Tl#V^fBsL`$F;2~4aY*EBP;HvBt zCpJ9hXmiU$G5UyY54n1`s>TngwFSNt+1$1txc4$X5F;Z1VK98*rj~(+CuFw6QbBva zL%K(JucS?GhjCR*(WDut=r1d(l8LN5;*?5Kk)d&Ix1QA_Y1fGaFw zO^pskh3eCOmYh7!kJ2zQj7;5%Q%rg_^6w54P6`VTjA18CY~y1ByduK3adPTgIBcs$rg5`K`r{~!T>9hw_0kY$!Au+MgG7bWdUyeP3yJl(6<5Q{x_YWt z$jRDYIB%BfTF9)iOYsBmPos=?>?pE@p`=V2x}NXYP(Bcz?s*bu?{!Uhw05+$wExrH zBUmBRL5%9E8^s4Ocq@_AjEs40tV*0?UD;I&ViSg#)KL=#8X3=myI zEsPA$@T3SG92lEMMab{~V9VdA_Ua`d$EcD##{7&#;W<3tA&43jKpcl&DcmgXKtIy> zmf(}E?w3C%7G0GSNnBjYX>JunoMJiAQ=$5{&jiqnVcA6bz6CdcCso!7+C!(*`tlvR z*1og^2#_SG&Xm1C8kLDrD=XfzM2@cKRb%~Ao1LsCPDDwAPoq5I;9dBsz0d#Mh@uq( z!g!^dL)Nj?Av}=-@!wB~WZ0vLxy5o-%EI}RKi&3D`{^)^!dpeL8mkvS=?F4;TSc-( z5BUcmf${D|r%@aTTANwEL*eiw7ABMyjx!@9>U_t3l-+&U&vlR7bx8H!8|BlE@ zy%l2V|c9McW%k zkJv{tz$`5L`h*|JmtT(-fSJz;H2kgV0s^8lTBzZXjDZ*|J4^5=u$f9rWiV}YM<0E{ zAAOzL=?H-}Sh=~hD+Y$oi(lUfwI(Pav&->C`K2@Z{_#SosHppefTs(ODE%cY3@%Ne z4}{J@2RA|Ex1Z5zJ(00wcJN@~Ws=5R+LY^IPX%v9msNA{iAKhKG!qxAprP|S_29!p z3(}FhBfRFk+s%G4E4Y~BXJ=$1;(NaoxR$2x`EsuFUt0LJKOb=+1e>&!9{VvrBg!F8 zphh&F;1liMk^DoeWD);)P(v$|w9vH0z#u5#y-G{LST}=rmZ>7<7=HmGJdTwVP}E2i z%!A*Fs3eR+rt2VOMz~UH7!^&cS03Mnh*e&SA79^|Zt7Qh_=j!1#3gb4 zFE_LWDlEAvH5CGiO$kcK^cfjJ5%b&T4i<-JY?$AE#rL3Va{;ZvzZGoGy>8z^%u8GI zW84k1>XmwxF2VB5Hs_F=(iR^lPtV|_U{Nqw%zs-{+^Mwmp1CN>)qP2{%XfRrNE})? zQM=$xZfsQC;`8n3?ax8p@gL~l$LREyd+8G9K9E;1_+eJ&$mVid>{H57wc*Z$OraH4d=9S2se%f_kRIBMUhJY From 645d8238d321dcff144e8e1f30967f190038a574 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 17 Mar 2026 20:02:43 -0400 Subject: [PATCH 118/468] Backport update go version 1.26.1 into ce/main (#13099) * update go version to 1.26.1 (#13061) --------- Co-authored-by: Maithy Ton --- builtin/credential/aws/path_role.go | 2 +- go.mod | 4 ++-- http/auth_token_test.go | 8 ++++---- physical/raft/fsm.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 52695e6ab0..9198862618 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -563,7 +563,7 @@ func (b *backend) upgradeRole(ctx context.Context, s logical.Storage, roleEntry roleEntry.Version = currentRoleStorageVersion default: - return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version) + return false, fmt.Errorf("unrecognized role version: %d", roleEntry.Version) } // Add tokenutil upgrades. These don't need to be persisted, they're fine diff --git a/go.mod b/go.mod index 11440bd45b..9bb554ab1a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ module github.com/hashicorp/vault // semantic related to Go module handling), this comment should be updated to explain that. // // Whenever this value gets updated, sdk/go.mod should be updated to the same value. -go 1.25.3 +go 1.26.1 replace github.com/hashicorp/vault/api => ./api @@ -480,7 +480,7 @@ require ( github.com/microsoft/kiota-serialization-text-go v1.1.2 // indirect github.com/microsoftgraph/msgraph-sdk-go v1.86.0 // indirect github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 // indirect - github.com/miekg/dns v1.1.50 // indirect + github.com/miekg/dns v1.1.50 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect diff --git a/http/auth_token_test.go b/http/auth_token_test.go index 32f5aa5e17..e0867cd098 100644 --- a/http/auth_token_test.go +++ b/http/auth_token_test.go @@ -63,7 +63,7 @@ func TestAuthTokenCreate(t *testing.T) { t.Fatal(err) } if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) + t.Errorf("expected 1h, got %d", secret.Auth.LeaseDuration) } renewCreateRequest := &api.TokenCreateRequest{ @@ -76,7 +76,7 @@ func TestAuthTokenCreate(t *testing.T) { t.Fatal(err) } if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) + t.Errorf("expected 1h, got %d", secret.Auth.LeaseDuration) } if secret.Auth.Renewable { t.Errorf("expected non-renewable token") @@ -88,7 +88,7 @@ func TestAuthTokenCreate(t *testing.T) { t.Fatal(err) } if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 1h, got %q", secret.Auth.LeaseDuration) + t.Errorf("expected 1h, got %d", secret.Auth.LeaseDuration) } if !secret.Auth.Renewable { t.Errorf("expected renewable token") @@ -113,7 +113,7 @@ func TestAuthTokenCreate(t *testing.T) { t.Fatal(err) } if secret.Auth.LeaseDuration != 3600 { - t.Errorf("expected 3600 seconds, got %q", secret.Auth.LeaseDuration) + t.Errorf("expected 3600 seconds, got %d", secret.Auth.LeaseDuration) } } diff --git a/physical/raft/fsm.go b/physical/raft/fsm.go index 3e7bf052e5..ccf6381daf 100644 --- a/physical/raft/fsm.go +++ b/physical/raft/fsm.go @@ -781,7 +781,7 @@ func (f *FSM) ApplyBatch(logs []*raft.Log) []interface{} { go f.restoreCb(context.Background()) } default: - return fmt.Errorf("%q is not a supported transaction operation", op.OpType) + return fmt.Errorf("%d is not a supported transaction operation", op.OpType) } if err != nil { return err From 4071ce00ca8ff8ad994455432ae0f9e0b06676fa Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Tue, 17 Mar 2026 21:02:21 -0400 Subject: [PATCH 119/468] Update vault-plugin-auth-alicloud to v0.23.0 (#13058) (#13106) --------- Co-authored-by: hc-github-team-secure-vault-ecosystem Co-authored-by: Maithy Ton --- changelog/_13058.txt | 3 +++ go.mod | 3 ++- go.sum | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog/_13058.txt diff --git a/changelog/_13058.txt b/changelog/_13058.txt new file mode 100644 index 0000000000..b3101a3fdb --- /dev/null +++ b/changelog/_13058.txt @@ -0,0 +1,3 @@ +```release-note:change +auth/alicloud: Update plugin to [v0.23.0](https://github.com/hashicorp/vault-plugin-auth-alicloud/releases/tag/v0.23.0) +``` diff --git a/go.mod b/go.mod index 9bb554ab1a..a863ea8509 100644 --- a/go.mod +++ b/go.mod @@ -140,7 +140,8 @@ require ( github.com/hashicorp/raft-snapshot v1.0.4 github.com/hashicorp/raft-wal v0.4.0 github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16 - github.com/hashicorp/vault-plugin-auth-alicloud v0.22.0 + github.com/hashicorp/vault-licensing v1.10.5 + github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0 github.com/hashicorp/vault-plugin-auth-azure v0.22.0 github.com/hashicorp/vault-plugin-auth-cf v0.22.0 github.com/hashicorp/vault-plugin-auth-gcp v0.22.1 diff --git a/go.sum b/go.sum index fbb5fcf839..adb5482c72 100644 --- a/go.sum +++ b/go.sum @@ -1550,8 +1550,10 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16 h1:OYPMX3Td3XTL0Xk5N3Se1KLde630diD1X+v3wgJXDhQ= github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16/go.mod h1:v4RnW8isIioLAc11prbTczNCq9TiEWE5MwizMsgY5mE= -github.com/hashicorp/vault-plugin-auth-alicloud v0.22.0 h1:lukm7hwIDQfDyG6IyvBu2tcT6j2bRuyyo22sWqH4w50= -github.com/hashicorp/vault-plugin-auth-alicloud v0.22.0/go.mod h1:QCuzE/JFqigUf4DyyWyYKq6bvf2dmRKUW340CTy+HZ0= +github.com/hashicorp/vault-licensing v1.10.5 h1:BgQYt2cGeAs5+ZVq3VbVzjF6bsdwjU97YMORQHbkV5g= +github.com/hashicorp/vault-licensing v1.10.5/go.mod h1:Ei2vm5kazTXrw3lbTQS8Wj0PJlJArGfDnhHRQ14Qv/A= +github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0 h1:qXmSsujFJRbl6xH58hSIp9qR+s2oMZ8kjaqq4ViLFeY= +github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0/go.mod h1:iVWrHCm50k3s5WzS3WjJJgtNdLqmP0UeFnOT1A6Nm5w= github.com/hashicorp/vault-plugin-auth-azure v0.22.0 h1:xvRYu2Xirn6gl/GvhPei7DJgJDDTEvsxxEHkBxuj/Iw= github.com/hashicorp/vault-plugin-auth-azure v0.22.0/go.mod h1:G/wFIXYlPQDXusYD54tlZIb3CDk2cy9OB9hUXyWkqf4= github.com/hashicorp/vault-plugin-auth-cf v0.22.0 h1:DTK733vsB2lBVwSLxra3/IuhlGdkQVW11T/q0qdXyis= From 06f79b593091f4ef006a978857ece878447c7740 Mon Sep 17 00:00:00 2001 From: Maithy Ton Date: Tue, 17 Mar 2026 22:12:54 -0700 Subject: [PATCH 120/468] revert vault-licensing addition to ce/main mistake (#13109) --- go.mod | 1 - go.sum | 4 ---- 2 files changed, 5 deletions(-) diff --git a/go.mod b/go.mod index a863ea8509..dca177ee8e 100644 --- a/go.mod +++ b/go.mod @@ -140,7 +140,6 @@ require ( github.com/hashicorp/raft-snapshot v1.0.4 github.com/hashicorp/raft-wal v0.4.0 github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16 - github.com/hashicorp/vault-licensing v1.10.5 github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0 github.com/hashicorp/vault-plugin-auth-azure v0.22.0 github.com/hashicorp/vault-plugin-auth-cf v0.22.0 diff --git a/go.sum b/go.sum index adb5482c72..85d4d75a4e 100644 --- a/go.sum +++ b/go.sum @@ -1550,16 +1550,12 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16 h1:OYPMX3Td3XTL0Xk5N3Se1KLde630diD1X+v3wgJXDhQ= github.com/hashicorp/vault-hcp-lib v0.0.0-20250306185756-615fe2449b16/go.mod h1:v4RnW8isIioLAc11prbTczNCq9TiEWE5MwizMsgY5mE= -github.com/hashicorp/vault-licensing v1.10.5 h1:BgQYt2cGeAs5+ZVq3VbVzjF6bsdwjU97YMORQHbkV5g= -github.com/hashicorp/vault-licensing v1.10.5/go.mod h1:Ei2vm5kazTXrw3lbTQS8Wj0PJlJArGfDnhHRQ14Qv/A= github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0 h1:qXmSsujFJRbl6xH58hSIp9qR+s2oMZ8kjaqq4ViLFeY= github.com/hashicorp/vault-plugin-auth-alicloud v0.23.0/go.mod h1:iVWrHCm50k3s5WzS3WjJJgtNdLqmP0UeFnOT1A6Nm5w= github.com/hashicorp/vault-plugin-auth-azure v0.22.0 h1:xvRYu2Xirn6gl/GvhPei7DJgJDDTEvsxxEHkBxuj/Iw= github.com/hashicorp/vault-plugin-auth-azure v0.22.0/go.mod h1:G/wFIXYlPQDXusYD54tlZIb3CDk2cy9OB9hUXyWkqf4= github.com/hashicorp/vault-plugin-auth-cf v0.22.0 h1:DTK733vsB2lBVwSLxra3/IuhlGdkQVW11T/q0qdXyis= github.com/hashicorp/vault-plugin-auth-cf v0.22.0/go.mod h1:qToMQoW7dX1egtJwEHd21I/7pgzg+DBEwRAytd+Pgtc= -github.com/hashicorp/vault-plugin-auth-gcp v0.22.0 h1:c5LEJmHNV6VzbKTM9nn05uGLhu0VRnIsacCshj0AJ8M= -github.com/hashicorp/vault-plugin-auth-gcp v0.22.0/go.mod h1:6WhVeAZUu+67H4tkXsnFoUU3+UaBFLlE6ffUHDkVM0o= github.com/hashicorp/vault-plugin-auth-gcp v0.22.1 h1:ahbC0bbzUXAckxSmPA9V12VH+4CIg1efJqRq1biHSr4= github.com/hashicorp/vault-plugin-auth-gcp v0.22.1/go.mod h1:6WhVeAZUu+67H4tkXsnFoUU3+UaBFLlE6ffUHDkVM0o= github.com/hashicorp/vault-plugin-auth-jwt v0.25.0 h1:YdrZ+fGutoaaF/Hiw3+xtmcRalmqGH8sXRKIrkr5dsk= From eb835a028df400404094f9edd2dd81fb6b350b0c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 18 Mar 2026 08:33:27 -0400 Subject: [PATCH 121/468] Create plugin SDK scenario infrastructure (#13073) (#13111) Co-authored-by: Luis (LT) Carbonell --- enos/enos-scenario-plugin.hcl | 537 ++++++++++++++++++ .../modules/vault_run_blackbox_test/plugin.tf | 23 + .../testcluster/blackbox/session_plugin.go | 15 + .../blackbox/plugin/plugin_test.go | 12 + 4 files changed, 587 insertions(+) create mode 100644 enos/enos-scenario-plugin.hcl create mode 100644 enos/modules/vault_run_blackbox_test/plugin.tf create mode 100644 vault/external_tests/blackbox/plugin/plugin_test.go diff --git a/enos/enos-scenario-plugin.hcl b/enos/enos-scenario-plugin.hcl new file mode 100644 index 0000000000..6c81e09340 --- /dev/null +++ b/enos/enos-scenario-plugin.hcl @@ -0,0 +1,537 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +scenario "plugin" { + description = <<-EOF + The plugin scenario deploys a Vault cluster with external integration services and runs comprehensive + plugin blackbox tests. This scenario validates plugin functionality including: + + The scenario creates dedicated external services (LDAP, databases, etc.) using containers and + configures them with test data required for comprehensive plugin testing. + + # How to run this scenario + + For general instructions on running a scenario, refer to the Enos docs: https://eng-handbook.hashicorp.services/internal-tools/enos/running-a-scenario/ + For troubleshooting tips and common errors, see https://eng-handbook.hashicorp.services/internal-tools/enos/troubleshooting/. + + Variables required for all scenario variants: + - aws_ssh_private_key_path (more info about AWS SSH keypairs: https://eng-handbook.hashicorp.services/internal-tools/enos/getting-started/#set-your-aws-key-pair-name-and-private-key) + - aws_ssh_keypair_name + - vault_build_date* + - vault_product_version + - vault_revision* + + * If you don't already know what build date and revision you should be using, see + https://eng-handbook.hashicorp.services/internal-tools/enos/troubleshooting/#execution-error-expected-vs-got-for-vault-versioneditionrevisionbuild-date. + + Variables required for some scenario variants: + - artifactory_token (if using `artifact_source:artifactory` in your filter) + - aws_region (if different from the default value in enos-variables.hcl) + - vault_artifact_path (the path to where you have a Vault artifact already downloaded, + if using `artifact_source:crt` in your filter) + - vault_license_path (if using an ENT edition of Vault) + EOF + + matrix { + arch = global.archs + artifact_source = global.artifact_sources + artifact_type = global.artifact_types + backend = global.backends + config_mode = global.config_modes + consul_edition = global.consul_editions + consul_version = global.consul_versions + distro = global.distros + edition = global.editions + ip_version = global.ip_versions + seal = global.seals + + // Our local builder always creates bundles + exclude { + artifact_source = ["local"] + artifact_type = ["package"] + } + + // PKCS#11 can only be used on ent.hsm and ent.hsm.fips1403. + exclude { + seal = ["pkcs11"] + edition = [for e in matrix.edition : e if !strcontains(e, "hsm")] + } + + // softhsm packages not available for sles (at the time of development) + exclude { + seal = ["pkcs11"] + distro = ["sles"] + } + + // Testing in IPV6 mode is currently implemented for integrated Raft storage only + exclude { + ip_version = ["6"] + backend = ["consul"] + } + + // plugin scenario cannot be run in CE + exclude { + edition = ["ce"] + } + } + + terraform_cli = terraform_cli.default + terraform = terraform.default + providers = [ + provider.aws.default, + provider.enos.ec2_user, + provider.enos.ubuntu + ] + + locals { + artifact_path = matrix.artifact_source != "artifactory" ? abspath(var.vault_artifact_path) : null + enos_provider = { + amzn = provider.enos.ec2_user + rhel = provider.enos.ec2_user + sles = provider.enos.ec2_user + ubuntu = provider.enos.ubuntu + } + manage_service = matrix.artifact_type == "bundle" + test_names = ["TestAlwaysPass"] + } + + step "build_vault" { + description = global.description.build_vault + module = "build_${matrix.artifact_source}" + + variables { + build_tags = var.vault_local_build_tags != null ? var.vault_local_build_tags : global.build_tags[matrix.edition] + artifact_path = local.artifact_path + goarch = matrix.arch + goos = "linux" + artifactory_host = matrix.artifact_source == "artifactory" ? var.artifactory_host : null + artifactory_repo = matrix.artifact_source == "artifactory" ? var.artifactory_repo : null + artifactory_token = matrix.artifact_source == "artifactory" ? var.artifactory_token : null + arch = matrix.artifact_source == "artifactory" ? matrix.arch : null + product_version = var.vault_product_version + artifact_type = matrix.artifact_type + distro = matrix.artifact_source == "artifactory" ? matrix.distro : null + edition = matrix.artifact_source == "artifactory" ? matrix.edition : null + revision = var.vault_revision + } + } + + step "ec2_info" { + description = global.description.ec2_info + module = module.ec2_info + } + + step "create_vpc" { + description = global.description.create_vpc + module = module.create_vpc + + variables { + common_tags = global.tags + ip_version = matrix.ip_version + } + } + + step "read_backend_license" { + description = global.description.read_backend_license + module = module.read_license + skip_step = matrix.backend == "raft" || matrix.consul_edition == "ce" + + variables { + file_name = global.backend_license_path + } + } + + step "read_vault_license" { + description = global.description.read_vault_license + skip_step = matrix.edition == "ce" + module = module.read_license + + variables { + file_name = global.vault_license_path + } + } + + step "create_seal_key" { + description = global.description.create_seal_key + module = "seal_${matrix.seal}" + depends_on = [step.create_vpc] + + providers = { + enos = provider.enos.ubuntu + } + + variables { + cluster_id = step.create_vpc.id + common_tags = global.tags + } + } + + step "create_plugin_integration_target" { + description = "Create dedicated EC2 instance for external plugin services (LDAP, databases, etc.)" + module = module.target_ec2_instances + depends_on = [step.create_vpc] + + providers = { + enos = provider.enos.ubuntu + } + + variables { + ami_id = step.ec2_info.ami_ids["arm64"]["ubuntu"]["24.04"] + cluster_tag_key = "plugin-integration" + common_tags = global.tags + instance_count = 1 + vpc_id = step.create_vpc.id + } + } + + step "create_vault_cluster_targets" { + description = global.description.create_vault_cluster_targets + module = module.target_ec2_instances + depends_on = [step.create_vpc] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + variables { + ami_id = step.ec2_info.ami_ids[matrix.arch][matrix.distro][global.distro_version[matrix.distro]] + cluster_tag_key = global.vault_tag_key + common_tags = global.tags + seal_key_names = step.create_seal_key.resource_names + vpc_id = step.create_vpc.id + } + } + + step "create_vault_cluster_backend_targets" { + description = global.description.create_vault_cluster_targets + module = matrix.backend == "consul" ? module.target_ec2_instances : module.target_ec2_shim + depends_on = [step.create_vpc] + + providers = { + enos = provider.enos.ubuntu + } + + variables { + ami_id = step.ec2_info.ami_ids["arm64"]["ubuntu"][global.distro_version["ubuntu"]] + cluster_tag_key = global.backend_tag_key + common_tags = global.tags + seal_key_names = step.create_seal_key.resource_names + vpc_id = step.create_vpc.id + } + } + + step "set_up_plugin_services" { + description = "Set up external plugin services (LDAP server, databases, etc.) for plugin testing" + module = module.set_up_external_integration_target + depends_on = [ + step.create_plugin_integration_target + ] + + providers = { + enos = provider.enos.ubuntu + } + + variables { + hosts = step.create_plugin_integration_target.hosts + ip_version = matrix.ip_version + packages = concat(global.packages, global.distro_packages["ubuntu"]["24.04"], ["podman", "podman-docker"]) + ports = global.integration_host_ports + } + } + + step "create_backend_cluster" { + description = global.description.create_backend_cluster + module = "backend_${matrix.backend}" + depends_on = [ + step.create_vault_cluster_backend_targets + ] + + providers = { + enos = provider.enos.ubuntu + } + + verifies = [ + // verified in modules + quality.consul_autojoin_aws, + quality.consul_config_file, + quality.consul_ha_leader_election, + quality.consul_service_start_server, + // verified in enos_consul_start resource + quality.consul_api_agent_host_read, + quality.consul_api_health_node_read, + quality.consul_api_operator_raft_config_read, + quality.consul_cli_validate, + quality.consul_health_state_passing_read_nodes_minimum, + quality.consul_operator_raft_configuration_read_voters_minimum, + quality.consul_service_systemd_notified, + quality.consul_service_systemd_unit, + ] + + variables { + cluster_name = step.create_vault_cluster_backend_targets.cluster_name + cluster_tag_key = global.backend_tag_key + hosts = step.create_vault_cluster_backend_targets.hosts + license = (matrix.backend == "consul" && matrix.consul_edition == "ent") ? step.read_backend_license.license : null + release = { + edition = matrix.consul_edition + version = matrix.consul_version + } + } + } + + step "create_vault_cluster" { + description = global.description.create_vault_cluster + module = module.vault_cluster + depends_on = [ + step.create_backend_cluster, + step.build_vault, + step.create_vault_cluster_targets, + step.set_up_plugin_services + ] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + // verified in modules + quality.consul_service_start_client, + quality.vault_artifact_bundle, + quality.vault_artifact_deb, + quality.vault_artifact_rpm, + quality.vault_audit_log, + quality.vault_audit_socket, + quality.vault_audit_syslog, + quality.vault_autojoin_aws, + quality.vault_config_env_variables, + quality.vault_config_file, + quality.vault_config_log_level, + quality.vault_init, + quality.vault_license_required_ent, + quality.vault_listener_ipv4, + quality.vault_listener_ipv6, + quality.vault_service_start, + quality.vault_storage_backend_consul, + quality.vault_storage_backend_raft, + // verified in enos_vault_start resource + quality.vault_api_sys_config_read, + quality.vault_api_sys_ha_status_read, + quality.vault_api_sys_health_read, + quality.vault_api_sys_host_info_read, + quality.vault_api_sys_replication_status_read, + quality.vault_api_sys_seal_status_api_read_matches_sys_health, + quality.vault_api_sys_storage_raft_autopilot_configuration_read, + quality.vault_api_sys_storage_raft_autopilot_state_read, + quality.vault_api_sys_storage_raft_configuration_read, + quality.vault_cli_status_exit_code, + quality.vault_service_systemd_notified, + quality.vault_service_systemd_unit, + ] + + variables { + artifactory_release = matrix.artifact_source == "artifactory" ? step.build_vault.vault_artifactory_release : null + backend_cluster_name = step.create_vault_cluster_backend_targets.cluster_name + backend_cluster_tag_key = global.backend_tag_key + cluster_name = step.create_vault_cluster_targets.cluster_name + config_mode = matrix.config_mode + consul_license = (matrix.backend == "consul" && matrix.consul_edition == "ent") ? step.read_backend_license.license : null + consul_release = matrix.backend == "consul" ? { + edition = matrix.consul_edition + version = matrix.consul_version + } : null + enable_audit_devices = var.vault_enable_audit_devices + hosts = step.create_vault_cluster_targets.hosts + install_dir = global.vault_install_dir[matrix.artifact_type] + ip_version = matrix.ip_version + license = matrix.edition != "ce" ? step.read_vault_license.license : null + local_artifact_path = local.artifact_path + manage_service = local.manage_service + packages = concat(global.packages, global.distro_packages[matrix.distro][global.distro_version[matrix.distro]]) + seal_attributes = step.create_seal_key.attributes + seal_type = matrix.seal + storage_backend = matrix.backend + } + } + + step "get_local_metadata" { + description = global.description.get_local_metadata + skip_step = matrix.artifact_source != "local" + module = module.get_local_metadata + } + + // Wait for our cluster to elect a leader + step "wait_for_leader" { + description = global.description.wait_for_cluster_to_have_leader + module = module.vault_wait_for_leader + depends_on = [step.create_vault_cluster] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + quality.vault_api_sys_leader_read, + quality.vault_unseal_ha_leader_election, + ] + + variables { + timeout = 120 // seconds + ip_version = matrix.ip_version + hosts = step.create_vault_cluster_targets.hosts + vault_addr = step.create_vault_cluster.api_addr_localhost + vault_install_dir = global.vault_install_dir[matrix.artifact_type] + vault_root_token = step.create_vault_cluster.root_token + } + } + + step "get_vault_cluster_ips" { + description = global.description.get_vault_cluster_ip_addresses + module = module.vault_get_cluster_ips + depends_on = [step.wait_for_leader] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + quality.vault_api_sys_ha_status_read, + quality.vault_api_sys_leader_read, + quality.vault_cli_operator_members, + ] + + variables { + hosts = step.create_vault_cluster_targets.hosts + ip_version = matrix.ip_version + vault_addr = step.create_vault_cluster.api_addr_localhost + vault_install_dir = global.vault_install_dir[matrix.artifact_type] + vault_root_token = step.create_vault_cluster.root_token + } + } + + step "verify_vault_unsealed" { + description = global.description.verify_vault_unsealed + module = module.vault_wait_for_cluster_unsealed + depends_on = [step.wait_for_leader] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + quality.vault_seal_awskms, + quality.vault_seal_pkcs11, + quality.vault_seal_shamir, + ] + + variables { + hosts = step.create_vault_cluster_targets.hosts + vault_addr = step.create_vault_cluster.api_addr_localhost + vault_install_dir = global.vault_install_dir[matrix.artifact_type] + } + } + + step "verify_vault_version" { + description = global.description.verify_vault_version + module = module.vault_verify_version + depends_on = [step.verify_vault_unsealed] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + quality.vault_api_sys_version_history_keys, + quality.vault_api_sys_version_history_key_info, + quality.vault_version_build_date, + quality.vault_version_edition, + quality.vault_version_release, + ] + + variables { + hosts = step.create_vault_cluster_targets.hosts + vault_addr = step.create_vault_cluster.api_addr_localhost + vault_edition = matrix.edition + vault_install_dir = global.vault_install_dir[matrix.artifact_type] + vault_product_version = matrix.artifact_source == "local" ? step.get_local_metadata.version : var.vault_product_version + vault_revision = matrix.artifact_source == "local" ? step.get_local_metadata.revision : var.vault_revision + vault_build_date = matrix.artifact_source == "local" ? step.get_local_metadata.build_date : var.vault_build_date + vault_root_token = step.create_vault_cluster.root_token + } + } + + // Run comprehensive plugin blackbox tests + step "run_plugin_blackbox_tests" { + description = "Run comprehensive plugin blackbox tests" + module = module.vault_run_blackbox_test + depends_on = [step.get_vault_cluster_ips, step.set_up_plugin_services, step.verify_vault_version] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + // Plugin testing quality - tests will define their own verification + ] + + variables { + leader_host = step.get_vault_cluster_ips.leader_host + leader_public_ip = step.get_vault_cluster_ips.leader_public_ip + vault_root_token = step.create_vault_cluster.root_token + test_names = local.test_names + test_package = "./vault/external_tests/blackbox/plugin" + integration_host_state = step.set_up_plugin_services.state + vault_edition = matrix.edition + } + } + + output "audit_device_file_path" { + description = "The file path for the file audit device, if enabled" + value = step.create_vault_cluster.audit_device_file_path + } + + output "plugin_services_state" { + description = "The external plugin services configuration and state" + value = step.set_up_plugin_services.state + } + + output "cluster_name" { + description = "The Vault cluster name" + value = step.create_vault_cluster.cluster_name + } + + output "hosts" { + description = "The Vault cluster target hosts" + value = step.create_vault_cluster.hosts + } + + output "private_ips" { + description = "The Vault cluster private IPs" + value = step.create_vault_cluster.private_ips + } + + output "public_ips" { + description = "The Vault cluster public IPs" + value = step.create_vault_cluster.public_ips + } + + output "root_token" { + description = "The Vault cluster root token" + value = step.create_vault_cluster.root_token + } + + output "unseal_keys_b64" { + description = "The Vault cluster unseal keys" + value = step.create_vault_cluster.unseal_keys_b64 + } + + output "unseal_keys_hex" { + description = "The Vault cluster unseal keys hex" + value = step.create_vault_cluster.unseal_keys_hex + } + + output "plugin_test_results" { + description = "Results from plugin blackbox tests" + sensitive = true + value = step.run_plugin_blackbox_tests.test_results_summary + } +} diff --git a/enos/modules/vault_run_blackbox_test/plugin.tf b/enos/modules/vault_run_blackbox_test/plugin.tf new file mode 100644 index 0000000000..77262a49be --- /dev/null +++ b/enos/modules/vault_run_blackbox_test/plugin.tf @@ -0,0 +1,23 @@ +# Copyright IBM Corp. 2016, 2025 +# SPDX-License-Identifier: BUSL-1.1 + +# Plugin blackbox test configuration + +variable "plugin_config" { + type = object({ + enabled = bool + type = string + }) + description = "Plugin configuration for blackbox tests. Set enabled=true and specify plugin type to enable plugin tests." + default = { + enabled = false + type = "" + } +} + +# Local variables for plugin environment setup +locals { + plugin_environment = var.plugin_config.enabled ? { + PLUGIN_TYPE = var.plugin_config.type + } : {} +} diff --git a/sdk/helper/testcluster/blackbox/session_plugin.go b/sdk/helper/testcluster/blackbox/session_plugin.go index eef2eb06db..5da6bbf0be 100644 --- a/sdk/helper/testcluster/blackbox/session_plugin.go +++ b/sdk/helper/testcluster/blackbox/session_plugin.go @@ -669,3 +669,18 @@ func isNotFoundError(err error) bool { return fmt.Sprintf("%v", err) == "404 Not Found" || fmt.Sprintf("%v", err) == "405 Method Not Allowed" } + +// TestExpectedError tests that a write operation fails as expected +func (ps *PluginSession) TestExpectedError(path string, data map[string]interface{}) error { + ps.t.Helper() + + fullPath := ps.buildPath(path) + _, err := ps.Client.Logical().Write(fullPath, data) + + if err == nil { + return fmt.Errorf("expected write to %s to fail, but it succeeded", fullPath) + } + + // Return nil to indicate the error was expected + return nil +} diff --git a/vault/external_tests/blackbox/plugin/plugin_test.go b/vault/external_tests/blackbox/plugin/plugin_test.go new file mode 100644 index 0000000000..e42982d3ea --- /dev/null +++ b/vault/external_tests/blackbox/plugin/plugin_test.go @@ -0,0 +1,12 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package plugin + +import "testing" + +// TestAlwaysPass is a placeholder test that always passes. +// This ensures the test directory has at least one test to run. +func TestAlwaysPass(t *testing.T) { + // This test intentionally does nothing and always passes +} From 767e99a87590fbaa713f3e1ea44339c7e63f5dfb Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 18 Mar 2026 09:04:20 -0400 Subject: [PATCH 122/468] pki/acme: add redirect test for acme http-01 challenge (#13091) (#13095) Co-authored-by: mickael-hc <86245626+mickael-hc@users.noreply.github.com> --- builtin/logical/pki/acme_challenges_test.go | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/builtin/logical/pki/acme_challenges_test.go b/builtin/logical/pki/acme_challenges_test.go index c01ec3aa1b..21b0d9e114 100644 --- a/builtin/logical/pki/acme_challenges_test.go +++ b/builtin/logical/pki/acme_challenges_test.go @@ -885,6 +885,30 @@ func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { } } +// TestAcmeValidateHTTP01ChallengeRedirectHonorsPermittedIPRanges verifies that +// redirected validation targets are still subject to the HTTP-01 IP allowlist. +func TestAcmeValidateHTTP01ChallengeRedirectHonorsPermittedIPRanges(t *testing.T) { + t.Parallel() + + for _, redirectScheme := range []string{"http", "https"} { + t.Run(redirectScheme, func(st *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, fmt.Sprintf("%s://10.0.0.1:12345%s", redirectScheme, r.URL.Path), http.StatusMovedPermanently) + })) + defer ts.Close() + + host := ts.URL[7:] // Remove "http://" + + isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{ + ChallengePermittedIPRanges: []string{"127.0.0.1/32"}, + }) + require.False(st, isValid) + require.Error(st, err) + require.ErrorIs(st, err, ErrRejectedIdentifier) + }) + } +} + // TestIsValidChallengeIP tests the excluded CIDR list functionality func TestIsValidChallengeIP(t *testing.T) { t.Parallel() From 7cd77ffaebd7710134ef144ed1fe494f6a5a38f6 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 18 Mar 2026 10:41:17 -0400 Subject: [PATCH 123/468] fix tree chart label cut off, tree chart visibility state, validation (#13094) (#13119) Co-authored-by: lane-wetmore --- ui/app/components/wizard/namespaces/step-2.ts | 72 ++++++++++--------- ui/app/styles/components/wizard.scss | 5 ++ .../components/page/namespaces-wizard-test.js | 17 +++++ 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/ui/app/components/wizard/namespaces/step-2.ts b/ui/app/components/wizard/namespaces/step-2.ts index 81599fba01..a9b0e002c3 100644 --- a/ui/app/components/wizard/namespaces/step-2.ts +++ b/ui/app/components/wizard/namespaces/step-2.ts @@ -8,6 +8,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { service } from '@ember/service'; import type NamespaceService from 'vault/services/namespace'; +import { isEmpty } from '@ember/utils'; interface Project { name: string; @@ -40,7 +41,8 @@ class Block { // briefly and then remains blank. get hasMultipleNodes() { const hasMultipleOrgs = this.hasMultipleItems(this.orgs); - const orgHasMultipleProjects = this.orgs.some((org) => this.hasMultipleItems(org.projects)); + const filledOrgs = this.orgs.filter((org) => !isEmpty(org.name)); + const orgHasMultipleProjects = filledOrgs.some((org) => this.hasMultipleItems(org.projects)); return hasMultipleOrgs || orgHasMultipleProjects; } @@ -110,7 +112,7 @@ export default class WizardNamespacesStepTemp extends Component { } checkForDuplicateGlobals() { - const globals = this.blocks.map((block) => block.global).filter((global) => global !== ''); + const globals = this.blocks.map((block) => block.global).filter((global) => !isEmpty(global)); const globalCounts = new Map(); globals.forEach((global) => { @@ -167,7 +169,9 @@ export default class WizardNamespacesStepTemp extends Component { updateOrgValue(block: Block, orgToUpdate: Org, event: Event) { const target = event.target as HTMLInputElement; const value = target.value.trim(); - const isDuplicate = block.orgs.some((org) => org !== orgToUpdate && org.name === value); + const isDuplicate = isEmpty(value) + ? false + : block.orgs.some((org) => org !== orgToUpdate && org.name === value); const updatedOrgs = block.orgs.map((org) => { if (org === orgToUpdate) { @@ -203,7 +207,9 @@ export default class WizardNamespacesStepTemp extends Component { updateProjectValue(block: Block, org: Org, projectToUpdate: Project, event: Event) { const target = event.target as HTMLInputElement; const value = target.value.trim(); - const isDuplicate = org.projects.some((project) => project !== projectToUpdate && project.name === value); + const isDuplicate = isEmpty(value) + ? false + : org.projects.some((project) => project !== projectToUpdate && project.name === value); const updatedOrgs = block.orgs.map((currentOrg) => { if (currentOrg === org) { @@ -263,25 +269,27 @@ export default class WizardNamespacesStepTemp extends Component { } get treeData() { - const parsed = this.blocks.map((block) => { - return { - name: block.global, - children: block.orgs - .filter((org) => org.name !== '') - .map((org) => { - return { - name: org.name, - children: org.projects - .filter((project) => project.name !== '') - .map((project) => { - return { - name: project.name, - }; - }), - }; - }), - }; - }); + const parsed = this.blocks + .filter((block) => !isEmpty(block.global)) + .map((block) => { + return { + name: block.global, + children: block.orgs + .filter((org) => !isEmpty(org.name)) + .map((org) => { + return { + name: org.name, + children: org.projects + .filter((project) => !isEmpty(project.name)) + .map((project) => { + return { + name: project.name, + }; + }), + }; + }), + }; + }); return parsed; } @@ -289,15 +297,15 @@ export default class WizardNamespacesStepTemp extends Component { // The Carbon tree chart only supports displaying nodes with at least 1 "fork" i.e. at least 2 globals, 2 orgs or 2 projects get shouldShowTreeChart(): boolean { // Count total globals across blocks - const globalsCount = this.blocks.filter((block) => block.global !== '').length; + const filledBlocks = this.blocks.filter((block) => !isEmpty(block.global)); // Check if there are multiple globals - if (globalsCount > 1) { + if (filledBlocks.length > 1) { return true; } // Check for multiple projects or orgs within a block - return this.blocks.some((block) => block.hasMultipleNodes); + return filledBlocks.some((block) => block.hasMultipleNodes); } // Store namespace paths to be used for code snippets in the format "global", "global/org", "global/org/project" @@ -307,23 +315,23 @@ export default class WizardNamespacesStepTemp extends Component { const results: string[] = []; // Add global namespace if it exists - if (block.global !== '') { + if (!isEmpty(block.global)) { results.push(block.global); } block.orgs.forEach((org) => { - if (org.name !== '') { + if (!isEmpty(org.name)) { // Add global/org namespace - const globalOrg = [block.global, org.name].filter((value) => value !== '').join('/'); + const globalOrg = [block.global, org.name].filter((value) => !isEmpty(value)).join('/'); if (globalOrg && !results.includes(globalOrg)) { results.push(globalOrg); } org.projects.forEach((project) => { - if (project.name !== '') { + if (!isEmpty(project.name)) { // Add global/org/project namespace const fullNamespace = [block.global, org.name, project.name] - .filter((value) => value !== '') + .filter((value) => !isEmpty(value)) .join('/'); if (fullNamespace && !results.includes(fullNamespace)) { results.push(fullNamespace); @@ -335,6 +343,6 @@ export default class WizardNamespacesStepTemp extends Component { return results; }) .flat() - .filter((namespace) => namespace !== ''); + .filter((namespace) => !isEmpty(namespace)); } } diff --git a/ui/app/styles/components/wizard.scss b/ui/app/styles/components/wizard.scss index 8706f1e11c..bd67a8b78f 100644 --- a/ui/app/styles/components/wizard.scss +++ b/ui/app/styles/components/wizard.scss @@ -53,6 +53,11 @@ .tree { border: 1px solid var(--token-color-border-primary); border-radius: var(--token-border-radius-medium); + + // prevents last character of node labels from being cut off + svg { + overflow: visible; + } } } } diff --git a/ui/tests/integration/components/page/namespaces-wizard-test.js b/ui/tests/integration/components/page/namespaces-wizard-test.js index 221781e25c..6f6eef3915 100644 --- a/ui/tests/integration/components/page/namespaces-wizard-test.js +++ b/ui/tests/integration/components/page/namespaces-wizard-test.js @@ -214,6 +214,11 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('org-1')}`, 'org2'); assert.dom(SELECTORS.tree).exists('Tree chart shows with multiple orgs'); + // Add empty global - tree show not show empty global + await click(GENERAL.button('add namespace')); + assert.dom(`${SELECTORS.tree} .nodes > g`).exists({ count: 5 }, 'Only renders non-empty input nodes'); + await click(`${SELECTORS.inputRow(1)} ${GENERAL.button('delete namespace')}`); + // Remove second org - tree is hidden again await click(`${SELECTORS.inputRow(0)} ${GENERAL.button('delete org')}`); assert.dom(SELECTORS.tree).doesNotExist('Tree chart hidden after removing second org'); @@ -222,5 +227,17 @@ module('Integration | Component | page/namespaces | Namespace Wizard', function await click(`${SELECTORS.inputRow(0)} ${GENERAL.button('add project')}`); await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('project-1')}`, 'project2'); assert.dom(SELECTORS.tree).exists('Tree chart shows with multiple projects'); + + // Clear global - tree is hidden + await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('global-0')}`, ''); + assert.dom(SELECTORS.tree).doesNotExist('Tree chart hidden after clearing parent global'); + await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('global-0')}`, 'global1'); + assert.dom(SELECTORS.tree).exists('Tree chart is rendered'); + + // Clear org - tree is hidden + await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('org-0')}`, ''); + assert.dom(SELECTORS.tree).doesNotExist('Tree chart hidden after clearing parent org'); + await fillIn(`${SELECTORS.inputRow(0)} ${GENERAL.inputByAttr('org-0')}`, 'org1'); + assert.dom(SELECTORS.tree).exists('Tree chart is rendered'); }); }); From 9932623861dd2ae760282b7fc6b3bd2dca0ad92a Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 18 Mar 2026 12:03:29 -0400 Subject: [PATCH 124/468] UI: Feature Descriptions (#12737) (#13055) * add and update feature descriptions * add description doc links, improve spacing, remove dividers * update tests * Update ui/app/components/recovery/page/snapshots/load.hbs * update seal action and tests * use description argument for simple descriptions * clean up * update with finalized descriptions * updated policy descriptions * update client count description * fix typo and update tests * update tests * more test updates * Apply suggestions from code review --------- Co-authored-by: lane-wetmore Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/components/clients/date-range.hbs | 3 -- ui/app/components/clients/page-header.hbs | 36 +++++++++------- ui/app/components/clients/page/counts.hbs | 9 +--- .../dashboard/vault-version-title.hbs | 6 ++- ui/app/components/identity/entity-nav.hbs | 24 +++++------ ui/app/components/identity/entity-nav.js | 8 ---- ui/app/components/identity/entity-nav.ts | 23 ++++++++++ ui/app/components/license-info.hbs | 6 +++ ui/app/components/page/methods.hbs | 6 +++ ui/app/components/page/namespaces.hbs | 9 +++- ui/app/components/page/policies.hbs | 6 +++ ui/app/components/page/policies.ts | 18 ++++++-- ui/app/components/recovery/page/header.hbs | 29 ------------- ui/app/components/recovery/page/header.ts | 18 -------- ui/app/components/recovery/page/snapshots.hbs | 21 ++++++--- .../recovery/page/snapshots/load.hbs | 6 ++- .../page/snapshots/snapshot-details.hbs | 36 ++++++++++------ .../page/snapshots/snapshot-manage.hbs | 18 +++++--- ui/app/components/seal-action.hbs | 32 +++----------- ui/app/components/seal-action.js | 22 ---------- ui/app/components/seal-action.ts | 33 ++++++++++++++ ui/app/components/secret-engine/list.hbs | 13 +++--- ui/app/components/secret-engine/list.ts | 4 -- ui/app/components/tools/hash.hbs | 9 ++-- ui/app/components/tools/lookup.hbs | 9 ++-- ui/app/components/tools/random.hbs | 9 ++-- ui/app/components/tools/rewrap.hbs | 9 ++-- ui/app/components/tools/unwrap.hbs | 7 ++- ui/app/components/tools/wrap.hbs | 11 ++++- ui/app/styles/helper-classes/spacing.scss | 8 ++++ .../vault/cluster/access/control-groups.hbs | 6 ++- .../vault/cluster/access/leases/list.hbs | 6 ++- .../cluster/access/mfa/enforcements/index.hbs | 6 +++ .../vault/cluster/access/mfa/index.hbs | 10 ++--- .../cluster/access/mfa/methods/create.hbs | 22 ++++++---- .../cluster/access/mfa/methods/index.hbs | 6 +++ .../templates/vault/cluster/access/oidc.hbs | 32 ++++++-------- .../templates/vault/cluster/settings/seal.hbs | 18 ++++++-- .../components/login-settings/page/list.hbs | 8 +++- .../components/messages/tab-page-header.hbs | 9 ++++ ui/lib/core/addon/components/page/header.hbs | 2 +- .../addon/components/swagger-ui.hbs | 8 +++- .../components/enable-replication-form.hbs | 2 +- .../addon/components/page/mode-index.hbs | 31 +++---------- .../addon/components/page/mode-index.js | 11 +++++ .../addon/components/secrets/landing-cta.hbs | 43 ++++++++----------- .../components/secrets/page/overview.hbs | 1 + ui/tests/acceptance/clients/counts-test.js | 8 ++-- .../acceptance/oidc-config/clients-test.js | 7 +-- .../acceptance/reduced-disclosure-test.js | 2 +- ui/tests/acceptance/unseal-test.js | 2 +- ui/tests/helpers/oidc-config.js | 1 - .../components/clients/date-range-test.js | 13 ------ .../components/clients/page-header-test.js | 6 --- .../recovery/page/snapshot-manage-test.js | 27 ++++++------ .../recovery/page/snapshots-test.js | 12 +++--- .../components/seal-action-test.js | 10 +++-- .../sync/secrets/landing-cta-test.js | 22 +--------- .../sync/secrets/page/overview-test.js | 4 -- 59 files changed, 413 insertions(+), 370 deletions(-) delete mode 100644 ui/app/components/identity/entity-nav.js create mode 100644 ui/app/components/identity/entity-nav.ts delete mode 100644 ui/app/components/recovery/page/header.hbs delete mode 100644 ui/app/components/recovery/page/header.ts delete mode 100644 ui/app/components/seal-action.js create mode 100644 ui/app/components/seal-action.ts diff --git a/ui/app/components/clients/date-range.hbs b/ui/app/components/clients/date-range.hbs index 0699385039..f40ce29ca7 100644 --- a/ui/app/components/clients/date-range.hbs +++ b/ui/app/components/clients/date-range.hbs @@ -7,9 +7,6 @@