Compare commits

..
Author SHA1 Message Date
Hugo caa6c4eda2 feat(storage): Make checksum verification optional
Some 3rd party services the MinIO SDK can connect
with do not return a checksum whenever a file is
uploaded. Before this change, this would lead to
an error, rendering it impossible to operate with
those 3rd party services. With this change, we'll
continue to verify checksums if they come through,
however we won't error out if they don't and the
PutObject call is successful.

Note: There are no new tests with this change as
this behaviour is not unit-testeable at the
moment. I suggest we could have end-to-end tests
for this.
2024-06-10 16:19:20 +01:00
dani 9299172dd1 move service account validation to only check for service account on credential rotation. should allow other S3-compatible providers (#16) 2024-05-15 11:45:47 -04:00
Danielle Miu 07cb85645b update all vulnerable packages 2024-04-11 22:41:40 -04:00
hashicorp-copywrite[bot] a0067502b9 [COMPLIANCE] Add Copyright and License Headers (#15)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-04-09 11:17:43 -07:00
Hugo bde78ecccd docs(plugin): Add Credential Rotation attribute and explanation 2024-04-04 18:03:37 +01:00
Hugo eb0c2b4682 feat(plugin): Implement Credential Rotation 2024-04-04 18:03:37 +01:00
Hugo 0214399017 feat(plugin): Add credential rotation primitives 2024-04-04 18:03:37 +01:00
Hugo 2c2bd3429b feat(values): Add methods to get bool and time values from proto 2024-04-04 18:03:37 +01:00
dani 09cece64bc feat(plugin): implement DeleteObjects functionality along with associated automated tests (#14) 2024-04-04 12:19:10 -04:00
Hugo 4ffe4d227f feat(plugin): Implement GetObject 2024-03-15 17:39:24 +00:00
dani b335b9336b implement PutObject functionality and associated tests (#10) 2024-03-15 13:33:47 -04:00
Hugo 4fc2ce9449 chore(plugin): Remove unnecessary comments 2024-03-12 18:47:26 +00:00
Hugo a2235d93ce feat(plugin): Implement HeadObject 2024-03-12 18:47:26 +00:00
dani e0da13c89a add onupdate functionality (#6) 2024-03-12 13:00:11 -04:00
11 changed files with 3140 additions and 157 deletions

No files matched your search

+7 -7
View File
@@ -1,6 +1,6 @@
module github.com/hashicorp/boundary-plugin-minio
go 1.21.5
go 1.22.2
require (
github.com/google/go-cmp v0.6.0
@@ -11,7 +11,7 @@ require (
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.8.4
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.32.0
google.golang.org/protobuf v1.33.0
)
require (
@@ -22,7 +22,7 @@ require (
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -54,7 +54,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -76,11 +76,11 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
+12 -44
View File
@@ -1,6 +1,5 @@
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
@@ -17,24 +16,18 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
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/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ=
github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
@@ -43,15 +36,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
@@ -136,7 +126,6 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -175,7 +164,6 @@ github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQh
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -183,17 +171,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
@@ -222,12 +207,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc=
github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
@@ -238,8 +221,6 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@@ -248,7 +229,6 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -256,16 +236,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -285,8 +261,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -302,12 +278,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -318,20 +293,13 @@ golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -340,8 +308,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@@ -374,9 +343,8 @@ google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+47
View File
@@ -5,6 +5,7 @@ package values
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -37,6 +38,52 @@ func GetStringValue(in *structpb.Struct, k string, required bool) (string, error
return s, nil
}
// GetBoolValue returns a boolean 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 and required is true, or the value type is not a boolean.
func GetBoolValue(in *structpb.Struct, k string, required bool) (bool, error) {
mv := in.GetFields()
v, ok := mv[k]
if !ok {
if required {
return false, fmt.Errorf("missing required value %q", k)
}
return false, nil
}
b, ok := v.AsInterface().(bool)
if !ok {
return false, fmt.Errorf("unexpected type for value %q: want bool, got %T", k, v.AsInterface())
}
return b, nil
}
// GetTimeValue returns a time.Time object and no error if the given key is
// found in the provided proto struct input. An error is returned if the value
// in the struct isn't a string or the value is not parsable by time.Parse. The
// expected time layout is RFC3339Nano.
func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
mv := in.GetFields()
v, ok := mv[k]
if !ok {
return time.Time{}, nil
}
tRaw, ok := v.AsInterface().(string)
if !ok {
return time.Time{}, fmt.Errorf("unexpected type for value %q: want string, got %T", k, v.AsInterface())
}
t, err := time.Parse(time.RFC3339Nano, tRaw)
if err != nil {
return time.Time{}, fmt.Errorf("could not parse time in value %q: %w", k, err)
}
return t, nil
}
// StructFields returns a map[string]struct{} of the proto struct input.
func StructFields(s *structpb.Struct) map[string]struct{} {
m := make(map[string]struct{}, len(s.GetFields()))
+130
View File
@@ -5,6 +5,7 @@ package values
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
@@ -78,3 +79,132 @@ func TestGetStringValue(t *testing.T) {
})
}
}
func TestGetBoolValue(t *testing.T) {
cases := []struct {
name string
in map[string]any
key string
required bool
expected bool
expectedErr string
}{
{
name: "required missing",
in: map[string]any{},
key: "foo",
required: true,
expectedErr: "missing required value \"foo\"",
},
{
name: "optional missing",
in: map[string]any{},
key: "foo",
expected: false,
},
{
name: "non-bool value",
in: map[string]any{
"foo": "bar",
},
key: "foo",
expectedErr: "unexpected type for value \"foo\": want bool, got string",
},
{
name: "good",
in: map[string]any{
"foo": true,
},
key: "foo",
expected: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := GetBoolValue(input, tc.key, tc.required)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetTimeValue(t *testing.T) {
staticTime := time.Now()
cases := []struct {
name string
in map[string]any
key string
expected time.Time
expectedErr string
}{
{
name: "missing",
in: map[string]any{},
key: "foo",
expected: time.Time{},
},
{
name: "non-time value",
in: map[string]any{
"foo": 1,
},
key: "foo",
expectedErr: "unexpected type for value \"foo\": want string, got float64",
},
{
name: "bad parse",
in: map[string]any{
"foo": "bar",
},
key: "foo",
expectedErr: "could not parse time in value \"foo\": parsing time \"bar\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"bar\" as \"2006\"",
},
{
name: "good",
in: map[string]any{
"foo": staticTime.Format(time.RFC3339Nano),
},
key: "foo",
expected: func() time.Time {
u, err := time.Parse(time.RFC3339Nano, staticTime.Format(time.RFC3339Nano))
if err != nil {
panic(err)
}
return u
}(),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
actual, err := GetTimeValue(input, tc.key)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
+6
View File
@@ -13,6 +13,7 @@ boundary storage-buckets create \
-worker-filter '"minio-access" in "/tags/type"' \
-attr endpoint_url="https://my-minio-instance.dev/s3-api" \
-attr region=REGION \
-attr disable_credential_rotation=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
@@ -24,6 +25,11 @@ The following `attributes` are valid on a MinIO storage bucket resource:
- `endpoint_url` (string, required): Fully-qualified endpoint pointing to a
MinIO S3 API.
- `region` (string, optional): The region to configure the storage bucket for.
- `disable_credential_rotation` (bool, optional): Controls whether the plugin
will rotate the incoming credentials and manage a new MinIO service account.
If this attribute is set to `false`, or not provided, the plugin will rotate
the incoming credentials, using them to create a new MinIO service account,
then delete the incoming credentials.
### Secrets
+44 -7
View File
@@ -6,6 +6,7 @@ package storage
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary-plugin-minio/internal/errors"
"github.com/hashicorp/boundary-plugin-minio/internal/values"
@@ -15,10 +16,12 @@ import (
)
const (
ConstEndpointUrl = "endpoint_url"
ConstAccessKeyId = "access_key_id"
ConstSecretAccessKey = "secret_access_key"
ConstRegion = "region"
ConstEndpointUrl = "endpoint_url"
ConstAccessKeyId = "access_key_id"
ConstSecretAccessKey = "secret_access_key"
ConstRegion = "region"
ConstDisableCredentialRotation = "disable_credential_rotation"
ConstLastRotatedTime = "creds_last_rotated_time"
)
type StorageAttributes struct {
@@ -31,6 +34,11 @@ type StorageAttributes struct {
// UseSSL determines if the MinIO SDK will use SSL when communicating with
// the server. This field is determined based on the endpoint URL prefix.
UseSSL bool
// DisableCredentialRotation is used to configure whether this plugin should
// manage the MinIO credentials or not. By default, this field is set to
// false. See the plugin's OnCreateStorageBucket, OnUpdateStorageBucket and
// OnDeleteStorageBucket for how we use this.
DisableCredentialRotation bool
}
type StorageSecrets struct {
@@ -40,6 +48,10 @@ type StorageSecrets struct {
// SecretAccessKey is the MinIO Secret Access Key. This field is required
// and comes from user input.
SecretAccessKey string
// LastRotatedTime is the time the secrets contained in this object were
// last rotated. Will be set to the zero-value (time.Time{}) if the
// credentials haven't been rotated.
LastRotatedTime time.Time
}
// AsMap returns a map StorageAttributes's secret fields as a map.
@@ -47,6 +59,17 @@ func (sa *StorageSecrets) AsMap() map[string]any {
return map[string]any{
ConstAccessKeyId: sa.AccessKeyId,
ConstSecretAccessKey: sa.SecretAccessKey,
ConstLastRotatedTime: sa.LastRotatedTime.Format(time.RFC3339Nano),
}
}
// Clone returns a new StorageSecrets object with the same state as the incoming
// one.
func (sec *StorageSecrets) Clone() *StorageSecrets {
return &StorageSecrets{
AccessKeyId: sec.AccessKeyId,
SecretAccessKey: sec.SecretAccessKey,
LastRotatedTime: sec.LastRotatedTime,
}
}
@@ -83,6 +106,12 @@ func getStorageAttributes(inAttributes *structpb.Struct) (*StorageAttributes, er
}
delete(unknownAttrFields, ConstRegion)
disableCredentialRotation, err := values.GetBoolValue(inAttributes, ConstDisableCredentialRotation, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
}
delete(unknownAttrFields, ConstDisableCredentialRotation)
for s := range unknownAttrFields {
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
}
@@ -91,9 +120,10 @@ func getStorageAttributes(inAttributes *structpb.Struct) (*StorageAttributes, er
return nil, errors.InvalidArgumentError("invalid or unrecognized attributes found", badFields)
}
return &StorageAttributes{
EndpointUrl: endpointUrl,
Region: region,
UseSSL: useSSL,
EndpointUrl: endpointUrl,
Region: region,
UseSSL: useSSL,
DisableCredentialRotation: disableCredentialRotation,
}, nil
}
@@ -118,6 +148,12 @@ func getStorageSecrets(inSecrets *structpb.Struct) (*StorageSecrets, error) {
}
delete(unknownSecretFields, ConstSecretAccessKey)
lastRotated, err := values.GetTimeValue(inSecrets, ConstLastRotatedTime)
if err != nil {
badFields[fmt.Sprintf("secrets.%s", ConstLastRotatedTime)] = err.Error()
}
delete(unknownSecretFields, ConstLastRotatedTime)
for s := range unknownSecretFields {
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
}
@@ -128,5 +164,6 @@ func getStorageSecrets(inSecrets *structpb.Struct) (*StorageSecrets, error) {
return &StorageSecrets{
AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey,
LastRotatedTime: lastRotated,
}, nil
}
+63 -7
View File
@@ -5,6 +5,7 @@ package storage
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
@@ -89,18 +90,30 @@ func TestGetStorageAttributes(t *testing.T) {
},
expErrMsg: "attributes.endpoint_url: missing required value \"endpoint_url\"",
},
{
name: "disableCredRotationBadType",
inAttributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstEndpointUrl: structpb.NewStringValue("http://bar"),
ConstDisableCredentialRotation: structpb.NewStringValue("notbool"),
},
},
expErrMsg: "attributes.disable_credential_rotation: unexpected type for value \"disable_credential_rotation\": want bool, got string",
},
{
name: "successWithNoSSL",
inAttributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
ConstRegion: structpb.NewStringValue("us-east-1"),
ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"),
ConstRegion: structpb.NewStringValue("us-east-1"),
ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
expStorageAttributes: &StorageAttributes{
EndpointUrl: "foo.bar",
Region: "us-east-1",
UseSSL: false,
EndpointUrl: "foo.bar",
Region: "us-east-1",
UseSSL: false,
DisableCredentialRotation: true,
},
},
{
@@ -215,6 +228,17 @@ func TestGetStorageSecrets(t *testing.T) {
},
expErrMsg: "secrets.secret_access_key: unexpected type for value \"secret_access_key\": want string, got bool",
},
{
name: "lastRotatedBadType",
inSecrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foo"),
ConstSecretAccessKey: structpb.NewStringValue("bar"),
ConstLastRotatedTime: structpb.NewStringValue("nottime"),
},
},
expErrMsg: "secrets.creds_last_rotated_time: could not parse time in value \"creds_last_rotated_time\"",
},
{
name: "unknownSecret",
inSecrets: &structpb.Struct{
@@ -239,6 +263,21 @@ func TestGetStorageSecrets(t *testing.T) {
SecretAccessKey: "bar",
},
},
{
name: "successWithLastRotatedTime",
inSecrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstAccessKeyId: structpb.NewStringValue("foo"),
ConstSecretAccessKey: structpb.NewStringValue("bar"),
ConstLastRotatedTime: structpb.NewStringValue(time.Date(2024, 01, 02, 03, 04, 05, 06, time.UTC).Format(time.RFC3339Nano)),
},
},
expStorageSecrets: &StorageSecrets{
AccessKeyId: "foo",
SecretAccessKey: "bar",
LastRotatedTime: time.Date(2024, 01, 02, 03, 04, 05, 06, time.UTC),
},
},
}
for _, tt := range tests {
@@ -268,10 +307,12 @@ func TestStorageSecretsAsMap(t *testing.T) {
in: &StorageSecrets{
AccessKeyId: "access_key_id_value",
SecretAccessKey: "secret_access_key_value",
LastRotatedTime: time.Date(2024, 01, 02, 03, 04, 05, 06, time.UTC),
},
expMap: map[string]any{
"access_key_id": "access_key_id_value",
"secret_access_key": "secret_access_key_value",
"access_key_id": "access_key_id_value",
"secret_access_key": "secret_access_key_value",
"creds_last_rotated_time": "2024-01-02T03:04:05.000000006Z",
},
},
}
@@ -283,3 +324,18 @@ func TestStorageSecretsAsMap(t *testing.T) {
})
}
}
func TestTestStorageSecretsClone(t *testing.T) {
now := time.Now()
in := &StorageSecrets{
AccessKeyId: "foo",
SecretAccessKey: "bar",
LastRotatedTime: now,
}
secCl := in.Clone()
require.Equal(t, in.AccessKeyId, secCl.AccessKeyId)
require.Equal(t, in.AccessKeyId, secCl.AccessKeyId)
require.Equal(t, in.LastRotatedTime, secCl.LastRotatedTime)
require.NotSame(t, in, secCl)
}
+402 -19
View File
@@ -4,10 +4,17 @@
package storage
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"os"
"path"
"strings"
"sync"
"github.com/google/uuid"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
@@ -18,10 +25,15 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil)
const (
defaultGetObjectChunkSize = uint32(65536)
)
// StoragePlugin implements the StoragePluginServiceServer interface for the
// MinIO storage service plugin.
type StoragePlugin struct {
@@ -40,7 +52,7 @@ func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCr
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err // getStorageAttributes already returns a "status"-type error.
return nil, err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
@@ -48,9 +60,14 @@ func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCr
return nil, err
}
err = ensureServiceAccount(ctx, sa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err)
deleteOldCredsFn := func() error { return nil }
if !sa.DisableCredentialRotation {
newSec, fn, err := rotateCredentials(ctx, bucket.GetBucketName(), sa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
}
sec = newSec
deleteOldCredsFn = fn
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
@@ -71,6 +88,13 @@ func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCr
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert storage attributes to protobuf struct: %v", err)
}
// Given that at this point everything is OK with the bucket, we shouldn't
// let a failure to delete the old credentials fail the entire process. If
// we ever establish a way to log non-critical errors in the plugin, log
// this.
_ = deleteOldCredsFn()
return &pb.OnCreateStorageBucketResponse{
Persisted: &storagebuckets.StorageBucketPersisted{Data: persistedData},
}, nil
@@ -85,24 +109,22 @@ func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUp
if newBucket.BucketName == "" {
return nil, status.Error(codes.InvalidArgument, "new bucketName is required")
}
oldBucket := req.GetCurrentBucket()
if oldBucket == nil {
return nil, status.Error(codes.InvalidArgument, "current bucket is required")
}
// current attributes
osa, err := getStorageAttributes(oldBucket.GetAttributes())
if err != nil {
return nil, err
}
// new attributes and secrets
nsa, err := getStorageAttributes(newBucket.GetAttributes())
if err != nil {
return nil, err
}
sec, err := getStorageSecrets(newBucket.GetSecrets())
sec, err := getStorageSecrets(req.GetPersisted().GetData())
if err != nil {
return nil, err
}
@@ -118,8 +140,64 @@ func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUp
return nil, status.Errorf(codes.InvalidArgument, "cannot update attribute EndpointUrl")
}
if err = ensureServiceAccount(ctx, nsa, sec); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %s", err)
if nsa.DisableCredentialRotation && newBucket.GetSecrets() == nil {
if !sec.LastRotatedTime.IsZero() {
return nil, status.Error(codes.InvalidArgument, "cannot disable rotation for already-rotated credentials")
}
}
deleteRotatedCredsFn := func() error { return nil }
deleteOldPersistedCredsFn := func() error { return nil }
if newBucket.GetSecrets() != nil {
// Preemptively check if we're managing the current persisted
// credentials and schedule them for later deletion if we are, since in
// both rotation and non-rotation cases below, we'll be replacing these.
if !sec.LastRotatedTime.IsZero() {
sc := sec.Clone()
deleteOldPersistedCredsFn = sync.OnceValue(func() error {
cl, err := newMadminClient(nsa, sc)
if err != nil {
return fmt.Errorf("failed to create new minio admin client: %w", err)
}
err = cl.DeleteServiceAccount(ctx, sc.AccessKeyId)
if err != nil {
return fmt.Errorf("failed to delete minio service account: %w", err)
}
return nil
})
}
newSec, err := getStorageSecrets(newBucket.GetSecrets())
if err != nil {
return nil, err
}
if !nsa.DisableCredentialRotation {
// We have new incoming credentials and we're also rotating,
// therefore we use incoming credentials to create a new service
// account, then delete the incoming credentials. The existing
// persisted credentials have already been scheduled for deletion
// above (if applicable), so we don't need to do it here.
rotatedSec, fn, err := rotateCredentials(ctx, newBucket.GetBucketName(), nsa, newSec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
}
sec = rotatedSec
deleteRotatedCredsFn = fn
} else {
// We have new incoming credentials but we're not rotating. Set them
// directly for validation below. The existing persisted credentials
// have already been scheduled for deletion above (if applicable),
// so we don't need to do it here.
sec = newSec
}
} else {
if osa.DisableCredentialRotation && !nsa.DisableCredentialRotation {
// Handle the case where the user enables credential rotation but
// provides no new credentials. This is an error.
return nil, status.Errorf(codes.InvalidArgument, "credential rotation enabled with no new credentials provided")
}
}
cl, err := minio.New(nsa.EndpointUrl, &minio.Options{
@@ -140,14 +218,46 @@ func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUp
return nil, status.Errorf(codes.InvalidArgument, "failed to convert storage attributes to protobuf struct: %s", err)
}
// Given that at this point everything is OK with the bucket, we shouldn't
// let a failure to delete credentials fail the entire process. If we ever
// establish a way to log non-critical errors in the plugin, log this.
_ = deleteRotatedCredsFn()
_ = deleteOldPersistedCredsFn()
return &pb.OnUpdateStorageBucketResponse{
Persisted: &storagebuckets.StorageBucketPersisted{Data: persisted},
}, nil
}
// OnDeleteStorageBucket is a hook that runs when a storage bucket is deleted.
// Since this plugin manages no state at the moment, this function is a no-op.
func (sp *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDeleteStorageBucketRequest) (*pb.OnDeleteStorageBucketResponse, error) {
bucket := req.GetBucket()
if bucket == nil {
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
}
sec, err := getStorageSecrets(req.GetPersisted().GetData())
if err != nil {
return nil, err
}
if !sec.LastRotatedTime.IsZero() {
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err
}
ac, err := newMadminClient(sa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to create minio admin client: %v", err)
}
err = ac.DeleteServiceAccount(ctx, sec.AccessKeyId)
if err != nil {
return nil, status.Errorf(codes.Unknown, "failed to delete minio service account: %v", err)
}
}
return &pb.OnDeleteStorageBucketResponse{}, nil
}
@@ -164,7 +274,7 @@ func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Valida
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err // getStorageAttributes already returns a "status"-type error.
return nil, err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
@@ -172,9 +282,13 @@ func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Valida
return nil, err
}
err = ensureServiceAccount(ctx, sa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err)
if !sec.LastRotatedTime.IsZero() {
// since these are existing secrets, we only want to validate the
// service account if the credentials have been rotated previously
err = ensureServiceAccount(ctx, sa, sec)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err)
}
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
@@ -196,24 +310,293 @@ func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Valida
// HeadObject is a hook that retrieves metadata about an object.
func (sp *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectRequest) (*pb.HeadObjectResponse, error) {
return nil, fmt.Errorf("unimplemented")
bucket := req.GetBucket()
if bucket == nil {
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
}
if bucket.GetBucketName() == "" {
return nil, status.Error(codes.InvalidArgument, "storage bucket name is required")
}
if req.GetKey() == "" {
return nil, status.Error(codes.InvalidArgument, "object key is required")
}
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
if err != nil {
return nil, err
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
Secure: sa.UseSSL,
Region: sa.Region,
})
if err != nil {
return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
}
objKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
oi, err := cl.StatObject(ctx, bucket.GetBucketName(), objKey, minio.StatObjectOptions{})
if err != nil {
return nil, status.Errorf(codes.Unknown, "failed to stat object: %v", err)
}
return &pb.HeadObjectResponse{
ContentLength: oi.Size,
LastModified: timestamppb.New(oi.LastModified),
}, nil
}
// GetObject is a hook that retrieves objects.
func (sp *StoragePlugin) GetObject(req *pb.GetObjectRequest, objServer pb.StoragePluginService_GetObjectServer) error {
return fmt.Errorf("unimplemented")
bucket := req.GetBucket()
if bucket == nil {
return status.Error(codes.InvalidArgument, "no storage bucket information found")
}
if bucket.GetBucketName() == "" {
return status.Error(codes.InvalidArgument, "storage bucket name is required")
}
if req.GetKey() == "" {
return status.Error(codes.InvalidArgument, "object key is required")
}
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
if err != nil {
return err
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
Secure: sa.UseSSL,
Region: sa.Region,
})
if err != nil {
return status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
}
objKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
obj, err := cl.GetObject(objServer.Context(), bucket.GetBucketName(), objKey, minio.GetObjectOptions{})
if err != nil {
return status.Errorf(codes.Unknown, "failed to get object: %v", err)
}
defer obj.Close()
chunkSize := req.GetChunkSize()
if chunkSize == 0 {
chunkSize = defaultGetObjectChunkSize
}
rd := bufio.NewReader(obj)
for {
select {
case <-objServer.Context().Done():
return status.Errorf(codes.Canceled, "server context done while streaming: %v", objServer.Context().Err())
default:
buffer := make([]byte, chunkSize)
n, err := rd.Read(buffer)
if err != nil && err != io.EOF {
return status.Errorf(codes.Unknown, "failed to read chunk from minio object: %v", err)
}
if n > 0 {
err := objServer.Send(&pb.GetObjectResponse{FileChunk: buffer[:n]})
if err != nil {
return status.Errorf(codes.Unknown, "failed to send chunk to client: %v", err)
}
}
if err == io.EOF {
return nil
}
}
}
}
// PutObject is a hook that reads a file stored on local disk and stores it to
// an external object store.
func (sp *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) (*pb.PutObjectResponse, error) {
return nil, fmt.Errorf("unimplemented")
bucket := req.GetBucket()
if bucket == nil {
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
}
if bucket.GetBucketName() == "" {
return nil, status.Error(codes.InvalidArgument, "storage bucket name is required")
}
if req.GetKey() == "" {
return nil, status.Error(codes.InvalidArgument, "object key is required")
}
if req.GetPath() == "" {
return nil, status.Error(codes.InvalidArgument, "path is required")
}
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
if err != nil {
return nil, err
}
file, err := os.Open(req.GetPath())
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to open file: %v", err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to read file info: %v", err)
}
if info.IsDir() {
return nil, status.Error(codes.InvalidArgument, "path is not a file")
}
if info.Size() == 0 {
return nil, status.Error(codes.InvalidArgument, "file is empty")
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
Secure: sa.UseSSL,
Region: sa.Region,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create minio sdk client: %v", err)
}
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return nil, status.Errorf(codes.Internal, "failed to calculate hash: %v", err)
}
checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil))
if _, err = file.Seek(0, io.SeekStart); err != nil {
return nil, status.Errorf(codes.Internal, "failed to rewind file pointer: %v", err)
}
key := path.Join(bucket.GetBucketPrefix(), req.GetKey())
res, err := cl.PutObject(ctx, bucket.GetBucketName(), key, file, info.Size(), minio.PutObjectOptions{
UserMetadata: map[string]string{
"x-amz-checksum-algorithm": "SHA256",
"x-amz-checksum-sha256": checksum,
},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to put object into minio: %v", err)
}
resChecksum := res.ChecksumSHA256
if resChecksum == "" {
// We were successful in putting the object but we didn't get a file
// SHA256 checksum from the server. Given that the call was successful,
// assume everything went ok and force the checksum to match so we can
// send it correctly back to Boundary.
resChecksum = checksum
}
if checksum != resChecksum {
return nil, status.Error(codes.Internal, "mismatched checksum")
}
decodedChecksum, err := base64.StdEncoding.DecodeString(resChecksum)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to decode checksum value from minio: %v", err)
}
return &pb.PutObjectResponse{
ChecksumSha_256: decodedChecksum,
}, nil
}
// DeleteObjects deletes one or many files in an external object store via a
// provided key prefix.
func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjectsRequest) (*pb.DeleteObjectsResponse, error) {
return nil, fmt.Errorf("unimplemented")
bucket := req.GetBucket()
if bucket == nil {
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
}
if bucket.GetBucketName() == "" {
return nil, status.Error(codes.InvalidArgument, "storage bucket name is required")
}
if req.GetKeyPrefix() == "" {
return nil, status.Error(codes.InvalidArgument, "key prefix is required")
}
sa, err := getStorageAttributes(bucket.GetAttributes())
if err != nil {
return nil, err
}
sec, err := getStorageSecrets(bucket.GetSecrets())
if err != nil {
return nil, err
}
cl, err := minio.New(sa.EndpointUrl, &minio.Options{
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
Secure: sa.UseSSL,
Region: sa.Region,
})
if err != nil {
return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
}
prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix())
if strings.HasSuffix(req.GetKeyPrefix(), "/") {
// path.Join ends by "cleaning" the path, including removing a trailing slash, if
// it exists. given that a slash is used to denote a folder, it is required here.
prefix += "/"
}
if !req.Recursive {
if err := cl.RemoveObject(ctx, bucket.GetBucketName(), prefix, minio.RemoveObjectOptions{}); err != nil {
return nil, status.Errorf(codes.Unknown, "error deleting minio object: %v", err)
}
return &pb.DeleteObjectsResponse{
ObjectsDeleted: uint32(1),
}, nil
}
objects := []minio.ObjectInfo{}
for obj := range cl.ListObjects(ctx, bucket.GetBucketName(), minio.ListObjectsOptions{
Prefix: prefix,
Recursive: true,
}) {
if obj.Err != nil {
return nil, status.Errorf(codes.Unknown, "error iterating minio bucket contents: %v", err)
}
objects = append(objects, obj)
}
emitter := make(chan minio.ObjectInfo)
go func() {
defer close(emitter)
// we could loop the listobjects respose here, but we want to check all the errs ahead of time
for _, obj := range objects {
emitter <- obj
}
}()
removed := 0
for res := range cl.RemoveObjectsWithResult(ctx, bucket.GetBucketName(), emitter, minio.RemoveObjectsOptions{}) {
if res.Err != nil {
return nil, status.Errorf(codes.Unknown, "error deleting minio object(s): %v", err)
}
removed++
}
return &pb.DeleteObjectsResponse{
ObjectsDeleted: uint32(removed),
}, nil
}
func dryRun(ctx context.Context, cl *minio.Client, bucket *storagebuckets.StorageBucket) error {
File diff suppressed because it is too large. Load diff
+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/minio/madmin-go/v3"
)
const (
defaultPolicyTemplate = `{
"Statement": [
{
"Action": [
"s3:GetObject",
"s3:GetObjectAttributes",
"s3:PutObject",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::%[1]s/*"
},
{
"Action": "s3:ListBucket",
"Effect": "Allow",
"Resource": "arn:aws:s3:::%[1]s"
},
{
"Action": [
"admin:CreateServiceAccount",
"admin:RemoveServiceAccount"
],
"Effect": "Allow"
}
],
"Version": "2012-10-17"
}`
)
type deleteInputCredsFn func() error
// rotateCredentials uses the incoming service account credentials to generate a
// new service account. It returns a new StorageSecrets object with the new
// service account credentials and a callback function to delete the service
// account provided in the input.
func rotateCredentials(ctx context.Context, bucketName string, sa *StorageAttributes, inSec *StorageSecrets) (*StorageSecrets, deleteInputCredsFn, error) {
err := ensureServiceAccount(ctx, sa, inSec)
if err != nil {
return nil, nil, fmt.Errorf("failed to ensure minio service account credentials: %w", err)
}
ac, err := newMadminClient(sa, inSec)
if err != nil {
return nil, nil, fmt.Errorf("failed to create minio admin client: %w", err)
}
policy := json.RawMessage(fmt.Sprintf(defaultPolicyTemplate, bucketName))
newCreds, err := ac.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Name: "Boundary MinIO Service Account",
Description: fmt.Sprintf("Boundary-managed MinIO service account for storage bucket '%s'", bucketName),
Policy: policy,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to create new minio service account: %w", err)
}
newSec := &StorageSecrets{
AccessKeyId: newCreds.AccessKey,
SecretAccessKey: newCreds.SecretKey,
LastRotatedTime: time.Now(),
}
inCl := inSec.Clone()
return newSec, sync.OnceValue(func() error {
ac, err := newMadminClient(sa, inCl)
if err != nil {
return fmt.Errorf("failed to create minio admin client: %w", err)
}
err = ac.DeleteServiceAccount(ctx, inCl.AccessKeyId)
if err != nil {
return fmt.Errorf("failed to delete minio service account: %w", err)
}
return nil
}), nil
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package storage
import (
"context"
"encoding/json"
"testing"
internaltest "github.com/hashicorp/boundary-plugin-minio/internal/testing"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7"
"github.com/stretchr/testify/require"
)
func TestRotateCredentials(t *testing.T) {
ctx := context.Background()
server := internaltest.NewMinioServer(t)
bucketName := "test-bucket"
err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
require.NoError(t, err)
tests := []struct {
name string
inSec *StorageSecrets
expErrMsg string
credDelErr bool
}{
{
name: "notUsingServiceAccount",
inSec: &StorageSecrets{
AccessKeyId: server.RootUsername,
SecretAccessKey: server.RootPassword,
},
expErrMsg: "failed to ensure minio service account credentials: failed to obtain service account info: The specified service account is not found",
},
{
name: "addServiceAccountFail",
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Policy: json.RawMessage(`
{
"Statement": [
{
"Action": [
"admin:CreateServiceAccount",
"admin:RemoveServiceAccount"
],
"Effect": "Deny"
}
],
"Version": "2012-10-17"
}
`),
})
require.NoError(t, err)
return &StorageSecrets{
AccessKeyId: creds.AccessKey,
SecretAccessKey: creds.SecretKey,
}
}(),
expErrMsg: "failed to create new minio service account: Access Denied",
},
{
name: "credentialDeletionFail",
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{})
require.NoError(t, err)
return &StorageSecrets{
AccessKeyId: creds.AccessKey,
SecretAccessKey: creds.SecretKey,
}
}(),
credDelErr: true,
},
{
name: "success",
inSec: func() *StorageSecrets {
creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{})
require.NoError(t, err)
return &StorageSecrets{
AccessKeyId: creds.AccessKey,
SecretAccessKey: creds.SecretKey,
}
}(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sa := &StorageAttributes{
EndpointUrl: server.ApiAddr,
UseSSL: false,
}
outSec, delFn, err := rotateCredentials(context.Background(), bucketName, sa, tt.inSec)
if tt.expErrMsg != "" {
require.ErrorContains(t, err, tt.expErrMsg)
require.Nil(t, outSec)
require.Nil(t, delFn)
return
}
require.NoError(t, err)
require.NotNil(t, outSec)
require.NotNil(t, delFn)
require.NotEqual(t, outSec.AccessKeyId, tt.inSec.AccessKeyId)
require.NotEqual(t, outSec.SecretAccessKey, tt.inSec.SecretAccessKey)
require.NotEmpty(t, outSec.LastRotatedTime)
// Ensure old creds still exist since the callback hasn't been
// called. We then make the delete call and finally assert that the
// credential was removed if there was no error when attempting to
// delete.
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.NoError(t, err)
if tt.credDelErr {
sa.EndpointUrl = "localhost:1"
require.ErrorContains(t, delFn(), "failed to delete minio service account")
require.ErrorContains(t, delFn(), "failed to delete minio service account") // 2nd call should return the same state (due to sync.Once).
_, err := server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.NoError(t, err)
return
} else {
require.NoError(t, delFn())
require.NoError(t, delFn()) // 2nd call should return the same state (due to sync.Once).
}
_, err = server.AdminClient.InfoServiceAccount(ctx, tt.inSec.AccessKeyId)
require.ErrorContains(t, err, "service account is not found")
})
}
}