diff --git a/go.mod b/go.mod index 0aa42a2d67..8810feb159 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,8 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/IBM/sarama v1.45.2 github.com/aerospike/aerospike-client-go/v6 v6.12.0 + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 + github.com/akeylesslabs/akeyless-go/v5 v5.0.16 github.com/alibaba/sentinel-golang v1.0.4 github.com/alibabacloud-go/darabonba-openapi v0.2.1 github.com/alibabacloud-go/oos-20190601 v1.0.4 @@ -156,11 +158,15 @@ require ( require ( cel.dev/expr v0.23.0 // indirect cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go/ai v0.7.0 // indirect + cloud.google.com/go/aiplatform v1.86.0 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/vertexai v0.12.0 // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect @@ -281,6 +287,7 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/generative-ai-go v0.15.1 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect diff --git a/go.sum b/go.sum index 0725b38e4b..85e95fd945 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,17 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE= +cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= +cloud.google.com/go/aiplatform v1.86.0 h1:b8FVN8Jv4R0c1qMzqzURiJYXLp9R6Wx7d0q4MPGlTeM= +cloud.google.com/go/aiplatform v1.86.0/go.mod h1:xp3wFix8imliXkVpgMRkjnreJYTaNzLF44GOrnIENto= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -61,6 +70,8 @@ cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6Q cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8= +cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= @@ -81,8 +92,11 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0 h1:FQOmDxJj1If0D0khZR00MDa2Eb+k9BBsSaK7cEbLwkk= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0/go.mod h1:X0+PSrHOZdTjkiEhgv53HS5gplbzVVl2jd6hQRYSS3c= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= @@ -91,6 +105,8 @@ github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3 h1:gBWC0dYF3aO+7xGxL0 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3/go.mod h1:7LBWaO4KRASAo9VpfhpxQKkdY6PBwkv9UDKzL9Sajuw= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0 h1:aJG+Jxd9/rrLwf8R1Ko0RlOBTJASs/lGQJ8b9AdlKTc= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0/go.mod h1:41ONblJrPxDcnVr+voS+3xXWy/KnZLh+7zY5s6woAlQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.2.1 h1:0f6XnzroY1yCQQwxGf/n/2xlaBF02Qhof2as99dGNsY= @@ -121,6 +137,7 @@ github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -175,6 +192,10 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 h1:ly0WKARATneFzwBlTZ2lUyjtLqoOEYqt1vOlf89za/4= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5/go.mod h1:W6DMNwPyIE3jpXDaJOvCKUT/kHPZrpl/BGiIVUILbMk= +github.com/akeylesslabs/akeyless-go/v5 v5.0.16 h1:nH0ExvPnfWMhHL3DovUQBXST/2Dj02KJxIHFYMqRauo= +github.com/akeylesslabs/akeyless-go/v5 v5.0.16/go.mod h1:4oo5+/uOcshVr/+hLxxL4UQIALyQNWwOCskLGgTL6nk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -271,6 +292,7 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a/g github.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.41.13/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 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/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= @@ -466,6 +488,7 @@ github.com/cloudwego/thriftgo v0.2.8/go.mod h1:dAyXHEmKXo0LfMCrblVEY3mUZsdeuA5+i github.com/cloudwego/thriftgo v0.3.0 h1:BBb9hVcqmu9p4iKUP/PSIaDB21Vfutgd7k2zgK37Q9Q= github.com/cloudwego/thriftgo v0.3.0/go.mod h1:AvH0iEjvKHu3cdxG7JvhSAaffkS4h2f4/ZxpJbm48W4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -550,6 +573,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -597,6 +622,7 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= @@ -775,6 +801,7 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -816,6 +843,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -841,6 +869,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ= +github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -869,6 +899,7 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -878,6 +909,10 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20220608213341-c488b8fa1db3/go.mod h1:gSuNB+gJaOiQKLEZ+q+PK9Mq3SOzhRcw2GsGS/FhYDk= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= @@ -889,6 +924,7 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= @@ -1290,6 +1326,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= @@ -1430,6 +1467,7 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1792,6 +1830,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6-0.20201102222123-380f4078db9f/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= @@ -1914,7 +1953,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= @@ -1968,6 +2009,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -2019,8 +2062,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -2049,7 +2095,9 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2057,6 +2105,13 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= @@ -2134,28 +2189,35 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2209,6 +2271,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= @@ -2219,6 +2282,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -2303,9 +2367,14 @@ golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -2350,6 +2419,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -2359,6 +2434,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2394,7 +2470,18 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -2425,11 +2512,15 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md new file mode 100644 index 0000000000..9ab86acabb --- /dev/null +++ b/secretstores/akeyless/README.md @@ -0,0 +1,216 @@ +# Akeyless Secret Store + +This component provides a Dapr secret store implementation for [Akeyless](https://www.akeyless.io/), a cloud-native secrets management platform. + +## Configuration + +The Akeyless Dapr Secret Store component only supports the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + +- [API Key](https://docs.akeyless.io/docs/api-key) +- [OAuth2.0/JWT](https://docs.akeyless.io/docs/oauth20jwt) +- [AWS IAM](https://docs.akeyless.io/docs/aws-iam) +- [Kubernetes](https://docs.akeyless.io/docs/kubernetes-auth) + +### Authentication + +The Akeyless secret store component supports the following configuration options: + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `gatewayUrl` | No | The Akeyless Gateway API URL. Default is https://api.akeyless.io. | `https://gw.akeyless.svc.cluster.local:8000/api/v2` | +| `gatewayTlsCa` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. The Akeyless client will be set to a 30 second timeout. | `LS0tLS1CRUdJTi...` | +| `accessId` | Yes | The Akeyless authentication access ID. | `p-123456780wm` | +| `jwt` | No | If using an OAuth2.0/JWT access ID, specify the JSON Web Token | `eyJ...` | +| `accessKey` | No | If using an API Key access ID, specify the API key | `ABCD123...=` | +| `k8sAuthConfigName` | No | If using the k8s auth method, specify the name of the k8s auth config. | `k8s-auth-config` | +| `k8sGatewayUrl` | No | The gateway URL that where the k8s auth config is located. | `http://gw.akeyless.svc.cluster.local:8000` | +| `k8sServiceAccountToken` | No | If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file `/var/run/secrets/kubernetes.io/serviceaccount/token`. | `eyJ...` | + + + +## Examples + +We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + + +## Examples + +## Example Configuration: API Key + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: gatewayTlsCa + value: "LS0tLS1CRUdJTi...." + - name: accessId + value: "p-1234Abcdam" + - name: accessKey + value: "ABCD1233...=" +``` + + +## Example Configuration: JWT + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdom" + - name: jwt + value: "eyJ....." +``` + +## Example Configuration: AWS IAM + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdwm" +``` + +## Example Configuration: Kubernetes + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdkm" + - name: k8sAuthConfigName + value: "us-east-1-prod-akeyless-k8s-conf" + - name: k8sGatewayUrl + value: https://gw.akeyless.svc.cluster.local +``` + +## Usage + +Once configured, you can retrieve secrets using the Dapr secrets API: + +```bash +# Get a single secret +curl http://localhost:3500/v1.0/secrets/akeyless/my-secret + +# Get all secrets (static, dynamic, rotated) from root (/) path +curl http://localhost:3500/v1.0/secrets/akeyless/bulk + +# Get all secrets static secrets +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static + +# Get all static and dynamic secrets from a specific path (/my/org) +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static,dynamic&metadata.path=/my/org +``` + +Or using the Dapr SDK. The example below retrieves all static secrets from path `/path/to/department`. +```go +log.Println("Starting test application") +client, err := dapr.NewClient() +if err != nil { + log.Printf("Error creating Dapr client: %v\n", err) + panic(err) +} +log.Println("Dapr client created successfully") +const daprSecretStore = "akeyless" + +defer client.Close() +ctx := context.Background() +akeylessBulkMetadata := map[string]string{ + "path": "/path/to/department", + "secrets_type": "static", +} +secrets, err := client.GetBulkSecret(ctx, daprSecretStore, akeylessBulkMetadata) +if err != nil { + log.Printf("Error fetching secrets: %v\n", err) + panic(err) +} +log.Printf("Found %d secrets: ", len(secrets)) +for secretName, secretValue := range secrets { + log.Printf("Secret: %s, Value: %s", secretName, secretValue) +} +``` + +## Features + +- Supports static, dynamic and rotated secrets. +- **GetSecret**: Retrieve an individual value secret by path. +- **BulkGetSecret**: Retrieve all secrets from a specified path (or `/` by default) recursively. + +## Response Formats + +The Akeyless secret store returns different response formats depending on the secret type: + +### Static Secrets +Static secrets return their value directly as a string: + +```json +{ + "my-static-secret": "secret-value" +} +``` + +### Dynamic Secrets +Dynamic secrets return a JSON string containing the credentials. The exact structure depends on the target system: + +**MySQL Dynamic Secret:** +```json +{ + "my-mysql-secret": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" +} +``` + +**Azure AD Dynamic Secret:** +```json +{ + "my-azure-secret": "{\"user\":{\"id\":\"user_id\",\"displayName\":\"user_name\",\"mail\":\"email@domain.com\"},\"secret\":{\"keyId\":\"secret_key_id\",\"displayName\":\"secret_name\",\"tenantId\":\"tenant_id\"},\"ttl_in_minutes\":\"60\",\"id\":\"user_id\",\"msg\":\"User has been added successfully...\"}" +} +``` + +**GCP Dynamic Secret:** +```json +{ + "my-gcp-secret": "{\"encoded_key\":\"base64_encoded_service_account_key\",\"ttl_in_minutes\":\"60\",\"id\":\"service_account_name\"}" +} +``` + +### Rotated Secrets +Rotated secrets return a JSON object containing all available fields: + +```json +{ + "my-rotated-secret": "{\"value\":{\"username\":\"rotated_user\",\"password\":\"rotated_password\",\"application_id\":\"1234567890\"}}" +} +``` + +**Note:** The exact fields in dynamic and rotated secret responses vary by target system and configuration. Applications should parse the JSON string to extract the specific credentials they need. diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go new file mode 100644 index 0000000000..587a3c782b --- /dev/null +++ b/secretstores/akeyless/akeyless.go @@ -0,0 +1,958 @@ +package akeyless + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "strings" + "sync" + "time" + + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + "github.com/akeylesslabs/akeyless-go/v5" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +var _ secretstores.SecretStore = (*akeylessSecretStore)(nil) + +// akeylessSecretStore is a secret store implementation for Akeyless. +type akeylessSecretStore struct { + v2 *akeyless.V2ApiService + token string + tokenExpiry time.Time + metadata *akeylessMetadata + mu sync.RWMutex + logger logger.Logger + closeCh chan struct{} + wg sync.WaitGroup +} + +// NewAkeylessSecretStore returns a new Akeyless secret store. +func NewAkeylessSecretStore(logger logger.Logger) secretstores.SecretStore { + return &akeylessSecretStore{ + logger: logger, + } +} + +// akeylessMetadata contains the metadata for the Akeyless secret store. +type akeylessMetadata struct { + GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` + GatewayTlsCa string `json:"gatewayTlsCa" mapstructure:"gatewayTlsCa"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + K8SGatewayURL string `json:"k8sGatewayUrl" mapstructure:"k8sGatewayUrl"` + K8SAuthConfigName string `json:"k8sAuthConfigName" mapstructure:"k8sAuthConfigName"` + K8sServiceAccountToken string `json:"k8sServiceAccountToken" mapstructure:"k8sServiceAccountToken"` +} + +// Init creates a new Akeyless secret store client and sets up the Akeyless API client +// with authentication method based on the accessId. +func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metadata) error { + a.logger.Info("Initializing Akeyless secret store...") + m, err := a.parseMetadata(meta) + if err != nil { + return errors.New("failed to parse metadata: " + err.Error()) + } + + a.metadata = m + a.closeCh = make(chan struct{}) + + err = a.authenticate(ctx, m) + if err != nil { + return errors.New("failed to authenticate with Akeyless: " + err.Error()) + } + + // Start background token refresh routine if we have expiration time + if !a.tokenExpiry.IsZero() { + a.startTokenRefreshRoutine(ctx, m) + } + + return nil +} + +// Authenticate authenticates with Akeyless using the provided metadata. +// It returns an error if the authentication fails. +func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeylessMetadata) error { + + a.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + + // Get the authentication method + a.logger.Debug("extracting access type from accessId...") + accessTypeChar, err := extractAccessTypeChar(metadata.AccessID) + if err != nil { + return errors.New("unable to extract access type character from accessId, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + a.logger.Debugf("getting access type display name for character '%s'...", accessTypeChar) + accessType, err := getAccessTypeDisplayName(accessTypeChar) + if err != nil { + return errors.New("unable to get access type from character '" + accessTypeChar + "': " + err.Error()) + } + + a.logger.Debugf("authenticating using access type '%s'", accessType) + + // Depending on the access type we set the appropriate authentication method + switch accessType { + case AUTH_DEFAULT: + if metadata.AccessKey == "" { + return errors.New("accessKey is required for API key authentication") + } + authRequest.SetAccessKey(metadata.AccessKey) + case AUTH_IAM: + authRequest.SetAccessType(AUTH_IAM) + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID: " + err.Error()) + } + authRequest.SetCloudId(id) + case AUTH_JWT: + authRequest.SetAccessType(AUTH_JWT) + if metadata.JWT == "" { + return errors.New("jwt is required for JWT authentication") + } + authRequest.SetJwt(metadata.JWT) + case AUTH_K8S: + authRequest.SetAccessType(AUTH_K8S) + err := setK8SAuthConfiguration(*metadata, authRequest, a) + if err != nil { + return errors.New("failed to set k8s auth configuration: " + err.Error()) + } + } + + // Create Akeyless API client configuration + a.logger.Debug("creating Akeyless API client configuration...") + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = USER_AGENT + config.AddDefaultHeader(CLIENT_SOURCE, USER_AGENT) + + // Configure TLS if gatewayTlsCa is provided + if metadata.GatewayTlsCa != "" { + a.logger.Debug("configuring TLS for Akeyless client...") + tlsConfig, err := createTLSConfig(metadata.GatewayTlsCa) + if err != nil { + return errors.New("failed to create TLS configuration: " + err.Error()) + } + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + config.HTTPClient = httpClient + } + + a.v2 = akeyless.NewAPIClient(config).V2Api + + a.logger.Debug("authenticating with Akeyless...") + out, httpResponse, err := a.v2.Auth(ctx).Body(*authRequest).Execute() + if err != nil { + if httpResponse != nil { + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %w", httpResponse.StatusCode, err) + } + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + if httpResponse == nil || httpResponse.StatusCode != 200 { + statusCode := 0 + status := "unknown" + if httpResponse != nil { + statusCode = httpResponse.StatusCode + status = httpResponse.Status + } + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %s", statusCode, status) + } + if out != nil && out.GetToken() == "" { + return errors.New("authentication failed, no token returned") + } + if out != nil && out.GetExpiration() == "" { + return errors.New("authentication failed, no expiration time returned") + } + + a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) + + // Store token and expiration with mutex protection + a.mu.Lock() + a.token = out.GetToken() + expirationStr := out.GetExpiration() + a.mu.Unlock() + + // Parse and store expiration time + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + a.logger.Warnf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + a.mu.Lock() + a.tokenExpiry = expiration + a.mu.Unlock() + a.logger.Debugf("token expiration parsed and set successfully: %s", expiration.Format(time.RFC3339)) + } + } + + return nil +} + +// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. +func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { + if a.v2 == nil { + return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") + } + + a.logger.Debugf("getting secret type for '%s'...", req.Name) + secretType, err := a.getSecretType(ctx, req.Name) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New("failed to get secret type: " + err.Error()) + } + + a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) + + secretValue, err := a.getSingleSecretValue(ctx, req.Name, secretType) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New(err.Error()) + } + a.logger.Debugf("successfully retrieved secret '%s'", req.Name) + + return getDaprSingleSecretResponse(req.Name, secretValue) +} + +// BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. +// The method performs the following steps: +// 1. Recursively list all items in Akeyless +// 2. Filter out inactive/failing secrets +// 3. Separate items by type since only static secrets are supported for bulk get +// 4. Get secret values concurrently, each item type in a separate goroutine +func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { + if a.v2 == nil { + return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") + } + + // initialize response + response := secretstores.BulkGetSecretResponse{ + Data: make(map[string]map[string]string), + } + + // get secrets path to retrieve secrets from + // use root path if not specified + var secretsPath string + if value, ok := req.Metadata[METADATA_PATH_KEY]; ok { + + // normalize path + if !strings.HasPrefix(value, "/") { + secretsPath = "/" + value + } + + a.logger.Debugf("using path '%s' from metadata...", secretsPath) + } else { + a.logger.Debugf("no path found in metadata, using default path '%s'", PATH_DEFAULT) + secretsPath = PATH_DEFAULT + } + + // get secrets type to retrieve secrets from + // use all types if not specified + var requestedTypes []string + if value, ok := req.Metadata[METADATA_SECRETS_TYPE_KEY]; ok { + parsedTypes, err := parseSecretTypes(value) + if err != nil { + return response, fmt.Errorf("invalid secrets_type metadata: %w", err) + } + requestedTypes = parsedTypes + a.logger.Debugf("using secrets types '%v' from metadata...", requestedTypes) + } else { + a.logger.Debugf("no '%s' found in metadata, using all supported secret types '%v'", METADATA_SECRETS_TYPE_KEY, supportedSecretTypes) + requestedTypes = supportedSecretTypes + } + + // For bulk get, we need to list all secrets first + a.logger.Debugf("listing items from '%s' path with types '%v'...", secretsPath, requestedTypes) + listItems, err := a.listItemsRecursively(ctx, secretsPath, requestedTypes) + if err != nil { + return response, fmt.Errorf("failed to list items from Akeyless: %w", err) + } + + // if no items returned, return empty response + if len(listItems) == 0 { + a.logger.Debug("no items returned from / path") + return response, nil + } + + // filter out inactive secrets + a.logger.Debugf("%d items before filtering out inactive secrets", len(listItems)) + listItems = a.filterInactiveSecrets(listItems) + a.logger.Debugf("%d items remaining after filtering out inactive secrets", len(listItems)) + + // separate items by type since only static secrets are supported for bulk get + staticItemNames, dynamicItemNames, rotatedItemNames := a.separateItemsByType(listItems) + a.logger.Infof("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItemNames), len(dynamicItemNames), len(rotatedItemNames)) + + haveStaticItems := len(staticItemNames) > 0 + haveDynamicItems := len(dynamicItemNames) > 0 + haveRotatedItems := len(rotatedItemNames) > 0 + + secretResultChannels := make(chan secretResultCollection, len(listItems)) + + // get secret values concurrently, each item type in a separate goroutine + wg := sync.WaitGroup{} + if haveStaticItems { + wg.Add(1) + go func() { + defer wg.Done() + if len(staticItemNames) == 1 { + staticSecretName := staticItemNames[0] + value, err := a.getSingleSecretValue(ctx, staticSecretName, STATIC_SECRET_RESPONSE) + if err != nil { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} + } + } else { + secretResponse := a.getBulkStaticSecretValues(ctx, staticItemNames) + if len(secretResponse) > 0 { + for _, result := range secretResponse { + secretResultChannels <- result + } + } + } + }() + } + if haveDynamicItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range dynamicItemNames { + value, err := a.getSingleSecretValue(ctx, item, DYNAMIC_SECRET_RESPONSE) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + if haveRotatedItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range rotatedItemNames { + value, err := a.getSingleSecretValue(ctx, item, ROTATED_SECRET_RESPONSE) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + + // close the channel when all goroutines are done + go func() { + wg.Wait() + close(secretResultChannels) + }() + + // collect results and populate response + for result := range secretResultChannels { + if result.err != nil { + a.logger.Errorf("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) + continue + } + + response.Data[result.name] = map[string]string{result.name: result.value} + } + + // Use the new BulkGetSecretResponse function to handle all secret types properly + // return BulkGetSecretResponse(ctx, itemsList.Items, a) + return response, nil +} + +// Features returns the features available in this secret store. +func (a *akeylessSecretStore) Features() []secretstores.Feature { + return []secretstores.Feature{} +} + +// Close closes the secret store. +func (a *akeylessSecretStore) Close() error { + if a.closeCh != nil { + close(a.closeCh) + a.wg.Wait() + } + return nil +} + +// parseMetadata parses the metadata from the component configuration. +func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeylessMetadata, error) { + + a.logger.Debug("Parsing metadata...") + var m akeylessMetadata + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return nil, err + } + + // Validate access ID + if m.AccessID == "" { + return nil, errors.New("accessId is required") + } + + if !isValidAccessIdFormat(m.AccessID) { + return nil, errors.New("invalid accessId format, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + // Set default gateway URL if not specified + if m.GatewayURL == "" { + a.logger.Infof("Gateway URL is not set, using default value %s...", PUBLIC_GATEWAY_URL) + m.GatewayURL = PUBLIC_GATEWAY_URL + } else { + _, err = url.ParseRequestURI(m.GatewayURL) + if err != nil { + return nil, fmt.Errorf("invalid gateway URL '%s': %w", m.GatewayURL, err) + } + } + + // Trim trailing slash from gateway URL + m.GatewayURL = strings.TrimSuffix(m.GatewayURL, "/") + + return &m, nil +} + +func (a *akeylessSecretStore) getSecretType(ctx context.Context, secretName string) (string, error) { + + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + + describeItem := akeyless.NewDescribeItem(secretName) + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + describeItem.SetToken(token) + + result, _, err := a.executeWithRetryOn401( + ctx, + "DescribeItem", + describeItem, + func(newToken string) { + describeItem.SetToken(newToken) + }, + ) + + if err != nil { + return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) + } + + describeItemResp, ok := result.(*akeyless.Item) + if !ok { + return "", fmt.Errorf("unexpected result type from DescribeItem: %T", result) + } + + if describeItemResp.ItemType == nil { + return "", errors.New("unable to retrieve secret type, missing type in describe item response") + } + + return *describeItemResp.ItemType, nil +} + +// executeWithRetryOn401 executes an API call using reflection and retries once if it receives a 401 Unauthorized response. +// It takes the method name (e.g., "GetSecretValue"), the body object, and a function to update the token in the body. +// Returns the result, httpResponse, and error using reflection. +func (a *akeylessSecretStore) executeWithRetryOn401( + ctx context.Context, + methodName string, + body interface{}, + updateToken func(string), +) (interface{}, *http.Response, error) { + // Helper to get current token (with mutex protection) + getToken := func() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.token + } + + // Helper function to execute the API call using reflection + executeCall := func() (interface{}, *http.Response, error) { + // Use reflection to call the method dynamically + v2Value := reflect.ValueOf(a.v2) + method := v2Value.MethodByName(methodName) + if !method.IsValid() { + return nil, nil, fmt.Errorf("method %s not found on V2ApiService", methodName) + } + + // Call the method with context: a.v2.MethodName(ctx) + ctxValue := reflect.ValueOf(ctx) + callResult := method.Call([]reflect.Value{ctxValue}) + if len(callResult) == 0 { + return nil, nil, fmt.Errorf("method %s returned no values", methodName) + } + + // Get the Body() method from the result: result.Body() + bodyMethod := callResult[0].MethodByName("Body") + if !bodyMethod.IsValid() { + return nil, nil, fmt.Errorf("Body method not found on result of %s", methodName) + } + + // Call Body(*body): result.Body(*body) + // Body() expects a value (not a pointer), so we need to dereference if it's a pointer + bodyValue := reflect.ValueOf(body) + if bodyValue.Kind() == reflect.Ptr { + // Dereference the pointer to get the value + bodyValue = bodyValue.Elem() + } + // Pass the value to Body() + bodyCallResult := bodyMethod.Call([]reflect.Value{bodyValue}) + if len(bodyCallResult) == 0 { + return nil, nil, fmt.Errorf("Body method returned no values") + } + + // Get the Execute() method: result.Body(*body).Execute() + executeMethod := bodyCallResult[0].MethodByName("Execute") + if !executeMethod.IsValid() { + return nil, nil, fmt.Errorf("Execute method not found on Body result") + } + + // Execute the API call: result.Body(*body).Execute() + executeResult := executeMethod.Call([]reflect.Value{}) + if len(executeResult) < 3 { + return nil, nil, fmt.Errorf("Execute method did not return 3 values (result, response, error)") + } + + // Extract results + var result interface{} + var httpResponse *http.Response + var apiErr error + + if !executeResult[0].IsNil() { + result = executeResult[0].Interface() + } + if !executeResult[1].IsNil() { + httpResponse = executeResult[1].Interface().(*http.Response) + } + if !executeResult[2].IsNil() { + apiErr = executeResult[2].Interface().(error) + } + + return result, httpResponse, apiErr + } + + // Execute the API call + result, httpResponse, apiErr := executeCall() + + // Check for 401 Unauthorized using the actual HTTP status code + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debugf("received 401 unauthorized in %s, re-authenticating...", methodName) + if reauthErr := a.ensureValidToken(ctx); reauthErr != nil { + return nil, httpResponse, fmt.Errorf("failed to re-authenticate after 401: %w", reauthErr) + } + // Update token in the request object before retry + newToken := getToken() + updateToken(newToken) + + // Retry the API call once + return executeCall() + } + + return result, httpResponse, apiErr +} + +// getSingleSecretValue gets the value of a single secret from Akeyless. +// It returns the value of the secret or an error if the secret is not found. +func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretName string, secretType string) (string, error) { + + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + + var secretValue string + var err error + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + switch secretType { + case STATIC_SECRET_RESPONSE: + getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(token) + + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetSecretValue", + getSecretValue, + func(newToken string) { + getSecretValue.SetToken(newToken) + }, + ) + + if apiErr != nil { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetSecretValue: %T", result) + break + } + + // check if secret key is in response + value, ok := secretRespMap[secretName] + if !ok { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + break + } + + // single static secrets can be of type string, or map[string]string + // if it's a map[string]string, we need to transform it to a string + secretValue, err = stringifyStaticSecret(value, secretName) + if err != nil { + err = fmt.Errorf("failed to stringify static secret '%s': %w", secretName, err) + break + } + + case DYNAMIC_SECRET_RESPONSE: + getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) + getDynamicSecretValue.SetToken(token) + + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetDynamicSecretValue", + getDynamicSecretValue, + func(newToken string) { + getDynamicSecretValue.SetToken(newToken) + }, + ) + + if apiErr != nil { + err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetDynamicSecretValue: %T", result) + break + } + + // Parse response to extract value and check for errors + var dynamicSecretResp struct { + Value string `json:"value"` + Error string `json:"error"` + } + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal(jsonBytes, &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response: %w", unmarshalErr) + break + } + + // Check if the response contains an error + if dynamicSecretResp.Error != "" { + err = fmt.Errorf("dynamic secret retrieval error: %s", dynamicSecretResp.Error) + break + } + + // Return the value field directly (already a JSON string with credentials) + secretValue = dynamicSecretResp.Value + + case ROTATED_SECRET_RESPONSE: + getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) + getRotatedSecretValue.SetToken(token) + + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetRotatedSecretValue", + getRotatedSecretValue, + func(newToken string) { + getRotatedSecretValue.SetToken(newToken) + }, + ) + + if apiErr != nil { + err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetRotatedSecretValue: %T", result) + break + } + + // Marshal the entire response value object + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal rotated secret response to JSON: %w", marshalErr) + break + } + secretValue = string(jsonBytes) + } + + return secretValue, err +} + +// getBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. +// It returns a map of secret names and their values. +func (a *akeylessSecretStore) getBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { + if err := a.ensureValidToken(ctx); err != nil { + return []secretResultCollection{ + {name: "", value: "", err: fmt.Errorf("failed to ensure valid token: %w", err)}, + } + } + + var secretResponse []secretResultCollection + + getSecretsValues := akeyless.NewGetSecretValue(secretNames) + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + + secretRespMap, httpResponse, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + + // Handle 401 Unauthorized by re-authenticating and retrying once + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debug("received 401 Unauthorized in bulk get, re-authenticating...") + if err := a.ensureValidToken(ctx); err != nil { + secretResponse = append(secretResponse, secretResultCollection{ + name: "", value: "", err: fmt.Errorf("failed to re-authenticate after 401: %w", err), + }) + return secretResponse + } + + a.mu.RLock() + token = a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + secretRespMap, _, apiErr = a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + } + + if apiErr != nil { + secretResponse = append(secretResponse, secretResultCollection{ + name: "", value: "", err: fmt.Errorf("failed to get static secrets' '%s' value from Akeyless API: %w", secretNames, apiErr), + }) + } else { + for secretName, secretValue := range secretRespMap { + value, err := stringifyStaticSecret(secretValue, secretName) + secretResponse = append(secretResponse, secretResultCollection{name: secretName, value: value, err: err}) + } + } + + return secretResponse +} + +// listItemsRecursively lists all items in a given path recursively. +// It returns a list of items and an error if the list items request fails. +func (a *akeylessSecretStore) listItemsRecursively(ctx context.Context, path string, types []string) ([]akeyless.Item, error) { + if err := a.ensureValidToken(ctx); err != nil { + return nil, fmt.Errorf("failed to ensure valid token: %w", err) + } + + var allItems []akeyless.Item + + // Create the list items request + listItems := akeyless.NewListItems() + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + listItems.SetToken(token) + listItems.SetPath(path) + listItems.SetAutoPagination("enabled") + listItems.SetType(types) + + // Execute the list items request + a.logger.Debugf("listing items from path '%s'...", path) + result, _, err := a.executeWithRetryOn401( + ctx, + "ListItems", + listItems, + func(newToken string) { + listItems.SetToken(newToken) + }, + ) + + if err != nil { + return nil, err + } + + itemsList, ok := result.(*akeyless.ListItemsInPathOutput) + if !ok { + return nil, fmt.Errorf("unexpected result type from ListItems: %T", result) + } + + // Add items from current path + if itemsList.Items != nil { + allItems = append(allItems, itemsList.Items...) + } + + // Recursively process each subfolder + if itemsList.Folders != nil { + for _, folder := range itemsList.Folders { + subItems, err := a.listItemsRecursively(ctx, folder, types) + if err != nil { + return nil, err + } + allItems = append(allItems, subItems...) + } + } + + return allItems, nil +} + +func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]string, []string, []string) { + var staticItems []akeyless.Item + var dynamicItems []akeyless.Item + var rotatedItems []akeyless.Item + for _, item := range items { + itemType := *item.ItemType + + switch itemType { + case STATIC_SECRET_RESPONSE: + staticItems = append(staticItems, item) + case DYNAMIC_SECRET_RESPONSE: + dynamicItems = append(dynamicItems, item) + case ROTATED_SECRET_RESPONSE: + rotatedItems = append(rotatedItems, item) + } + } + + // listItems can get quite large, so we don't need all item details, we can use the item names instead + // and free memory + items = nil + staticItemNames := getItemNames(staticItems) + dynamicItemNames := getItemNames(dynamicItems) + rotatedItemNames := getItemNames(rotatedItems) + a.logger.Debugf("static items: %v", staticItemNames) + a.logger.Debugf("dynamic items: %v", dynamicItemNames) + a.logger.Debugf("rotated items: %v", rotatedItemNames) + + return staticItemNames, dynamicItemNames, rotatedItemNames +} + +func (a *akeylessSecretStore) filterInactiveSecrets(secrets []akeyless.Item) []akeyless.Item { + + filteredSecrets := []akeyless.Item{} + + for _, secret := range secrets { + if isSecretActive(secret, a.logger) { + filteredSecrets = append(filteredSecrets, secret) + } + } + + return filteredSecrets +} + +// ensureValidToken checks if the token is valid and refreshes it if needed (5 minutes before expiration) +// It returns an error if the token refresh fails. +func (a *akeylessSecretStore) ensureValidToken(ctx context.Context) error { + + a.mu.RLock() + expiry := a.tokenExpiry + metadata := a.metadata + a.mu.RUnlock() + + // If token expiry is zero, we can't validate it, so skip validation + // This can happen if expiration parsing failed or wasn't provided + if expiry.IsZero() { + a.logger.Debug("token expiration not set, skipping validation") + return nil + } + + tokenValid := time.Now().Before(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) + if tokenValid { + return nil + } + + // Token expired or about to expire, need to refresh/reauthenticate + a.logger.Debug("token expired or about to expire, reauthenticating...") + a.mu.Lock() + defer a.mu.Unlock() + + // Double-check after acquiring lock (another goroutine might have refreshed) + expiry = a.tokenExpiry + if expiry.IsZero() || time.Now().Before(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) { + return nil + } + + return a.authenticate(ctx, metadata) +} + +// startTokenRefreshRoutine starts a bg goroutine that refreshes the token +func (a *akeylessSecretStore) startTokenRefreshRoutine(ctx context.Context, metadata *akeylessMetadata) { + a.wg.Add(1) + go func() { + defer a.wg.Done() + // Use background context for the refresh routine, not the init context + refreshCtx := context.Background() + + for { + // Check if we should stop first, before acquiring any locks + select { + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + default: + } + + a.mu.RLock() + expiry := a.tokenExpiry + a.mu.RUnlock() + + if expiry.IsZero() { + a.logger.Warn("token expiration is zero, stopping refresh routine...") + return + } + + refreshDuration := time.Until(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) + if refreshDuration <= 0 { + refreshDuration = time.Minute // Refresh immediately if less than 1 minute left + } + + a.logger.Debugf("next token refresh scheduled in %v", refreshDuration) + + select { + case <-time.After(refreshDuration): + a.logger.Debug("refreshing token...") + if err := a.authenticate(refreshCtx, metadata); err != nil { + a.logger.Errorf("failed to refresh token: %v", err) + // Retry after 1 minute on failure + time.Sleep(time.Minute) + continue + } + a.logger.Debug("token refreshed successfully") + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + } + } + }() +} + +func (a *akeylessSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := akeylessMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) + return +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go new file mode 100644 index 0000000000..9f6d3eb273 --- /dev/null +++ b/secretstores/akeyless/akeyless_test.go @@ -0,0 +1,1309 @@ +package akeyless + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testAccessIdIAM = "p-xt3sT2nah7gpwm" + testAccessIdJwt = "p-xt3sT2nah7gpom" + testAccessIdKey = "p-xt3sT2nah7gpam" + testAccessKey = "ABCD1233xxx=" + // { + // "sub": "1234567890", + // "name": "John Doe", + // "iat": 1516239022 + // } + testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" + testSecretValue = "r3vE4L3D" +) + +var ( + mockStaticSecretItem = "/static-secret-test" + mockStaticSecretJSONItemName = "/static-secret-json-test" + mockStaticSecretPasswordItemName = "/static-secret-password-test" + mockDynamicSecretItemName = "/dynamic-secret-test" + mockRotatedSecretItemName = "/rotated-secret-test" + mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretItem) + mockDescribeStaticSecretType = STATIC_SECRET_RESPONSE + mockDescribeStaticSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeStaticSecretName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretJSONItemName) + mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ + mockStaticSecretJSONName: { + "some": "json", + }, + } + mockStaticSecretJSONItemResponse = akeyless.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mockStaticSecretPasswordName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretPasswordItemName) + mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ + mockStaticSecretPasswordName: { + "password": testSecretValue, + "username": "akeyless", + }, + } + mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) + mockDescribeDynamicSecretType = DYNAMIC_SECRET_RESPONSE + mockDescribeDynamicSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeDynamicSecretName, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeyless.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, + } + mockGetSingleDynamicSecretValueResponse = map[string]interface{}{ + "value": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", + "error": "", + } + mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless%s", mockRotatedSecretItemName) + mockDescribeRotatedSecretType = ROTATED_SECRET_RESPONSE + mockDescribeRotatedSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeRotatedSecretName, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + RotatedSecretDetails: &akeyless.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, + } + mockGetSingleRotatedSecretValueResponse = map[string]interface{}{ + "value": map[string]interface{}{ + "username": "abcdefghijklmnopqrstuvwxyz", + "password": testSecretValue, + "application_id": "1234567890", + }, + } +) + +var mockGetSingleSecretValueResponse = map[string]string{ + mockDescribeStaticSecretName: testSecretValue, +} + +// Global mock server for all tests +var mockGateway *httptest.Server + +// mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID +func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + // Initialize closeCh if not already set + if akeylessSecretStore.closeCh == nil { + akeylessSecretStore.closeCh = make(chan struct{}) + } + + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + + authRequest.SetAccessKey(metadata.AccessKey) + + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = USER_AGENT + config.AddDefaultHeader("akeylessclienttype", USER_AGENT) + + akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api + + out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + akeylessSecretStore.mu.Lock() + akeylessSecretStore.token = out.GetToken() + expirationStr := out.GetExpiration() + akeylessSecretStore.mu.Unlock() + + // Parse and store expiration time (same as in authenticate) + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + // Log warning but don't fail - expiration parsing is optional + akeylessSecretStore.logger.Debugf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + akeylessSecretStore.mu.Lock() + akeylessSecretStore.tokenExpiry = expiration + akeylessSecretStore.mu.Unlock() + } + } + + return nil +} + +// TestMain sets up and tears down the mock server for all tests +func TestMain(m *testing.M) { + // Setup mock server that returns an *akeyless.AuthOutput + mockGateway = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/list-items": + listItemsResponse := akeyless.NewListItemsInPathOutput() + listItemsResponse.SetItems( + []akeyless.Item{mockDescribeStaticSecretItemResponse}, + ) + jsonResponse, _ := json.Marshal(listItemsResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + // Run tests + code := m.Run() + + // Exit with the same code as the tests + os.Exit(code) +} + +func TestNewAkeylessSecretStore(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + assert.NotNil(t, store) +} + +func TestInit(t *testing.T) { + tests := []struct { + name string + metadata secretstores.Metadata + expectError bool + }{ + { + name: "gw, access id and key", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id and jwt", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id (aws_iam)", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "missing access id", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + defer store.Close() // Clean up background goroutine + + tt.metadata.Properties["gatewayUrl"] = mockGateway.URL + + // For AWS IAM test, use mock authentication to avoid AWS dependency + if tt.name == "gw, access id (aws_iam)" { + // Parse metadata first + m, err := store.parseMetadata(tt.metadata) + require.NoError(t, err) + + // Use mock authentication instead of the real one + err = mockAuthenticate(m, store) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } + } else { + // Use normal Init for other test cases + err := store.Init(context.Background(), tt.metadata) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } + } + }) + } +} + +func TestGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.GetSecretRequest{ + Name: "test-secret", + } + + _, err := store.GetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestBulkGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.BulkGetSecretRequest{} + + _, err := store.BulkGetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestFeatures(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + features := store.Features() + assert.Empty(t, features) +} + +func TestClose(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + err := store.Close() + assert.NoError(t, err) +} + +func TestParseMetadata(t *testing.T) { + tests := []struct { + name string + properties map[string]string + expectError bool + expected *akeylessMetadata + }{ + { + name: "valid metadata with access id and key", + properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdKey, + AccessKey: testAccessKey, + GatewayURL: "https://api.akeyless.io", // Default gateway URL + }, + }, + { + name: "valid metadata with access id and jwt", + properties: map[string]string{ + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdJwt, + JWT: testJWT, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "valid metadata with access id aws_iam", + properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdIAM, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "missing access id", + properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, + expectError: true, + }, + { + name: "invalid gateway url", + properties: map[string]string{ + "gatewayUrl": "http:/invalidaddress", + }, + expectError: true, + }, + { + name: "invalid access id format", + properties: map[string]string{ + "accessId": "invalid", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: tt.properties, + }, + } + + result, err := store.parseMetadata(meta) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestMockServerReturnsAuthOutput(t *testing.T) { + // Test that the mock server properly returns an AuthOutput response + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with access key authentication + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) + defer store.Close() // Clean up background goroutine +} + +func TestMockAWSCloudID(t *testing.T) { + // Test that the mock AWS cloud ID works correctly + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with AWS IAM authentication using mock cloud ID + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + // Parse metadata first + m, err := store.parseMetadata(meta) + require.NoError(t, err) + + // Use mock authentication with mock cloud ID + err = mockAuthenticate(m, store) + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) +} + +func TestGetSecret(t *testing.T) { + // Setup a properly initialized store + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + tests := []struct { + name string + request secretstores.GetSecretRequest + expectError bool + expectedSecret string + }{ + { + name: "test text single static secret", + request: secretstores.GetSecretRequest{ + Name: mockDescribeStaticSecretName, + }, + expectError: false, + expectedSecret: testSecretValue, + }, + // { + // name: "get non-existing secret", + // request: secretstores.GetSecretRequest{ + // Name: mockDescribeStaticSecretName, + // }, + // expectError: true, + // expectedSecret: "", + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := store.GetSecret(context.Background(), tt.request) + if tt.expectError { + assert.Error(t, err) + assert.Empty(t, response.Data) + } else { + assert.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, tt.request.Name) + assert.Equal(t, tt.expectedSecret, response.Data[tt.request.Name]) + } + }) + } +} + +func TestGetSingleSecretJSON(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretJSONValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeyless.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: mockStaticSecretJSONName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretJSONName) + assert.Equal(t, "{\"some\":\"json\"}", response.Data[mockStaticSecretJSONName]) + + mockGateway.Close() +} + +func TestGetSingleSecretPassword(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretPasswordValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeyless.Item{ + ItemName: &mockStaticSecretPasswordName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: mockStaticSecretPasswordName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretPasswordName) + assert.Equal(t, "{\"password\":\"r3vE4L3D\",\"username\":\"akeyless\"}", response.Data[mockStaticSecretPasswordName]) + + mockGateway.Close() +} + +// Test GetSecretType functions +func TestGetSecretType(t *testing.T) { + // Test GetSecretType + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretType, err := store.getSecretType(ctx, mockDescribeStaticSecretName) + assert.NoError(t, err) + assert.Equal(t, STATIC_SECRET_RESPONSE, secretType) +} + +func TestGetSingleDynamicSecret(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeDynamicSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleDynamicSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretValue, err := store.getSingleSecretValue(ctx, mockDescribeDynamicSecretName, DYNAMIC_SECRET_RESPONSE) + assert.NoError(t, err) + assert.Equal(t, "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", secretValue) + mockGateway.Close() +} +func TestGetSingleRotatedSecret(t *testing.T) { + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeRotatedSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleRotatedSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretValue, err := store.getSingleSecretValue(ctx, mockDescribeRotatedSecretName, ROTATED_SECRET_RESPONSE) + assert.NoError(t, err) + assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", secretValue) + + mockGateway.Close() +} + +func TestGetBulkSecretValues(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + mockStaticSecretItem: testSecretValue, + mockStaticSecretJSONItemName: "{\"some\":\"json\"}", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + items := akeyless.NewListItemsInPathOutput() + items.SetItems( + []akeyless.Item{ + mockDescribeStaticSecretItemResponse, + mockStaticSecretJSONItemResponse, + mockDescribeDynamicSecretItemResponse, + mockDescribeRotatedSecretItemResponse, + }, + ) + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got all 4 secrets (excluding any empty keys) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 4, nonEmptySecrets) + + // Check static secret (text) - using the actual key from the response + staticSecretKey := "/static-secret-test" + assert.Contains(t, response.Data, staticSecretKey) + assert.Equal(t, testSecretValue, response.Data[staticSecretKey][staticSecretKey]) + + // Check static secret (JSON) + jsonSecretKey := "/static-secret-json-test" + assert.Contains(t, response.Data, jsonSecretKey) + assert.Equal(t, "{\"some\":\"json\"}", response.Data[jsonSecretKey][jsonSecretKey]) + + // Check dynamic secret + dynamicSecretKey := "/path/to/akeyless/dynamic-secret-test" + assert.Contains(t, response.Data, dynamicSecretKey) + expectedDynamicValue := "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" + assert.Equal(t, expectedDynamicValue, response.Data[dynamicSecretKey][dynamicSecretKey]) + + // Check rotated secret + rotatedSecretKey := "/path/to/akeyless/rotated-secret-test" + assert.Contains(t, response.Data, rotatedSecretKey) + assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) + + mockGateway.Close() +} + +func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { + // Test recursive secret retrieval from different hierarchical paths + // This test simulates a folder structure where: + // - Root "/" contains 4 subfolders + // - Each subfolder contains different types of secrets + // - The listItemsRecursively method should traverse all folders + + // Define mock secrets for different paths + staticSecret1 := "/path/to/static/secrets/secret1" + staticSecret2 := "/path/to/static/secrets/secret2" + staticSecret3 := "/path/to/static/secrets/secret3" + dynamicSecret1 := "/path/to/dynamic/secrets/dynamic1" + dynamicSecret2 := "/path/to/dynamic/secrets/dynamic2" + rotatedSecret1 := "/path/to/rotated/secrets/rotated1" + mixedStaticSecret := "/path/to/mixed/secrets/mixed-static" + mixedDynamicSecret := "/path/to/mixed/secrets/mixed-dynamic" + mixedRotatedSecret := "/path/to/mixed/secrets/mixed-rotated" + + // Create mock items for different paths + staticItem1 := akeyless.Item{ + ItemName: &staticSecret1, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem2 := akeyless.Item{ + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem3 := akeyless.Item{ + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem1 := akeyless.Item{ + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem2 := akeyless.Item{ + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + rotatedItem1 := akeyless.Item{ + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedStaticItem := akeyless.Item{ + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedDynamicItem := akeyless.Item{ + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedRotatedItem := akeyless.Item{ + ItemName: &mixedRotatedSecret, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + staticSecret1: testSecretValue, + staticSecret2: "static-secret-2-value", + staticSecret3: "static-secret-3-value", + mixedStaticSecret: "mixed-static-secret-value", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + // Parse the path from request body to determine what to return + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var listItemsRequest akeyless.ListItems + if err := json.Unmarshal(body, &listItemsRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + path := "" + if listItemsRequest.Path != nil { + path = *listItemsRequest.Path + } + // Debug: Uncomment to see recursive calls + // fmt.Printf("DEBUG: list-items called for path: '%s'\n", path) + + var items akeyless.ListItemsInPathOutput + + switch path { + case "/": + // Root path returns only folders, no items + folders := []string{ + "/path/to/static/secrets", + "/path/to/dynamic/secrets", + "/path/to/rotated/secrets", + "/path/to/mixed/secrets", + } + items.SetFolders(folders) + items.SetItems([]akeyless.Item{}) + + case "/path/to/static/secrets": + // Static secrets folder + items.SetItems([]akeyless.Item{staticItem1, staticItem2, staticItem3}) + items.SetFolders([]string{}) + + case "/path/to/dynamic/secrets": + // Dynamic secrets folder + items.SetItems([]akeyless.Item{dynamicItem1, dynamicItem2}) + items.SetFolders([]string{}) + + case "/path/to/rotated/secrets": + // Rotated secrets folder + items.SetItems([]akeyless.Item{rotatedItem1}) + items.SetFolders([]string{}) + + case "/path/to/mixed/secrets": + // Mixed secrets folder + items.SetItems([]akeyless.Item{mixedStaticItem, mixedDynamicItem, mixedRotatedItem}) + items.SetFolders([]string{}) + + default: + // Unknown path + items.SetItems([]akeyless.Item{}) + items.SetFolders([]string{}) + } + + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-dynamic-secret-value": + // Create dynamic secret responses for each secret + dynamicSecretResponse := map[string]interface{}{ + "value": "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}", + "error": "", + } + jsonResponse, _ := json.Marshal(&dynamicSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + // Create rotated secret response + rotatedSecretResponse := map[string]interface{}{ + "value": map[string]interface{}{ + "username": "rotated-user", + "password": "rotated-secret-1-value", + "application_id": "1234567890", + }, + } + jsonResponse, _ := json.Marshal(&rotatedSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/describe-item": + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var describeItemRequest akeyless.DescribeItem + if err := json.Unmarshal(body, &describeItemRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + var itemResponse akeyless.Item + switch describeItemRequest.Name { + case staticSecret1, staticSecret2, staticSecret3, mixedStaticSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeyless.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, + } + case rotatedSecret1, mixedRotatedSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + RotatedSecretDetails: &akeyless.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, + } + default: + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "invalid item name"}`)) + return + } + + jsonResponse, _ := json.Marshal(&itemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got all 9 secrets (4 static, 3 dynamic, 2 rotated) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 9, nonEmptySecrets) + + // Check static secrets from /path/to/static/secrets + assert.Contains(t, response.Data, staticSecret1) + assert.Equal(t, testSecretValue, response.Data[staticSecret1][staticSecret1]) + assert.Contains(t, response.Data, staticSecret2) + assert.Equal(t, "static-secret-2-value", response.Data[staticSecret2][staticSecret2]) + assert.Contains(t, response.Data, staticSecret3) + assert.Equal(t, "static-secret-3-value", response.Data[staticSecret3][staticSecret3]) + + // Check dynamic secrets from /path/to/dynamic/secrets + assert.Contains(t, response.Data, dynamicSecret1) + expectedDynamicValue1 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.Equal(t, expectedDynamicValue1, response.Data[dynamicSecret1][dynamicSecret1]) + assert.Contains(t, response.Data, dynamicSecret2) + expectedDynamicValue2 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.Equal(t, expectedDynamicValue2, response.Data[dynamicSecret2][dynamicSecret2]) + + // Check rotated secret from /path/to/rotated/secrets + assert.Contains(t, response.Data, rotatedSecret1) + expectedRotatedValue1 := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" + assert.Equal(t, expectedRotatedValue1, response.Data[rotatedSecret1][rotatedSecret1]) + + // Check mixed secrets from /path/to/mixed/secrets + assert.Contains(t, response.Data, mixedStaticSecret) + assert.Equal(t, "mixed-static-secret-value", response.Data[mixedStaticSecret][mixedStaticSecret]) + assert.Contains(t, response.Data, mixedDynamicSecret) + expectedMixedDynamicValue := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.Equal(t, expectedMixedDynamicValue, response.Data[mixedDynamicSecret][mixedDynamicSecret]) + assert.Contains(t, response.Data, mixedRotatedSecret) + expectedMixedRotatedValue := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" + assert.Equal(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) + + mockGateway.Close() +} + +func TestParseSecretTypes(t *testing.T) { + tests := []struct { + name string + input string + expected []string + expectError bool + }{ + { + name: "all", + input: "all", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "static", + input: "static", + expected: []string{STATIC_SECRET_TYPE}, + }, + { + name: "dynamic", + input: "dynamic", + expected: []string{DYNAMIC_SECRET_TYPE}, + }, + { + name: "rotated", + input: "rotated", + expected: []string{ROTATED_SECRET_TYPE}, + }, + { + name: "static,dynamic", + input: "static,dynamic", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE}, + }, + { + name: "static,dynamic,rotated", + input: "static,dynamic,rotated", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "invalid", + input: "invalid", + expectError: true, + }, + { + name: "empty", + input: "", + expectError: false, + expected: supportedSecretTypes, + }, + { + name: "mixed case", + input: "Static,Dynamic,ROTATED", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "duplicates", + input: "static-secret,dynamic-secret,static-secret", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE}, + }, + { + name: "mixed sdk format and direct format", + input: "static-secret,dynamic-secret,rotated-secret,static", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "invalid type", + input: "invalid", + expectError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsedTypes, err := parseSecretTypes(tt.input) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, parsedTypes) + } + }) + } +} diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml new file mode 100644 index 0000000000..5063fec9fd --- /dev/null +++ b/secretstores/akeyless/metadata.yaml @@ -0,0 +1,86 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: secretstores +name: akeyless +version: v1 +status: beta +title: "Akeyless Secret Store" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-secret-stores/akeyless/ +metadata: + - name: gatewayUrl + required: false + description: | + The URL to the Akeyless Gateway API. Default is https://api.akeyless.io. + default: "https://api.akeyless.io" + example: "https://your.akeyless.gw" + type: string + - name: gatewayTlsCa + required: false + description: | + base64-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway + with a self-signed or custom CA certificate. + example: "LS0tLS1CRUdJTi..." + type: string + sensitive: true + - name: accessId + required: true + description: | + The Akeyless Access ID. Currently supported authentication methods are: API keys (`access_key`, default), JWT (`jwt`) and AWS IAM (`aws_iam`). + example: "p-123456780wm" + type: string + - name: jwt + required: false + description: | + If using the JWT authentication method, specify it here. + example: "eyJ..." + type: string + sensitive: true + - name: accessKey + required: false + description: | + If using the API key (access_key) authentication method, specify it here. + example: "ABCD1233...=" + type: string + sensitive: true + - name: k8sAuthConfigName + required: false + description: | + If using the k8s auth method, specify the name of the k8s auth config. + example: "k8s-auth-config" + type: string + - name: k8sGatewayUrl + required: false + description: | + The gateway URL that where the k8s auth config is located. + example: "http://gw.akeyless.svc.cluster.local:8000" + type: string + - name: k8sServiceAccountToken + required: false + description: | + If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. + example: "eyJ..." + type: string + sensitive: true + - name: k8sAuthConfigName + required: false + description: | + If using the k8s auth method, specify the name of the k8s auth config. + example: "k8s-auth-config" + type: string + - name: k8sGatewayUrl + required: false + description: | + The gateway URL that where the k8s auth config is located. + example: "https://gw.akeyless.svc.cluster.local" + type: string + - name: k8sServiceAccountToken + required: false + description: | + If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. + example: "eyJ..." + type: string + sensitive: true diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go new file mode 100644 index 0000000000..f4f252bf49 --- /dev/null +++ b/secretstores/akeyless/utils.go @@ -0,0 +1,326 @@ +package akeyless + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" +) + +const ( + AUTH_JWT = "jwt" + AUTH_DEFAULT = "access_key" + AUTH_IAM = "aws_iam" + AUTH_K8S = "k8s" + PUBLIC_GATEWAY_URL = "https://api.akeyless.io" + USER_AGENT = "dapr.io/akeyless-secret-store" + STATIC_SECRET_RESPONSE = "STATIC_SECRET" + DYNAMIC_SECRET_RESPONSE = "DYNAMIC_SECRET" + ROTATED_SECRET_RESPONSE = "ROTATED_SECRET" + STATIC_SECRET_TYPE = "static-secret" + DYNAMIC_SECRET_TYPE = "dynamic-secret" + ROTATED_SECRET_TYPE = "rotated-secret" + ALL_SECRET_TYPES = "all" + CLIENT_SOURCE = "akeylessclienttype" + PATH_DEFAULT = "/" + METADATA_PATH_KEY = "path" + METADATA_SECRETS_TYPE_KEY = "secrets_type" + TOKEN_REFRESH_GRACE_PERIOD = 5 * time.Minute +) + +var supportedSecretTypes = []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE} + +// AccessTypeCharMap maps single-character access types to their display names. +var accessTypeCharMap = map[string]string{ + "a": AUTH_DEFAULT, + "o": AUTH_JWT, + "w": AUTH_IAM, + "k": AUTH_K8S, +} + +// AccessIdRegex is the compiled regular expression for validating Akeyless Access IDs. +var accessIdRegex = regexp.MustCompile(`^p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})$`) + +// isValidAccessIdFormat validates the format of an Akeyless Access ID. +// The format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12}). +// It returns true if the format is valid, and false otherwise. +func isValidAccessIdFormat(accessId string) bool { + return accessIdRegex.MatchString(accessId) +} + +// extractAccessTypeChar extracts the Akeyless Access Type character from a valid Access ID. +// The access type character is the second to last character of the ID part. +// It returns the single-character access type (e.g., 'a', 'o') or an empty string and an error if the format is invalid. +func extractAccessTypeChar(accessId string) (string, error) { + if !isValidAccessIdFormat(accessId) { + return "", errors.New("invalid access ID format") + } + parts := strings.Split(accessId, "-") + idPart := parts[1] // Get the part after "p-" + // The access type char is the second-to-last character + return string(idPart[len(idPart)-2]), nil +} + +// getAccessTypeDisplayName gets the full display name of the access type from the character. +// It returns the display name (e.g., 'api_key') or an error if the type character is unknown. +func getAccessTypeDisplayName(typeChar string) (string, error) { + if typeChar == "" { + return "", errors.New("unable to retrieve access type, missing type char") + } + displayName, ok := accessTypeCharMap[typeChar] + if !ok { + return "Unknown", errors.New("access type character not found in map") + } + return displayName, nil +} + +func getDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { + return secretstores.GetSecretResponse{ + Data: map[string]string{ + secretName: secretValue, + }, + }, nil +} + +func getItemNames(items []akeyless.Item) []string { + itemNames := []string{} + for _, item := range items { + itemNames = append(itemNames, *item.ItemName) + } + return itemNames +} + +func stringifyStaticSecret(secretValue any, secretName string) (string, error) { + var err error + + switch valueType := secretValue.(type) { + case string: + secretValue = string(valueType) + case map[string]string: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + } else { + secretValue = string(encoded) + } + case any: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + break + } else { + secretValue = string(encoded) + break + } + + default: + err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) + } + + return string(secretValue.(string)), err +} + +type secretResultCollection struct { + name string + value string + err error +} + +func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { + + var isActive bool + + // check if secret has isEnabled field + if secret.IsEnabled == nil { + logger.Debugf("secret '%s' is missing isEnabled field, skipping...", *secret.ItemName) + return false + } + + if !*secret.IsEnabled { + logger.Debugf("secret '%s' is not enabled, skipping...", *secret.ItemName) + return false + } + + switch *secret.ItemType { + case STATIC_SECRET_RESPONSE: + logger.Debugf("static secret '%s' is active", *secret.ItemName) + isActive = true + case DYNAMIC_SECRET_RESPONSE: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus != nil { + status := *secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus + if status == "ProducerConnected" { + logger.Debugf("dynamic secret '%s' is active, adding to filtered secrets...", *secret.ItemName) + isActive = true + } else { + logger.Debugf("dynamic secret '%s' producer status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("dynamic secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + case ROTATED_SECRET_RESPONSE: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.RotatedSecretDetails != nil && + secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus != nil { + status := *secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus + if status == "RotationSucceeded" || status == "RotationInitialStatus" { + isActive = true + } else { + logger.Debugf("rotated secret '%s' rotation status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("rotated secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + default: + logger.Debugf("secret '%s' is of unsupported type '%s', skipping...", *secret.ItemName, *secret.ItemType) + isActive = false + } + + return isActive +} + +func setK8SAuthConfiguration(metadata akeylessMetadata, authRequest *akeyless.Auth, a *akeylessSecretStore) error { + if metadata.K8SAuthConfigName == "" { + return fmt.Errorf("k8s auth config name is required") + } + authRequest.SetK8sAuthConfigName(metadata.K8SAuthConfigName) + if metadata.K8sServiceAccountToken == "" { + a.logger.Debug("k8s service account token is missing, attempting to read from default service account token file") + token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return fmt.Errorf("failed to read default service account token file: %w", err) + } + metadata.K8sServiceAccountToken = string(token) + } + + // base64 encode the token if it's not already encoded + if _, err := base64.StdEncoding.DecodeString(metadata.K8sServiceAccountToken); err != nil { + a.logger.Info("k8sServiceAccountToken is not base64 encoded, encoding it...") + metadata.K8sServiceAccountToken = base64.StdEncoding.EncodeToString([]byte(metadata.K8sServiceAccountToken)) + } + authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) + + if metadata.K8SGatewayURL == "" { + a.logger.Debug("k8s gateway url is missing, using gatewayUrl") + metadata.K8SGatewayURL = metadata.GatewayURL + } + metadata.K8SGatewayURL = strings.TrimSuffix(metadata.K8SGatewayURL, "/api/v2") + authRequest.SetGatewayUrl(metadata.K8SGatewayURL) + return nil +} + +// `parseSecretTypes` parses the `secret_types` metadata parameter +// and returns a slice of supported secret types in the format expected +// by the Akeyless `POST /list-items` API. +// It accepts a comma-separated string of secret types and returns a slice of supported secret types. +func parseSecretTypes(secretTypes string) ([]string, error) { + // Handle "all" or empty string which returns all supported secret types + if secretTypes == ALL_SECRET_TYPES || secretTypes == "" { + return supportedSecretTypes, nil + } + + // Parse comma-separated values + types := strings.Split(secretTypes, ",") + if len(types) == 0 { + return nil, fmt.Errorf("no secret types provided") + } + result := make([]string, 0, len(types)) + + // Map metadata.secret_types to supportedSecretTypes + typeMap := map[string]string{ + "static": STATIC_SECRET_TYPE, + "dynamic": DYNAMIC_SECRET_TYPE, + "rotated": ROTATED_SECRET_TYPE, + } + + for _, t := range types { + t = strings.ToLower(strings.TrimSpace(t)) + if mappedType, ok := typeMap[t]; ok { + result = append(result, mappedType) + } else { + // Allow direct SDK format + if t == STATIC_SECRET_TYPE || t == DYNAMIC_SECRET_TYPE || t == ROTATED_SECRET_TYPE { + result = append(result, t) + } else { + return nil, fmt.Errorf("invalid secret type '%s', supported types: static[-secret], dynamic[-secret], rotated[-secret]", t) + } + } + } + + // Dedup + seen := make(map[string]bool) + unique := []string{} + for _, t := range result { + if !seen[t] { + seen[t] = true + unique = append(unique, t) + } + } + + return unique, nil +} + +func createTLSConfig(gatewayTlsCa string) (*tls.Config, error) { + + // Decode base64 to PEM + certBytes, err := base64.StdEncoding.DecodeString(gatewayTlsCa) + if err != nil { + return nil, fmt.Errorf("failed to decode base64-encoded gateway TLS CA: %w", err) + } + + // Validate PEM format + block, _ := pem.Decode(certBytes) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM certificate: invalid PEM format") + } + + // Cereate cert pool and add certificate + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(certBytes) { + return nil, errors.New("failed to add certificate to cert pool") + } + + return &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: caCertPool, + }, nil +} + +func parseTokenExpirationDate(expirationStr string) (time.Time, error) { + // Try multiple formats to handle different expiration date formats + // Format 1: ISO 8601 format "2025-01-01T00:00:00Z" (used in tests) + layouts := []string{ + time.RFC3339, // "2006-01-02T15:04:05Z07:00" + time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00" + "2006-01-02T15:04:05Z", // "2006-01-02T15:04:05Z" + "2006-01-02 15:04:05 -0700 MST", // "2025-12-09 21:35:00 +0000 UTC" (custom format) + "2006-01-02 15:04:05 -0700", // "2025-12-09 21:35:00 +0000" (without MST) + } + + for _, layout := range layouts { + parsedTime, err := time.Parse(layout, expirationStr) + if err == nil { + return parsedTime, nil + } + } + + return time.Time{}, fmt.Errorf("failed to parse token expiration date '%s' with any supported format", expirationStr) +}