diff --git a/go.mod b/go.mod index 1091aeb..93f3bdc 100644 --- a/go.mod +++ b/go.mod @@ -4,37 +4,38 @@ go 1.20 require ( github.com/aws/aws-sdk-go v1.44.80 - github.com/aws/aws-sdk-go-v2 v1.18.0 - github.com/aws/aws-sdk-go-v2/config v1.18.19 - github.com/aws/aws-sdk-go-v2/credentials v1.13.18 + github.com/aws/aws-sdk-go-v2 v1.20.1 github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 - github.com/aws/smithy-go v1.13.5 + github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.21.2 + github.com/aws/smithy-go v1.14.1 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/hashicorp/boundary/sdk v0.0.33 - github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 + github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0 github.com/hashicorp/terraform-json v0.14.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 // indirect + github.com/aws/aws-sdk-go-v2/config v1.18.33 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.32 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.14.1 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -53,7 +54,6 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/zclconf/go-cty v1.10.0 // indirect golang.org/x/crypto v0.8.0 // indirect diff --git a/go.sum b/go.sum index 08e5d0e..ddf0c17 100644 --- a/go.sum +++ b/go.sum @@ -4,51 +4,53 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= -github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI= github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= -github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= -github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY= -github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c= -github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= +github.com/aws/aws-sdk-go-v2 v1.20.1 h1:rZBf5DWr7YGrnlTK4kgDQGn1ltqOg5orCYb/UhOFZkg= +github.com/aws/aws-sdk-go-v2 v1.20.1/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 h1:lN6L3LrYHeZ6xCxaIYtoWCx4GMLk4nRknsh29OMSqHY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12/go.mod h1:TDCkEAkMTXxTs0oLBGBKpBZbk3NLh8EvAfF0Q3x8/0c= +github.com/aws/aws-sdk-go-v2/config v1.18.33 h1:JKcw5SFxFW/rpM4mOPjv0VQ11E2kxW13F3exWOy7VZU= +github.com/aws/aws-sdk-go-v2/config v1.18.33/go.mod h1:hXO/l9pgY3K5oZJldamP0pbZHdPqqk+4/maa7DSD3cA= +github.com/aws/aws-sdk-go-v2/credentials v1.13.32 h1:lIH1eKPcCY1ylR4B6PkBGRWMHO3aVenOKJHWiS4/G2w= +github.com/aws/aws-sdk-go-v2/credentials v1.13.32/go.mod h1:lL8U3v/Y79YRG69WlAho0OHIKUXCyFvSXaIvfo81sls= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 h1:DK/9C+UN/X+1+Wm8pqaDksQr2tSLzq+8X1/rI/ZxKEQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8/go.mod h1:ce7BgLQfYr5hQFdy67oX2svto3ufGtm6oBvmsHScI1Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 h1:c8ed/T9T2K5I+h/JzmF5tpI46+OODQ74dzmdo+QnaMg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38/go.mod h1:qggunOChCMu9ZF/UkAfhTz25+U2rLVb3ya0Ua6TTfCA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 h1:hNeAAymUY5gu11WrrmFb3CVIp9Dar9hbo44yzzcQpzA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32/go.mod h1:0ZXSqrty4FtQ7p8TEuRde/SZm9X05KT18LAUlR40Ln0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 h1:fc0ukRAiP1syoSGZYu+DaE+FulSYhTiJ8WpVu5jElU4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39/go.mod h1:WLAW8PT7+JhjZfLSWe7WEJaJu0GNo0cKc2Zyo003RBs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 h1:vUh7dBFNS3oFCtVv6CiYKh5hP9ls8+kIpKLeFruIBLk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1/go.mod h1:sFMeinkhj/SZKQM8BxtvNtSPjJEo0Xrz+w3g2e4FSKI= github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg= github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= +github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 h1:DPFxx/6Zwes/MiadlDteVqDKov7yQ5v9vuwfhZuJm1s= +github.com/aws/aws-sdk-go-v2/service/iam v1.22.2/go.mod h1:cQTMNdo/Z5t1DDRsUnx0a2j6cPnytMBidUYZw2zks28= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 h1:iV/W5OMBys+66OeXJi/7xIRrKZNsu0ylsLGu+6nbmQE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13/go.mod h1:ReJb6xYmtGyu9KoFtRreWegbN9dZqvZIIv4vWnhcsyI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 h1:QviNkc+vGSuEHx8P+pVNKOdWLXBPIwMFv7p0fphgE4U= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33/go.mod h1:fABTUmOrAgAalG2i9WJpjBvlnk7UK8YmnYaxN+Q2CwE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1/go.mod h1:ncltU6n4Nof5uJttDtcNQ537uNuwYqsZZQcpkd2/GUQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 h1:dGAseBFEYxth10V23b5e2mAS+tX7oVbfYHD6dnDdAsg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32/go.mod h1:4jwAWKEkCR0anWk5+1RbfSg1R5Gzld7NLiuaq5bTR/Y= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 h1:PT6PBCycRwhpEW5hJnRiceCeoWJ+r3bdgXtV+VKG7Pk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1/go.mod h1:TqoxCLwT2nrxrBGA+z7t6OWM7LBkgRckK3gOjYE+7JA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2 h1:v346f1h8sUBKXnEbrv43L37MTBlFHyKXQPIZHNAaghA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2/go.mod h1:cwCATiyNrXK9P2FsWdZ89g9mpsYv2rhk0UA/KByl5fY= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 h1:A2RlEMo4SJSwbNoUUgkxTAEMduAy/8wG3eB2b2lP4gY= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.2/go.mod h1:ju+nNXUunfIFamXUIZQiICjnO/TPlOmWcYhZcSy7xaE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 h1:OJELEgyaT2kmaBGZ+myyZbTTLobfe3ox3FSh5eYK9Qs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2/go.mod h1:ubDBBaDFs1GHijSOTi8ljppML15GLG0HxhILtbjNNYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.2 h1:ympg1+Lnq33XLhcK/xTG4yZHPs1Oyxu+6DEWbl7qOzA= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.2/go.mod h1:FQ/DQcOfESELfJi5ED+IPPAjI5xC6nxtSolVVB773jM= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.14.1 h1:EFKMUmH/iHMqLiwoEDx2rRjRQpI1YCn5jTysoaDujFs= +github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -57,7 +59,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -93,8 +94,8 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU= -github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 h1:kWg2vyKl7BRXrNxYziqDJ55n+vtOQ1QsGORjzoeB+uM= -github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg= +github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0 h1:ca5TSI4AgaOncPpyzLDtCGjVEtKukONpeM95vFxXCOQ= +github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0/go.mod h1:7CUvZtfTp2U0CYQCLzMtS2ngckjAZePSfwrE2aeDP1M= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k= @@ -118,7 +119,6 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -126,7 +126,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -153,12 +152,10 @@ github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -167,17 +164,12 @@ github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdk github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= @@ -204,7 +196,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= diff --git a/internal/credential/attributes.go b/internal/credential/attributes.go index 689bf90..467861a 100644 --- a/internal/credential/attributes.go +++ b/internal/credential/attributes.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/hashicorp/boundary-plugin-aws/internal/errors" "github.com/hashicorp/boundary-plugin-aws/internal/values" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "google.golang.org/protobuf/types/known/structpb" ) @@ -68,25 +68,45 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field" } - if len(badFields) > 0 { - return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields) - } - - opts := []awsutil.Option{ - awsutil.WithEnvironmentCredentials(true), - } - if accessKey != "" && secretKey != "" { + opts := []awsutil.Option{} + // logic for parsing the credential type + // supported types: + // - user static credential + // - ec2 assume role dynamic credential + // - enviornment variables credential + switch { + // static credentials and dynamic credentials cannot be used together + case accessKey != "" && secretKey != "" && attrs.RoleArn != "": + badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "conflicts with role_arn value" + badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "conflicts with role_arn value" + badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = "conflicts with access_key_id and secret_access_key values" + // static credential is missing it's secret_access_key + case accessKey != "" && secretKey == "": + badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "missing required value" + // static credential is missing it's access_key_id + case accessKey == "" && secretKey != "": + badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "missing required value" + // dynamic credentials and credential rotation is not supported + case len(attrs.RoleArn) > 0 && !attrs.DisableCredentialRotation: + badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = "disable_credential_rotation attribute is required when providing a role_arn" + // add static credentials + case accessKey != "" && secretKey != "": opts = append(opts, awsutil.WithAccessKey(accessKey), awsutil.WithSecretKey(secretKey), ) + // add dynamic credentials + case attrs.RoleArn != "": + opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn)) } + + if len(badFields) > 0 { + return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields) + } + if attrs.Region != "" { opts = append(opts, awsutil.WithRegion(attrs.Region)) } - if attrs.RoleArn != "" { - opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn)) - } if attrs.RoleExternalId != "" { opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId)) } diff --git a/internal/credential/attributes_test.go b/internal/credential/attributes_test.go index 7bac81c..fa9ac2c 100644 --- a/internal/credential/attributes_test.go +++ b/internal/credential/attributes_test.go @@ -6,7 +6,7 @@ package credential import ( "testing" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -118,7 +118,7 @@ func TestGetCredentialsConfig(t *testing.T) { expectedErrContains string }{ { - name: "no secrets", + name: "no credentials", attrs: &CredentialAttributes{ Region: "us-west-2", }, @@ -130,7 +130,7 @@ func TestGetCredentialsConfig(t *testing.T) { name: "with static credentials", secrets: &structpb.Struct{ Fields: map[string]*structpb.Value{ - ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), ConstSecretAccessKey: structpb.NewStringValue("bazqux"), }, }, @@ -138,7 +138,7 @@ func TestGetCredentialsConfig(t *testing.T) { Region: "us-west-2", }, expected: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", Region: "us-west-2", }, @@ -160,7 +160,7 @@ func TestGetCredentialsConfig(t *testing.T) { name: "missing secret key", secrets: &structpb.Struct{ Fields: map[string]*structpb.Value{ - ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), }, }, required: true, @@ -173,7 +173,7 @@ func TestGetCredentialsConfig(t *testing.T) { name: "unknown fields", secrets: &structpb.Struct{ Fields: map[string]*structpb.Value{ - ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), ConstSecretAccessKey: structpb.NewStringValue("bazqux"), "foo": structpb.NewBoolValue(true), "bar": structpb.NewBoolValue(true), @@ -188,7 +188,7 @@ func TestGetCredentialsConfig(t *testing.T) { name: "valid ignore creds_last_rotated_time", secrets: &structpb.Struct{ Fields: map[string]*structpb.Value{ - ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), ConstSecretAccessKey: structpb.NewStringValue("bazqux"), ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"), }, @@ -197,7 +197,7 @@ func TestGetCredentialsConfig(t *testing.T) { Region: "us-west-2", }, expected: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", Region: "us-west-2", }, @@ -215,6 +215,7 @@ func TestGetCredentialsConfig(t *testing.T) { RoleTags: map[string]string{ "foo": "bar", }, + DisableCredentialRotation: true, }, expected: &awsutil.CredentialsConfig{ Region: "us-west-2", @@ -230,7 +231,7 @@ func TestGetCredentialsConfig(t *testing.T) { name: "with static credential & assume role", secrets: &structpb.Struct{ Fields: map[string]*structpb.Value{ - ConstAccessKeyId: structpb.NewStringValue("foobar"), + ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), ConstSecretAccessKey: structpb.NewStringValue("bazqux"), }, }, @@ -243,17 +244,21 @@ func TestGetCredentialsConfig(t *testing.T) { "foo": "bar", }, }, - expected: &awsutil.CredentialsConfig{ - AccessKey: "foobar", - SecretKey: "bazqux", + expectedErrContains: "attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value", + }, + { + name: "with dynamic credentials and no disable credential rotation", + secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + attrs: &CredentialAttributes{ Region: "us-west-2", - RoleARN: "arn:aws:iam::123456789012:role/S3Access", + RoleArn: "arn:aws:iam::123456789012:role/S3Access", RoleExternalId: "1234567890", RoleSessionName: "test-session", RoleTags: map[string]string{ "foo": "bar", }, }, + expectedErrContains: "attributes.disable_credential_rotation: disable_credential_rotation attribute is required when providing a role_arn", }, } diff --git a/internal/credential/state.go b/internal/credential/state.go index d25ffd0..7286dda 100644 --- a/internal/credential/state.go +++ b/internal/credential/state.go @@ -4,19 +4,41 @@ package credential import ( + "context" "errors" "fmt" "strings" "time" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go-v2/aws" + iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/hashicorp/boundary-plugin-aws/internal/values" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "google.golang.org/protobuf/types/known/structpb" ) +type CredentialType int + +const ( + // StaticAWS denotes an Access Key Id that begins with "AKIA". These are + // long-term access keys, provided by AWS, for an IAM user or an AWS account + // root user. + // https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys + StaticAWS CredentialType = iota + + // DynamicAWS denotes the presence of a RoleARN, or an Access Key Id that + // begins with "ASIA". The latter are temporary credentials access keys that + // are created using AWS STS operations. + DynamicAWS + + // StaticOther denotes the presence of an Access Key Id that does not follow + // the AKIA/ASIA convention outlined by AWS. + StaticOther + + // Unknown is a catch-all for everything else. + Unknown +) + // rotationWaitTimeout controls the time we wait for credential rotation to // succeed. This is important to ensure that rotated credentials can be used // right away. @@ -85,11 +107,11 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) ( // ValidateCreds takes the credentials configuration from the persisted state // and runs sts.GetCallerIdentity for the current credentials, which is done // to check that the credentials are valid. -func (s *AwsCredentialPersistedState) ValidateCreds() error { +func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error { if s.CredentialsConfig == nil { return errors.New("missing credentials config") } - if _, err := s.CredentialsConfig.GetCallerIdentity(s.testOpts...); err != nil { + if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil { return fmt.Errorf("error validating credentials: %w", err) } return nil @@ -100,11 +122,14 @@ func (s *AwsCredentialPersistedState) ValidateCreds() error { // written into the credentials config and the persisted state. On any error, the old credentials are not overwritten. // This ensures that any generated new secret key never leaves this function in case of an error, even though it will // still result in an extraneous access key existing. -func (s *AwsCredentialPersistedState) RotateCreds() error { +func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error { if s.CredentialsConfig == nil { return errors.New("missing credentials config") } - if err := s.CredentialsConfig.RotateKeys(append([]awsutil.Option{ + if GetCredentialType(s.CredentialsConfig) != StaticAWS { + return errors.New("invalid credential type") + } + if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{ awsutil.WithValidityCheckTimeout(rotationWaitTimeout), }, s.testOpts...)...); err != nil { return fmt.Errorf("error rotating credentials: %w", err) @@ -116,7 +141,7 @@ func (s *AwsCredentialPersistedState) RotateCreds() error { // ReplaceCreds replaces the access key in the state with a new key. // If the existing key was rotated at any point in time, it is // deleted first, otherwise it's left alone. -func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.CredentialsConfig) error { +func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error { if credentialsConfig == nil { return errors.New("missing new credentials config") } @@ -124,11 +149,11 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.Cr return errors.New("missing credentials config") } - if !s.CredsLastRotatedTime.IsZero() { - // Delete old/existing credentials. This is done with the same - // credentials to ensure that it has the proper permissions to do - // it. - if err := s.DeleteCreds(); err != nil { + // Delete old/existing credentials. This action is only possible for static credential types. + // This is done with the same credentials to ensure that it has the proper permissions to do it. + ct := GetCredentialType(s.CredentialsConfig) + if !s.CredsLastRotatedTime.IsZero() && ct == StaticAWS { + if err := s.DeleteCreds(ctx); err != nil { return err } } @@ -142,21 +167,22 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.Cr // DeleteCreds deletes the credentials in the state. The access key // ID, secret access key, and rotation time fields are zeroed out in // the state just to ensure that they cannot be re-used after. -func (s *AwsCredentialPersistedState) DeleteCreds() error { +func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error { if s.CredentialsConfig == nil { return errors.New("missing credentials config") } + if GetCredentialType(s.CredentialsConfig) != StaticAWS { + return errors.New("invalid credential type") + } - if err := s.CredentialsConfig.DeleteAccessKey(s.CredentialsConfig.AccessKey, s.testOpts...); err != nil { + if err := s.CredentialsConfig.DeleteAccessKey(ctx, s.CredentialsConfig.AccessKey, s.testOpts...); err != nil { // Determine if the deletion error was due to a missing // resource. If it was, just pass it. - var awsErr awserr.Error - if errors.As(err, &awsErr) { - if awsErr.Code() == iam.ErrCodeNoSuchEntityException { - s.CredentialsConfig = nil - s.CredsLastRotatedTime = time.Time{} - return nil - } + var awsErr *iamTypes.NoSuchEntityException + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" { + s.CredentialsConfig = nil + s.CredsLastRotatedTime = time.Time{} + return nil } // Otherwise treat it as an actual error. @@ -168,10 +194,9 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error { return nil } -// GetSession returns a configured AWS session for the credentials in -// the state. -func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) { - return s.CredentialsConfig.GetSession(s.testOpts...) +// GenerateCredentialChain returns a AWS configuration for the credentials in the state. +func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) { + return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...) } // ToMap returns a map of the credentials stored in the persisted state. @@ -179,7 +204,15 @@ func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) { // return a map for long-term credentials with following keys: // access_key_id, secret_access_key & creds_last_rotated_time func (s *AwsCredentialPersistedState) ToMap() map[string]any { - if HasDynamicCredentials(s.CredentialsConfig.AccessKey) { + ct := GetCredentialType(s.CredentialsConfig) + // Dynamic AWS credentials are temporary credentials that expire within an hour. + // ToMap() returns an empty map here so that Boundary does not store temporary + // credentials into the database. + // + // Uknown credentials are not aws compatiable secrets that may have been provided + // for a non-aws s3 compatiable service. These credentials will not be returned + // because the plugin does not know how to manage non-aws credentials. + if ct == DynamicAWS || ct == Unknown { return map[string]any{} } return map[string]any{ @@ -256,18 +289,22 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede return s, nil } -// HasDynamicCredentials returns true if the access key is a dynamic credential type. -// -// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys -// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations. -func HasDynamicCredentials(accessKey string) bool { - return strings.HasPrefix(accessKey, "ASIA") -} +// GetCredentialType returns the credential type based on the given +// AccessKey/RoleARN. See CredentialType definition for more information. +func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType { + if cc == nil { + return Unknown + } -// HasStaticCredentials returns true if the access key is a static credential type. -// -// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys -// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user. -func HasStaticCredentials(accessKey string) bool { - return strings.HasPrefix(accessKey, "AKIA") + if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") { + return DynamicAWS + } + if strings.HasPrefix(cc.AccessKey, "AKIA") { + return StaticAWS + } + if len(cc.AccessKey) > 0 { + return StaticOther + } + + return Unknown } diff --git a/internal/credential/state_test.go b/internal/credential/state_test.go index 02467a9..e5c405c 100644 --- a/internal/credential/state_test.go +++ b/internal/credential/state_test.go @@ -4,15 +4,16 @@ package credential import ( + "context" "errors" "fmt" "testing" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/iam" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" ) @@ -383,7 +384,7 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { require := require.New(t) - err := tc.in.ValidateCreds() + err := tc.in.ValidateCreds(context.Background()) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -407,11 +408,33 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { in: &AwsCredentialPersistedState{}, expectedErr: "missing credentials config", }, + { + name: "cannot delete dynamic credential type", + in: &AwsCredentialPersistedState{ + CredentialsConfig: &awsutil.CredentialsConfig{ + AccessKey: "ASIAfoobar", + SecretKey: "bazqux", + }, + CredsLastRotatedTime: time.Now(), + }, + expectedErr: "invalid credential type", + }, + { + name: "cannot delete unknown credential type", + in: &AwsCredentialPersistedState{ + CredentialsConfig: &awsutil.CredentialsConfig{ + AccessKey: "DNEfoobar", + SecretKey: "bazqux", + }, + CredsLastRotatedTime: time.Now(), + }, + expectedErr: "invalid credential type", + }, { name: "rotation error", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", Region: "us-west-2", }, @@ -423,13 +446,13 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { ), }, }, - expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), + expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), }, { name: "good", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", Region: "us-west-2", }, @@ -441,7 +464,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { awsutil.NewMockIAM( awsutil.WithGetUserOutput( &iam.GetUserOutput{ - User: &iam.User{ + User: &iamTypes.User{ Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), UserName: aws.String("JohnDoe"), @@ -450,7 +473,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { ), awsutil.WithCreateAccessKeyOutput( &iam.CreateAccessKeyOutput{ - AccessKey: &iam.AccessKey{ + AccessKey: &iamTypes.AccessKey{ AccessKeyId: aws.String("one"), SecretAccessKey: aws.String("two"), UserName: aws.String("JohnDoe"), @@ -474,7 +497,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { require := require.New(t) - err := tc.in.RotateCreds() + err := tc.in.RotateCreds(context.Background()) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -514,7 +537,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { name: "deletion error", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, CredsLastRotatedTime: time.Now(), @@ -533,7 +556,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { name: "good with delete of old rotated", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, CredsLastRotatedTime: time.Now(), @@ -557,7 +580,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { name: "good without delete of old rotated", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, testOpts: []awsutil.Option{ @@ -583,7 +606,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { t.Run(tc.name, func(t *testing.T) { require := require.New(t) state.Reset() - err := tc.in.ReplaceCreds(tc.credentialConfig) + err := tc.in.ReplaceCreds(context.Background(), tc.credentialConfig) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -609,11 +632,33 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { in: &AwsCredentialPersistedState{}, expectedErr: "missing credentials config", }, + { + name: "cannot delete dynamic credential type", + in: &AwsCredentialPersistedState{ + CredentialsConfig: &awsutil.CredentialsConfig{ + AccessKey: "ASIAfoobar", + SecretKey: "bazqux", + }, + CredsLastRotatedTime: time.Now(), + }, + expectedErr: "invalid credential type", + }, + { + name: "cannot delete unknown credential type", + in: &AwsCredentialPersistedState{ + CredentialsConfig: &awsutil.CredentialsConfig{ + AccessKey: "DNEfoobar", + SecretKey: "bazqux", + }, + CredsLastRotatedTime: time.Now(), + }, + expectedErr: "invalid credential type", + }, { name: "deletion error", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, testOpts: []awsutil.Option{ @@ -630,14 +675,14 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { name: "deletion error, but OK because key was just gone", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, testOpts: []awsutil.Option{ awsutil.WithIAMAPIFunc( awsutil.NewMockIAM( awsutil.WithDeleteAccessKeyError( - awserr.New(iam.ErrCodeNoSuchEntityException, "", nil), + &iamTypes.NoSuchEntityException{}, ), ), ), @@ -648,7 +693,7 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { name: "good", in: &AwsCredentialPersistedState{ CredentialsConfig: &awsutil.CredentialsConfig{ - AccessKey: "foobar", + AccessKey: "AKIAfoobar", SecretKey: "bazqux", }, testOpts: []awsutil.Option{ @@ -664,7 +709,7 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { require := require.New(t) - err := tc.in.DeleteCreds() + err := tc.in.DeleteCreds(context.Background()) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -677,7 +722,7 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { } } -func TestAwsCatalogPersistedState_GetSession(t *testing.T) { +func TestAwsCatalogPersistedState_GenerateCredentialChain(t *testing.T) { require := require.New(t) cases := []struct { name string @@ -693,7 +738,7 @@ func TestAwsCatalogPersistedState_GetSession(t *testing.T) { }, testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, }, - expectedErr: fmt.Sprintf("error reading options in GetSession: %s", testOptionErr), + expectedErr: fmt.Sprintf("error reading options in GenerateCredentialChain: %s", testOptionErr), }, { name: "static credentials", @@ -736,7 +781,7 @@ func TestAwsCatalogPersistedState_GetSession(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - _, err := tc.in.GetSession() + _, err := tc.in.GenerateCredentialChain(context.Background()) if tc.expectedErr != "" { require.EqualError(err, tc.expectedErr) return @@ -746,74 +791,53 @@ func TestAwsCatalogPersistedState_GetSession(t *testing.T) { } } -func TestHasStaticCredentials(t *testing.T) { - require := require.New(t) - cases := []struct { - name string - accessKey string - expected bool +func TestGetCredentialType(t *testing.T) { + tests := []struct { + name string + credConfig *awsutil.CredentialsConfig + expectedCredType CredentialType }{ { - name: "dynamic credential", - accessKey: "ASIAEXAMPLE", - expected: false, + name: "nil credential config", + credConfig: nil, + expectedCredType: Unknown, }, { - name: "static credential", - accessKey: "AKIAEXAMPLE", - expected: true, + name: "empty credential config", + credConfig: &awsutil.CredentialsConfig{}, + expectedCredType: Unknown, }, { - name: "other credential", - accessKey: "EXAMPLE", - expected: false, + name: "static aws", + credConfig: &awsutil.CredentialsConfig{AccessKey: "AKIAfoobar"}, + expectedCredType: StaticAWS, }, { - name: "empty", - accessKey: "", - expected: false, + name: "static other", + credConfig: &awsutil.CredentialsConfig{AccessKey: "cK3kNFa24foobar"}, + expectedCredType: StaticOther, + }, + { + name: "dynamic aws - role arn", + credConfig: &awsutil.CredentialsConfig{RoleARN: "arn:aws:iam::123456789012:role/S3Access"}, + expectedCredType: DynamicAWS, + }, + { + name: "dynamic aws - access key", + credConfig: &awsutil.CredentialsConfig{AccessKey: "ASIAfoobar"}, + expectedCredType: DynamicAWS, + }, + { + name: "dynamic aws - role arn and access key", + credConfig: &awsutil.CredentialsConfig{AccessKey: "ASIAfoobar", RoleARN: "arn:aws:iam::123456789012:role/S3Access"}, + expectedCredType: DynamicAWS, }, } - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - require.Equal(tc.expected, HasStaticCredentials(tc.accessKey)) - }) - } -} -func TestHasDynamicCredentials(t *testing.T) { - require := require.New(t) - cases := []struct { - name string - accessKey string - expected bool - }{ - { - name: "dynamic credential", - accessKey: "ASIAEXAMPLE", - expected: true, - }, - { - name: "static credential", - accessKey: "AKIAEXAMPLE", - expected: false, - }, - { - name: "other credential", - accessKey: "EXAMPLE", - expected: false, - }, - { - name: "empty", - accessKey: "", - expected: false, - }, - } - for _, tc := range cases { - tc := tc + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey)) + ct := GetCredentialType(tc.credConfig) + require.Equal(t, tc.expectedCredType, ct) }) } } diff --git a/internal/credential/testing.go b/internal/credential/testing.go index d7b753c..3fcb7c7 100644 --- a/internal/credential/testing.go +++ b/internal/credential/testing.go @@ -4,10 +4,11 @@ package credential import ( - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/iam" - "github.com/aws/aws-sdk-go/service/iam/iamiface" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "google.golang.org/protobuf/types/known/structpb" ) @@ -27,25 +28,25 @@ func (s *testMockIAMState) Reset() { } type testMockIAM struct { - iamiface.IAMAPI + awsutil.IAMClient State *testMockIAMState } -func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) { +func (m *testMockIAM) DeleteAccessKey(ctx context.Context, input *iam.DeleteAccessKeyInput, opts ...func(*iam.Options)) (*iam.DeleteAccessKeyOutput, error) { m.State.DeleteAccessKeyCalled = true - return m.IAMAPI.DeleteAccessKey(input) + return m.IAMClient.DeleteAccessKey(ctx, input, opts...) } func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc { - return func(sess *session.Session) (iamiface.IAMAPI, error) { + return func(awsConfig *aws.Config) (awsutil.IAMClient, error) { m := &testMockIAM{ State: state, } f := awsutil.NewMockIAM(opts...) var err error - m.IAMAPI, err = f(sess) + m.IAMClient, err = f(awsConfig) return m, err } } diff --git a/plugin/service/host/plugin.go b/plugin/service/host/plugin.go index 406a985..4ec8fa2 100644 --- a/plugin/service/host/plugin.go +++ b/plugin/service/host/plugin.go @@ -83,15 +83,14 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) } - // Try to rotate static credentials - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { + if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS { if !catalogAttributes.DisableCredentialRotation { - if err := credState.RotateCreds(); err != nil { + if err := credState.RotateCreds(ctx); err != nil { return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err) } } else { // Simply validate if we aren't rotating. - if err := credState.ValidateCreds(); err != nil { + if err := credState.ValidateCreds(ctx); err != nil { return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err) } } @@ -160,7 +159,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) } - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { + if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS { if catalogAttributes.DisableCredentialRotation && !updateSecrets { // This is a validate check to make sure that we aren't disabling // rotation for credentials currently being managed by rotation. @@ -179,7 +178,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo // Replace the credentials. This checks the timestamp on the last // rotation time as well and deletes the credentials if we are // managing them (ie: if we've rotated them before). - if err := credState.ReplaceCreds(catalogSecrets); err != nil { + if err := credState.ReplaceCreds(ctx, catalogSecrets); err != nil { return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err) } } @@ -187,7 +186,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() { // If we're enabling rotation now but didn't before, or have // freshly replaced credentials, we can rotate here. - if err := credState.RotateCreds(); err != nil { + if err := credState.RotateCreds(ctx); err != nil { return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err) } } @@ -244,12 +243,12 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo } // try to delete static credentials - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { + if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS { if !credState.CredsLastRotatedTime.IsZero() { // Delete old/existing credentials. This is done with the same // credentials to ensure that it has the proper permissions to do // it. - if err := credState.DeleteCreds(); err != nil { + if err := credState.DeleteCreds(ctx); err != nil { return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err) } } @@ -332,11 +331,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest return nil, err } - opts := []ec2Option{} - if catalogAttributes.Region != "" { - opts = append(opts, WithRegion(catalogAttributes.Region)) - } - ec2Client, err := catalogState.EC2Client(ctx, opts...) + ec2Client, err := catalogState.EC2Client(ctx) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } @@ -409,11 +404,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest return nil, err } - opts := []ec2Option{} - if catalogAttributes.Region != "" { - opts = append(opts, WithRegion(catalogAttributes.Region)) - } - ec2Client, err := catalogState.EC2Client(ctx, opts...) + ec2Client, err := catalogState.EC2Client(ctx) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } @@ -516,11 +507,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (* } } - opts := []ec2Option{} - if catalogAttributes.Region != "" { - opts = append(opts, WithRegion(catalogAttributes.Region)) - } - ec2Client, err := catalogState.EC2Client(ctx, opts...) + ec2Client, err := catalogState.EC2Client(ctx) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } diff --git a/plugin/service/host/plugin_test.go b/plugin/service/host/plugin_test.go index 6d40581..20e0990 100644 --- a/plugin/service/host/plugin_test.go +++ b/plugin/service/host/plugin_test.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -153,7 +153,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), + expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), expectedErrCode: codes.InvalidArgument, }, { @@ -411,7 +411,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), + expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), expectedErrCode: codes.InvalidArgument, }, } diff --git a/plugin/service/host/state.go b/plugin/service/host/state.go index 534a8a4..523e74d 100644 --- a/plugin/service/host/state.go +++ b/plugin/service/host/state.go @@ -12,8 +12,6 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/hashicorp/boundary-plugin-aws/internal/credential" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" @@ -88,75 +86,16 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) { // EC2Client returns a configured EC2 client based on the session // information stored in the state. -func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) { - opts, err := getOpts(opt...) +func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error) { + awsCfg, err := s.GenerateCredentialChain(ctx) if err != nil { - return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err) + return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err) } - - awsConfigOpts := []func(*config.LoadOptions) error{} - sess, err := s.GetSession() - if err != nil { - return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err) + if awsCfg == nil { + return nil, fmt.Errorf("nil aws configuration") } - creds, err := sess.Config.Credentials.Get() - if err != nil { - return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err) - } - customCredentials := config.WithCredentialsProvider( - credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken), - ) - awsConfigOpts = append(awsConfigOpts, customCredentials) - if sess.Config.Region != nil { - awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region)) - } - if opts.withRegion != "" { - awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion)) - } - cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...) - if err != nil { - return nil, fmt.Errorf("error loading aws configuration when fetching EC2 client: %w", err) - } - if s.testEC2APIFunc != nil { - return s.testEC2APIFunc(cfg) - } - - cfg.HTTPClient = customClient - - return ec2.NewFromConfig(cfg), nil -} - -// getOpts iterates the inbound ec2Options and returns a struct -func getOpts(opt ...ec2Option) (ec2Options, error) { - opts := getDefaultEC2Options() - for _, o := range opt { - if o == nil { - continue - } - if err := o(&opts); err != nil { - return ec2Options{}, err - } - } - return opts, nil -} - -// ec2Option - how ec2Options are passed as arguments -type ec2Option func(*ec2Options) error - -// options = how options are represented -type ec2Options struct { - withRegion string -} - -func getDefaultEC2Options() ec2Options { - return ec2Options{} -} - -// WithRegion contains the region to use -func WithRegion(with string) ec2Option { - return func(o *ec2Options) error { - o.withRegion = with - return nil + return s.testEC2APIFunc(*awsCfg) } + return ec2.NewFromConfig(*awsCfg), nil } diff --git a/plugin/service/host/state_test.go b/plugin/service/host/state_test.go index 79ca6c1..4be814f 100644 --- a/plugin/service/host/state_test.go +++ b/plugin/service/host/state_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/hashicorp/boundary-plugin-aws/internal/credential" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" ) @@ -29,7 +29,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) { ) require.NoError(err) - clientRaw, err := state.EC2Client(context.Background(), WithRegion("us-west-2")) + clientRaw, err := state.EC2Client(context.Background()) require.NoError(err) require.NotNil(clientRaw) client, ok := clientRaw.(*testMockEC2) diff --git a/plugin/service/storage/plugin.go b/plugin/service/storage/plugin.go index f0269aa..8e6a8c8 100644 --- a/plugin/service/storage/plugin.go +++ b/plugin/service/storage/plugin.go @@ -76,17 +76,10 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) } - // Try to rotate static credentials - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { - if !storageAttributes.DisableCredentialRotation { - if err := credState.RotateCreds(); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err) - } - } else { - // Simply validate if we aren't rotating. - if err := credState.ValidateCreds(); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err) - } + // Try to rotate AWS static credentials + if cred.GetCredentialType(credConfig) == cred.StaticAWS && !storageAttributes.DisableCredentialRotation { + if err := credState.RotateCreds(ctx); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err) } } @@ -119,76 +112,100 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // For storage buckets we only need to really validate what is // currently in the new copy of the storage bucket data, so skip // fetching the new copy to keep things a little less messy. - bucket := req.GetNewBucket() - if bucket == nil { + newBucket := req.GetNewBucket() + if newBucket == nil { return nil, status.Error(codes.InvalidArgument, "new bucket is required") } - if bucket.BucketName == "" { + if newBucket.BucketName == "" { return nil, status.Error(codes.InvalidArgument, "new bucketName is required") } - - var updateSecrets bool - secrets := bucket.GetSecrets() - if secrets != nil { - // We will be updating secrets this run, but what exactly that - // means will be determined later. - updateSecrets = true + currentBucket := req.GetCurrentBucket() + if currentBucket == nil { + return nil, status.Error(codes.InvalidArgument, "current bucket is required") } - attrs := bucket.GetAttributes() - if attrs == nil { + oldAttrs := currentBucket.GetAttributes() + if oldAttrs == nil { + return nil, status.Error(codes.InvalidArgument, "current bucket attributes is required") + } + oldStorageAttributes, err := getStorageAttributes(oldAttrs) + if err != nil { + return nil, err + } + newAttrs := newBucket.GetAttributes() + if newAttrs == nil { return nil, status.Error(codes.InvalidArgument, "new bucket attributes is required") } - - storageAttributes, err := getStorageAttributes(attrs) + newStorageAttributes, err := getStorageAttributes(newAttrs) if err != nil { return nil, err } - // Get the persisted data. - // NOTE: We might need to change this at a later time. I'm not too - // sure *exactly* what scenarios we might encounter that would - // ultimately mean that we would have to handle an empty or missing - // state in update, but what we are ultimately assuming here - // (implicitly through awsStoragePersistedStateFromProto) is that - // the state will exist and be populated. Personally I think this - // is fine and important, but this may change in the future. credState, err := cred.AwsCredentialPersistedStateFromProto( req.GetPersisted().GetData(), - storageAttributes.CredentialAttributes, + oldStorageAttributes.CredentialAttributes, p.testCredStateOpts...) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) } - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { - if storageAttributes.DisableCredentialRotation && !updateSecrets { - // This is a validate check to make sure that we aren't disabling - // rotation for credentials currently being managed by rotation. - // This is not allowed. + // Verify the incoming credentials are valid and return any errors to the + // user if they're not. Note this doesn't validate the credentials against + // AWS - it only does logical validation on the fields. + updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false) + if err != nil { + return nil, err + } + + // We will be updating credentials when one of two changes occur: + // 1. the new bucket has secrets for static credentials, in which case we need to delete the old static credentials + // 2. the new bucket has a different roleARN value for dynamic credentials + if newBucket.GetSecrets() != nil || newStorageAttributes.RoleArn != oldStorageAttributes.RoleArn { + // Ensure the incoming credentials are valid for interaction with the S3 + // Bucket before replacing them. + newCredState, err := cred.NewAwsCredentialPersistedState( + append([]cred.AwsCredentialPersistedStateOption{ + cred.WithCredentialsConfig(updatedCredentials), + }, p.testCredStateOpts...)..., + ) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "error setting up new credential persisted state: %s", err) + } + newStorageState, err := newAwsStoragePersistedState( + append([]awsStoragePersistedStateOption{ + withCredentials(newCredState), + }, p.testStorageStateOpts...)..., + ) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + } + if err := dryRunValidation(ctx, newStorageState, newStorageAttributes, newBucket); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to validate incoming credentials: %s", err) + } + + // Replace the existing credential state. + // This checks the timestamp on the last rotation time as well + // and deletes the credentials if we are managing them + // (ie: if we've rotated them before). + if err := credState.ReplaceCreds(ctx, updatedCredentials); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err) + } + } + + if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS { + // This is a validate check to make sure that we aren't disabling + // rotation for credentials currently being managed by rotation. + // This is not allowed. + if newStorageAttributes.DisableCredentialRotation && newBucket.GetSecrets() == nil { if !credState.CredsLastRotatedTime.IsZero() { return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials") } } - if updateSecrets { - storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.CredentialAttributes, false) - if err != nil { - return nil, err - } - - // Replace the credentials. This checks the timestamp on the last - // rotation time as well and deletes the credentials if we are - // managing them (ie: if we've rotated them before). - if err := credState.ReplaceCreds(storageSecrets); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err) - } - } - - if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() { - // If we're enabling rotation now but didn't before, or have - // freshly replaced credentials, we can rotate here. - if err := credState.RotateCreds(); err != nil { + // If we're enabling rotation now but didn't before, or have + // freshly replaced credentials, we can rotate here. + if !newStorageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() { + if err := credState.RotateCreds(ctx); err != nil { return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err) } } @@ -204,7 +221,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd } // perform dry run to ensure we can interact with the bucket as expected. - if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil { + if err := dryRunValidation(ctx, storageState, newStorageAttributes, newBucket); err != nil { return nil, err } @@ -259,12 +276,12 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel } // try to delete static credentials - if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) { + if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS { if !credState.CredsLastRotatedTime.IsZero() { // Delete old/existing credentials. This is done with the same // credentials to ensure that it has the proper permissions to do // it. - if err := credState.DeleteCreds(); err != nil { + if err := credState.DeleteCreds(ctx); err != nil { return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err) } } @@ -323,9 +340,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques } opts := []s3Option{} - if storageAttributes.Region != "" { - opts = append(opts, WithRegion(storageAttributes.Region)) - } if storageAttributes.EndpointUrl != "" { opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl)) } @@ -452,9 +466,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu } opts := []s3Option{} - if storageAttributes.Region != "" { - opts = append(opts, WithRegion(storageAttributes.Region)) - } if storageAttributes.EndpointUrl != "" { opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl)) } @@ -566,9 +577,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) } opts := []s3Option{} - if storageAttributes.Region != "" { - opts = append(opts, WithRegion(storageAttributes.Region)) - } if storageAttributes.EndpointUrl != "" { opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl)) } @@ -632,9 +640,6 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr } opts := []s3Option{} - if attrs.Region != "" { - opts = append(opts, WithRegion(attrs.Region)) - } if attrs.EndpointUrl != "" { opts = append(opts, WithEndpoint(attrs.EndpointUrl)) } diff --git a/plugin/service/storage/plugin_test.go b/plugin/service/storage/plugin_test.go index b1157df..7d47fa2 100644 --- a/plugin/service/storage/plugin_test.go +++ b/plugin/service/storage/plugin_test.go @@ -17,12 +17,15 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/aws-sdk-go-v2/service/sts/types" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -103,6 +106,23 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { expectedErrContains: "not a valid region: foobar", expectedErrCode: codes.InvalidArgument, }, + { + name: "dynamic credentials without disable credential rotation", + req: &pb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + }, + }, + }, + }, + expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn", + expectedErrCode: codes.InvalidArgument, + }, { name: "persisted state setup error", req: validRequest(), @@ -137,22 +157,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), - expectedErrCode: codes.InvalidArgument, - }, - { - name: "validation error", - req: validRequest(), - credOpts: []credential.AwsCredentialPersistedStateOption{ - credential.WithStateTestOpts([]awsutil.Option{ - awsutil.WithSTSAPIFunc( - awsutil.NewMockSTS( - awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)), - ), - ), - }), - }, - expectedErrContains: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr), + expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), expectedErrCode: codes.InvalidArgument, }, { @@ -287,7 +292,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { awsutil.NewMockIAM( awsutil.WithGetUserOutput( &iam.GetUserOutput{ - User: &iam.User{ + User: &iamTypes.User{ Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), UserName: aws.String("JohnDoe"), @@ -296,7 +301,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { ), awsutil.WithCreateAccessKeyOutput( &iam.CreateAccessKeyOutput{ - AccessKey: &iam.AccessKey{ + AccessKey: &iamTypes.AccessKey{ AccessKeyId: aws.String("one"), SecretAccessKey: aws.String("two"), UserName: aws.String("JohnDoe"), @@ -323,6 +328,44 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { credential.ConstSecretAccessKey: "two", }, }, + { + name: "success with dynamic credentials", + req: &pb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Secrets: &structpb.Struct{}, + Attributes: credential.MockAssumeRoleAttributes("us-west-2", true), + }, + }, + credOpts: []credential.AwsCredentialPersistedStateOption{ + credential.WithStateTestOpts( + []awsutil.Option{ + awsutil.WithSTSAPIFunc( + awsutil.NewMockSTS( + awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{ + Credentials: &types.Credentials{ + AccessKeyId: aws.String("ASIAfoobar"), + Expiration: aws.Time(time.Now().Add(time.Hour)), + SecretAccessKey: aws.String("secretkeyfoobar"), + }, + }), + ), + ), + }, + ), + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{}, // No credentials sent back to Boundary as they are temporary. + }, } for _, tc := range cases { @@ -343,8 +386,10 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { require.NotNil(resp) require.NotNil(resp.GetPersisted()) require.NotNil(resp.GetPersisted().GetData()) + actualPersistedData := resp.GetPersisted().GetData().AsMap() - if len(actualPersistedData) != 0 { + if len(tc.expectedPersistedData) != 0 { + require.NotEmpty(actualPersistedData) require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId]) require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey]) require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime]) @@ -353,6 +398,10 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { } } +// TODO: add tests for changing: +// roleARN values +// access/secret keys +// swithcing between static & dynamic credentials func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { validRequest := func() *pb.OnUpdateStorageBucketRequest { return &pb.OnUpdateStorageBucketRequest{ @@ -365,18 +414,27 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: credential.MockStaticCredentialSecrets(), }, } } cases := []struct { - name string - req *pb.OnUpdateStorageBucketRequest - credOpts []credential.AwsCredentialPersistedStateOption - storageOpts []awsStoragePersistedStateOption - expectedErrContains string - expectedErrCode codes.Code + name string + req *pb.OnUpdateStorageBucketRequest + credOpts []credential.AwsCredentialPersistedStateOption + storageOpts []awsStoragePersistedStateOption + expectedPersistedData map[string]any + expectCredsLastRotatedTime bool + expectedErrContains string + expectedErrCode codes.Code }{ { name: "nil new bucket", @@ -392,6 +450,27 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { expectedErrContains: "new bucketName is required", expectedErrCode: codes.InvalidArgument, }, + { + name: "nil current bucket", + req: &pb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + }, + }, + expectedErrContains: "current bucket is required", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "nil current bucket attributes", + req: &pb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + }, + CurrentBucket: &storagebuckets.StorageBucket{}, + }, + expectedErrContains: "current bucket attributes is required", + expectedErrCode: codes.InvalidArgument, + }, { name: "nil new bucket attributes", req: &pb.OnUpdateStorageBucketRequest{ @@ -399,6 +478,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { BucketName: "foo", Secrets: new(structpb.Struct), }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, }, expectedErrContains: "new bucket attributes is required", expectedErrCode: codes.InvalidArgument, @@ -411,6 +497,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { Secrets: new(structpb.Struct), Attributes: new(structpb.Struct), }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, }, expectedErrContains: "missing required value \"region\"", expectedErrCode: codes.InvalidArgument, @@ -426,6 +519,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, }, expectedErrContains: "not a valid region: foobar", expectedErrCode: codes.InvalidArgument, @@ -441,6 +541,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ @@ -471,6 +578,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ @@ -484,6 +598,36 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { expectedErrContains: "cannot disable rotation for already-rotated credentials", expectedErrCode: codes.FailedPrecondition, }, + { + name: "attempt to enable credential rotation with dynamic credentials", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: credential.MockAssumeRoleAttributes("us-west-2", true), + }, + }, + expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn", + expectedErrCode: codes.InvalidArgument, + }, { name: "updating secrets, replace error", req: &pb.OnUpdateStorageBucketRequest{ @@ -502,6 +646,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ @@ -512,6 +663,16 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, credOpts: []credential.AwsCredentialPersistedStateOption{ credential.WithStateTestOpts([]awsutil.Option{ awsutil.WithIAMAPIFunc( @@ -535,6 +696,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ @@ -553,7 +721,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), + expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), expectedErrCode: codes.InvalidArgument, }, { @@ -659,6 +827,13 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, }, + CurrentBucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, Persisted: &storagebuckets.StorageBucketPersisted{ Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ @@ -677,7 +852,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { awsutil.NewMockIAM( awsutil.WithGetUserOutput( &iam.GetUserOutput{ - User: &iam.User{ + User: &iamTypes.User{ Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), UserName: aws.String("JohnDoe"), @@ -686,7 +861,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), awsutil.WithCreateAccessKeyOutput( &iam.CreateAccessKeyOutput{ - AccessKey: &iam.AccessKey{ + AccessKey: &iamTypes.AccessKey{ AccessKeyId: aws.String("AKIA_one"), SecretAccessKey: aws.String("two"), UserName: aws.String("JohnDoe"), @@ -707,6 +882,11 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), ), }, + expectedPersistedData: map[string]any{ + credential.ConstAccessKeyId: "AKIA_one", + credential.ConstSecretAccessKey: "two", + }, + expectCredsLastRotatedTime: true, }, { name: "success", @@ -722,6 +902,508 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), ), }, + expectedPersistedData: map[string]any{ + credential.ConstAccessKeyId: "AKIA_foobar", + credential.ConstSecretAccessKey: "bazqux", + }, + }, + { + name: "dry run fail on dynamic to dynamic credentials update", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{Data: &structpb.Struct{}}, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithHeadObjectError(fmt.Errorf("head object failed oops")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + ), + ), + }, + expectedErrContains: "head object failed oops", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "dynamic to dynamic credentials success", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{Data: &structpb.Struct{}}, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{}, + }, + { + name: "dry run fail on static non rotated to dynamic credentials update", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectError(fmt.Errorf("put object failed oops")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedErrContains: "put object failed oops", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "success static non rotated to dynamic credentials", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{}, + }, + { + name: "DeleteAccessKey error on static rotated to dynamic credentials update", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar_rotated_accesskeyid"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Add(-time.Hour).Format(time.RFC3339Nano)), + }, + }, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + credOpts: []credential.AwsCredentialPersistedStateOption{ + credential.WithStateTestOpts([]awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithDeleteAccessKeyError(fmt.Errorf("delete access key fail oops")), + ), + ), + }), + }, + expectedErrContains: "delete access key fail oops", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "success static rotated to dynamic credentials", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar_rotated_accesskeyid"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Add(-time.Hour).Format(time.RFC3339Nano)), + }, + }, + }, + }, + credOpts: []credential.AwsCredentialPersistedStateOption{ + credential.WithStateTestOpts([]awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM(), // DeleteAccessKey Success + ), + }), + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{}, + }, + { + name: "dry run fail on dynamic to static non-rotated credentials update", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectError(fmt.Errorf("put object fail oops")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedErrContains: "put object fail oops", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "success dynamic to static non-rotated credentials", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{ + credential.ConstAccessKeyId: "AKIA_foobar", + credential.ConstSecretAccessKey: "secretkey", + }, + }, + { + name: "create access key error on dynamic to static rotated credentials update", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + credOpts: []credential.AwsCredentialPersistedStateOption{ + credential.WithStateTestOpts([]awsutil.Option{ + awsutil.WithSTSAPIFunc( + awsutil.NewMockSTS(), + ), + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithGetUserOutput( + &iam.GetUserOutput{ + User: &iamTypes.User{ + Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), + UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), + UserName: aws.String("JohnDoe"), + }, + }, + ), + awsutil.WithCreateAccessKeyError(fmt.Errorf("create access key fail oops")), + ), + ), + }), + }, + expectedErrContains: "create access key fail oops", + expectedErrCode: codes.InvalidArgument, + }, + { + name: "success dynamic to static rotated credentials", + req: &pb.OnUpdateStorageBucketRequest{ + CurrentBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"), + credential.ConstSecretAccessKey: structpb.NewStringValue("secretkey"), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + }, + credOpts: []credential.AwsCredentialPersistedStateOption{ + credential.WithStateTestOpts([]awsutil.Option{ + awsutil.WithSTSAPIFunc( + awsutil.NewMockSTS(), + ), + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithGetUserOutput( + &iam.GetUserOutput{ + User: &iamTypes.User{ + Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), + UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), + UserName: aws.String("JohnDoe"), + }, + }, + ), + awsutil.WithCreateAccessKeyOutput( + &iam.CreateAccessKeyOutput{ + AccessKey: &iamTypes.AccessKey{ + AccessKeyId: aws.String("AKIA_one"), + SecretAccessKey: aws.String("two"), + UserName: aws.String("JohnDoe"), + }, + }, + ), + ), + ), + }), + }, + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + expectedPersistedData: map[string]any{ + credential.ConstAccessKeyId: "AKIA_one", + credential.ConstSecretAccessKey: "two", + }, + expectCredsLastRotatedTime: true, }, } @@ -735,6 +1417,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { } resp, err := p.OnUpdateStorageBucket(context.Background(), tc.req) if tc.expectedErrContains != "" { + require.NotNil(err) require.Contains(err.Error(), tc.expectedErrContains) require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) return @@ -744,18 +1427,18 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { require.NotNil(resp.GetPersisted()) require.NotNil(resp.GetPersisted().GetData()) - var disableCredentialRotation bool - expectedAccessKey := tc.req.Persisted.Data.AsMap()[credential.ConstAccessKeyId] - expectedSecretKey := tc.req.Persisted.Data.AsMap()[credential.ConstSecretAccessKey] - if value, ok := tc.req.GetNewBucket().GetAttributes().AsMap()[credential.ConstDisableCredentialRotation]; ok { - disableCredentialRotation = value.(bool) + actualPersistedData := resp.GetPersisted().GetData().AsMap() + if len(actualPersistedData) > 0 && len(tc.expectedPersistedData) == 0 { + t.Fatalf("test case expected no persisted data, but got %#v", actualPersistedData) } - if !disableCredentialRotation { - expectedAccessKey = "AKIA_one" - expectedSecretKey = "two" + if len(tc.expectedPersistedData) != 0 { + require.NotEmpty(actualPersistedData) + require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId]) + require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey]) + if tc.expectCredsLastRotatedTime { + require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime]) + } } - require.Equal(expectedAccessKey, resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId]) - require.Equal(expectedSecretKey, resp.GetPersisted().GetData().AsMap()[credential.ConstSecretAccessKey]) }) } } @@ -899,6 +1582,23 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) { }), }, }, + { + name: "success with dynamic credentials", + req: &pb.OnDeleteStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::098765432109:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + }, + }, } for _, tc := range cases { @@ -1082,6 +1782,37 @@ func TestStoragePlugin_HeadObject(t *testing.T) { ), }, }, + { + name: "success with dynamic credentials", + req: &pb.HeadObjectRequest{ + Key: "/foo/bar/key", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Secrets: new(structpb.Struct), + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + }, + credOpts: validSTSMock(), + contentLength: 1024, + lastModified: createTime(t, "2006-01-02T15:04:05Z"), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{ + ContentLength: 1024, + LastModified: aws.Time(createTime(t, "2006-01-02T15:04:05Z")), + }), + ), + ), + }, + }, } for _, tc := range cases { @@ -1312,6 +2043,32 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { ), }, }, + { + name: "success with dynamic credentials", + req: &pb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + }, + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + ), + ), + }, + }, } for _, tc := range cases { @@ -1513,6 +2270,34 @@ func TestStoragePlugin_GetObject(t *testing.T) { ), }, }, + { + name: "success with dynamic credentials", + req: &pb.GetObjectRequest{ + Key: "/foo/bar/key", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "foo", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + }, + credOpts: validSTSMock(), + objectData: []byte("test"), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{ + Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))), + }), + ), + ), + }, + }, } for _, tc := range cases { @@ -1881,6 +2666,43 @@ func TestStoragePlugin_PutObject(t *testing.T) { } }(), }, + { + name: "valid file using dynamic credentials", + request: &pb.PutObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "external-obj-store", + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + credential.ConstRegion: structpb.NewStringValue("us-west-2"), + credential.ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), + credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true), + }, + }, + }, + Key: "mock-object", + Path: validFilePath, + }, + credOpts: validSTSMock(), + expectedObject: []byte("CONTENT CHECK"), + storageOpts: func() []awsStoragePersistedStateOption { + hash := sha256.New() + _, err := hash.Write([]byte("CONTENT CHECK")) + require.NoError(t, err) + checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + return []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + &testMockS3State{ + PutObjectBody: []byte("CONTENT CHECK"), + }, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{ + ChecksumSHA256: aws.String(checksum), + }), + ), + ), + } + }(), + }, } for _, tc := range cases { diff --git a/plugin/service/storage/state.go b/plugin/service/storage/state.go index ed769f3..1a0032f 100644 --- a/plugin/service/storage/state.go +++ b/plugin/service/storage/state.go @@ -9,12 +9,12 @@ import ( "fmt" "net" "net/http" + "net/url" "time" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" + smithyEndpoints "github.com/aws/smithy-go/endpoints" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" "google.golang.org/protobuf/types/known/structpb" @@ -97,55 +97,23 @@ func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option if err != nil { return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err) } - - awsConfigOpts := []func(*config.LoadOptions) error{} - sess, err := s.GetSession() + awsCfg, err := s.GenerateCredentialChain(ctx) if err != nil { - return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err) + return nil, fmt.Errorf("error getting AWS configuration when fetching S3 client: %w", err) } - creds, err := sess.Config.Credentials.Get() - if err != nil { - return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err) - } - customCredentials := config.WithCredentialsProvider( - credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken), - ) - awsConfigOpts = append(awsConfigOpts, customCredentials) - if sess.Config.Region != nil { - awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region)) - } - if opts.withRegion != "" { - awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion)) + if awsCfg == nil { + return nil, fmt.Errorf("nil aws configuration") } + var s3Opts []func(*s3.Options) if opts.withEndpoint != "" { - customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { - if service == s3.ServiceID { - if opts.withRegion != "" { - region = opts.withRegion - } - return aws.Endpoint{ - PartitionID: "aws", - URL: opts.withEndpoint, - SigningRegion: region, - }, nil - } - // returning EndpointNotFoundError will allow the service to fallback to it's default resolution - return aws.Endpoint{}, &aws.EndpointNotFoundError{} - }) - awsConfigOpts = append(awsConfigOpts, config.WithEndpointResolverWithOptions(customResolver)) + s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{ + endpoint: opts.withEndpoint, + })) } - cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...) - if err != nil { - return nil, fmt.Errorf("error loading aws configuration when fetching S3 client: %w", err) - } - if s.testS3APIFunc != nil { - return s.testS3APIFunc(cfg) + return s.testS3APIFunc(*awsCfg) } - - cfg.HTTPClient = customClient - - return s3.NewFromConfig(cfg), nil + return s3.NewFromConfig(*awsCfg, s3Opts...), nil } // getOpts iterates the inbound s3Options and returns a struct @@ -167,7 +135,6 @@ type s3Option func(*s3Options) error // options = how options are represented type s3Options struct { - withRegion string withEndpoint string } @@ -175,14 +142,6 @@ func getDefaultS3Options() s3Options { return s3Options{} } -// WithRegion contains the region to use -func WithRegion(with string) s3Option { - return func(o *s3Options) error { - o.withRegion = with - return nil - } -} - // WithEndpoint contains the endpoint to use func WithEndpoint(with string) s3Option { return func(o *s3Options) error { @@ -190,3 +149,31 @@ func WithEndpoint(with string) s3Option { return nil } } + +type endpointResolver struct { + endpoint string +} + +func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) { + var uri *url.URL + resolver, err = s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) + if err != nil { + return + } + if e.endpoint == "" { + // TODO: not sure if we should set an error or ignore the missing endpoint value and continue with the default resolver + return + } + uri, err = url.Parse(e.endpoint) + if err != nil { + return + } + if uri == nil { + // TODO: not sure if we should set an error or ignore the missing uri value and continue with the default resolver + return + } + resolver = smithyEndpoints.Endpoint{ + URI: *uri, + } + return +} diff --git a/plugin/service/storage/state_test.go b/plugin/service/storage/state_test.go index 8efa3fc..a47831b 100644 --- a/plugin/service/storage/state_test.go +++ b/plugin/service/storage/state_test.go @@ -7,9 +7,8 @@ import ( "context" "testing" - "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/hashicorp/boundary-plugin-aws/internal/credential" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" ) @@ -43,7 +42,6 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) { withTestS3APIFunc(newTestMockS3(nil)), }, s3Opts: []s3Option{ - WithRegion("us-west-2"), WithEndpoint("0.0.0.0"), }, expectedRegion: "us-west-2", @@ -73,14 +71,6 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) { require.True(ok) require.NotNil(client) require.Equal(tc.expectedRegion, client.Region) - - if tc.expectedEndpoint != "" { - endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region) - require.NoError(err) - require.Equal(tc.expectedEndpoint, endpoint.URL) - require.Equal(tc.expectedRegion, endpoint.SigningRegion) - require.Equal("aws", endpoint.PartitionID) - } }) } } diff --git a/plugin/service/storage/testing.go b/plugin/service/storage/testing.go index 50e4521..33a3453 100644 --- a/plugin/service/storage/testing.go +++ b/plugin/service/storage/testing.go @@ -13,11 +13,11 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/structpb" @@ -69,9 +69,8 @@ func (s *testMockS3State) Reset() { type testMockS3 struct { S3API - State *testMockS3State - Region string - Endpoint aws.EndpointResolverWithOptions + State *testMockS3State + Region string // mocked responses for getObject GetObjectOutput *s3.GetObjectOutput @@ -147,9 +146,6 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc { if cfg.Region != "" { m.Region = cfg.Region } - if cfg.EndpointResolverWithOptions != nil { - m.Endpoint = cfg.EndpointResolverWithOptions - } } return m, nil diff --git a/testing/e2e_storage_test.go b/testing/e2e_storage_test.go index dfa8238..f71deef 100644 --- a/testing/e2e_storage_test.go +++ b/testing/e2e_storage_test.go @@ -75,7 +75,7 @@ func TestStoragePlugin(t *testing.T) { iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj") require.NoError(err) - // Start the workflow now. Set up the host catalog. Note that this + // Start the workflow now. Set up the storage bucket. Note that this // will cause the state to go out of drift above in the sense that // the access key ID/secret access key will no longer be valid. We // will assert this through the returned state. diff --git a/testing/testing.go b/testing/testing.go index e97cc92..682a64e 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -13,10 +13,10 @@ import ( "testing" "time" - "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/smithy-go" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary-plugin-aws/internal/values" - "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/go-secure-stdlib/awsutil/v2" tfjson "github.com/hashicorp/terraform-json" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" @@ -293,7 +293,7 @@ func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string defer cancel() waitErr: for { - _, err = c.GetCallerIdentity() + _, err = c.GetCallerIdentity(context.Background()) if err != nil { break } @@ -308,9 +308,9 @@ waitErr: } require.NotNil(err) - awsErr, ok := err.(awserr.Error) - require.True(ok) - require.Equal("InvalidClientTokenId", awsErr.Code()) + var ae smithy.APIError + require.True(errors.As(err, &ae)) + require.Equal("InvalidClientTokenId", ae.ErrorCode()) } func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) { @@ -322,7 +322,7 @@ func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) awsutil.WithSecretKey(secretAccessKey), ) require.NoError(err) - _, err = c.GetCallerIdentity() + _, err = c.GetCallerIdentity(context.Background()) require.NoError(err) }