Compare commits
5
Commits
release/0.13.x
...
v0.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e5368b71 | ||
|
|
07f015be28 | ||
|
|
334732967d | ||
|
|
7bf9fe2ecf | ||
|
|
c6687840d5 |
No files matched your search
@@ -5,15 +5,32 @@ Boundary](https://www.boundaryproject.io/).
|
||||
|
||||
## Credential Rotation
|
||||
|
||||
Although credentials are stored encrypted within Boundary, by default this
|
||||
This is the following priority for the credential chain:
|
||||
static, assume role, environment variables.
|
||||
|
||||
### Static Credentials
|
||||
|
||||
Although static credentials are stored encrypted within Boundary, by default this
|
||||
plugin will attempt to rotate credentials when they are supplied through the
|
||||
`secrets` object. The given credentials will be used to create a new credential,
|
||||
and then the given credential will be revoked. In this way, after rotation,
|
||||
only Boundary knows the client secret in use by this plugin.
|
||||
only Boundary knows the client secret in use by this plugin. More information
|
||||
about AWS static credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html).
|
||||
|
||||
Credential rotation can be turned off by setting the
|
||||
`disable_credential_rotation` attribute to `true`.
|
||||
|
||||
### Assume Role Credentials
|
||||
|
||||
This plugin will attempt to assume a role when a `role_arn` is supplied through the
|
||||
`attributes` object. More information about assume an AWS role can be found [here](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). This feature only works when the plugin is running on a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
### Environment Credentials
|
||||
|
||||
This plugin will attempt to retrieve credentials from environment variables. More
|
||||
information about environment variables for AWS credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html). This feature only works when the plugin is running on
|
||||
a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
||||
|
||||
## Dynamic Hosts
|
||||
|
||||
This plugin supports dynamically sourcing hosts from Amazon EC2.
|
||||
|
||||
@@ -4,14 +4,16 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.44.80
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1
|
||||
github.com/aws/smithy-go v1.13.5
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.33
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
@@ -22,18 +24,17 @@ require (
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
|
||||
@@ -4,11 +4,12 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI=
|
||||
github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw=
|
||||
@@ -17,20 +18,25 @@ github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE=
|
||||
@@ -48,7 +54,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
@@ -80,7 +85,6 @@ github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca4458
|
||||
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24/go.mod h1:EQPLoX6CONA9BSYUovTQBHfPGE91g7wOxv03sO29FzY=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.4 h1:8z+Zi0uLYXONkxwWb/D7O5t1IUucB6Ft3uvSOa950bM=
|
||||
@@ -89,8 +93,8 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2 h1:kWg2vyKl7BRXrNxYziqDJ55n+vtOQ1QsGORjzoeB+uM=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.2.2/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
|
||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
|
||||
@@ -128,14 +132,10 @@ 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=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -169,7 +169,6 @@ github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -215,12 +214,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
@@ -17,33 +20,47 @@ type CredentialAttributes struct {
|
||||
|
||||
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
|
||||
DisableCredentialRotation bool
|
||||
|
||||
// RoleArn is the role arn associated with the aws credentials
|
||||
RoleArn string
|
||||
|
||||
// RoleExternalId is the external id associated with the aws credentials
|
||||
RoleExternalId string
|
||||
|
||||
// RoleSessionName is the session name associated with the aws credentials
|
||||
RoleSessionName string
|
||||
|
||||
// RoleTags is the tags associated with the aws credentials
|
||||
RoleTags map[string]string
|
||||
}
|
||||
|
||||
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
|
||||
// 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 of the following fields are missing from the protobuf struct input or have
|
||||
// invalid value types: access_key_id, secret_access_key. An error is returned if
|
||||
// any unrecognized fields are found in the protobuf struct input.
|
||||
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
|
||||
unknownFields := values.StructFields(in)
|
||||
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
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
}
|
||||
}
|
||||
|
||||
unknownFields := values.StructFields(secrets)
|
||||
badFields := make(map[string]string)
|
||||
|
||||
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, required)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAccessKeyId)
|
||||
|
||||
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstSecretAccessKey)
|
||||
|
||||
if region == "" {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||
}
|
||||
|
||||
// the creds_last_rotated_time field will be found in the input struct for
|
||||
// persisted secrets, this value is not needed for creating the CredentialsConfig
|
||||
delete(unknownFields, ConstCredsLastRotatedTime)
|
||||
@@ -55,11 +72,32 @@ func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.Credenti
|
||||
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
|
||||
}
|
||||
|
||||
return awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
awsutil.WithRegion(region),
|
||||
)
|
||||
opts := []awsutil.Option{
|
||||
awsutil.WithEnvironmentCredentials(true),
|
||||
}
|
||||
if accessKey != "" && secretKey != "" {
|
||||
opts = append(opts,
|
||||
awsutil.WithAccessKey(accessKey),
|
||||
awsutil.WithSecretKey(secretKey),
|
||||
)
|
||||
}
|
||||
if attrs.Region != "" {
|
||||
opts = append(opts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleArn != "" {
|
||||
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
|
||||
return awsutil.NewCredentialsConfig(opts...)
|
||||
}
|
||||
|
||||
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
|
||||
@@ -85,6 +123,26 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
|
||||
}
|
||||
|
||||
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
|
||||
}
|
||||
|
||||
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
|
||||
}
|
||||
|
||||
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
|
||||
}
|
||||
|
||||
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||
}
|
||||
@@ -92,5 +150,9 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
||||
return &CredentialAttributes{
|
||||
Region: region,
|
||||
DisableCredentialRotation: disableCredentialRotation,
|
||||
RoleArn: roleArn,
|
||||
RoleExternalId: roleExternalId,
|
||||
RoleSessionName: roleSessionName,
|
||||
RoleTags: roleTags,
|
||||
}, nil
|
||||
}
|
||||
@@ -61,6 +61,28 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
DisableCredentialRotation: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role",
|
||||
in: map[string]any{
|
||||
ConstRegion: "us-west-2",
|
||||
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
ConstRoleExternalId: "1234567890",
|
||||
ConstRoleSessionName: "test-session",
|
||||
ConstRoleTags: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: false,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -89,44 +111,91 @@ func TestGetCredentialAttributes(t *testing.T) {
|
||||
func TestGetCredentialsConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
region string
|
||||
secrets *structpb.Struct
|
||||
required bool
|
||||
attrs *CredentialAttributes
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing access_key_id",
|
||||
in: map[string]any{},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
name: "no secrets",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing secret_access_key",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
name: "with static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "missing required value \"secret_access_key\"",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing access key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
required: true,
|
||||
expectedErrContains: "secrets.access_key_id: missing required value",
|
||||
},
|
||||
{
|
||||
name: "missing secret key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErrContains: "secrets.secret_access_key: missing required value",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "valid ignore creds_last_rotated_time",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
@@ -134,16 +203,56 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
name: "with assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
region: "us-west-2",
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with static credential & assume role",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -153,10 +262,7 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := GetCredentialsConfig(input, tc.region)
|
||||
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
@@ -168,6 +274,10 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
require.Equal(tc.expected.AccessKey, actual.AccessKey)
|
||||
require.Equal(tc.expected.SecretKey, actual.SecretKey)
|
||||
require.Equal(tc.expected.Region, actual.Region)
|
||||
require.Equal(tc.expected.RoleARN, actual.RoleARN)
|
||||
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
|
||||
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
|
||||
require.Equal(tc.expected.RoleTags, actual.RoleTags)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
const (
|
||||
@@ -15,4 +18,16 @@ const (
|
||||
|
||||
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
|
||||
ConstCredsLastRotatedTime = "creds_last_rotated_time"
|
||||
|
||||
// ConstRoleArn is the key for assuming a IAM role.
|
||||
ConstRoleArn = "role_arn"
|
||||
|
||||
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
|
||||
ConstRoleExternalId = "role_external_id"
|
||||
|
||||
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
|
||||
ConstRoleSessionName = "role_session_name"
|
||||
|
||||
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
||||
ConstRoleTags = "role_tags"
|
||||
)
|
||||
+97
-113
@@ -1,8 +1,12 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
@@ -22,19 +26,13 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
|
||||
|
||||
// AwsCredentialPersistedState is the persisted state for the AWS credential.
|
||||
type AwsCredentialPersistedState struct {
|
||||
// AccessKeyId is the access key id for the AWS credential.
|
||||
AccessKeyId string
|
||||
// SecretAccessKey is the secret access key for the AWS credential.
|
||||
SecretAccessKey string
|
||||
// CredentialsConfig is the credential configuration for the AWS credential.
|
||||
CredentialsConfig *awsutil.CredentialsConfig
|
||||
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
|
||||
CredsLastRotatedTime time.Time
|
||||
|
||||
// testOpts are options that should be used for testing only
|
||||
testOpts []awsutil.Option
|
||||
|
||||
// Region is not a part of persisted state but if set is passed to clients
|
||||
// created with this state
|
||||
region string
|
||||
}
|
||||
|
||||
// WithStateTestOpts enables unit testing different edge cases
|
||||
@@ -47,31 +45,19 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
|
||||
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
|
||||
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
|
||||
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.AccessKeyId != "" {
|
||||
return errors.New("access key id already set")
|
||||
if s.CredentialsConfig != nil {
|
||||
return errors.New("credentials config already set")
|
||||
}
|
||||
|
||||
s.AccessKeyId = x
|
||||
s.CredentialsConfig = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSecretAccessKey sets the value for SecretAccessKey in the storage persisted state.
|
||||
func WithSecretAccessKey(x string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.SecretAccessKey != "" {
|
||||
return errors.New("secret access key already set")
|
||||
}
|
||||
|
||||
s.SecretAccessKey = x
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the storage persisted state.
|
||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
|
||||
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
@@ -83,18 +69,6 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegion sets the value for region in the storage persisted state.
|
||||
func WithRegion(with string) AwsCredentialPersistedStateOption {
|
||||
return func(s *AwsCredentialPersistedState) error {
|
||||
if s.region != "" {
|
||||
return errors.New("region already set")
|
||||
}
|
||||
|
||||
s.region = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
|
||||
// Supported options include: WithAccessKeyId, WithSecretAccessKey
|
||||
// WithCredsLastRotatedTime, & WithRegion.
|
||||
@@ -105,27 +79,19 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ValidateCreds takes the access key and secret key from the persisted state
|
||||
// ValidateCreds takes the credentials configuration from the persisted state
|
||||
// and runs sts.GetCallerIdentity for the current credentials, which is done
|
||||
// to check that the credentials are valid.
|
||||
func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
||||
if _, err := s.CredentialsConfig.GetCallerIdentity(s.testOpts...); err != nil {
|
||||
return fmt.Errorf("error validating credentials: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -135,42 +101,27 @@ func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
|
||||
// still result in an extraneous access key existing.
|
||||
func (s *AwsCredentialPersistedState) RotateCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if err := c.RotateKeys(append([]awsutil.Option{
|
||||
if err := s.CredentialsConfig.RotateKeys(append([]awsutil.Option{
|
||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||
}, s.testOpts...)...); err != nil {
|
||||
return fmt.Errorf("error rotating credentials: %w", err)
|
||||
}
|
||||
|
||||
s.AccessKeyId = c.AccessKey
|
||||
s.SecretAccessKey = c.SecretKey
|
||||
s.CredsLastRotatedTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceCreds replaces the access key in the state with a new key.
|
||||
// If the existing key was rotated at any point in time, it is
|
||||
// deleted first, otherwise it's left alone.
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||
if accessKeyId == "" {
|
||||
return errors.New("access key id cannot be empty")
|
||||
func (s *AwsCredentialPersistedState) ReplaceCreds(credentialsConfig *awsutil.CredentialsConfig) error {
|
||||
if credentialsConfig == nil {
|
||||
return errors.New("missing new credentials config")
|
||||
}
|
||||
|
||||
if secretAccessKey == "" {
|
||||
return errors.New("secret access key cannot be empty")
|
||||
}
|
||||
|
||||
if accessKeyId == s.AccessKeyId {
|
||||
return errors.New("attempting to replace access key with the same one")
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if !s.CredsLastRotatedTime.IsZero() {
|
||||
@@ -183,8 +134,7 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
|
||||
}
|
||||
|
||||
// Set the new attributes and clear the rotated time.
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -193,23 +143,17 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey
|
||||
// ID, secret access key, and rotation time fields are zeroed out in
|
||||
// the state just to ensure that they cannot be re-used after.
|
||||
func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading credentials: %w", err)
|
||||
if s.CredentialsConfig == nil {
|
||||
return errors.New("missing credentials config")
|
||||
}
|
||||
|
||||
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
|
||||
if err := s.CredentialsConfig.DeleteAccessKey(s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
|
||||
// Determine if the deletion error was due to a missing
|
||||
// resource. If it was, just pass it.
|
||||
var awsErr awserr.Error
|
||||
if errors.As(err, &awsErr) {
|
||||
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredentialsConfig = nil
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -219,8 +163,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.AccessKeyId = ""
|
||||
s.SecretAccessKey = ""
|
||||
s.CredentialsConfig = nil
|
||||
s.CredsLastRotatedTime = time.Time{}
|
||||
return nil
|
||||
}
|
||||
@@ -228,50 +171,50 @@ func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||
// GetSession returns a configured AWS session for the credentials in
|
||||
// the state.
|
||||
func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) {
|
||||
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||
awsutil.WithAccessKey(s.AccessKeyId),
|
||||
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||
awsutil.WithRegion(s.region),
|
||||
}, s.testOpts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetSession()
|
||||
return s.CredentialsConfig.GetSession(s.testOpts...)
|
||||
}
|
||||
|
||||
// ToMap returns a map of the credentials stored in the persisted state,
|
||||
// which includes the following keys: access_key_id, secret_access_key,
|
||||
// & creds_last_rotated_time
|
||||
// ToMap returns a map of the credentials stored in the persisted state.
|
||||
// ToMap will return an empty map for temporary credentials. ToMap will
|
||||
// return a map for long-term credentials with following keys:
|
||||
// access_key_id, secret_access_key & creds_last_rotated_time
|
||||
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||
if HasDynamicCredentials(s.CredentialsConfig.AccessKey) {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
ConstAccessKeyId: s.AccessKeyId,
|
||||
ConstSecretAccessKey: s.SecretAccessKey,
|
||||
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
|
||||
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
|
||||
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
||||
// and returns a AwsCredentialPersistedState 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: access_key_id, secret_access_key, &
|
||||
// creds_last_rotated_time.
|
||||
func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
if in == nil {
|
||||
return nil, errors.New("missing persisted secrets")
|
||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
secrets = &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}
|
||||
}
|
||||
|
||||
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||
if attrs == nil {
|
||||
return nil, fmt.Errorf("missing credential attributes")
|
||||
}
|
||||
|
||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
|
||||
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
}
|
||||
@@ -281,9 +224,50 @@ func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCreden
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.AccessKeyId = accessKeyId
|
||||
s.SecretAccessKey = secretAccessKey
|
||||
awsOpts := append([]awsutil.Option{}, s.testOpts...)
|
||||
if accessKeyId != "" && secretAccessKey != "" {
|
||||
awsOpts = append(awsOpts,
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
}
|
||||
if attrs.Region != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
|
||||
}
|
||||
if attrs.RoleArn != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
|
||||
}
|
||||
if attrs.RoleExternalId != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
||||
}
|
||||
if attrs.RoleSessionName != "" {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
||||
}
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.CredentialsConfig = credentialsConfig
|
||||
s.CredsLastRotatedTime = credsLastRotatedTime
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HasDynamicCredentials returns true if the access key is a dynamic credential type.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with ASIA are temporary credentials access keys that you create using AWS STS operations.
|
||||
func HasDynamicCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "ASIA")
|
||||
}
|
||||
|
||||
// HasStaticCredentials returns true if the access key is a static credential type.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
||||
// Access key IDs beginning with AKIA are long-term access keys for an IAM user or an AWS account root user.
|
||||
func HasStaticCredentials(accessKey string) bool {
|
||||
return strings.HasPrefix(accessKey, "AKIA")
|
||||
}
|
||||
+437
-193
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
@@ -33,21 +36,22 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
expectedErr: testOptionErr,
|
||||
},
|
||||
{
|
||||
name: "access key id",
|
||||
name: "with credentials config",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("foobar"),
|
||||
WithCredentialsConfig(
|
||||
&awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret access key",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithSecretAccessKey("bazqux"),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -60,29 +64,12 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "region key",
|
||||
name: "double set credentials config",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithRegion("foobar"),
|
||||
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
|
||||
WithCredentialsConfig(&awsutil.CredentialsConfig{}),
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
region: "foobar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double set access key id",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("foobar"),
|
||||
WithAccessKeyId("onetwo"),
|
||||
},
|
||||
expectedErr: "access key id already set",
|
||||
},
|
||||
{
|
||||
name: "double set secret access key",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithSecretAccessKey("bazqux"),
|
||||
WithSecretAccessKey("threefour"),
|
||||
},
|
||||
expectedErr: "secret access key already set",
|
||||
expectedErr: "credentials config already set",
|
||||
},
|
||||
{
|
||||
name: "double set rotation time",
|
||||
@@ -92,14 +79,6 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
},
|
||||
expectedErr: "last rotation time already set",
|
||||
},
|
||||
{
|
||||
name: "double set region",
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithRegion("foobar"),
|
||||
WithRegion("barfoo"),
|
||||
},
|
||||
expectedErr: "region already set",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -113,7 +92,13 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
|
||||
if tc.expected.CredentialsConfig != nil {
|
||||
require.NotNil(actual.CredentialsConfig)
|
||||
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,43 +106,95 @@ func TestNewAwsCredentialPersistedState(t *testing.T) {
|
||||
func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
secrets *structpb.Struct
|
||||
attrs *CredentialAttributes
|
||||
opts []AwsCredentialPersistedStateOption
|
||||
expected *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no secrets",
|
||||
in: nil,
|
||||
expectedErr: "missing persisted secrets",
|
||||
name: "missing credential attributes",
|
||||
expectedErr: "missing credential attributes",
|
||||
},
|
||||
{
|
||||
name: "missing access key ID",
|
||||
in: map[string]any{},
|
||||
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
|
||||
},
|
||||
{
|
||||
name: "missing secret access key",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
name: "missing secrets",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing long-term access key id",
|
||||
secrets: &structpb.Struct{},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing long-term secret access key",
|
||||
secrets: &structpb.Struct{},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with long-term static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
},
|
||||
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
|
||||
},
|
||||
{
|
||||
name: "bad last rotated time",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "notatime",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("notatime"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
},
|
||||
{
|
||||
name: "option error",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
func(s *AwsCredentialPersistedState) error {
|
||||
@@ -168,14 +205,22 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good with non-zero timestamp",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
CredsLastRotatedTime: func() time.Time {
|
||||
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
|
||||
if err != nil {
|
||||
@@ -187,33 +232,79 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good with zero timestamp",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
CredsLastRotatedTime: time.Time{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good (ignoring non-test options)",
|
||||
in: map[string]any{
|
||||
ConstAccessKeyId: "foobar",
|
||||
ConstSecretAccessKey: "bazqux",
|
||||
ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||
},
|
||||
opts: []AwsCredentialPersistedStateOption{
|
||||
WithAccessKeyId("ignored"),
|
||||
WithRegion("us-west-2"),
|
||||
name: "with assume role",
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with assume role & static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
ConstCredsLastRotatedTime: structpb.NewStringValue((time.Time{}).Format(time.RFC3339Nano)),
|
||||
},
|
||||
},
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
CredsLastRotatedTime: time.Time{},
|
||||
region: "us-west-2",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -223,55 +314,67 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
var (
|
||||
input *structpb.Struct
|
||||
err error
|
||||
)
|
||||
if tc.in != nil {
|
||||
input, err = structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
actual, err := AwsCredentialPersistedStateFromProto(input, tc.opts...)
|
||||
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected.AccessKeyId, actual.AccessKeyId)
|
||||
require.Equal(tc.expected.SecretAccessKey, actual.SecretAccessKey)
|
||||
require.Equal(tc.expected.region, actual.region)
|
||||
require.NotNil(actual)
|
||||
require.Equal(tc.expected.CredsLastRotatedTime, actual.CredsLastRotatedTime)
|
||||
if tc.expected.CredentialsConfig != nil {
|
||||
require.NotNil(actual.CredentialsConfig)
|
||||
require.Equal(tc.expected.CredentialsConfig.AccessKey, actual.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.SecretKey, actual.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expected.CredentialsConfig.Region, actual.CredentialsConfig.Region)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleARN, actual.CredentialsConfig.RoleARN)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "validation error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))},
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(
|
||||
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -291,27 +394,27 @@ func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedAccessKeyId string
|
||||
expectedSecretAccessKey string
|
||||
exepected *awsutil.CredentialsConfig
|
||||
expectedNonZeroRotatedTime bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "rotation error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -325,8 +428,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(),
|
||||
@@ -355,8 +461,11 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
exepected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedNonZeroRotatedTime: true,
|
||||
},
|
||||
}
|
||||
@@ -372,52 +481,42 @@ func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
|
||||
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
|
||||
require.Equal(tc.exepected.AccessKey, tc.in.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.exepected.SecretKey, tc.in.CredentialsConfig.SecretKey)
|
||||
require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
|
||||
// NOTE: Not safe to run this test in parallel
|
||||
state := new(testMockIAMState)
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
accessKeyId string
|
||||
secretAccessKey string
|
||||
expectedAccessKeyId string
|
||||
expectedSecretAccessKey string
|
||||
expectedDeleteCalled bool
|
||||
expectedErr string
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
credentialConfig *awsutil.CredentialsConfig
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedDeleteCalled bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "missing access key id",
|
||||
name: "missing new credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "access key id cannot be empty",
|
||||
expectedErr: "missing new credentials config",
|
||||
},
|
||||
{
|
||||
name: "missing access key id",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
accessKeyId: "one",
|
||||
expectedErr: "secret access key cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "identical access key id",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
},
|
||||
accessKeyId: "foobar",
|
||||
secretAccessKey: "bazqux",
|
||||
expectedErr: "attempting to replace access key with the same one",
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "deletion error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
@@ -427,15 +526,16 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
|
||||
},
|
||||
{
|
||||
name: "good with delete of old rotated",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
CredsLastRotatedTime: time.Now(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
@@ -443,28 +543,38 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
expectedDeleteCalled: true,
|
||||
credentialConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expectedDeleteCalled: true,
|
||||
},
|
||||
{
|
||||
name: "good without delete of old rotated",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
newTestMockIAM(state),
|
||||
),
|
||||
},
|
||||
},
|
||||
accessKeyId: "one",
|
||||
secretAccessKey: "two",
|
||||
expectedAccessKeyId: "one",
|
||||
expectedSecretAccessKey: "two",
|
||||
expectedDeleteCalled: false,
|
||||
credentialConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "one",
|
||||
SecretKey: "two",
|
||||
},
|
||||
expectedDeleteCalled: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -473,39 +583,39 @@ func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
state.Reset()
|
||||
err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey)
|
||||
err := tc.in.ReplaceCreds(tc.credentialConfig)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
|
||||
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
|
||||
require.Equal(tc.expected.AccessKey, tc.in.CredentialsConfig.AccessKey)
|
||||
require.Equal(tc.expected.SecretKey, tc.in.CredentialsConfig.SecretKey)
|
||||
require.Zero(tc.in.CredsLastRotatedTime)
|
||||
require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "could not load credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
name: "missing credentials config",
|
||||
in: &AwsCredentialPersistedState{},
|
||||
expectedErr: "missing credentials config",
|
||||
},
|
||||
{
|
||||
name: "deletion error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -519,8 +629,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
{
|
||||
name: "deletion error, but OK because key was just gone",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
@@ -535,8 +647,10 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
{
|
||||
name: "good",
|
||||
in: &AwsCredentialPersistedState{
|
||||
AccessKeyId: "foobar",
|
||||
SecretAccessKey: "bazqux",
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
@@ -557,19 +671,149 @@ func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Empty(tc.in.AccessKeyId)
|
||||
require.Empty(tc.in.SecretAccessKey)
|
||||
require.Nil(tc.in.CredentialsConfig)
|
||||
require.Zero(tc.in.CredsLastRotatedTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) {
|
||||
func TestAwsCatalogPersistedState_GetSession(t *testing.T) {
|
||||
require := require.New(t)
|
||||
state := &AwsCredentialPersistedState{
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
cases := []struct {
|
||||
name string
|
||||
in *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "error",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
|
||||
},
|
||||
expectedErr: fmt.Sprintf("error reading options in GetSession: %s", testOptionErr),
|
||||
},
|
||||
{
|
||||
name: "static credentials",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
AccessKey: "AKIA_foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assume role",
|
||||
in: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: func() *awsutil.CredentialsConfig {
|
||||
config, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-west-2"),
|
||||
awsutil.WithRoleArn("arn:aws:iam::123456789012:role/S3Access"),
|
||||
awsutil.WithRoleExternalId("1234567890"),
|
||||
awsutil.WithRoleSessionName("assume-role"),
|
||||
awsutil.WithRoleTags(map[string]string{
|
||||
"foo": "bar",
|
||||
}),
|
||||
)
|
||||
require.NoError(err)
|
||||
return config
|
||||
}(),
|
||||
testOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.in.GetSession()
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasStaticCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasStaticCredentials(tc.accessKey))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDynamicCredentials(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic credential",
|
||||
accessKey: "ASIAEXAMPLE",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "static credential",
|
||||
accessKey: "AKIAEXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "other credential",
|
||||
accessKey: "EXAMPLE",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
accessKey: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(tc.expected, HasDynamicCredentials(tc.accessKey))
|
||||
})
|
||||
}
|
||||
|
||||
_, err := state.GetSession()
|
||||
require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr))
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
import (
|
||||
@@ -5,6 +8,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,3 +49,39 @@ func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsu
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
|
||||
// MockStaticCredentialSecrets returns a *structpb.Struct that contains two
|
||||
// key pair values: (access_key_id, AKIA_foobar) & (secret_access_key, bazqux)
|
||||
func MockStaticCredentialSecrets() *structpb.Struct {
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockAssumeRoleAttributes returns a *structpb.Struct that contains six key pair values:
|
||||
//
|
||||
// (region, region)
|
||||
// (disable_credential_rotation, disableRotate)
|
||||
// (role_arn, arn:aws:iam::123456789012:role/S3Access)
|
||||
// (role_external_id, 1234567890)
|
||||
// (role_session_name, ec2-assume-role-provider)
|
||||
// (role_tags, struct{foo:bar})
|
||||
func MockAssumeRoleAttributes(region string, disableRotate bool) *structpb.Struct {
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstRegion: structpb.NewStringValue(region),
|
||||
ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotate),
|
||||
ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
ConstRoleExternalId: structpb.NewStringValue("1234567890"),
|
||||
ConstRoleSessionName: structpb.NewStringValue("ec2-assume-role-provider"),
|
||||
ConstRoleTags: structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
@@ -81,6 +84,33 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// GetMapStringString returns a map[string]string value and no error if the given key
|
||||
// is found in the provided proto struct input. An error is returned if the key
|
||||
// is not found or the value type is not map[string]string.
|
||||
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
|
||||
mv := in.GetFields()
|
||||
v, ok := mv[k]
|
||||
if !ok {
|
||||
if required {
|
||||
return nil, fmt.Errorf("missing required value %q", k)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
result := map[string]string{}
|
||||
m, ok := v.AsInterface().(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
|
||||
}
|
||||
for mapKey, mapValue := range m {
|
||||
e, ok := mapValue.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
|
||||
}
|
||||
result[mapKey] = e
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StructFields returns a map[string]struct{} of the
|
||||
// proto struct input.
|
||||
func StructFields(s *structpb.Struct) map[string]struct{} {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
@@ -205,3 +208,95 @@ func TestGetTimeValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMapStringString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
key string
|
||||
required bool
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "required missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
required: true,
|
||||
expectedErr: "missing required value \"foo\"",
|
||||
},
|
||||
{
|
||||
name: "optional missing",
|
||||
in: &structpb.Struct{},
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
name: "non-map[string]string value",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "non-string value in map",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"bar": structpb.NewStringValue("zoo"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
key: "foo",
|
||||
expected: map[string]string{
|
||||
"bar": "zoo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
|
||||
@@ -105,18 +105,19 @@ a configured IAM user for this provider:
|
||||
The following `attributes` are valid on an AWS host catalog resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed. See the [Credential Rotation](#credential-rotation) section
|
||||
above.
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
- `region` (string): The region to configure the host catalog for. All host sets
|
||||
in this catalog will be configured for this region.
|
||||
|
||||
The following `secrets` are valid on an AWS host catalog resource:
|
||||
The following `secrets` are required on an AWS host catalog resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
host catalog.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this host catalog.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Host Set
|
||||
|
||||
The following attributes are valid on an AWS host Set resource:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
@@ -35,6 +38,14 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
@@ -6,6 +9,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -17,36 +21,34 @@ import (
|
||||
func TestGetCatalogAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
in *structpb.Struct
|
||||
expected *CatalogAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "bad value for disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: "sure",
|
||||
},
|
||||
expectedErrContains: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
@@ -56,15 +58,31 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with disable_credential_rotation",
|
||||
in: map[string]any{
|
||||
credential.ConstRegion: "us-west-2",
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,10 +93,7 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getCatalogAttributes(input)
|
||||
actual, err := getCatalogAttributes(tc.in)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
const (
|
||||
|
||||
+125
-139
@@ -9,9 +9,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/smithy-go"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -58,7 +59,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,9 +67,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := credential.NewAwsCredentialPersistedState(
|
||||
append([]credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithAccessKeyId(credConfig.AccessKey),
|
||||
credential.WithSecretAccessKey(credConfig.SecretKey),
|
||||
credential.WithRegion(credConfig.Region),
|
||||
credential.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -84,15 +83,17 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate the credentials if we're not skipping them.
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
// Try to rotate static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +145,8 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -158,38 +160,39 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(catalogSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(catalogSecrets.AccessKey, catalogSecrets.SecretKey); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// That's it!
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
@@ -225,7 +228,8 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
// may be corrupt/and or legitimately missing.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -239,12 +243,15 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +304,8 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -324,7 +332,11 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
@@ -334,12 +346,13 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||
}
|
||||
|
||||
_, err = ec2Client.DescribeInstances(input)
|
||||
_, err = ec2Client.DescribeInstances(ctx, input)
|
||||
if err == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||
}
|
||||
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
|
||||
// Success
|
||||
return &pb.OnCreateSetResponse{}, nil
|
||||
}
|
||||
@@ -366,7 +379,8 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -395,7 +409,11 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
@@ -405,12 +423,13 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||
}
|
||||
|
||||
_, err = ec2Client.DescribeInstances(input)
|
||||
_, err = ec2Client.DescribeInstances(ctx, input)
|
||||
if err == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||
}
|
||||
|
||||
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
|
||||
// Success
|
||||
return &pb.OnUpdateSetResponse{}, nil
|
||||
}
|
||||
@@ -442,7 +461,10 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(req.GetPersisted().GetSecrets(), p.testCredStateOpts...)
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -494,7 +516,11 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.Region != "" {
|
||||
opts = append(opts, WithRegion(catalogAttributes.Region))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
}
|
||||
@@ -502,7 +528,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
// Run all queries now and assemble output.
|
||||
var maxLen int
|
||||
for i, query := range queries {
|
||||
output, err := ec2Client.DescribeInstances(query.Input)
|
||||
output, err := ec2Client.DescribeInstances(ctx, query.Input)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err)
|
||||
}
|
||||
@@ -556,8 +582,8 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
|
||||
var filters []*ec2.Filter
|
||||
func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
||||
var filters []types.Filter
|
||||
var foundStateFilter bool
|
||||
for _, filterAttr := range attrs.Filters {
|
||||
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
|
||||
@@ -576,14 +602,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
|
||||
foundStateFilter = true
|
||||
}
|
||||
|
||||
var filterValues []*string
|
||||
for _, val := range strings.Split(filterValue, ",") {
|
||||
filterValues = append(filterValues, aws.String(val))
|
||||
}
|
||||
|
||||
filters = append(filters, &ec2.Filter{
|
||||
filters = append(filters, types.Filter{
|
||||
Name: aws.String(filterKey),
|
||||
Values: filterValues,
|
||||
Values: strings.Split(filterValue, ","),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -592,9 +613,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
|
||||
// instance-state-name = ["running"] to the filter set. This
|
||||
// ensures that we filter on running instances only at the API
|
||||
// side, saving time when processing results.
|
||||
filters = append(filters, &ec2.Filter{
|
||||
filters = append(filters, types.Filter{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -616,97 +637,49 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
|
||||
// awsInstanceToHost processes data from an ec2.Instance and returns
|
||||
// a ListHostsResponseHost with the ID and network addresses
|
||||
// populated.
|
||||
func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error) {
|
||||
// Integrity check: some fields should always be non-nil. Check
|
||||
// those.
|
||||
if instance == nil {
|
||||
return nil, errors.New("response integrity error: missing instance entry")
|
||||
}
|
||||
|
||||
if aws.StringValue(instance.InstanceId) == "" {
|
||||
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
|
||||
if aws.ToString(instance.InstanceId) == "" {
|
||||
return nil, errors.New("response integrity error: missing instance id")
|
||||
}
|
||||
|
||||
result := new(pb.ListHostsResponseHost)
|
||||
|
||||
// External ID is the instance ID.
|
||||
result.ExternalId = aws.StringValue(instance.InstanceId)
|
||||
result.ExternalId = aws.ToString(instance.InstanceId)
|
||||
|
||||
// External Name is the AWS "Name" tag, if it exists.
|
||||
for _, t := range instance.Tags {
|
||||
if aws.StringValue(t.Key) == ConstInstanceNameTagKey {
|
||||
result.ExternalName = aws.StringValue(t.Value)
|
||||
if aws.ToString(t.Key) == ConstInstanceNameTagKey {
|
||||
result.ExternalName = aws.ToString(t.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// First IP address/dns name are always the private fields if they
|
||||
// are populated
|
||||
if aws.StringValue(instance.PrivateIpAddress) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(instance.PrivateDnsName) != "" {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PrivateDnsName))
|
||||
}
|
||||
|
||||
// Public IP address/dns names are next
|
||||
if aws.StringValue(instance.PublicIpAddress) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PublicIpAddress))
|
||||
}
|
||||
if aws.StringValue(instance.PublicDnsName) != "" {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PublicDnsName))
|
||||
}
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
|
||||
|
||||
// Now go through all of the interfaces and log the IP address of
|
||||
// every interface.
|
||||
for _, iface := range instance.NetworkInterfaces {
|
||||
if iface == nil {
|
||||
// Probably will never happen, but just in case
|
||||
return nil, errors.New("response integrity error: interface entry is nil")
|
||||
}
|
||||
|
||||
// Populate default IP addresses/DNS name similar to how we do
|
||||
// for the entire instance.
|
||||
if aws.StringValue(iface.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(iface.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(iface.PrivateDnsName))
|
||||
}
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
|
||||
|
||||
// Iterate through the private IP addresses and log the
|
||||
// information.
|
||||
for _, addr := range iface.PrivateIpAddresses {
|
||||
if addr == nil {
|
||||
return nil, errors.New("response integrity error: interface address entry is nil")
|
||||
}
|
||||
|
||||
// Add private address/dns name if they have not been added yet
|
||||
if aws.StringValue(addr.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress))
|
||||
}
|
||||
if aws.StringValue(addr.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.PrivateDnsName))
|
||||
}
|
||||
|
||||
// Add public address/dns name if they have not been added yet
|
||||
if addr.Association != nil && aws.StringValue(addr.Association.PublicIp) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Association.PublicIp))
|
||||
}
|
||||
if addr.Association != nil && aws.StringValue(addr.Association.PublicDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) {
|
||||
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName))
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
|
||||
if addr.Association != nil {
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
|
||||
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the IPv6 addresses.
|
||||
for _, addr := range iface.Ipv6Addresses {
|
||||
if addr == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.Ipv6Address != nil && aws.StringValue(addr.Ipv6Address) != "" {
|
||||
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Ipv6Address))
|
||||
}
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,12 +687,25 @@ func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// appendDistinct will append the elements to the slice
|
||||
// if an element is not nil, empty, and does not exist in slice.
|
||||
func appendDistinct(slice []string, elems ...*string) []string {
|
||||
for _, e := range elems {
|
||||
value := aws.ToString(e)
|
||||
if value == "" || stringInSlice(slice, value) {
|
||||
continue
|
||||
}
|
||||
slice = append(slice, value)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// stringInSlice returns true if the slice contains the given element.
|
||||
func stringInSlice(s []string, x string) bool {
|
||||
for _, y := range s {
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+156
-133
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
@@ -7,8 +10,9 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
@@ -35,14 +39,6 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
expectedErrContains: "catalog is nil",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{},
|
||||
},
|
||||
expectedErrContains: "secrets are required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
@@ -89,7 +85,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -110,7 +106,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -136,7 +132,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -166,7 +162,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
@@ -277,7 +273,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -308,7 +304,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -334,7 +330,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -350,7 +346,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
NewCatalog: &hostcatalogs.HostCatalog{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
|
||||
},
|
||||
},
|
||||
@@ -366,7 +362,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -400,7 +396,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -459,7 +455,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -489,7 +485,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -594,7 +590,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -624,7 +620,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -649,7 +645,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -675,7 +671,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -710,7 +706,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -737,7 +733,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -755,7 +751,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -801,7 +797,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -846,7 +842,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -963,7 +959,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -993,7 +989,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1018,7 +1014,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1044,7 +1040,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1079,7 +1075,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1106,7 +1102,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -1124,7 +1120,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1170,7 +1166,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1287,7 +1283,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1317,7 +1313,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1342,7 +1338,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1368,7 +1364,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1398,7 +1394,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1436,7 +1432,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1466,7 +1462,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
awsutil.MockOptionErr(errors.New(testOptionErr)),
|
||||
}),
|
||||
},
|
||||
expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session when fetching EC2 client: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
@@ -1484,7 +1480,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1533,7 +1529,7 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -1563,9 +1559,9 @@ func TestPluginListHostsErr(t *testing.T) {
|
||||
nil,
|
||||
testMockEC2WithDescribeInstancesOutput(
|
||||
&ec2.DescribeInstancesOutput{
|
||||
Reservations: []*ec2.Reservation{
|
||||
Reservations: []types.Reservation{
|
||||
{
|
||||
Instances: []*ec2.Instance{
|
||||
Instances: []types.Instance{
|
||||
{
|
||||
// Blank so we error out
|
||||
},
|
||||
@@ -1600,7 +1596,7 @@ func TestBuildFilters(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
expected []*ec2.Filter
|
||||
expected []types.Filter
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
@@ -1608,14 +1604,14 @@ func TestBuildFilters(t *testing.T) {
|
||||
in: map[string]any{
|
||||
ConstDescribeInstancesFilters: []any{"foo=bar"},
|
||||
},
|
||||
expected: []*ec2.Filter{
|
||||
expected: []types.Filter{
|
||||
{
|
||||
Name: aws.String("foo"),
|
||||
Values: aws.StringSlice([]string{"bar"}),
|
||||
Values: []string{"bar"},
|
||||
},
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1627,14 +1623,14 @@ func TestBuildFilters(t *testing.T) {
|
||||
"instance-state-name=static",
|
||||
},
|
||||
},
|
||||
expected: []*ec2.Filter{
|
||||
expected: []types.Filter{
|
||||
{
|
||||
Name: aws.String("foo"),
|
||||
Values: aws.StringSlice([]string{"bar"}),
|
||||
Values: []string{"bar"},
|
||||
},
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{"static"}),
|
||||
Values: []string{"static"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1645,14 +1641,14 @@ func TestBuildFilters(t *testing.T) {
|
||||
"foo=bar,baz",
|
||||
},
|
||||
},
|
||||
expected: []*ec2.Filter{
|
||||
expected: []types.Filter{
|
||||
{
|
||||
Name: aws.String("foo"),
|
||||
Values: aws.StringSlice([]string{"bar", "baz"}),
|
||||
Values: []string{"bar", "baz"},
|
||||
},
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1661,10 +1657,10 @@ func TestBuildFilters(t *testing.T) {
|
||||
in: map[string]any{
|
||||
ConstDescribeInstancesFilters: []any{},
|
||||
},
|
||||
expected: []*ec2.Filter{
|
||||
expected: []types.Filter{
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1711,14 +1707,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
|
||||
dryRun: true,
|
||||
expected: &ec2.DescribeInstancesInput{
|
||||
DryRun: aws.Bool(true),
|
||||
Filters: []*ec2.Filter{
|
||||
Filters: []types.Filter{
|
||||
{
|
||||
Name: aws.String("foo"),
|
||||
Values: aws.StringSlice([]string{"bar"}),
|
||||
Values: []string{"bar"},
|
||||
},
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1733,14 +1729,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
|
||||
dryRun: false,
|
||||
expected: &ec2.DescribeInstancesInput{
|
||||
DryRun: aws.Bool(false),
|
||||
Filters: []*ec2.Filter{
|
||||
Filters: []types.Filter{
|
||||
{
|
||||
Name: aws.String("foo"),
|
||||
Values: aws.StringSlice([]string{"bar"}),
|
||||
Values: []string{"bar"},
|
||||
},
|
||||
{
|
||||
Name: aws.String("instance-state-name"),
|
||||
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||
Values: []string{string(types.InstanceStateNameRunning)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1772,55 +1768,30 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
|
||||
func TestAwsInstanceToHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
instance *ec2.Instance
|
||||
instance types.Instance
|
||||
expected *pb.ListHostsResponseHost
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "nil instance",
|
||||
instance: nil,
|
||||
expectedErr: "response integrity error: missing instance entry",
|
||||
},
|
||||
{
|
||||
name: "missing instance id",
|
||||
instance: &ec2.Instance{},
|
||||
instance: types.Instance{},
|
||||
expectedErr: "response integrity error: missing instance id",
|
||||
},
|
||||
{
|
||||
name: "nil interface entry",
|
||||
instance: &ec2.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{nil},
|
||||
},
|
||||
expectedErr: "response integrity error: interface entry is nil",
|
||||
},
|
||||
{
|
||||
name: "nil interface address entry",
|
||||
instance: &ec2.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "response integrity error: interface address entry is nil",
|
||||
},
|
||||
{
|
||||
name: "good, single IP w/public addr",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -1839,15 +1810,15 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, private",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
@@ -1864,17 +1835,17 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, multiple interfaces",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.2"),
|
||||
PrivateDnsName: aws.String("test2.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.2"),
|
||||
PrivateDnsName: aws.String("test2.example.internal"),
|
||||
@@ -1884,9 +1855,9 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -1905,19 +1876,19 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, multiple public interfaces",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.2"),
|
||||
PrivateDnsName: aws.String("test2.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.2"),
|
||||
PublicDnsName: aws.String("test2.example.com"),
|
||||
},
|
||||
@@ -1929,9 +1900,9 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -1950,19 +1921,19 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, multiple addresses on single interface",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -1985,25 +1956,25 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, single IP w/public addr and IPv6",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
},
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
},
|
||||
},
|
||||
Ipv6Addresses: []*ec2.InstanceIpv6Address{
|
||||
nil, // Just coverage for nil assertion which is skipped
|
||||
Ipv6Addresses: []types.InstanceIpv6Address{
|
||||
{Ipv6Address: nil}, // Just coverage for nil assertion which is skipped
|
||||
{Ipv6Address: aws.String("some::fake::address")},
|
||||
},
|
||||
},
|
||||
@@ -2017,24 +1988,24 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, single IP w/public addr and external name",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
Tags: []*ec2.Tag{
|
||||
Tags: []types.Tag{
|
||||
{Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name".
|
||||
{Key: aws.String("name"), Value: aws.String("test-instance-fake-name")},
|
||||
{Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")},
|
||||
},
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -2054,24 +2025,24 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good, single IP w/public addr and external name",
|
||||
instance: &ec2.Instance{
|
||||
instance: types.Instance{
|
||||
InstanceId: aws.String("foobar"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PublicIpAddress: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
Tags: []*ec2.Tag{
|
||||
Tags: []types.Tag{
|
||||
{Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name".
|
||||
{Key: aws.String("name"), Value: aws.String("test-instance-fake-name")},
|
||||
{Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")},
|
||||
},
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
|
||||
NetworkInterfaces: []types.InstanceNetworkInterface{
|
||||
{
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PrivateDnsName: aws.String("test.example.internal"),
|
||||
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
|
||||
PrivateIpAddresses: []types.InstancePrivateIpAddress{
|
||||
{
|
||||
Association: &ec2.InstanceNetworkInterfaceAssociation{
|
||||
Association: &types.InstanceNetworkInterfaceAssociation{
|
||||
PublicIp: aws.String("1.1.1.1"),
|
||||
PublicDnsName: aws.String("test.example.com"),
|
||||
},
|
||||
@@ -2106,3 +2077,55 @@ func TestAwsInstanceToHost(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDistinct(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
slice []string
|
||||
elems []*string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "empty slice, empty elems",
|
||||
slice: []string{},
|
||||
elems: []*string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "empty elems",
|
||||
slice: []string{"bar"},
|
||||
elems: []*string{},
|
||||
expected: []string{"bar"},
|
||||
},
|
||||
{
|
||||
name: "skip nil elem",
|
||||
slice: []string{"bar"},
|
||||
elems: []*string{nil},
|
||||
expected: []string{"bar"},
|
||||
},
|
||||
{
|
||||
name: "skip empty elem",
|
||||
slice: []string{"bar"},
|
||||
elems: []*string{aws.String("")},
|
||||
expected: []string{"bar"},
|
||||
},
|
||||
{
|
||||
name: "skip duplicate elem",
|
||||
slice: []string{"bar"},
|
||||
elems: []*string{
|
||||
aws.String("bar"),
|
||||
aws.String("foo"),
|
||||
aws.String("foo"),
|
||||
},
|
||||
expected: []string{"bar", "foo"},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
actual := appendDistinct(tc.slice, tc.elems...)
|
||||
require.ElementsMatch(actual, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type EC2API interface {
|
||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||
}
|
||||
|
||||
var customClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
@@ -27,10 +35,6 @@ var customClient = &http.Client{
|
||||
},
|
||||
}
|
||||
|
||||
type ec2APIFunc func(*session.Session, ...*aws.Config) (ec2iface.EC2API, error)
|
||||
|
||||
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
||||
|
||||
type awsCatalogPersistedState struct {
|
||||
*credential.AwsCredentialPersistedState
|
||||
|
||||
@@ -38,6 +42,10 @@ type awsCatalogPersistedState struct {
|
||||
testEC2APIFunc ec2APIFunc
|
||||
}
|
||||
|
||||
type ec2APIFunc func(...aws.Config) (EC2API, error)
|
||||
|
||||
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
||||
|
||||
func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption {
|
||||
return func(s *awsCatalogPersistedState) error {
|
||||
if s.testEC2APIFunc != nil {
|
||||
@@ -80,22 +88,75 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) {
|
||||
|
||||
// EC2Client returns a configured EC2 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsCatalogPersistedState) EC2Client(region string) (ec2iface.EC2API, error) {
|
||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
awsConfigOpts := []func(*config.LoadOptions) error{}
|
||||
sess, err := s.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
if s.testEC2APIFunc != nil {
|
||||
return s.testEC2APIFunc(sess, aws.NewConfig().WithRegion(region))
|
||||
creds, err := sess.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err)
|
||||
}
|
||||
customCredentials := config.WithCredentialsProvider(
|
||||
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
|
||||
)
|
||||
awsConfigOpts = append(awsConfigOpts, customCredentials)
|
||||
if sess.Config.Region != nil {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
|
||||
}
|
||||
if opts.withRegion != "" {
|
||||
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
|
||||
}
|
||||
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading aws configuration when fetching EC2 client: %w", err)
|
||||
}
|
||||
|
||||
cfg := aws.NewConfig()
|
||||
if region != "" {
|
||||
cfg = cfg.WithRegion(region)
|
||||
if s.testEC2APIFunc != nil {
|
||||
return s.testEC2APIFunc(cfg)
|
||||
}
|
||||
|
||||
cfg.HTTPClient = customClient
|
||||
|
||||
return ec2.New(sess, cfg), nil
|
||||
return ec2.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound ec2Options and returns a struct
|
||||
func getOpts(opt ...ec2Option) (ec2Options, error) {
|
||||
opts := getDefaultEC2Options()
|
||||
for _, o := range opt {
|
||||
if o == nil {
|
||||
continue
|
||||
}
|
||||
if err := o(&opts); err != nil {
|
||||
return ec2Options{}, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// ec2Option - how ec2Options are passed as arguments
|
||||
type ec2Option func(*ec2Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type ec2Options struct {
|
||||
withRegion string
|
||||
}
|
||||
|
||||
func getDefaultEC2Options() ec2Options {
|
||||
return ec2Options{}
|
||||
}
|
||||
|
||||
// WithRegion contains the region to use
|
||||
func WithRegion(with string) ec2Option {
|
||||
return func(o *ec2Options) error {
|
||||
o.withRegion = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "barfoo",
|
||||
}),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
@@ -22,7 +29,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.EC2Client("us-west-2")
|
||||
clientRaw, err := state.EC2Client(context.Background(), WithRegion("us-west-2"))
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockEC2)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,10 +30,11 @@ func (s *testMockEC2State) Reset() {
|
||||
}
|
||||
|
||||
type testMockEC2 struct {
|
||||
ec2iface.EC2API
|
||||
EC2API
|
||||
|
||||
State *testMockEC2State
|
||||
Region string
|
||||
|
||||
State *testMockEC2State
|
||||
Region string
|
||||
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
||||
DescribeInstancesError error
|
||||
}
|
||||
@@ -51,7 +56,7 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option {
|
||||
}
|
||||
|
||||
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
||||
return func(sess *session.Session, cfgs ...*aws.Config) (ec2iface.EC2API, error) {
|
||||
return func(cfgs ...aws.Config) (EC2API, error) {
|
||||
m := &testMockEC2{
|
||||
State: state,
|
||||
}
|
||||
@@ -64,14 +69,17 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
// Last region takes precedence
|
||||
m.Region = *cfg.Region
|
||||
if cfg.Region != "" {
|
||||
m.Region = cfg.Region
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
|
||||
func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.DescribeInstancesCalled = true
|
||||
m.State.DescribeInstancesInputParams = input
|
||||
@@ -84,7 +92,7 @@ func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2
|
||||
return m.DescribeInstancesOutput, nil
|
||||
}
|
||||
|
||||
type ec2FilterSorter []*ec2.Filter
|
||||
type ec2FilterSorter []types.Filter
|
||||
|
||||
func (s ec2FilterSorter) Len() int { return len(s) }
|
||||
func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name }
|
||||
|
||||
@@ -42,7 +42,7 @@ Example policy:
|
||||
```
|
||||
|
||||
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
|
||||
the IAM user to allow credential rotation. Example policy:
|
||||
the IAM user to allow static credential rotation. Example policy:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -66,18 +66,24 @@ Example policy:
|
||||
The following `attributes` are valid on an AWS storage bucket resource:
|
||||
|
||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||
be performed. See the [Credential Rotation](#credential-rotation) section
|
||||
above.
|
||||
be performed.
|
||||
- `region` (string): The region to configure the storage bucket for.
|
||||
- `endpoint-url` (string): The endpoint to configure the storage.
|
||||
- `role_arn` (string): The role arn configured for the assume role provider.
|
||||
- `role_external_id` (string): The external id configured for the assume role provider.
|
||||
- `role_session_name` (string): The session name configured for the assume role provider.
|
||||
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
|
||||
|
||||
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
|
||||
### Secrets
|
||||
|
||||
The following `secrets` are valid on an AWS storage bucket resource:
|
||||
The following `secrets` are optional on an AWS storage bucket resource:
|
||||
|
||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||
storage bucket.
|
||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||
with this storage bucket.
|
||||
with this storage bucket.
|
||||
|
||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -41,6 +44,14 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
continue
|
||||
case cred.ConstDisableCredentialRotation:
|
||||
continue
|
||||
case cred.ConstRoleArn:
|
||||
continue
|
||||
case cred.ConstRoleExternalId:
|
||||
continue
|
||||
case cred.ConstRoleSessionName:
|
||||
continue
|
||||
case cred.ConstRoleTags:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -13,29 +16,35 @@ import (
|
||||
func TestGetStorageAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
in *structpb.Struct
|
||||
expected *StorageAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing region",
|
||||
in: map[string]any{},
|
||||
name: "missing region",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
"region": "us-west-2",
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "good w/ endpoint",
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
ConstAwsEndpointUrl: "0.0.0.0",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -46,8 +55,10 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "good w/o endpoint",
|
||||
in: map[string]any{
|
||||
cred.ConstRegion: "us-west-2",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
@@ -55,6 +66,35 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credential attributes",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
||||
"role_session_name": structpb.NewStringValue("test-session"),
|
||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"foo": structpb.NewStringValue("bar"),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: true,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
||||
RoleExternalId: "1234567890",
|
||||
RoleSessionName: "test-session",
|
||||
RoleTags: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -62,10 +102,7 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getStorageAttributes(input)
|
||||
actual, err := getStorageAttributes(tc.in)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -48,11 +51,6 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -63,7 +61,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,24 +69,24 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate the credentials if we're not skipping them.
|
||||
if !storageAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
// Try to rotate static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !storageAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,39 +155,42 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
||||
storageAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if storageAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if storageAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(storageSecrets.AccessKey, storageSecrets.SecretKey); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
if updateSecrets {
|
||||
storageSecrets, err := cred.GetCredentialsConfig(secrets, storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(storageSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !storageAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +243,8 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
// may be corrupt/and or legitimately missing.
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
append(p.testCredStateOpts, cred.WithRegion(storageAttributes.Region))...)
|
||||
storageAttributes.CredentialAttributes,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
@@ -256,12 +258,15 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
|
||||
// try to delete static credentials
|
||||
if cred.HasStaticCredentials(credState.CredentialsConfig.AccessKey) {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during storage bucket deletion: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,11 +288,6 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -298,7 +298,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -306,9 +306,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -361,11 +359,6 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -376,7 +369,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -384,9 +377,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -426,11 +417,6 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
return status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -441,7 +427,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
return err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -449,9 +435,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -530,10 +514,6 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
if bucket.GetBucketName() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "bucketName is required")
|
||||
}
|
||||
secrets := bucket.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets is required")
|
||||
}
|
||||
attrs := bucket.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes is required")
|
||||
@@ -561,7 +541,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, storageAttributes.Region)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -569,9 +549,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
// Set up the creds in our persisted state.
|
||||
credState, err := cred.NewAwsCredentialPersistedState(
|
||||
append([]cred.AwsCredentialPersistedStateOption{
|
||||
cred.WithAccessKeyId(credConfig.AccessKey),
|
||||
cred.WithSecretAccessKey(credConfig.SecretKey),
|
||||
cred.WithRegion(credConfig.Region),
|
||||
cred.WithCredentialsConfig(credConfig),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -31,11 +34,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
return &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -46,12 +45,13 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateStorageBucketRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
storageOpts []awsStoragePersistedStateOption
|
||||
expectedErrContains string
|
||||
expectedErrCode codes.Code
|
||||
name string
|
||||
req *pb.OnCreateStorageBucketRequest
|
||||
credOpts []credential.AwsCredentialPersistedStateOption
|
||||
storageOpts []awsStoragePersistedStateOption
|
||||
expectedErrContains string
|
||||
expectedErrCode codes.Code
|
||||
expectedPersistedData map[string]any
|
||||
}{
|
||||
{
|
||||
name: "nil storage bucket",
|
||||
@@ -67,22 +67,11 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes is required",
|
||||
@@ -93,39 +82,17 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: new(structpb.Struct),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"region\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "error reading secrets",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "invalid region",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("foobar"),
|
||||
@@ -153,11 +120,7 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -298,6 +261,67 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedPersistedData: credential.MockStaticCredentialSecrets().AsMap(),
|
||||
},
|
||||
{
|
||||
name: "success rotate static creds",
|
||||
req: &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithStateTestOpts(
|
||||
[]awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(),
|
||||
),
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(
|
||||
awsutil.WithGetUserOutput(
|
||||
&iam.GetUserOutput{
|
||||
User: &iam.User{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
|
||||
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
},
|
||||
),
|
||||
awsutil.WithCreateAccessKeyOutput(
|
||||
&iam.CreateAccessKeyOutput{
|
||||
AccessKey: &iam.AccessKey{
|
||||
AccessKeyId: aws.String("one"),
|
||||
SecretAccessKey: aws.String("two"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
storageOpts: []awsStoragePersistedStateOption{
|
||||
withTestS3APIFunc(
|
||||
newTestMockS3(
|
||||
nil,
|
||||
testMockS3WithPutObjectOutput(&s3.PutObjectOutput{}),
|
||||
testMockS3WithGetObjectOutput(&s3.GetObjectOutput{}),
|
||||
testMockS3WithHeadObjectOutput(&s3.HeadObjectOutput{}),
|
||||
),
|
||||
),
|
||||
},
|
||||
expectedPersistedData: map[string]any{
|
||||
credential.ConstAccessKeyId: "one",
|
||||
credential.ConstSecretAccessKey: "two",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -319,12 +343,12 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
|
||||
require.NotNil(resp)
|
||||
require.NotNil(resp.GetPersisted())
|
||||
require.NotNil(resp.GetPersisted().GetData())
|
||||
require.Equal(
|
||||
resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId],
|
||||
tc.req.Bucket.Secrets.AsMap()[credential.ConstAccessKeyId])
|
||||
require.Equal(
|
||||
resp.GetPersisted().GetData().AsMap()[credential.ConstSecretAccessKey],
|
||||
tc.req.Bucket.Secrets.AsMap()[credential.ConstSecretAccessKey])
|
||||
actualPersistedData := resp.GetPersisted().GetData().AsMap()
|
||||
if len(actualPersistedData) != 0 {
|
||||
require.Equal(tc.expectedPersistedData[credential.ConstAccessKeyId], actualPersistedData[credential.ConstAccessKeyId])
|
||||
require.Equal(tc.expectedPersistedData[credential.ConstSecretAccessKey], actualPersistedData[credential.ConstSecretAccessKey])
|
||||
require.NotEmpty(actualPersistedData[credential.ConstCredsLastRotatedTime])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -342,12 +366,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
Data: credential.MockStaticCredentialSecrets(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -425,7 +444,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -455,7 +474,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -465,31 +484,6 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
expectedErrContains: "cannot disable rotation for already-rotated credentials",
|
||||
expectedErrCode: codes.FailedPrecondition,
|
||||
},
|
||||
{
|
||||
name: "error reading secrets",
|
||||
req: &pb.OnUpdateStorageBucketRequest{
|
||||
NewBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: new(structpb.Struct),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrContains: "missing required value \"access_key_id\"",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "updating secrets, replace error",
|
||||
req: &pb.OnUpdateStorageBucketRequest{
|
||||
@@ -497,7 +491,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("onetwo"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_onetwo"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("threefour"),
|
||||
},
|
||||
},
|
||||
@@ -511,7 +505,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -544,7 +538,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -668,7 +662,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
@@ -693,7 +687,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
awsutil.WithCreateAccessKeyOutput(
|
||||
&iam.CreateAccessKeyOutput{
|
||||
AccessKey: &iam.AccessKey{
|
||||
AccessKeyId: aws.String("one"),
|
||||
AccessKeyId: aws.String("AKIA_one"),
|
||||
SecretAccessKey: aws.String("two"),
|
||||
UserName: aws.String("JohnDoe"),
|
||||
},
|
||||
@@ -757,7 +751,7 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
|
||||
disableCredentialRotation = value.(bool)
|
||||
}
|
||||
if !disableCredentialRotation {
|
||||
expectedAccessKey = "one"
|
||||
expectedAccessKey = "AKIA_one"
|
||||
expectedSecretKey = "two"
|
||||
}
|
||||
require.Equal(expectedAccessKey, resp.GetPersisted().GetData().AsMap()[credential.ConstAccessKeyId])
|
||||
@@ -804,11 +798,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
req: &pb.OnDeleteStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("foobar"),
|
||||
@@ -832,7 +822,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -860,7 +850,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -892,7 +882,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||
},
|
||||
@@ -921,6 +911,7 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
|
||||
}
|
||||
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())
|
||||
return
|
||||
@@ -937,11 +928,7 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -983,17 +970,6 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.HeadObjectRequest{
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.HeadObjectRequest{
|
||||
@@ -1137,11 +1113,7 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
return &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1172,16 +1144,6 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.ValidatePermissionsRequest{
|
||||
@@ -1380,11 +1342,7 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
}},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1425,17 +1383,6 @@ func TestStoragePlugin_GetObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil secrets",
|
||||
req: &pb.GetObjectRequest{
|
||||
Key: "/foo/bar/key",
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "foo",
|
||||
},
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
req: &pb.GetObjectRequest{
|
||||
@@ -1613,12 +1560,7 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
return &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "external-obj-store",
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
Secrets: credential.MockStaticCredentialSecrets(),
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||
@@ -1668,17 +1610,6 @@ func TestStoragePlugin_PutObject(t *testing.T) {
|
||||
expectedErrContains: "bucketName is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing bucket secrets",
|
||||
request: &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: "external-obj-store",
|
||||
},
|
||||
Key: "mock-object",
|
||||
},
|
||||
expectedErrContains: "secrets is required",
|
||||
expectedErrCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing attributes",
|
||||
request: &pb.PutObjectRequest{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,40 +1,86 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithRegion("us-west-2"),
|
||||
credential.WithAccessKeyId("foobar"),
|
||||
credential.WithSecretAccessKey("barfoo"),
|
||||
)
|
||||
require.NoError(err)
|
||||
cases := []struct {
|
||||
name string
|
||||
creds *awsutil.CredentialsConfig
|
||||
opts []awsStoragePersistedStateOption
|
||||
awsOpts []awsutil.Option
|
||||
s3Opts []s3Option
|
||||
expectedRegion string
|
||||
expectedEndpoint string
|
||||
}{
|
||||
{
|
||||
name: "static credentials",
|
||||
creds: func() *awsutil.CredentialsConfig {
|
||||
creds, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithRegion("us-west-2"),
|
||||
awsutil.WithAccessKey("foobar"),
|
||||
awsutil.WithSecretKey("barfoo"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return creds
|
||||
}(),
|
||||
awsOpts: []awsutil.Option{
|
||||
awsutil.WithIAMAPIFunc(
|
||||
awsutil.NewMockIAM(),
|
||||
),
|
||||
},
|
||||
opts: []awsStoragePersistedStateOption{
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
},
|
||||
s3Opts: []s3Option{
|
||||
WithRegion("us-west-2"),
|
||||
WithEndpoint("0.0.0.0"),
|
||||
},
|
||||
expectedRegion: "us-west-2",
|
||||
expectedEndpoint: "0.0.0.0",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
state, err := newAwsStoragePersistedState(
|
||||
withCredentials(cred),
|
||||
withTestS3APIFunc(newTestMockS3(nil)),
|
||||
)
|
||||
require.NoError(err)
|
||||
cred, err := credential.NewAwsCredentialPersistedState(
|
||||
credential.WithCredentialsConfig(tc.creds),
|
||||
credential.WithStateTestOpts(tc.awsOpts),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal("us-west-2", client.Region)
|
||||
state, err := newAwsStoragePersistedState(
|
||||
append(tc.opts, withCredentials(cred))...,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||
require.NoError(err)
|
||||
require.Equal("0.0.0.0", endpoint.URL)
|
||||
require.Equal("us-west-2", endpoint.SigningRegion)
|
||||
require.Equal("aws", endpoint.PartitionID)
|
||||
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
require.True(ok)
|
||||
require.NotNil(client)
|
||||
require.Equal(tc.expectedRegion, client.Region)
|
||||
|
||||
if tc.expectedEndpoint != "" {
|
||||
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expectedEndpoint, endpoint.URL)
|
||||
require.Equal(tc.expectedRegion, endpoint.SigningRegion)
|
||||
require.Equal("aws", endpoint.PartitionID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
|
||||
+4
-198
@@ -11,26 +11,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
expectedIamUserCount = 6
|
||||
expectedEc2InstanceCount = 5
|
||||
)
|
||||
const expectedEc2InstanceCount = 5
|
||||
|
||||
var expectedTags = []string{"foo", "bar", "baz"}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
func TestHostPlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
@@ -195,36 +189,7 @@ func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPl
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
if rotate {
|
||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||
} else {
|
||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||
}
|
||||
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
if rotate {
|
||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||
} else {
|
||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||
}
|
||||
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
if rotate {
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnUpdateCatalog(
|
||||
@@ -293,116 +258,7 @@ func testPluginOnUpdateCatalog(
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
persistedSecrets := persisted.GetSecrets()
|
||||
require.NotNil(persistedSecrets)
|
||||
|
||||
// Complex checks based on the scenarios.
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
|
||||
// Our test scenarios are complex due the multi-dimensional nature
|
||||
// of criteria, so we lay them out in a switch below.
|
||||
switch {
|
||||
case newAccessKeyId != "" && rotated && rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential set is to be
|
||||
// rotated as well. In this case, the old credentials should have
|
||||
// been deleted, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero and updated.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && rotated && !rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential is *not*
|
||||
// rotated. In this case, the old credentials should have
|
||||
// been deleted, But the new credentials should have not been
|
||||
// rotated, and hence should be the same. Rotation time should be
|
||||
// zero.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId != "" && !rotated && rotate:
|
||||
// The new access key ID was provided, we *have not* previously
|
||||
// rotated the credentials, and the new credential set is to be
|
||||
// rotated. In this case, the old credentials should have been
|
||||
// left alone, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero, but updated.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && !rotated && !rotate:
|
||||
// The new access key ID was provided, but we have not rotated
|
||||
// the credentials previously and we still don't plan on rotating
|
||||
// them. In this case, the old credentials should still be valid,
|
||||
// and the persisted ones should match the new ones provided.
|
||||
// Rotation time should be zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && rotated && rotate:
|
||||
// No new credentials have been provided, but we have previously
|
||||
// rotated and are still rotating credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should be identical since
|
||||
// no new rotation occurred.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId == "" && rotated && !rotate:
|
||||
// No new credentials have been provided, and we have previously
|
||||
// rotated the credentials. This is actually an error, but we
|
||||
// don't test it here; it's covered in unit testing (see
|
||||
// plugin_test.go).
|
||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||
|
||||
case newAccessKeyId == "" && !rotated && rotate:
|
||||
// No new credentials have been provided, and while we did not
|
||||
// rotate before, we want to switch to rotation. In this case,
|
||||
// the existing persisted credentials should have been rotated,
|
||||
// with a new non-zero timestamp.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && !rotated && !rotate:
|
||||
// No new credentials have been provided and we have not, nor do
|
||||
// not, plan on rotating the credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should remain at zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
default:
|
||||
// Scenario was reached that was not covered by this function.
|
||||
require.FailNow("unknown test scenario")
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
@@ -588,53 +444,3 @@ func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin,
|
||||
// Success
|
||||
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
||||
}
|
||||
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(ok)
|
||||
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity()
|
||||
require.NoError(err)
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestStoragePlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
t.Skip("set AWS_REGION to use this test")
|
||||
}
|
||||
if envAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID"); envAccessKeyId == "" {
|
||||
t.Skip("set AWS_ACCESS_KEY_ID to use this test")
|
||||
}
|
||||
if envSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY"); envSecretAccessKey == "" {
|
||||
t.Skip("set AWS_SECRET_ACCESS_KEY to use this test")
|
||||
}
|
||||
|
||||
require := require.New(t)
|
||||
tf, err := NewTestTerraformer("testdata/storage")
|
||||
require.NoError(err)
|
||||
require.NotNil(tf)
|
||||
|
||||
t.Log("===== deploying test Terraform workspace =====")
|
||||
err = tf.Deploy()
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Log("===== destroying test Terraform workspace =====")
|
||||
if err := tf.Destroy(); err != nil {
|
||||
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
bucketName, err := tf.GetOutputString("bucket_name")
|
||||
require.NoError(err)
|
||||
require.NotEmpty(bucketName)
|
||||
|
||||
iamAccessKeyIds, err := tf.GetOutputSlice("iam_access_key_ids")
|
||||
require.NoError(err)
|
||||
require.Len(iamAccessKeyIds, expectedIamUserCount)
|
||||
|
||||
iamSecretAccessKeys, err := tf.GetOutputSlice("iam_secret_access_keys")
|
||||
require.NoError(err)
|
||||
require.Len(iamSecretAccessKeys, expectedIamUserCount)
|
||||
|
||||
iamAccessKeyMissingGetObject, err := tf.GetOutputString("iam_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingGetObject, err := tf.GetOutputString("iam_secret_access_key_missing_get_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamAccessKeyMissingPutObject, err := tf.GetOutputString("iam_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
// Start the workflow now. Set up the host catalog. Note that this
|
||||
// will cause the state to go out of drift above in the sense that
|
||||
// the access key ID/secret access key will no longer be valid. We
|
||||
// will assert this through the returned state.
|
||||
p := new(storage.StoragePlugin)
|
||||
ctx := context.Background()
|
||||
|
||||
// ********************
|
||||
// * OnCreateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotation (using primary user).
|
||||
keyid, secret := testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, iamAccessKeyIds[0].(string), iamSecretAccessKeys[0].(string), false)
|
||||
// Test rotation next.
|
||||
keyid, secret = testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * OnUpdateStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test no-op non-rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, false)
|
||||
// Switch to rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, true)
|
||||
// Test no-op with rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", true, true)
|
||||
// Switch credentials to next user. Don't rotate.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[1].(string), iamSecretAccessKeys[1].(string), true, false)
|
||||
// Switch credentials to next user. Add rotation.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[2].(string), iamSecretAccessKeys[2].(string), false, true)
|
||||
// Switch to next user, with rotation disabled.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[3].(string), iamSecretAccessKeys[3].(string), true, false)
|
||||
// Last case - switch to another user and keep rotation off.
|
||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[4].(string), iamSecretAccessKeys[4].(string), false, false)
|
||||
|
||||
// ********************
|
||||
// * OnDeleteStorageBucket
|
||||
// ********************
|
||||
//
|
||||
// Test non-rotated.
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, false)
|
||||
// Test as if we had rotated these credentials (note that this
|
||||
// makes this test set unusable).
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * Object Methods: PutObject, GetObject, HeadObject
|
||||
// ********************
|
||||
//
|
||||
// Reassign the keyid and secret first.
|
||||
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
|
||||
testPluginObjectMethods(ctx, t, p, bucketName, region, keyid, secret)
|
||||
|
||||
// ********************
|
||||
// * Validate Permissions
|
||||
// ********************
|
||||
//
|
||||
// Validate error is returned for missing get object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
|
||||
|
||||
// Validate error is returned for missing put object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject)
|
||||
}
|
||||
|
||||
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnCreateStorageBucket (region=%s, rotate=%t)", region, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnCreateStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Secrets: reqSecrets,
|
||||
Attributes: reqAttrs,
|
||||
},
|
||||
}
|
||||
response, err := p.OnCreateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validatePersistedSecrets(t, persisted.GetData(), accessKeyId, secretAccessKey, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnUpdateStorageBucket(
|
||||
ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName,
|
||||
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||
rotated, rotate bool,
|
||||
) (string, string) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnUpdateStorageBucket (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
||||
require := require.New(t)
|
||||
|
||||
// Take a timestamp of the current time to get a point in time to
|
||||
// reference, ensuring that we are updating credential rotation
|
||||
// timestamps.
|
||||
currentCredsLastRotatedTime := time.Now()
|
||||
|
||||
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotate,
|
||||
})
|
||||
require.NoError(err)
|
||||
var reqSecrets *structpb.Struct
|
||||
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
||||
reqSecrets, err = structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: newAccessKeyId,
|
||||
credential.ConstSecretAccessKey: newSecretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
}
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: currentAccessKeyId,
|
||||
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
require.NotNil(reqPersistedSecrets)
|
||||
request := &pb.OnUpdateStorageBucketRequest{
|
||||
CurrentBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqCurrentAttrs,
|
||||
},
|
||||
NewBucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqNewAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnUpdateStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
persisted := response.GetPersisted()
|
||||
require.NotNil(persisted)
|
||||
return validateUpdateSecrets(t, persisted.GetData(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||
}
|
||||
|
||||
func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||
t.Helper()
|
||||
t.Logf("testing OnDeleteStorageBucket (region=%s, rotated=%t)", region, rotated)
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: !rotated,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
credential.ConstCredsLastRotatedTime: func() string {
|
||||
if rotated {
|
||||
return time.Now().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
return (time.Time{}).Format(time.RFC3339Nano)
|
||||
}(),
|
||||
})
|
||||
require.NoError(err)
|
||||
request := &pb.OnDeleteStorageBucketRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
||||
Data: reqPersistedSecrets,
|
||||
},
|
||||
}
|
||||
response, err := p.OnDeleteStorageBucket(ctx, request)
|
||||
require.NoError(err)
|
||||
require.NotNil(response)
|
||||
|
||||
// We want to test the validity of the credentials post-deletion.
|
||||
if rotated {
|
||||
// The credentials should no longer be valid.
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
// The credentials should still be valid. Sleep 10s first just to
|
||||
// be sure, since we're not rotating.
|
||||
time.Sleep(time.Second * 10)
|
||||
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing ValidatePermissions")
|
||||
require := require.New(t)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
req := &pb.ValidatePermissionsRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = p.ValidatePermissions(ctx, req)
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
t.Logf("testing PutObject")
|
||||
require := require.New(t)
|
||||
|
||||
testDataPath, err := filepath.Abs("./testdata/storage/test_object_data")
|
||||
require.NoError(err)
|
||||
require.FileExists(testDataPath)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
credential.ConstDisableCredentialRotation: true,
|
||||
})
|
||||
require.NoError(err)
|
||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstAccessKeyId: accessKeyId,
|
||||
credential.ConstSecretAccessKey: secretAccessKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
objectKey := uuid.New().String()
|
||||
putObjReq := &pb.PutObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
Path: testDataPath,
|
||||
}
|
||||
putObjResp, err := p.PutObject(ctx, putObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(putObjResp)
|
||||
require.NotEmpty(putObjResp.GetChecksumSha_256())
|
||||
|
||||
t.Logf("testing HeadObject")
|
||||
headObjReq := &pb.HeadObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
}
|
||||
headObjResp, err := p.HeadObject(ctx, headObjReq)
|
||||
require.NoError(err)
|
||||
require.NotNil(headObjResp)
|
||||
require.NotEmpty(headObjResp.GetContentLength())
|
||||
require.NotEmpty(headObjResp.GetLastModified())
|
||||
|
||||
// Need to create a GRPC server to test GetObject
|
||||
t.Logf("testing GetObject")
|
||||
lis, err := net.Listen("tcp", "localhost:2030")
|
||||
require.NoError(err)
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
||||
go func() {
|
||||
grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
client := pb.NewStoragePluginServiceClient(conn)
|
||||
|
||||
stream, err := client.GetObject(ctx, &pb.GetObjectRequest{
|
||||
Bucket: &storagebuckets.StorageBucket{
|
||||
BucketName: bucketName,
|
||||
Attributes: reqAttrs,
|
||||
Secrets: reqSecrets,
|
||||
},
|
||||
Key: objectKey,
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
var actualData []byte
|
||||
expectedData, err := os.ReadFile(testDataPath)
|
||||
require.NoError(err)
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
require.NoError(stream.CloseSend())
|
||||
break
|
||||
} else if err != nil {
|
||||
require.NoError(err)
|
||||
}
|
||||
actualData = append(actualData, resp.GetFileChunk()...)
|
||||
}
|
||||
require.Equal(expectedData, actualData)
|
||||
require.NoError(conn.Close())
|
||||
grpcServer.Stop()
|
||||
}
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_iam_role" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "valid" {
|
||||
name = "${random_id.prefix.dec}-valid"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "valid" {
|
||||
role = aws_iam_role.valid.name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
resource "aws_iam_policy" "missing_put_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_put_obj" {
|
||||
role = aws_iam_role.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "missing_get_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObjectAttributes"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
||||
role = aws_iam_role.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
# Static Credential Testing
|
||||
resource "random_id" "user_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "test" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.user_name[count.index].dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "test" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_s3" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.valid.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "credentials" {
|
||||
count = var.iam_user_count
|
||||
name = aws_iam_user.test[count.index].name
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"iam:DeleteAccessKey",
|
||||
"iam:GetUser",
|
||||
"iam:CreateAccessKey"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_iam_user.test[count.index].arn}"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "test_credentials" {
|
||||
count = var.iam_user_count
|
||||
user = aws_iam_user.test[count.index].name
|
||||
policy_arn = aws_iam_policy.credentials[count.index].arn
|
||||
}
|
||||
|
||||
# Edge case for missing GetObject permission
|
||||
|
||||
resource "random_id" "missing_get_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_get_obj" {
|
||||
name = random_id.missing_get_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
||||
user = aws_iam_user.missing_get_obj.name
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
# Edge case for missing PutObject permission
|
||||
|
||||
resource "random_id" "missing_put_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_put_obj" {
|
||||
name = random_id.missing_put_obj.dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_put_obj" {
|
||||
user = aws_iam_user.missing_put_obj.name
|
||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
output "bucket_name" {
|
||||
value = aws_s3_bucket.test.id
|
||||
}
|
||||
|
||||
output "valid_policy_arn" {
|
||||
value = aws_iam_role.valid.arn
|
||||
}
|
||||
|
||||
output "missing_put_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_put_obj.arn
|
||||
}
|
||||
|
||||
output "missing_put_get_policy_arn" {
|
||||
value = aws_iam_role.missing_get_obj.arn
|
||||
}
|
||||
|
||||
output "iam_user_names" {
|
||||
value = aws_iam_user.test.*.name
|
||||
}
|
||||
|
||||
output "iam_user_arns" {
|
||||
value = aws_iam_user.test.*.arn
|
||||
}
|
||||
|
||||
output "iam_access_key_ids" {
|
||||
value = aws_iam_access_key.test.*.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_keys" {
|
||||
value = aws_iam_access_key.test.*.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_put_obj" {
|
||||
value = aws_iam_access_key.missing_put_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "iam_secret_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = random_id.prefix.dec
|
||||
force_destroy = true
|
||||
acl = "private"
|
||||
tags = local.tags
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
HELLO_WORLD!
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
resource "random_id" "prefix" {
|
||||
prefix = "storage-e2e-test"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
variable "iam_user_count" {
|
||||
default = 6
|
||||
}
|
||||
|
||||
locals {
|
||||
tags = {
|
||||
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
repo = "boundary-plugin-aws"
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,26 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
tfjson "github.com/hashicorp/terraform-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const expectedIamUserCount = 6
|
||||
|
||||
// TestTerraformer is an object representing the lifecycle of a
|
||||
// particular Terraform project.
|
||||
//
|
||||
@@ -264,3 +275,204 @@ func (t *TestTerraformer) showJson() ([]byte, error) {
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// We need to wait for invalidation as while awsutil waits for
|
||||
// credential creation, deletion of the old credentials returns
|
||||
// immediately.
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
waitErr:
|
||||
for {
|
||||
_, err = c.GetCallerIdentity()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
// pass
|
||||
|
||||
case <-timeoutCtx.Done():
|
||||
break waitErr
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(ok)
|
||||
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||
}
|
||||
|
||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
c, err := awsutil.NewCredentialsConfig(
|
||||
awsutil.WithAccessKey(accessKeyId),
|
||||
awsutil.WithSecretKey(secretAccessKey),
|
||||
)
|
||||
require.NoError(err)
|
||||
_, err = c.GetCallerIdentity()
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
func validatePersistedSecrets(t *testing.T, persistedSecrets *structpb.Struct, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
require.NotNil(persistedSecrets)
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
if rotate {
|
||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||
} else {
|
||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||
}
|
||||
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
if rotate {
|
||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||
} else {
|
||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||
}
|
||||
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
if rotate {
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||
} else {
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
|
||||
func validateUpdateSecrets(
|
||||
t *testing.T, persistedSecrets *structpb.Struct, currentCredsLastRotatedTime time.Time,
|
||||
currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||
rotated, rotate bool) (string, string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
// Complex checks based on the scenarios.
|
||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedAccessKeyId)
|
||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||
require.NoError(err)
|
||||
require.NotZero(persistedSecretAccessKey)
|
||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||
require.NoError(err)
|
||||
|
||||
// Our test scenarios are complex due the multi-dimensional nature
|
||||
// of criteria, so we lay them out in a switch below.
|
||||
switch {
|
||||
case newAccessKeyId != "" && rotated && rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential set is to be
|
||||
// rotated as well. In this case, the old credentials should have
|
||||
// been deleted, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero and updated.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && rotated && !rotate:
|
||||
// The new access key ID was provided, we had previously rotated
|
||||
// the credentials before, and the new credential is *not*
|
||||
// rotated. In this case, the old credentials should have
|
||||
// been deleted, But the new credentials should have not been
|
||||
// rotated, and hence should be the same. Rotation time should be
|
||||
// zero.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId != "" && !rotated && rotate:
|
||||
// The new access key ID was provided, we *have not* previously
|
||||
// rotated the credentials, and the new credential set is to be
|
||||
// rotated. In this case, the old credentials should have been
|
||||
// left alone, and the new credentials should have been rotated,
|
||||
// hence, should not match the new credentials initially
|
||||
// provided. Rotation time should be non-zero, but updated.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId != "" && !rotated && !rotate:
|
||||
// The new access key ID was provided, but we have not rotated
|
||||
// the credentials previously and we still don't plan on rotating
|
||||
// them. In this case, the old credentials should still be valid,
|
||||
// and the persisted ones should match the new ones provided.
|
||||
// Rotation time should be zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && rotated && rotate:
|
||||
// No new credentials have been provided, but we have previously
|
||||
// rotated and are still rotating credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should be identical since
|
||||
// no new rotation occurred.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||
|
||||
case newAccessKeyId == "" && rotated && !rotate:
|
||||
// No new credentials have been provided, and we have previously
|
||||
// rotated the credentials. This is actually an error, but we
|
||||
// don't test it here; it's covered in unit testing (see
|
||||
// plugin_test.go).
|
||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||
|
||||
case newAccessKeyId == "" && !rotated && rotate:
|
||||
// No new credentials have been provided, and while we did not
|
||||
// rotate before, we want to switch to rotation. In this case,
|
||||
// the existing persisted credentials should have been rotated,
|
||||
// with a new non-zero timestamp.
|
||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.NotZero(persistedCredsLastRotatedTime)
|
||||
|
||||
case newAccessKeyId == "" && !rotated && !rotate:
|
||||
// No new credentials have been provided and we have not, nor do
|
||||
// not, plan on rotating the credentials. This is a no-op.
|
||||
// Existing credentials should still be valid and match the ones
|
||||
// persisted to state. Rotation time should remain at zero.
|
||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||
require.Zero(persistedCredsLastRotatedTime)
|
||||
|
||||
default:
|
||||
// Scenario was reached that was not covered by this function.
|
||||
require.FailNow("unknown test scenario")
|
||||
}
|
||||
|
||||
return persistedAccessKeyId, persistedSecretAccessKey
|
||||
}
|
||||
Reference in new issue
Block a user