diff --git a/go.mod b/go.mod index 0d24d1cb13..a231a74b0a 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,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.8 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 diff --git a/go.sum b/go.sum index 5a8bc06ef4..d64fbdfdd8 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,11 @@ 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/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= @@ -81,8 +86,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 +99,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 +131,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 +186,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.8 h1:rOYxuhCHEZDt7e5J1agI2I4qNPueX28dL4PdTv1rhu0= +github.com/akeylesslabs/akeyless-go/v5 v5.0.8/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 +286,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 +482,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= @@ -548,6 +565,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= @@ -595,6 +614,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= @@ -773,6 +793,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= @@ -814,6 +835,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= @@ -867,6 +889,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= @@ -876,6 +899,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= @@ -887,6 +914,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= @@ -1288,6 +1316,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= @@ -1428,6 +1457,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= @@ -1790,6 +1820,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= @@ -1912,7 +1943,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= @@ -1966,6 +1999,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= @@ -2017,8 +2052,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= @@ -2047,7 +2085,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= @@ -2055,6 +2095,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= @@ -2132,28 +2179,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= @@ -2207,6 +2261,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= @@ -2217,6 +2272,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= @@ -2301,9 +2357,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= @@ -2348,6 +2409,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= @@ -2357,6 +2424,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= @@ -2392,7 +2460,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= @@ -2423,11 +2502,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..e4a97aa628 --- /dev/null +++ b/secretstores/akeyless/README.md @@ -0,0 +1,171 @@ +# Akeyless Secret Store + +This component provides a Dapr secret store implementation for [Akeyless](https://www.akeyless.io/), a cloud-native secrets management platform. + +## Configuration + +- [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 URL. Default is https://api.akeyless.io. | `https://your-gateway.akeyless.io` | +| `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. | `eyJ...` | + +We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + + +## 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: 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: "https://your-gateway.akeyless.io" + - 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: "https://your-gateway.akeyless.io" + - 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: "https://gw.akeyless.svc.cluster.local" + - name: accessId + value: "p-1234Abcdwm" + - 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/SDK: + +```bash +# Get a single secret +curl http://localhost:3500/v1.0/secrets/akeyless/my-secret + +# Get all secrets +curl http://localhost:3500/v1.0/secrets/akeyless/bulk +``` + +## Features + +- Supports static, dynamic and rotated secrets. +- **GetSecret**: Retrieve an individual value secret by path. +- **BulkGetSecret**: Retrieve an all secrets from the root path. + +## 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..642d08922d --- /dev/null +++ b/secretstores/akeyless/akeyless.go @@ -0,0 +1,539 @@ +package akeyless + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "sync" + + 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 + logger logger.Logger +} + +// 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"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + AccessType string `json:"accessType" mapstructure:"accessType"` + 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()) + } + + err = a.Authenticate(ctx, m) + if err != nil { + return errors.New("failed to authenticate with Akeyless: " + err.Error()) + } + + 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) + authRequest.SetAccessType(metadata.AccessType) + + var accessType = metadata.AccessType + + a.logger.Debugf("authenticating using access type: %s", accessType) + + // Depending on the access type we set the appropriate authentication method + switch accessType { + // If access type is AWS IAM we use the cloud ID + case AUTH_IAM: + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID") + } + authRequest.SetCloudId(id) + case AUTH_JWT: + authRequest.SetJwt(metadata.JWT) + case DEFAULT_AUTH_TYPE: + a.logger.Debug("authenticating using access key...") + authRequest.SetAccessKey(metadata.AccessKey) + case AUTH_K8S: + a.logger.Debug("authenticating using k8s...") + err := setK8SAuthConfiguration(*metadata, authRequest, a) + if err != nil { + return fmt.Errorf("failed to set k8s auth configuration: %w", err) + } + } + + // 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("akeylessclienttype", USER_AGENT) + + 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 || httpResponse.StatusCode != 200 { + return fmt.Errorf("failed to authenticate with Akeyless: %w", errors.New(httpResponse.Status)) + } + + a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) + a.token = out.GetToken() + + 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{}, err + } + + 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("secret '%s' value: %s", req.Name, secretValue[:3]+"[REDACTED]") + + // Return the secret in the expected format + 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), + } + + // For bulk get, we need to list all secrets first + a.logger.Debug("listing items from / path...") + listItems, err := a.listItemsRecursively(ctx, "/") + 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("filtering out inactive secrets, %d items before filtering", len(listItems)) + listItems = a.filterInactiveSecrets(listItems) + a.logger.Debugf("filtering out inactive secrets, %d items after filtering", len(listItems)) + + // separate items by type since only static secrets are supported for bulk get + staticItems, dynamicItems, rotatedItems := a.separateItemsByType(listItems) + a.logger.Infof("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItems), len(dynamicItems), len(rotatedItems)) + + // listItems can get quite large, so we don't need all item details, we can use the item names instead + // and free memory + listItems = 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) + + haveStaticItems := len(staticItemNames) > 0 + haveDynamicItems := len(dynamicItemNames) > 0 + haveRotatedItems := len(rotatedItemNames) > 0 + + secretResultChannels := make(chan secretResultCollection, boolToInt(haveStaticItems)+boolToInt(haveDynamicItems)+boolToInt(haveRotatedItems)) + + mutex := sync.Mutex{} + + // 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) + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} + } 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 + } + + // lock the mutex to prevent race conditions + mutex.Lock() + response.Data[result.name] = map[string]string{result.name: result.value} + mutex.Unlock() + } + + // 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{} +} + +// GetComponentMetadata returns the component metadata. +func (a *akeylessSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := akeylessMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) + return +} + +// Close closes the secret store. +func (a *akeylessSecretStore) Close() error { + 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})") + } + + // Get the authentication method + a.logger.Debug("extracting access type from accessId...") + accessTypeChar, err := ExtractAccessTypeChar(m.AccessID) + if err != nil { + return nil, 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) + accessTypeDisplayName, err := GetAccessTypeDisplayName(accessTypeChar) + if err != nil { + return nil, errors.New("unable to get access type display name, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + a.logger.Debugf("access type detected: %s", accessTypeDisplayName) + + switch accessTypeDisplayName { + case DEFAULT_AUTH_TYPE: + if m.AccessKey == "" { + return nil, errors.New("accessKey is required") + } + case AUTH_JWT: + if m.JWT == "" { + return nil, errors.New("jwt is required") + } + } + m.AccessType = accessTypeDisplayName + + // 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 + } + + return &m, nil +} + +func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName string) (string, error) { + describeItem := akeyless.NewDescribeItem(secretName) + describeItem.SetToken(a.token) + describeItemResp, _, err := a.v2.DescribeItem(ctx).Body(*describeItem).Execute() + if err != nil { + return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) + } + + if describeItemResp.ItemType == nil { + return "", errors.New("unable to retrieve secret type, missing type in describe item response") + } + + return *describeItemResp.ItemType, nil +} + +// 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) { + + var secretValue string + var err error + + switch secretType { + case STATIC_SECRET_RESPONSE: + getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(a.token) + secretRespMap, _, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) + 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(a.token) + secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(ctx).Body(*getDynamicSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) + 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(a.token) + secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(ctx).Body(*getRotatedSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) + 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 { + + var secretResponse = make([]secretResultCollection, len(secretNames)) + + getSecretsValues := akeyless.NewGetSecretValue(secretNames) + getSecretsValues.SetToken(a.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) ([]akeyless.Item, error) { + var allItems []akeyless.Item + + // Create the list items request + listItems := akeyless.NewListItems() + listItems.SetToken(a.token) + listItems.SetPath(path) + listItems.SetAutoPagination("enabled") + listItems.SetType(SUPPORTED_SECRET_TYPES) + + // Execute the list items request + a.logger.Debugf("listing items from path '%s'...", path) + itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() + if err != nil { + return nil, err + } + + // 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) + if err != nil { + return nil, err + } + allItems = append(allItems, subItems...) + } + } + + return allItems, nil +} + +func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akeyless.Item, []akeyless.Item, []akeyless.Item) { + staticItems := []akeyless.Item{} + dynamicItems := []akeyless.Item{} + 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) + } + } + return staticItems, dynamicItems, rotatedItems +} + +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 +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go new file mode 100644 index 0000000000..da184505c3 --- /dev/null +++ b/secretstores/akeyless/akeyless_test.go @@ -0,0 +1,1164 @@ +package akeyless + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "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 + +// Mock AWS cloud ID for testing +const mockCloudID = "123456789012" + +// mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID +func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + authRequest.SetAccessType(metadata.AccessType) + + // Depending on the access type we set the appropriate authentication method + switch metadata.AccessType { + // If access type is AWS IAM we use the mock cloud ID + case AUTH_IAM: + akeylessSecretStore.logger.Debug("Using mock cloud ID for AWS IAM...") + authRequest.SetCloudId(mockCloudID) + case AUTH_JWT: + akeylessSecretStore.logger.Debug("Setting JWT for authentication...") + authRequest.SetJwt(metadata.JWT) + case DEFAULT_AUTH_TYPE: + akeylessSecretStore.logger.Debug("Setting access key for authentication...") + 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.token = out.GetToken() + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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 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, + AccessType: DEFAULT_AUTH_TYPE, + 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, + AccessType: AUTH_JWT, + 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, + AccessType: AUTH_IAM, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "missing access id", + 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) + + 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 TestGetComponentMetadata(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + metadata := store.GetComponentMetadata() + require.NotNil(t, metadata) + + // Check that the metadata contains the expected fields + assert.Contains(t, metadata, "gatewayUrl") + assert.Contains(t, metadata, "accessId") + assert.Contains(t, metadata, "jwt") + assert.Contains(t, metadata, "accessKey") + + // Check that the metadata fields exist + accessIdField := metadata["accessId"] + require.NotNil(t, accessIdField) + + gatewayField := metadata["gatewayUrl"] + require.NotNil(t, gatewayField) +} + +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) +} + +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) + assert.Equal(t, AUTH_IAM, m.AccessType) + + // 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) + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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) + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + 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) + + 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() +} diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml new file mode 100644 index 0000000000..d34c612187 --- /dev/null +++ b/secretstores/akeyless/metadata.yaml @@ -0,0 +1,58 @@ +# 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: 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 \ No newline at end of file diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go new file mode 100644 index 0000000000..f0db11fe31 --- /dev/null +++ b/secretstores/akeyless/utils.go @@ -0,0 +1,214 @@ +package akeyless + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strings" + + "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" +) + +// Define constants for the access types. These are equivalent to the TypeScript consts. +const ( + AUTH_JWT = "jwt" + DEFAULT_AUTH_TYPE = "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" +) + +var SUPPORTED_SECRET_TYPES = []string{"static-secret", "dynamic-secret", "rotated-secret"} + +// AccessTypeCharMap maps single-character access types to their display names. +var AccessTypeCharMap = map[string]string{ + "a": DEFAULT_AUTH_TYPE, + "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 boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +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) + } + if metadata.K8SGatewayURL == "" { + a.logger.Debug("k8s gateway url is missing, using gatewayUrl") + metadata.K8SGatewayURL = metadata.GatewayURL + } + authRequest.SetGatewayUrl(metadata.K8SGatewayURL) + authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) + return nil +}