diff --git a/go.mod b/go.mod index 704d45f..07cf0d4 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,16 @@ require ( 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.1 - github.com/hashicorp/boundary/sdk v0.0.41-0.20231117205527-08045ddb050e + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.4.0 + github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a + github.com/hashicorp/go-multierror v1.1.1 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.4 - google.golang.org/grpc v1.59.0 - google.golang.org/protobuf v1.31.0 + google.golang.org/grpc v1.61.0 + google.golang.org/protobuf v1.33.0 ) require ( @@ -44,7 +45,6 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.5.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -55,10 +55,12 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/zclconf/go-cty v1.10.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index be3425b..df46f5c 100644 --- a/go.sum +++ b/go.sum @@ -74,12 +74,12 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/boundary/sdk v0.0.41-0.20231117205527-08045ddb050e h1:PzZklmCjnaqQq9JdZRI9SVPSd9xo0iXLlmOHoQX68aM= -github.com/hashicorp/boundary/sdk v0.0.41-0.20231117205527-08045ddb050e/go.mod h1:+XTDYf9YNeKIbGOPJwy7hlO2Le4zgzCtHCG/u+z4THI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a h1:SwVze6sYE5o+J9qdcgj29auY1k6O8oSo9DC2+2Gb9rw= +github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a/go.mod h1:9iOT7kDM6mYcSkKxNuZlv8rP7U5BG1kXoevjLLL8lNQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -144,8 +144,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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= @@ -179,8 +179,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= @@ -209,8 +210,8 @@ go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -223,8 +224,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -238,14 +239,14 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -258,19 +259,22 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= diff --git a/internal/credential/attributes.go b/internal/credential/attributes.go index b39d057..7e0438e 100644 --- a/internal/credential/attributes.go +++ b/internal/credential/attributes.go @@ -34,8 +34,9 @@ type CredentialAttributes struct { } // GetCredentialsConfig parses values out of a protobuf struct secrets and returns a -// CredentialsConfig used for configuring an AWS session. An error is returned if -// any unrecognized fields are found in the protobuf struct input. +// CredentialsConfig used for configuring an AWS session. An status error is returned +// with an InvalidArgument code if any unrecognized fields are found in the protobuf +// struct input. func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) { // initialize secrets if it is nil // secrets can be nil because static credentials are optional @@ -116,13 +117,18 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags)) } - return awsutil.NewCredentialsConfig(opts...) + cfg, err := awsutil.NewCredentialsConfig(opts...) + if err != nil { + return nil, errors.BadRequestStatus(err.Error()) + } + + return cfg, nil } // GetCredentialAttributes parses values out of a protobuf struct input and returns a -// CredentialAttributes used for configuring an AWS session. An error is returned if -// any of the following fields are missing from the protobuf struct input or have -// invalid value types: region, disableCredentialRotation +// CredentialAttributes used for configuring an AWS session. An status error is returned +// with an InvalidArgument code if any of the following fields are missing from the protobuf +// struct input or have invalid value types: region, disableCredentialRotation func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) { badFields := make(map[string]string) diff --git a/internal/credential/state.go b/internal/credential/state.go index 08587af..83c3ca8 100644 --- a/internal/credential/state.go +++ b/internal/credential/state.go @@ -5,13 +5,14 @@ package credential import ( "context" - "errors" + stderr "errors" "fmt" "strings" "time" "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/errors" "github.com/hashicorp/boundary-plugin-aws/internal/values" "github.com/hashicorp/go-secure-stdlib/awsutil/v2" "google.golang.org/protobuf/types/known/structpb" @@ -71,7 +72,7 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption { return func(s *AwsCredentialPersistedState) error { if s.CredentialsConfig != nil { - return errors.New("credentials config already set") + return stderr.New("credentials config already set") } s.CredentialsConfig = x @@ -83,7 +84,7 @@ func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedS func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption { return func(s *AwsCredentialPersistedState) error { if !s.CredsLastRotatedTime.IsZero() { - return errors.New("last rotation time already set") + return stderr.New("last rotation time already set") } s.CredsLastRotatedTime = t @@ -106,13 +107,15 @@ 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. +// to check that the credentials are valid. This method returns a status error +// with PluginError details. func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error { if s.CredentialsConfig == nil { - return errors.New("missing credentials config") + return errors.BadRequestStatus("missing credentials config") } if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil { - return fmt.Errorf("error validating credentials: %w", err) + st, _ := errors.ParseAWSError(err, "validating credentials") + return st.Err() } return nil } @@ -121,18 +124,19 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error { // then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are // 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. +// still result in an extraneous access key existing. This method returns a status error with PluginError details. func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error { if s.CredentialsConfig == nil { - return errors.New("missing credentials config") + return errors.BadRequestStatus("missing credentials config") } if GetCredentialType(s.CredentialsConfig) != StaticAWS { - return errors.New("invalid credential type") + return errors.BadRequestStatus("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) + st, _ := errors.ParseAWSError(err, "error rotating credentials") + return st.Err() } s.CredsLastRotatedTime = time.Now() return nil @@ -140,13 +144,14 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) 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. +// deleted first, otherwise it's left alone. This method returns a +// status error with PluginError details. func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error { if credentialsConfig == nil { - return errors.New("missing new credentials config") + return errors.BadRequestStatus("missing new credentials config") } if s.CredentialsConfig == nil { - return errors.New("missing credentials config") + return errors.BadRequestStatus("missing credentials config") } // Delete old/existing credentials. This action is only possible for static credential types. @@ -166,27 +171,29 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credenti // 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. +// the state just to ensure that they cannot be re-used after. This +// method returns a status error with PluginError details. func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error { if s.CredentialsConfig == nil { - return errors.New("missing credentials config") + return errors.BadRequestStatus("missing credentials config") } if GetCredentialType(s.CredentialsConfig) != StaticAWS { - return errors.New("invalid credential type") + return errors.BadRequestStatus("invalid credential type") } 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 *iamTypes.NoSuchEntityException - if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" { + if stderr.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" { s.CredentialsConfig = nil s.CredsLastRotatedTime = time.Time{} return nil } // Otherwise treat it as an actual error. - return err + st, _ := errors.ParseAWSError(err, "failed to delete access key") + return st.Err() } s.CredentialsConfig = nil diff --git a/internal/credential/state_test.go b/internal/credential/state_test.go index e5c405c..c74234e 100644 --- a/internal/credential/state_test.go +++ b/internal/credential/state_test.go @@ -14,7 +14,10 @@ import ( "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/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" ) @@ -340,14 +343,16 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) { func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) { cases := []struct { - name string - in *AwsCredentialPersistedState - expectedErr string + name string + in *AwsCredentialPersistedState + expectedStatusCode codes.Code + expectedErr string }{ { - name: "missing credentials config", - in: &AwsCredentialPersistedState{}, - expectedErr: "missing credentials config", + name: "missing credentials config", + in: &AwsCredentialPersistedState{}, + expectedStatusCode: codes.InvalidArgument, + expectedErr: "missing credentials config", }, { name: "validation error", @@ -365,7 +370,8 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) { ), }, }, - expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr), + expectedStatusCode: codes.Unknown, + expectedErr: "aws service unknown: unknown error: validating credentials", }, { name: "good", @@ -383,10 +389,12 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) err := tc.in.ValidateCreds(context.Background()) if tc.expectedErr != "" { - require.EqualError(err, tc.expectedErr) + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) + assert.Equal(tc.expectedStatusCode, status.Code(err)) return } @@ -402,11 +410,13 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { exepected *awsutil.CredentialsConfig expectedNonZeroRotatedTime bool expectedErr string + expectedStatusCode codes.Code }{ { - name: "missing credentials config", - in: &AwsCredentialPersistedState{}, - expectedErr: "missing credentials config", + name: "missing credentials config", + in: &AwsCredentialPersistedState{}, + expectedErr: "missing credentials config", + expectedStatusCode: codes.InvalidArgument, }, { name: "cannot delete dynamic credential type", @@ -417,7 +427,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { }, CredsLastRotatedTime: time.Now(), }, - expectedErr: "invalid credential type", + expectedErr: "invalid credential type", + expectedStatusCode: codes.InvalidArgument, }, { name: "cannot delete unknown credential type", @@ -428,7 +439,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { }, CredsLastRotatedTime: time.Now(), }, - expectedErr: "invalid credential type", + expectedErr: "invalid credential type", + expectedStatusCode: codes.InvalidArgument, }, { name: "rotation error", @@ -446,7 +458,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { ), }, }, - expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), + expectedErr: "aws service unknown: unknown error: error rotating credentials", + expectedStatusCode: codes.Unknown, }, { name: "good", @@ -496,10 +509,12 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) err := tc.in.RotateCreds(context.Background()) if tc.expectedErr != "" { - require.EqualError(err, tc.expectedErr) + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) + assert.Equal(tc.expectedStatusCode, status.Code(err)) return } @@ -521,17 +536,20 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { expected *awsutil.CredentialsConfig expectedDeleteCalled bool expectedErr string + expectedStatusCode codes.Code }{ { - name: "missing new credentials config", - in: &AwsCredentialPersistedState{}, - expectedErr: "missing new credentials config", + name: "missing new credentials config", + in: &AwsCredentialPersistedState{}, + expectedErr: "missing new credentials config", + expectedStatusCode: codes.InvalidArgument, }, { - name: "missing credentials config", - in: &AwsCredentialPersistedState{}, - credentialConfig: &awsutil.CredentialsConfig{}, - expectedErr: "missing credentials config", + name: "missing credentials config", + in: &AwsCredentialPersistedState{}, + credentialConfig: &awsutil.CredentialsConfig{}, + expectedErr: "missing credentials config", + expectedStatusCode: codes.InvalidArgument, }, { name: "deletion error", @@ -549,8 +567,9 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { ), }, }, - credentialConfig: &awsutil.CredentialsConfig{}, - expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr), + credentialConfig: &awsutil.CredentialsConfig{}, + expectedErr: "aws service unknown: unknown error: failed to delete access key", + expectedStatusCode: codes.Unknown, }, { name: "good with delete of old rotated", @@ -604,11 +623,12 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) state.Reset() err := tc.in.ReplaceCreds(context.Background(), tc.credentialConfig) if tc.expectedErr != "" { - require.EqualError(err, tc.expectedErr) + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) return } @@ -623,14 +643,16 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) { func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { cases := []struct { - name string - in *AwsCredentialPersistedState - expectedErr string + name string + in *AwsCredentialPersistedState + expectedErr string + expectedStatusCode codes.Code }{ { - name: "missing credentials config", - in: &AwsCredentialPersistedState{}, - expectedErr: "missing credentials config", + name: "missing credentials config", + in: &AwsCredentialPersistedState{}, + expectedErr: "missing credentials config", + expectedStatusCode: codes.InvalidArgument, }, { name: "cannot delete dynamic credential type", @@ -641,7 +663,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { }, CredsLastRotatedTime: time.Now(), }, - expectedErr: "invalid credential type", + expectedErr: "invalid credential type", + expectedStatusCode: codes.InvalidArgument, }, { name: "cannot delete unknown credential type", @@ -652,7 +675,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { }, CredsLastRotatedTime: time.Now(), }, - expectedErr: "invalid credential type", + expectedErr: "invalid credential type", + expectedStatusCode: codes.InvalidArgument, }, { name: "deletion error", @@ -669,7 +693,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { ), }, }, - expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr), + expectedErr: "aws service unknown: unknown error: failed to delete access key", + expectedStatusCode: codes.Unknown, }, { name: "deletion error, but OK because key was just gone", @@ -708,10 +733,11 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) err := tc.in.DeleteCreds(context.Background()) if tc.expectedErr != "" { - require.EqualError(err, tc.expectedErr) + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) return } diff --git a/internal/errors/error.go b/internal/errors/error.go index 92f8a6e..5487601 100644 --- a/internal/errors/error.go +++ b/internal/errors/error.go @@ -4,12 +4,63 @@ package errors import ( + "bytes" + "errors" "fmt" + "net/http" "sort" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/aws/aws-sdk-go-v2/aws/retry" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/smithy-go" + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // awsErrorAccessDenied is returned when the credentials do not have + // the required permissions to do a specific action on a resource. + // This error will persist until the IAM Role Policy is updated by + // the owner. This error cannot be resolved by retrying the request. + awsErrorAccessDenied = "AccessDenied" + + // awsErrorInvalidAccessKeyId is returned when the credential does + // not exist. This error will persist until the credentials attached + // to the storage bucket is updated. This error can occur when the + // given credentials are deleted, deactivated, or rotated. This + // error cannot be resolved by retrying the request. + awsErrorInvalidAccessKeyId = "InvalidAccessKeyId" + + // awsErrorExpiredToken is returned when the provided credential is + // expired. This error can only be returned by a dynamic credential + // type. This error can resolve itself by retrying the request. + awsErrorExpiredToken = "ExpiredToken" + + // NoSuchBucket is returned when the specified S3 storage bucket does + // not exist + awsErrorNoSuchBucket = "NoSuchBucket" + + // BadDigest is returned when an S3 PUT OBJECT request's calculated + // checksum does not match the checksum which was sent by the client + awsErrorBadDigest = "BadDigest" + + // NoSuchKey is returned when attempting to interact with an S3 object + // whose key does not exist + awsErrorNoSuchKey = "NoSuchKey" + + // The InvalidObjectState error is returned when trying to + // access an object that was moved into cold storage. + awsErrorInvalidObjectState = "InvalidObjectState" + + // RequestTimeout is returned when an http request takes longer than allowed + awsErrorRequestTimeout = "RequestTimeout" + + // RequestTimeoutException is returned when an http request takes longer than allowed + awsErrorRequestTimeoutException = "RequestTimeoutException" ) // InvalidArgumentError returns an grpc invalid argument status error. @@ -22,5 +73,146 @@ func InvalidArgumentError(msg string, f map[string]string) error { sort.Strings(fieldMsgs) msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", ")) } - return status.Error(codes.InvalidArgument, msg) + return BadRequestStatus(msg) +} + +// ParseAWSError converts an aws service error into a RPC status. This method +// will handle parsing the following aws error codes: throttling, connections, +// and credentials. This method will fallback to parsing the http status code +// when it cannot match an aws error code. This method does not handle specific +// service type errors such as S3 or EC2. +func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Permission) { + if err == nil { + return nil, nil + } + + // find the service name of the aws api + serviceName := "unknown" + var oe *smithy.OperationError + if errors.As(err, &oe) { + serviceName = oe.Service() + } + + // The unknown plugin error code will be used + // when the aws service returns an error that + // has failed to be parsed. By default, this + // error code is retryable. + + defer func() { + if st == nil { + statusMsg := fmt.Sprintf("aws service %s: unknown error: %s", serviceName, msg) + st = status.New(codes.Unknown, statusMsg) + } + if permission == nil { + permission = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()} + } + }() + + // evaluate against aws throttling error codes + throttleErr := retry.ThrottleErrorCode{ + Codes: retry.DefaultThrottleErrorCodes, + } + if throttleErr.IsErrorThrottle(err).Bool() { + statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg) + return status.New(codes.Unavailable, statusMsg), nil + } + + // parse some specific aws error codes that we have special cred states for + var apiErr smithy.APIError + if errors.As(err, &apiErr) { + switch apiErr.ErrorCode() { + case awsErrorAccessDenied: + fallthrough + case awsErrorInvalidAccessKeyId: + fallthrough + case awsErrorExpiredToken: + statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg) + return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: apiErr.ErrorMessage(), + CheckedAt: timestamppb.Now(), + } + case awsErrorNoSuchBucket: + statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg) + return status.New(codes.NotFound, statusMsg), &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: apiErr.ErrorMessage(), + CheckedAt: timestamppb.Now(), + } + case awsErrorBadDigest: + statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg) + return status.New(codes.Aborted, statusMsg), nil + case awsErrorNoSuchKey: + fallthrough + case awsErrorInvalidObjectState: + statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg) + return status.New(codes.NotFound, statusMsg), nil + case awsErrorRequestTimeout: + fallthrough + case awsErrorRequestTimeoutException: + statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg) + return status.New(codes.DeadlineExceeded, statusMsg), nil + } + } + + // default to evaluating the http status code + if httpErr, ok := err.(*awshttp.ResponseError); ok { + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(httpErr.Response.Body) + if err != nil { + return status.New(codes.Internal, err.Error()), nil + } + defer httpErr.Response.Body.Close() + switch httpErr.HTTPStatusCode() { + case http.StatusBadRequest: + statusMsg := fmt.Sprintf("aws service %s: bad request error: %s", serviceName, msg) + return status.New(codes.InvalidArgument, statusMsg), nil + case http.StatusUnauthorized: + statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg) + return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: buf.String(), + CheckedAt: timestamppb.Now(), + } + case http.StatusForbidden: + statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg) + return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: buf.String(), + CheckedAt: timestamppb.Now(), + } + case http.StatusNotFound: + statusMsg := fmt.Sprintf("aws service %s: resource not found error: %s", serviceName, msg) + return status.New(codes.NotFound, statusMsg), nil + case http.StatusTooManyRequests: + statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg) + return status.New(codes.Unavailable, statusMsg), nil + case http.StatusInternalServerError: + fallthrough + case http.StatusBadGateway: + fallthrough + case http.StatusServiceUnavailable: + statusMsg := fmt.Sprintf("aws service %s: connectivity error: %s", serviceName, msg) + return status.New(codes.Unavailable, statusMsg), nil + case http.StatusRequestTimeout: + fallthrough + case http.StatusGatewayTimeout: + statusMsg := fmt.Sprintf("aws service %s: timeout error: %s", serviceName, msg) + return status.New(codes.DeadlineExceeded, statusMsg), nil + } + } + + return st, nil +} + +// BadRequestStatus returns a status error with an invalid +// argument code +func BadRequestStatus(format string, args ...any) error { + return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err() +} + +// UnknownStatus returns a status error with an internal +// error code +func UnknownStatus(format string, args ...any) error { + return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err() } diff --git a/internal/errors/error_test.go b/internal/errors/error_test.go new file mode 100644 index 0000000..ec41ab5 --- /dev/null +++ b/internal/errors/error_test.go @@ -0,0 +1,346 @@ +package errors + +import ( + "fmt" + "testing" + + "net/http" + + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func Test_ParseAWSError(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + err error + expectedStatusCode codes.Code + expectedStatusMsg string + expectedPermission *pb.Permission + }{ + { + name: "nil-error", + expectedStatusCode: codes.OK, + }, + { + name: "default-to-unknwon", + err: fmt.Errorf("what can this error be?"), + expectedStatusCode: codes.Unknown, + expectedStatusMsg: "aws service unknown: unknown error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-bad-request", + err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"), + expectedStatusCode: codes.InvalidArgument, + expectedStatusMsg: "aws service unknown: bad request error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-unauthorized", + err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service unknown: invalid credentials error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "unauthorized request", + CheckedAt: timestamppb.Now(), + }, + }, + { + name: "http-status-code-forbidden", + err: TestAwsHttpResponseError(http.StatusForbidden, "forbidden request"), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service unknown: invalid credentials error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "forbidden request", + CheckedAt: timestamppb.Now(), + }, + }, + { + name: "http-status-code-not-found", + err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"), + expectedStatusCode: codes.NotFound, + expectedStatusMsg: "aws service unknown: resource not found error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-too-many-requests", + err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-request-timeout", + err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"), + expectedStatusCode: codes.DeadlineExceeded, + expectedStatusMsg: "aws service unknown: timeout error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-internal-server-error", + err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: connectivity error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-bad-gateway", + err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: connectivity error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-service-unavailable", + err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: connectivity error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "http-status-code-gateway-timeout", + err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"), + expectedStatusCode: codes.DeadlineExceeded, + expectedStatusMsg: "aws service unknown: timeout error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-throttling", + err: TestAwsError("Throttling", "throttling error"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-throttling-exception", + err: TestAwsError("ThrottlingException", "throttling exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-throttled-exception", + err: TestAwsError("ThrottledException", "throttled exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-throttled-exception", + err: TestAwsError("RequestThrottledException", "request throttled exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-too-many-requests-exception", + err: TestAwsError("TooManyRequestsException", "too many requests exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-provisioned-throughput-exceeded-exception", + err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-transaction-in-progress-exception", + err: TestAwsError("TransactionInProgressException", "transaction in progress exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-limit-exceeded", + err: TestAwsError("RequestLimitExceeded", "request limit exceeded"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-bandwidth-limit-exceeded", + err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-limit-exceeded-exception", + err: TestAwsError("LimitExceededException", "limit exceeded exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-throttled", + err: TestAwsError("RequestThrottled", "request throttled"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-slow-down", + err: TestAwsError("SlowDown", "slow down"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-prior-request-not-complete", + err: TestAwsError("PriorRequestNotComplete", "prior request not complete"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-ec2-throttled-exception", + err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service unknown: throttling error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-timeout", + err: TestAwsError("RequestTimeout", "request timeout"), + expectedStatusCode: codes.DeadlineExceeded, + expectedStatusMsg: "aws unknown error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-timeout-exception", + err: TestAwsError("RequestTimeoutException", "request timeout exception"), + expectedStatusCode: codes.DeadlineExceeded, + expectedStatusMsg: "aws unknown error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + }, + }, + { + name: "aws-request-access-denied", + err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service unknown: invalid credentials error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized to perform action", + CheckedAt: timestamppb.Now(), + }, + }, + { + name: "aws-request-invalid-access-key-id", + err: TestAwsError(awsErrorInvalidAccessKeyId, "The AWS Access Key Id you provided does not exist in our records."), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service unknown: invalid credentials error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.", + CheckedAt: timestamppb.Now(), + }, + }, + { + name: "aws-request-expired-token", + err: TestAwsError(awsErrorExpiredToken, "The security token included in the request is expired."), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service unknown: invalid credentials error: test", + expectedPermission: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "The security token included in the request is expired.", + CheckedAt: timestamppb.Now(), + }, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + actualStatus, permission := ParseAWSError(tt.err, "test") + if tt.expectedStatusCode == codes.OK { + require.Nil(actualStatus) + return + } + + require.Equal(tt.expectedStatusCode, actualStatus.Code()) + require.Equal(tt.expectedStatusMsg, actualStatus.Message()) + require.Len(actualStatus.Details(), 0) + CheckSimilarPermission(assert, tt.expectedPermission, permission, true) + }) + } +} + +func Test_BadRequestStatus(t *testing.T) { + require, assert := require.New(t), assert.New(t) + err := BadRequestStatus("test: %s", "hello world") + assert.ErrorContains(err, "test: hello world") + assert.Equal(status.Code(err), codes.InvalidArgument) + _, ok := status.FromError(err) + require.True(ok) +} + +func Test_UnknownStatus(t *testing.T) { + require, assert := require.New(t), assert.New(t) + err := UnknownStatus("test: %s", "hello world") + assert.ErrorContains(err, "test: hello world") + assert.Equal(status.Code(err), codes.Internal) + _, ok := status.FromError(err) + require.True(ok) +} diff --git a/internal/errors/testing.go b/internal/errors/testing.go new file mode 100644 index 0000000..f3f6522 --- /dev/null +++ b/internal/errors/testing.go @@ -0,0 +1,57 @@ +package errors + +import ( + "io" + "net/http" + "strings" + + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/smithy-go" + smithyhttp "github.com/aws/smithy-go/transport/http" + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// TestAwsHttpResponseError returns an aws http response error +func TestAwsHttpResponseError(code int, msg string) error { + return &awshttp.ResponseError{ + ResponseError: &smithyhttp.ResponseError{ + Response: &smithyhttp.Response{ + Response: &http.Response{ + StatusCode: code, + Body: io.NopCloser(strings.NewReader(msg)), + }, + }, + }, + } +} + +// TestAwsError returns an generic api error +func TestAwsError(code, msg string) error { + return &smithy.GenericAPIError{ + Code: code, + Message: msg, + Fault: smithy.FaultServer, + } +} + +func CheckSimilarPermission(assert *assert.Assertions, expected, actual *pb.Permission, substituteNilWithDefaultOk bool) { + if expected == nil && substituteNilWithDefaultOk { + expected = &pb.Permission{ + State: pb.StateType_STATE_TYPE_OK, + CheckedAt: timestamppb.Now(), + } + } + + if expected != nil { + assert.NotNil(actual) + if actual != nil { + assert.Equal(expected.State, actual.State, "StateType mismatch") + assert.Equal(expected.ErrorDetails, actual.ErrorDetails) + assert.NotNil(actual.CheckedAt) + } + } else { + assert.Equal(expected, actual) + } +} diff --git a/plugin/service/host/plugin_test.go b/plugin/service/host/plugin_test.go index 9006d05..bffe8c6 100644 --- a/plugin/service/host/plugin_test.go +++ b/plugin/service/host/plugin_test.go @@ -132,7 +132,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), + expectedErrContains: "aws service unknown: unknown error: error rotating credentials", expectedErrCode: codes.InvalidArgument, }, { @@ -163,7 +163,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr), + expectedErrContains: "aws service unknown: unknown error: validating credentials", expectedErrCode: codes.InvalidArgument, }, } @@ -342,7 +342,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr), + expectedErrContains: "aws service unknown: unknown error: failed to delete access key", expectedErrCode: codes.InvalidArgument, }, { @@ -375,7 +375,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), + expectedErrContains: "aws service unknown: unknown error: error rotating credentials", expectedErrCode: codes.InvalidArgument, }, } @@ -466,7 +466,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error removing rotated credentials during catalog deletion: error deleting old access key: %s", testDeleteAccessKeyErr), + expectedErrContains: "aws service unknown: unknown error: failed to delete access key", expectedErrCode: codes.Aborted, }, } diff --git a/plugin/service/storage/attributes_test.go b/plugin/service/storage/attributes_test.go index 344b9e1..c22e81f 100644 --- a/plugin/service/storage/attributes_test.go +++ b/plugin/service/storage/attributes_test.go @@ -7,6 +7,8 @@ import ( "testing" cred "github.com/hashicorp/boundary-plugin-aws/internal/credential" + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -19,6 +21,7 @@ func TestGetStorageAttributes(t *testing.T) { in *structpb.Struct expected *StorageAttributes expectedErrContains string + expectedDetails *pb.StorageBucketCredentialState }{ { name: "missing region", @@ -100,13 +103,17 @@ func TestGetStorageAttributes(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) actual, err := getStorageAttributes(tc.in) if tc.expectedErrContains != "" { require.Error(err) require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err), codes.InvalidArgument) + require.Equal(codes.InvalidArgument, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + assert.Len(st.Details(), 0) return } diff --git a/plugin/service/storage/error.go b/plugin/service/storage/error.go new file mode 100644 index 0000000..e7c6245 --- /dev/null +++ b/plugin/service/storage/error.go @@ -0,0 +1,68 @@ +package storage + +import ( + "github.com/hashicorp/boundary-plugin-aws/internal/errors" + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + s3StatusErrMsg = "aws s3 error: %s" + + // s3ErrorBadDigest is returned when there is a checksum + // mismatch between the request and the s3 service. This + // error code is returned for the PutObject request. + s3ErrorBadDigest = "BadDigest" +) + +func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCredentialState { + state := &pb.StorageBucketCredentialState{ + State: &pb.Permissions{}, + } + switch req.(type) { + case pb.HeadObjectRequest: + state.State.Read = permission + case *pb.HeadObjectRequest: + state.State.Read = permission + case pb.GetObjectRequest: + state.State.Read = permission + case *pb.GetObjectRequest: + state.State.Read = permission + case pb.PutObjectRequest: + state.State.Write = permission + case *pb.PutObjectRequest: + state.State.Write = permission + case pb.DeleteObjectsRequest: + state.State.Delete = permission + case *pb.DeleteObjectsRequest: + state.State.Delete = permission + } + return state +} + +// parseS3Error will convert an S3 api error into a RPC status. If the +// error does not match against an s3 error type, it will fallback to +// evaluating against generic aws service errors. This includes throttling, +// credentials, and connectivity error types. +// +// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html +func parseS3Error(err error, msg string, req any) *status.Status { + if err == nil { + return nil + } + + st, p := errors.ParseAWSError(err, msg) + if p != nil { + state := setStatePermission(req, p) + if st, err = st.WithDetails(state); err != nil { + st = status.New(codes.Internal, err.Error()) + } + } + + return st +} + +func checksumMistmatchStatus() error { + return status.New(codes.Aborted, "mismatched checksum").Err() +} diff --git a/plugin/service/storage/error_test.go b/plugin/service/storage/error_test.go new file mode 100644 index 0000000..8ee5712 --- /dev/null +++ b/plugin/service/storage/error_test.go @@ -0,0 +1,180 @@ +package storage + +import ( + "testing" + + "github.com/hashicorp/boundary-plugin-aws/internal/errors" + pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func Test_ParseS3Error(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + req any + err error + expectedStatusCode codes.Code + expectedStatusMsg string + expectedDetails *pb.StorageBucketCredentialState + }{ + { + name: "nil-error", + expectedStatusCode: codes.OK, + }, + { + name: "no-such-bucket", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("NoSuchBucket", "GetObject", "no such bucket"), + expectedStatusCode: codes.NotFound, + expectedStatusMsg: "aws s3 error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "no such bucket", + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "no-such-key", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("NoSuchKey", "GetObject", "no such key"), + expectedStatusCode: codes.NotFound, + expectedStatusMsg: "aws s3 error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "not-found", + req: &pb.GetObjectRequest{}, + err: errors.TestAwsHttpResponseError(404, "not found"), + expectedStatusCode: codes.NotFound, + // cannot embed services within mock http errors, therefore unknown service + expectedStatusMsg: "aws service unknown: resource not found error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "invalid-object-state", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("InvalidObjectState", "GetObject", "invalid object state"), + expectedStatusCode: codes.NotFound, + expectedStatusMsg: "aws s3 error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "bad-digest", + req: &pb.PutObjectRequest{}, + err: TestAwsS3Error(s3ErrorBadDigest, "PutObject", "checksum mismatch"), + expectedStatusCode: codes.Aborted, + expectedStatusMsg: "aws s3 error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "fallback-throttle-error", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("Throttling", "action", "throttling exception"), + expectedStatusCode: codes.Unavailable, + expectedStatusMsg: "aws service s3: throttling error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "fallback-connectivity-error", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("RequestTimeout", "action", "request timeout"), + expectedStatusCode: codes.DeadlineExceeded, + expectedStatusMsg: "aws s3 error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "fallback-credentials-error", + req: &pb.GetObjectRequest{}, + err: TestAwsS3Error("AccessDenied", "action", "access denied"), + expectedStatusCode: codes.PermissionDenied, + expectedStatusMsg: "aws service s3: invalid credentials error: test", + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "access denied", + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + actualStatus := parseS3Error(tt.err, "test", tt.req) + if tt.expectedStatusCode == codes.OK { + require.Nil(actualStatus) + return + } + + require.Equal(tt.expectedStatusCode, actualStatus.Code()) + require.Equal(tt.expectedStatusMsg, actualStatus.Message()) + require.Len(actualStatus.Details(), 1) + + foundDetail := false + for _, detail := range actualStatus.Details() { + switch errDetail := detail.(type) { + case *pb.StorageBucketCredentialState: + foundDetail = true + errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Read, errDetail.State.Read, false) + errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Write, errDetail.State.Write, false) + errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Delete, errDetail.State.Delete, false) + } + } + require.True(foundDetail, "did not find error details with status") + }) + } +} diff --git a/plugin/service/storage/plugin.go b/plugin/service/storage/plugin.go index c258348..ac1818c 100644 --- a/plugin/service/storage/plugin.go +++ b/plugin/service/storage/plugin.go @@ -9,19 +9,21 @@ import ( "context" "crypto/sha256" "encoding/base64" + "fmt" "io" "os" "path" "strings" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/google/uuid" cred "github.com/hashicorp/boundary-plugin-aws/internal/credential" + "github.com/hashicorp/boundary-plugin-aws/internal/errors" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/hashicorp/go-multierror" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" @@ -46,15 +48,15 @@ type StoragePlugin struct { func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCreateStorageBucketRequest) (*pb.OnCreateStorageBucketResponse, error) { bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } if bucket.BucketName == "" { - return nil, status.Error(codes.InvalidArgument, "bucketName is required") + return nil, errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -74,13 +76,13 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %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) + return nil, err } } @@ -90,17 +92,17 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } // perform dry run to ensure we can interact with the bucket as expected. - if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil { - return nil, err + if st := dryRunValidation(ctx, storageState, storageAttributes, bucket); st != nil { + return nil, st.Err() } persistedProto, err := storageState.toProto() if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) + return nil, errors.BadRequestStatus(err.Error()) } return &pb.OnCreateStorageBucketResponse{ @@ -115,19 +117,19 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // fetching the new copy to keep things a little less messy. newBucket := req.GetNewBucket() if newBucket == nil { - return nil, status.Error(codes.InvalidArgument, "new bucket is required") + return nil, errors.BadRequestStatus("new bucket is required") } if newBucket.BucketName == "" { - return nil, status.Error(codes.InvalidArgument, "new bucketName is required") + return nil, errors.BadRequestStatus("new bucketName is required") } currentBucket := req.GetCurrentBucket() if currentBucket == nil { - return nil, status.Error(codes.InvalidArgument, "current bucket is required") + return nil, errors.BadRequestStatus("current bucket is required") } oldAttrs := currentBucket.GetAttributes() if oldAttrs == nil { - return nil, status.Error(codes.InvalidArgument, "current bucket attributes is required") + return nil, errors.BadRequestStatus("current bucket attributes is required") } oldStorageAttributes, err := getStorageAttributes(oldAttrs) if err != nil { @@ -135,7 +137,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd } newAttrs := newBucket.GetAttributes() if newAttrs == nil { - return nil, status.Error(codes.InvalidArgument, "new bucket attributes is required") + return nil, errors.BadRequestStatus("new bucket attributes is required") } newStorageAttributes, err := getStorageAttributes(newAttrs) if err != nil { @@ -147,7 +149,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd oldStorageAttributes.CredentialAttributes, p.testCredStateOpts...) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + return nil, errors.BadRequestStatus("error loading persisted state: %s", err) } // Verify the incoming credentials are valid and return any errors to the @@ -170,7 +172,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up new credential persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up new credential persisted state: %s", err) } newStorageState, err := newAwsStoragePersistedState( append([]awsStoragePersistedStateOption{ @@ -178,10 +180,10 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + return nil, errors.BadRequestStatus("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) + if st := dryRunValidation(ctx, newStorageState, newStorageAttributes, newBucket); st != nil { + return nil, st.Err() } // Replace the existing credential state. @@ -189,7 +191,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // 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) + return nil, err } } @@ -199,7 +201,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // 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") + return nil, errors.BadRequestStatus("cannot disable rotation for already-rotated credentials") } } @@ -207,7 +209,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd // 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) + return nil, err } } } @@ -218,17 +220,17 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + return nil, errors.BadRequestStatus("error loading persisted state: %s", err) } // perform dry run to ensure we can interact with the bucket as expected. - if err := dryRunValidation(ctx, storageState, newStorageAttributes, newBucket); err != nil { - return nil, err + if st := dryRunValidation(ctx, storageState, newStorageAttributes, newBucket); st != nil { + return nil, st.Err() } persistedProto, err := storageState.toProto() if err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) + return nil, errors.BadRequestStatus(err.Error()) } return &pb.OnUpdateStorageBucketResponse{ @@ -240,12 +242,12 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDeleteStorageBucketRequest) (*pb.OnDeleteStorageBucketResponse, error) { bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -264,7 +266,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel storageAttributes.CredentialAttributes, p.testCredStateOpts...) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + return nil, errors.BadRequestStatus("error loading persisted state: %s", err) } _, err = newAwsStoragePersistedState( @@ -273,7 +275,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err) + return nil, errors.BadRequestStatus("error loading persisted state: %s", err) } // try to delete static credentials @@ -283,7 +285,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel // credentials to ensure that it has the proper permissions to do // it. if err := credState.DeleteCreds(ctx); err != nil { - return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err) + return nil, err } } } @@ -294,21 +296,21 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel // HeadObject is called to get the metadata of an object. func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectRequest) (*pb.HeadObjectResponse, error) { if req.GetKey() == "" { - return nil, status.Error(codes.InvalidArgument, "key is required") + return nil, errors.BadRequestStatus("key is required") } bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } if bucket.BucketName == "" { - return nil, status.Error(codes.InvalidArgument, "bucketName is required") + return nil, errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -328,7 +330,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } storageState, err := newAwsStoragePersistedState( @@ -337,7 +339,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } opts := []s3Option{} @@ -346,7 +348,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques } s3Client, err := storageState.S3Client(ctx, opts...) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err) + return nil, errors.BadRequestStatus("error getting S3 client: %s", err) } objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey()) @@ -355,7 +357,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques Key: aws.String(objectKey), }) if err != nil { - return nil, parseAWSErrCode("error getting head object from s3", err) + return nil, parseS3Error(err, "failed to head object", req).Err() } return &pb.HeadObjectResponse{ ContentLength: resp.ContentLength, @@ -367,16 +369,16 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.ValidatePermissionsRequest) (*pb.ValidatePermissionsResponse, error) { bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } if bucket.BucketName == "" { - return nil, status.Error(codes.InvalidArgument, "bucketName is required") + return nil, errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -396,7 +398,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } storageState, err := newAwsStoragePersistedState( @@ -405,12 +407,12 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } // perform dry run to ensure we can interact with the bucket as expected. - if err := dryRunValidation(ctx, storageState, storageAttributes, bucket); err != nil { - return nil, err + if st := dryRunValidation(ctx, storageState, storageAttributes, bucket); st != nil { + return nil, st.Err() } return &pb.ValidatePermissionsResponse{}, nil @@ -420,21 +422,21 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat // GetObject is a blocking call until the stream has been recieved in full. func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePluginService_GetObjectServer) error { if req.GetKey() == "" { - return status.Error(codes.InvalidArgument, "key is required") + return errors.BadRequestStatus("key is required") } bucket := req.GetBucket() if bucket == nil { - return status.Error(codes.InvalidArgument, "bucket is required") + return errors.BadRequestStatus("bucket is required") } if bucket.GetBucketName() == "" { - return status.Error(codes.InvalidArgument, "bucketName is required") + return errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return status.Error(codes.InvalidArgument, "attributes is required") + return errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -454,7 +456,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu }, p.testCredStateOpts...)..., ) if err != nil { - return status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return errors.BadRequestStatus("error setting up persisted state: %s", err) } storageState, err := newAwsStoragePersistedState( @@ -463,7 +465,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu }, p.testStorageStateOpts...)..., ) if err != nil { - return status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return errors.BadRequestStatus("error setting up persisted state: %s", err) } opts := []s3Option{} @@ -472,7 +474,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu } s3Client, err := storageState.S3Client(stream.Context(), opts...) if err != nil { - return status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err) + return errors.BadRequestStatus("error getting S3 client: %s", err) } objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey()) @@ -481,7 +483,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu Key: aws.String(objectKey), }) if err != nil { - return parseAWSErrCode("error getting object from s3", err) + return parseS3Error(err, "failed to get object", req).Err() } defer resp.Body.Close() @@ -494,13 +496,13 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu buffer := make([]byte, chunkSize) n, err := reader.Read(buffer) if err != nil && err != io.EOF { - return status.Errorf(codes.Internal, "error reading chunk from s3: %s", err) + return errors.UnknownStatus("error reading chunk from s3: %s", err) } if n > 0 { if err := stream.Send(&pb.GetObjectResponse{ FileChunk: buffer[:n], }); err != nil { - return status.Errorf(codes.Internal, "error sending chunk to client: %s", err) + return errors.UnknownStatus("error sending chunk to client: %s", err) } } if err == io.EOF { @@ -514,38 +516,38 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu // PutObject is called when putting objects into an s3 bucket. func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) (*pb.PutObjectResponse, error) { if req == nil { - return nil, status.Errorf(codes.InvalidArgument, "request is required") + return nil, errors.BadRequestStatus("request is required") } if req.GetKey() == "" { - return nil, status.Errorf(codes.InvalidArgument, "key is required") + return nil, errors.BadRequestStatus("key is required") } bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } if bucket.GetBucketName() == "" { - return nil, status.Error(codes.InvalidArgument, "bucketName is required") + return nil, errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } if req.GetPath() == "" { - return nil, status.Errorf(codes.InvalidArgument, "path is required") + return nil, errors.BadRequestStatus("path is required") } file, err := os.Open(req.GetPath()) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to open file") + return nil, errors.BadRequestStatus("failed to open file") } info, err := file.Stat() if err != nil { - return nil, status.Errorf(codes.Internal, "failed to read file info") + return nil, errors.BadRequestStatus("failed to read file info") } if info.IsDir() { - return nil, status.Errorf(codes.InvalidArgument, "path is not a file") + return nil, errors.BadRequestStatus("path is not a file") } if info.Size() == 0 { - return nil, status.Errorf(codes.InvalidArgument, "file is empty") + return nil, errors.BadRequestStatus("file is empty") } storageAttributes, err := getStorageAttributes(attrs) @@ -565,7 +567,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } storageState, err := newAwsStoragePersistedState( @@ -574,7 +576,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } opts := []s3Option{} @@ -583,18 +585,18 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) } s3Client, err := storageState.S3Client(ctx, opts...) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err) + return nil, errors.BadRequestStatus("error getting S3 client: %s", err) } hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { - return nil, status.Errorf(codes.Internal, "failed to calcualte hash") + return nil, errors.UnknownStatus("failed to calcualte hash") } checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil)) _, err = file.Seek(0, io.SeekStart) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to rewind file pointer") + return nil, errors.UnknownStatus("failed to rewind file pointer") } objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey()) @@ -606,17 +608,17 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) ChecksumSHA256: aws.String(checksum), }) if err != nil { - return nil, parseAWSErrCode("error putting object into s3", err) + return nil, parseS3Error(err, "failed to put object", req).Err() } if resp.ChecksumSHA256 == nil { - return nil, status.Errorf(codes.Internal, "missing checksum response from aws") + return nil, errors.UnknownStatus("missing checksum response from aws") } if checksum != *resp.ChecksumSHA256 { - return nil, status.Errorf(codes.Internal, "mismatched checksum") + return nil, checksumMistmatchStatus() } decodedChecksum, err := base64.StdEncoding.DecodeString(*resp.ChecksumSHA256) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to decode checksum value from aws") + return nil, errors.UnknownStatus("failed to decode checksum value from aws") } return &pb.PutObjectResponse{ ChecksumSha_256: decodedChecksum, @@ -626,21 +628,21 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) // DeleteObjects is used to delete one or many objects from an s3 bucket. func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjectsRequest) (*pb.DeleteObjectsResponse, error) { if req.GetKeyPrefix() == "" { - return nil, status.Error(codes.InvalidArgument, "key prefix is required") + return nil, errors.BadRequestStatus("key prefix is required") } bucket := req.GetBucket() if bucket == nil { - return nil, status.Error(codes.InvalidArgument, "bucket is required") + return nil, errors.BadRequestStatus("bucket is required") } if bucket.GetBucketName() == "" { - return nil, status.Error(codes.InvalidArgument, "bucketName is required") + return nil, errors.BadRequestStatus("bucketName is required") } attrs := bucket.GetAttributes() if attrs == nil { - return nil, status.Error(codes.InvalidArgument, "attributes is required") + return nil, errors.BadRequestStatus("attributes is required") } storageAttributes, err := getStorageAttributes(attrs) @@ -660,7 +662,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects }, p.testCredStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } storageState, err := newAwsStoragePersistedState( @@ -669,7 +671,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects }, p.testStorageStateOpts...)..., ) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err) + return nil, errors.BadRequestStatus("error setting up persisted state: %s", err) } opts := []s3Option{} @@ -678,7 +680,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects } client, err := storageState.S3Client(ctx, opts...) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err) + return nil, errors.BadRequestStatus("error getting S3 client: %s", err) } prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix()) @@ -694,7 +696,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects Key: aws.String(prefix), }) if err != nil { - return nil, parseAWSErrCode("error deleting S3 object", err) + return nil, parseS3Error(err, "failed to delete object", req).Err() } return &pb.DeleteObjectsResponse{ ObjectsDeleted: uint32(1), @@ -714,7 +716,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects ContinuationToken: conToken, }) if err != nil { - return nil, parseAWSErrCode("error iterating S3 bucket contents", err) + return nil, parseS3Error(err, "failed to list objects", req).Err() } truncated = res.IsTruncated conToken = res.NextContinuationToken @@ -736,7 +738,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects }, }) if err != nil { - return nil, parseAWSErrCode("error deleting S3 object(s)", err) + return nil, parseS3Error(err, "failed to delete objects", req).Err() } deleted += len(res.Deleted) } @@ -752,15 +754,15 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects // PutObject permissions. This function will read the same file it had uploaded to validate // it has GetObject permissions. This function will read the metadata of the same file it // had uploaded to validate it has GetObjectAttributes permissions. -func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attrs *StorageAttributes, bucket *storagebuckets.StorageBucket) error { +func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attrs *StorageAttributes, bucket *storagebuckets.StorageBucket) *status.Status { if state == nil { - return status.Error(codes.InvalidArgument, "persisted state is required") + return status.New(codes.InvalidArgument, "persisted state is required") } if attrs == nil { - return status.Error(codes.InvalidArgument, "attributes is required") + return status.New(codes.InvalidArgument, "attributes is required") } if bucket == nil { - return status.Error(codes.InvalidArgument, "bucket is required") + return status.New(codes.InvalidArgument, "bucket is required") } opts := []s3Option{} @@ -770,61 +772,76 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr client, err := state.S3Client(ctx, opts...) if err != nil { - return status.Errorf(codes.InvalidArgument, "error getting S3 client: %s", err) + return status.New(codes.InvalidArgument, fmt.Sprintf("error getting S3 client: %s", err)) } + var errs *multierror.Error + permissions := &pb.Permissions{} + // we track the codes returned by ParseAWSError and pick the most severe one by using the max func + objectKey := path.Join(bucket.GetBucketPrefix(), uuid.New().String()) if _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucket.GetBucketName()), Key: aws.String(objectKey), Body: bytes.NewReader([]byte("hashicorp boundary aws plugin access test")), }); err != nil { - return parseAWSErrCode("error failed to put object", err) + var st *status.Status + st, permissions.Write = errors.ParseAWSError(err, "failed to put object") + errs = multierror.Append(errs, st.Err()) + } else { + permissions.Write = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()} } if _, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucket.GetBucketName()), Key: aws.String(objectKey), }); err != nil { - return parseAWSErrCode("error failed to get object", err) + var st *status.Status + st, permissions.Read = errors.ParseAWSError(err, "failed to get object") + errs = multierror.Append(errs, st.Err()) + } else { + permissions.Read = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()} } if _, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucket.GetBucketName()), Key: aws.String(objectKey), }); err != nil { - return parseAWSErrCode("error failed to get head object", err) + var st *status.Status + st, permissions.Read = errors.ParseAWSError(err, "failed to head object") + errs = multierror.Append(errs, st.Err()) } if res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: aws.String(bucket.GetBucketName()), Prefix: aws.String(objectKey), }); err != nil { - return parseAWSErrCode("error failed to list objects", err) + var st *status.Status + st, permissions.Read = errors.ParseAWSError(err, "failed to list object") + errs = multierror.Append(errs, st.Err()) } else if res == nil || len(res.Contents) != 1 || *res.Contents[0].Key != objectKey { - return status.Errorf(codes.InvalidArgument, "list response did not contain the expected key: %+v", res) + return status.New(codes.Aborted, fmt.Sprintf("list response did not contain the expected key: %+v", res)) } if _, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(bucket.GetBucketName()), Key: aws.String(objectKey), }); err != nil { - return parseAWSErrCode("error failed to delete object", err) + var st *status.Status + st, permissions.Delete = errors.ParseAWSError(err, "failed to delete object") + errs = multierror.Append(errs, st.Err()) + } else { + permissions.Delete = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()} + } + + if errs != nil { + st := status.New(codes.FailedPrecondition, fmt.Sprintf("failed to validate provided aws credentials: %v", errs.Unwrap())) + state := &pb.StorageBucketCredentialState{State: permissions} + if st, err = st.WithDetails(state); err != nil { + st = status.New(codes.Internal, err.Error()) + } + return st } return nil } - -// parseAWSErrCode detects if an aws error is a throttle error. -// If the error is a throttle error, then an Unavailable status code -// is returned. Otherwise the default status code is an Internal error. -func parseAWSErrCode(msg string, err error) error { - errCode := codes.Internal - retryErr := retry.ThrottleErrorCode{ - Codes: retry.DefaultThrottleErrorCodes, - } - if retryErr.IsErrorThrottle(err).Bool() { - errCode = codes.Unavailable - } - return status.Errorf(errCode, "%s: %s", msg, err) -} diff --git a/plugin/service/storage/plugin_test.go b/plugin/service/storage/plugin_test.go index 5e07ca4..5674fb7 100644 --- a/plugin/service/storage/plugin_test.go +++ b/plugin/service/storage/plugin_test.go @@ -10,7 +10,7 @@ import ( "encoding/base64" "errors" "fmt" - "io/ioutil" + "io" "os" "path" "testing" @@ -24,13 +24,16 @@ import ( "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" + internal "github.com/hashicorp/boundary-plugin-aws/internal/errors" "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/v2" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { @@ -55,6 +58,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState expectedPersistedData map[string]any }{ { @@ -62,6 +66,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { req: &pb.OnCreateStorageBucketRequest{}, expectedErrContains: "bucket is required", expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "empty bucket name", @@ -70,6 +75,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, expectedErrContains: "bucketName is required", expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "nil attributes", @@ -80,6 +86,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, expectedErrContains: "attributes is required", expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "error reading attributes", @@ -91,6 +98,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, expectedErrContains: "missing required value \"region\"", expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "dynamic credentials without disable credential rotation", @@ -108,6 +116,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, expectedErrContains: "disable_credential_rotation attribute is required when providing a role_arn", expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "persisted state setup error", @@ -120,6 +129,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr), expectedErrCode: codes.InvalidArgument, + expectedDetails: nil, }, { name: "rotation error", @@ -143,21 +153,42 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service unknown: unknown error: error rotating credentials", + expectedErrCode: codes.Unknown, + expectedDetails: nil, }, { - name: "dryRunValidation failed putObject", + name: "dryRunValidation-failed-putObject", req: validRequest(), credOpts: validSTSMock(), storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(errors.New(testPutObjectErr)), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), )), }, - expectedErrContains: "error failed to put object: test error for PutObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation putObject throttle", @@ -166,11 +197,30 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(new(throttleErr)), + testMockS3WithPutObjectError(TestAwsS3Error("ThrottlingException", "PutObject", "throttling exception")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), )), }, - expectedErrContains: "error failed to put object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject", @@ -181,12 +231,31 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(errors.New(testGetObjectErr)), + testMockS3WithGetObjectError(TestAwsS3Error("AccessDenied", "GetObject", "not authorized")), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get object: test error for GetObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject throttle", @@ -197,12 +266,30 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(new(throttleErr)), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithGetObjectError(TestAwsS3Error("ThrottlingException", "GetObject", "throttling exception")), ), ), }, - expectedErrContains: "error failed to get object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject", @@ -214,12 +301,30 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithHeadObjectError(TestAwsS3Error("AccessDenied", "HeadObject", "not authorized")), ), ), }, - expectedErrContains: "error failed to get head object: test error for HeadObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject throttle", @@ -231,12 +336,65 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(new(throttleErr)), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithHeadObjectError(TestAwsS3Error("ThrottlingException", "HeadObject", "throttling exception")), ), ), }, - expectedErrContains: "error failed to get head object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "dryRunValidation failed deleteObject", + req: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithDeleteObjectError(TestAwsS3Error("AccessDenied", "DeleteObject", "not authorized")), + ), + ), + }, + expectedErrContains: "aws service s3: invalid credentials error: failed to delete object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success", @@ -258,6 +416,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { }, } }), + testMockS3WithDeleteObjectsOutput(&s3.DeleteObjectsOutput{}), ), ), }, @@ -384,15 +543,34 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, } resp, err := p.OnCreateStorageBucket(context.Background(), tc.req) if tc.expectedErrContains != "" { - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) + require.Error(err) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, true) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -411,10 +589,6 @@ 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{ @@ -448,6 +622,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { expectCredsLastRotatedTime bool expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "nil new bucket", @@ -587,7 +762,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { }, }, expectedErrContains: "cannot disable rotation for already-rotated credentials", - expectedErrCode: codes.FailedPrecondition, + expectedErrCode: codes.InvalidArgument, }, { name: "attempt to enable credential rotation with dynamic credentials", @@ -682,8 +857,8 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr), - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service unknown: unknown error: failed to delete access key", + expectedErrCode: codes.Unknown, }, { name: "rotation error", @@ -721,8 +896,8 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr), - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service unknown: unknown error: error rotating credentials", + expectedErrCode: codes.Unknown, }, { name: "dryRunValidation failed putObject", @@ -731,11 +906,31 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(errors.New(testPutObjectErr)), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), )), }, - expectedErrContains: "error failed to put object: test error for PutObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed putObject throttle", @@ -744,11 +939,30 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(new(throttleErr)), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), + testMockS3WithPutObjectError(TestAwsS3Error("ThrottlingException", "PutObject", "throttling exception")), )), }, - expectedErrContains: "error failed to put object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject", @@ -759,12 +973,31 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(errors.New(testGetObjectErr)), + testMockS3WithGetObjectError(TestAwsS3Error("AccessDenied", "GetObject", "not authorized")), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get object: test error for GetObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject throttle", @@ -775,12 +1008,30 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(new(throttleErr)), + testMockS3WithGetObjectError(TestAwsS3Error("ThrottlingException", "GetObject", "throttling exception")), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject", @@ -792,12 +1043,30 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("AccessDenied", "HeadObject", "not authorized")), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get head object: test error for HeadObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject throttle", @@ -809,12 +1078,29 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(new(throttleErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("ThrottlingException", "HeadObject", "throttling exception")), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get head object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success with credential rotation", @@ -954,14 +1240,32 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithHeadObjectError(fmt.Errorf("head object failed oops")), + testMockS3WithHeadObjectError(TestAwsS3Error("AccessDenied", "HeadObject", "not authorized")), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "head object failed oops", - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service s3: invalid credentials error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dynamic to dynamic credentials success", @@ -1044,14 +1348,32 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithPutObjectError(fmt.Errorf("put object failed oops")), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "put object failed oops", - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success static non rotated to dynamic credentials", @@ -1164,8 +1486,8 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: "delete access key fail oops", - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service unknown: unknown error: failed to delete access key", + expectedErrCode: codes.Unknown, }, { name: "success static rotated to dynamic credentials", @@ -1262,7 +1584,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithPutObjectError(fmt.Errorf("put object fail oops")), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { @@ -1277,8 +1599,17 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), ), }, - expectedErrContains: "put object fail oops", - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success dynamic to static non-rotated credentials", @@ -1407,8 +1738,8 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: "create access key fail oops", - expectedErrCode: codes.InvalidArgument, + expectedErrContains: "aws service unknown: unknown error: error rotating credentials", + expectedErrCode: codes.Unknown, }, { name: "success dynamic to static rotated credentials", @@ -1500,16 +1831,34 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, } 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()) + require.Error(err) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, true) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -1541,6 +1890,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "nil bucket", @@ -1623,8 +1973,8 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) { ), }), }, - expectedErrContains: fmt.Sprintf("error removing rotated credentials during storage bucket deletion: error deleting old access key: %s", testDeleteAccessKeyErr), - expectedErrCode: codes.Aborted, + expectedErrContains: "aws service unknown: unknown error: failed to delete access key", + expectedErrCode: codes.Unknown, }, { name: "success", @@ -1678,16 +2028,34 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, } resp, err := p.OnDeleteStorageBucket(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()) + require.Error(err) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, true) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -1720,6 +2088,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "empty object key", @@ -1799,12 +2168,21 @@ func TestStoragePlugin_HeadObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("AccessDenied", "HeadObject", "not authorized")), ), ), }, - expectedErrContains: "error getting head object from s3: test error for HeadObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to head object", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "headObject throttle error", @@ -1814,12 +2192,43 @@ func TestStoragePlugin_HeadObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithHeadObjectError(new(throttleErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("ThrottlingException", "HeadObject", "throttling exception")), ), ), }, - expectedErrContains: "error getting head object from s3: ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to head object", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "headObject not found error", + req: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithHeadObjectError(TestAwsS3Error("NoSuchKey", "HeadObject", "resource not found")), + ), + ), + }, + expectedErrContains: "aws s3 error: failed to head object", + expectedErrCode: codes.NotFound, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success", @@ -1875,7 +2284,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, @@ -1883,9 +2292,26 @@ func TestStoragePlugin_HeadObject(t *testing.T) { resp, err := p.HeadObject(context.Background(), tc.req) if tc.expectedErrContains != "" { require.Error(err) - require.Nil(resp) - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, false) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -1917,6 +2343,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "nil bucket", @@ -1984,11 +2411,31 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(errors.New(testPutObjectErr)), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), )), }, - expectedErrContains: "error failed to put object: test error for PutObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed putObject throttle", @@ -1997,11 +2444,30 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { storageOpts: []awsStoragePersistedStateOption{ withTestS3APIFunc(newTestMockS3( nil, - testMockS3WithPutObjectError(new(throttleErr)), + testMockS3WithPutObjectError(TestAwsS3Error("ThrottlingException", "PutObject", "throttling exception")), + testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), )), }, - expectedErrContains: "error failed to put object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to put object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject", @@ -2012,12 +2478,31 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(errors.New(testGetObjectErr)), + testMockS3WithGetObjectError(TestAwsS3Error("AccessDenied", "GetObject", "not authorized")), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get object: test error for GetObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed getObject throttle", @@ -2028,12 +2513,30 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { newTestMockS3( nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), - testMockS3WithGetObjectError(new(throttleErr)), + testMockS3WithGetObjectError(TestAwsS3Error("ThrottlingException", "GetObject", "throttling exception")), + testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to get object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject", @@ -2045,12 +2548,30 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(errors.New(testHeadObjectErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("AccessDenied", "HeadObject", "not authorized")), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get head object: test error for HeadObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "dryRunValidation failed headObject throttle", @@ -2062,12 +2583,29 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { nil, testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}), testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}), - testMockS3WithHeadObjectError(new(throttleErr)), + testMockS3WithHeadObjectError(TestAwsS3Error("ThrottlingException", "HeadObject", "throttling exception")), + testMockS3WithListObjectsV2OutputFunc(func(i *s3.ListObjectsV2Input) *s3.ListObjectsV2Output { + return &s3.ListObjectsV2Output{ + Contents: []s3types.Object{ + { + Key: i.Prefix, + }, + }, + } + }), ), ), }, - expectedErrContains: "error failed to get head object: ThrottlingException", - expectedErrCode: codes.Unavailable, + expectedErrContains: "aws service s3: throttling error: failed to head object", + expectedErrCode: codes.FailedPrecondition, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success", @@ -2133,7 +2671,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, @@ -2141,9 +2679,26 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) { resp, err := p.ValidatePermissions(context.Background(), tc.req) if tc.expectedErrContains != "" { require.Error(err) - require.Nil(resp) - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, true) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, true) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -2175,6 +2730,7 @@ func TestStoragePlugin_GetObject(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "empty object key", @@ -2254,12 +2810,91 @@ func TestStoragePlugin_GetObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithGetObjectError(errors.New(testGetObjectErr)), + testMockS3WithGetObjectError(TestAwsS3Error("AccessDenied", "GetObject", "not authorized")), ), ), }, - expectedErrContains: "error getting object from s3: test error for GetObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to get object", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "getObject no such key", + req: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithGetObjectError(TestAwsS3Error("NoSuchKey", "GetObject", "resource not found")), + ), + ), + }, + expectedErrContains: "aws s3 error: failed to get object", + expectedErrCode: codes.NotFound, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "getObject invalid state", + req: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithGetObjectError(TestAwsS3Error("InvalidObjectState", "GetObject", "resource is in cold storage")), + ), + ), + }, + expectedErrContains: "aws s3 error: failed to get object", + expectedErrCode: codes.NotFound, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "getObject no such bucket", + req: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithGetObjectError(TestAwsS3Error("NoSuchBucket", "GetObject", "bucket does not exist")), + ), + ), + }, + expectedErrContains: "aws s3 error: failed to get object", + expectedErrCode: codes.NotFound, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "bucket does not exist", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "getObject throttle error", @@ -2269,12 +2904,20 @@ func TestStoragePlugin_GetObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithGetObjectError(new(throttleErr)), + testMockS3WithGetObjectError(TestAwsS3Error("ThrottlingException", "GetObject", "throttling exception")), ), ), }, - expectedErrContains: "error getting object from s3: ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to get object", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Read: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "with chunk size", @@ -2290,7 +2933,7 @@ func TestStoragePlugin_GetObject(t *testing.T) { newTestMockS3( nil, testMockS3WithGetObjectOutput(&s3.GetObjectOutput{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))), + Body: io.NopCloser(bytes.NewReader([]byte("test"))), }), ), ), @@ -2306,7 +2949,7 @@ func TestStoragePlugin_GetObject(t *testing.T) { newTestMockS3( nil, testMockS3WithGetObjectOutput(&s3.GetObjectOutput{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))), + Body: io.NopCloser(bytes.NewReader([]byte("test"))), }), ), ), @@ -2334,7 +2977,7 @@ func TestStoragePlugin_GetObject(t *testing.T) { newTestMockS3( nil, testMockS3WithGetObjectOutput(&s3.GetObjectOutput{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("test"))), + Body: io.NopCloser(bytes.NewReader([]byte("test"))), }), ), ), @@ -2345,7 +2988,7 @@ func TestStoragePlugin_GetObject(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, @@ -2353,8 +2996,27 @@ func TestStoragePlugin_GetObject(t *testing.T) { stream := newGetObjectStreamMock() err := p.GetObject(tc.req, stream) if tc.expectedErrContains != "" { - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) + require.Error(err) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, false) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -2408,6 +3070,7 @@ func TestStoragePlugin_PutObject(t *testing.T) { expectedObject []byte expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState }{ { name: "missing request", @@ -2497,7 +3160,7 @@ func TestStoragePlugin_PutObject(t *testing.T) { Path: path.Join(td, "file-does-not-exist"), }, expectedErrContains: "failed to open file", - expectedErrCode: codes.Internal, + expectedErrCode: codes.InvalidArgument, }, { name: "path is a directory", @@ -2596,12 +3259,45 @@ func TestStoragePlugin_PutObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithPutObjectError(errors.New(testPutObjectErr)), + testMockS3WithPutObjectError(TestAwsS3Error("AccessDenied", "PutObject", "not authorized")), ), ), }, - expectedErrContains: "error putting object into s3: test error for PutObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to put object", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, + }, + { + name: "putObject no such bucket", + request: validRequest(), + credOpts: validSTSMock(), + storageOpts: []awsStoragePersistedStateOption{ + withTestS3APIFunc( + newTestMockS3( + nil, + testMockS3WithPutObjectError(TestAwsS3Error("NoSuchBucket", "PutObject", "bucket does not exist")), + ), + ), + }, + expectedErrContains: "aws s3 error: failed to put object", + expectedErrCode: codes.NotFound, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "bucket does not exist", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "throttle error", @@ -2611,12 +3307,20 @@ func TestStoragePlugin_PutObject(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithPutObjectError(new(throttleErr)), + testMockS3WithPutObjectError(TestAwsS3Error("ThrottlingException", "PutObject", "throttling exception")), ), ), }, - expectedErrContains: "error putting object into s3: ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to put object", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Write: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "missing checksum from aws", @@ -2635,6 +3339,7 @@ func TestStoragePlugin_PutObject(t *testing.T) { }, expectedErrContains: "missing checksum response from aws", expectedErrCode: codes.Internal, + expectedDetails: nil, }, { name: "mismatched checksum", @@ -2659,7 +3364,8 @@ func TestStoragePlugin_PutObject(t *testing.T) { } }(), expectedErrContains: "mismatched checksum", - expectedErrCode: codes.Internal, + expectedErrCode: codes.Aborted, + expectedDetails: nil, }, { name: "valid file", @@ -2727,7 +3433,7 @@ func TestStoragePlugin_PutObject(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, @@ -2740,8 +3446,26 @@ func TestStoragePlugin_PutObject(t *testing.T) { resp, err := p.PutObject(context.Background(), tc.request) if tc.expectedErrContains != "" { require.Error(err) - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(tc.expectedErrCode.String(), status.Code(err).String()) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, false) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) @@ -2794,6 +3518,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { storageOpts []awsStoragePersistedStateOption expectedErrContains string expectedErrCode codes.Code + expectedDetails *pb.StorageBucketCredentialState expected uint32 }{ { @@ -2874,12 +3599,21 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithDeleteObjectError(errors.New(testDeleteObjectErr)), + testMockS3WithDeleteObjectError(TestAwsS3Error("AccessDenied", "DeleteObject", "not authorized")), ), ), }, - expectedErrContains: "error deleting S3 object: test error for DeleteObject", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to delete object", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "ListObjectV2 error", @@ -2889,12 +3623,21 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithListObjectsV2Error(errors.New(testListObjectV2Err)), + testMockS3WithListObjectsV2Error(TestAwsS3Error("AccessDenied", "ListObjectsV2", "not authorized")), ), ), }, - expectedErrContains: "error iterating S3 bucket contents: test error for ListObjectV2", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to list objects", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "DeleteObjects error", @@ -2907,17 +3650,26 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { testMockS3WithListObjectsV2Output(&s3.ListObjectsV2Output{ IsTruncated: false, Contents: []s3types.Object{ - s3types.Object{ + { Key: aws.String("abc/abc"), }, }, }), - testMockS3WithDeleteObjectsError(errors.New(testDeleteObjectsErr)), + testMockS3WithDeleteObjectsError(TestAwsS3Error("AccessDenied", "DeleteObjects", "not authorized")), ), ), }, - expectedErrContains: "error deleting S3 object(s): test error for DeleteObjects", - expectedErrCode: codes.Internal, + expectedErrContains: "aws service s3: invalid credentials error: failed to delete objects", + expectedErrCode: codes.PermissionDenied, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_ERROR, + ErrorDetails: "not authorized", + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "DeleteObject throttle error", @@ -2927,12 +3679,20 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithDeleteObjectError(new(throttleErr)), + testMockS3WithDeleteObjectError(TestAwsS3Error("ThrottlingException", "DeleteObject", "throttling exception")), ), ), }, - expectedErrContains: "error deleting S3 object: ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to delete object", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "ListObjectsV2 throttle error", @@ -2942,12 +3702,20 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { withTestS3APIFunc( newTestMockS3( nil, - testMockS3WithListObjectsV2Error(new(throttleErr)), + testMockS3WithListObjectsV2Error(TestAwsS3Error("ThrottlingException", "ListObjectsV2", "throttling exception")), ), ), }, - expectedErrContains: "error iterating S3 bucket contents: ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to list objects", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "DeleteObjects throttle error", @@ -2965,12 +3733,20 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { }, }, }), - testMockS3WithDeleteObjectsError(new(throttleErr)), + testMockS3WithDeleteObjectsError(TestAwsS3Error("ThrottlingException", "DeleteObjects", "throttling exception")), ), ), }, - expectedErrContains: "error deleting S3 object(s): ThrottlingException", + expectedErrContains: "aws service s3: throttling error: failed to delete objects", expectedErrCode: codes.Unavailable, + expectedDetails: &pb.StorageBucketCredentialState{ + State: &pb.Permissions{ + Delete: &pb.Permission{ + State: pb.StateType_STATE_TYPE_UNKNOWN, + CheckedAt: timestamppb.Now(), + }, + }, + }, }, { name: "success", @@ -2997,22 +3773,16 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { testMockS3WithListObjectsV2Output(&s3.ListObjectsV2Output{ IsTruncated: false, Contents: []s3types.Object{ - s3types.Object{ - Key: aws.String("abc/abc1"), - }, - s3types.Object{ - Key: aws.String("abc/abc2"), - }, - s3types.Object{ - Key: aws.String("abc/abc3"), - }, + {Key: aws.String("abc/abc1")}, + {Key: aws.String("abc/abc2")}, + {Key: aws.String("abc/abc3")}, }, }), testMockS3WithDeleteObjectsOutput(&s3.DeleteObjectsOutput{ Deleted: []s3types.DeletedObject{ - s3types.DeletedObject{}, - s3types.DeletedObject{}, - s3types.DeletedObject{}, + {}, + {}, + {}, }, }), ), @@ -3072,7 +3842,7 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - require := require.New(t) + require, assert := require.New(t), assert.New(t) p := &StoragePlugin{ testCredStateOpts: tc.credOpts, testStorageStateOpts: tc.storageOpts, @@ -3080,8 +3850,26 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) { res, err := p.DeleteObjects(context.Background(), tc.req) if tc.expectedErrContains != "" { require.Error(err) - require.Contains(err.Error(), tc.expectedErrContains) - require.Equal(status.Code(err).String(), tc.expectedErrCode.String()) + assert.Contains(err.Error(), tc.expectedErrContains) + assert.Equal(tc.expectedErrCode, status.Code(err)) + + st, ok := status.FromError(err) + require.True(ok) + if tc.expectedDetails != nil { + var state *pb.StorageBucketCredentialState + for _, detail := range st.Details() { + if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok { + state = errDetail + break + } + } + require.NotNil(state) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, false) + internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, false) + } else { + assert.Len(st.Details(), 0) + } return } require.NoError(err) diff --git a/plugin/service/storage/state.go b/plugin/service/storage/state.go index d130f25..1cab6bf 100644 --- a/plugin/service/storage/state.go +++ b/plugin/service/storage/state.go @@ -84,7 +84,6 @@ func newAwsStoragePersistedState(opts ...awsStoragePersistedStateOption) (*awsSt } func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersisted, error) { - data, err := structpb.NewStruct(s.ToMap()) if err != nil { return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err) diff --git a/plugin/service/storage/testing.go b/plugin/service/storage/testing.go index d1ef852..e6c4822 100644 --- a/plugin/service/storage/testing.go +++ b/plugin/service/storage/testing.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" "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" @@ -28,25 +29,8 @@ const ( testGetUserErr = "test error for GetUser" testGetCallerIdentityErr = "test error for GetCallerIdentity" testDeleteAccessKeyErr = "test error for DeleteAccessKey" - testGetObjectErr = "test error for GetObject" - testPutObjectErr = "test error for PutObject" - testHeadObjectErr = "test error for HeadObject" - testListObjectV2Err = "test error for ListObjectV2" - testDeleteObjectErr = "test error for DeleteObject" - testDeleteObjectsErr = "test error for DeleteObjects" ) -// throttleErr is a mocked error used for testing the aws s3 client -type throttleErr struct{} - -func (e *throttleErr) ErrorCode() string { - return "ThrottlingException" -} - -func (e *throttleErr) Error() string { - return "ThrottlingException" -} - type testMockS3State struct { GetObjectCalled bool GetObjectInputParams *s3.GetObjectInput @@ -414,3 +398,16 @@ func validSTSMock() []credential.AwsCredentialPersistedStateOption { }), } } + +// TestAwsS3Error returns an s3 api error +func TestAwsS3Error(code, op, msg string) error { + return &smithy.OperationError{ + ServiceID: "s3", + OperationName: op, + Err: &smithy.GenericAPIError{ + Code: code, + Message: msg, + Fault: smithy.FaultServer, + }, + } +}