chore: update usage of minio admin library (#28)
* chore: update usage of minio admin library
This commit is contained in:
1 parent
7ceb652854
commit
c9e9377b4a
22 files changed
+1547
-389
No files matched your search
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-minio/madmin"
|
||||
"github.com/minio/minio-go/v7/pkg/signer"
|
||||
)
|
||||
|
||||
const adminPrefix = "/minio/admin/v3"
|
||||
|
||||
// AddServiceAccountReq contains the values used by the plugin when creating a minio service account
|
||||
type AddServiceAccountReq struct {
|
||||
// Policy to apply to service account
|
||||
Policy json.RawMessage `json:"policy,omitempty"`
|
||||
// Name for this access key
|
||||
Name string `json:"name,omitempty"`
|
||||
// Description for this access key
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// AddServiceAccountResp contains the credentials associated with the added service account
|
||||
type AddServiceAccountResp struct {
|
||||
Cred Credential `json:"credentials"`
|
||||
}
|
||||
|
||||
// Credential contains a minio AccessKeyId and SecretAccessKey
|
||||
type Credential struct {
|
||||
AccessKeyId string `json:"accessKey,omitempty"`
|
||||
SecretAccessKey string `json:"secretKey,omitempty"`
|
||||
}
|
||||
|
||||
// MinioError contains the error message from failed admin API requests
|
||||
type MinioError struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Client is used to access Minio admin API
|
||||
type Client struct {
|
||||
endpointUrl *url.URL
|
||||
accessKeyId string
|
||||
secretAccessKey string
|
||||
}
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how options are passed as arguments
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withUseSsl bool
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
// WithUseSsl provides an Option to use SSL.
|
||||
func WithUseSsl(b bool) Option {
|
||||
return func(o *options) {
|
||||
o.withUseSsl = b
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a Client that can be used to access Minio admin API
|
||||
func New(endpointUrl, accessKeyId, secretAccessKey string, opt ...Option) (*Client, error) {
|
||||
opts := getOpts(opt...)
|
||||
url, err := madmin.GetEndpointURL(endpointUrl, opts.withUseSsl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{endpointUrl: url, accessKeyId: accessKeyId, secretAccessKey: secretAccessKey}, nil
|
||||
}
|
||||
|
||||
// DeleteServiceAccount deletes the provided serviceAccount
|
||||
func (c *Client) DeleteServiceAccount(ctx context.Context, serviceAccount string) error {
|
||||
// construct the url
|
||||
url := c.endpointUrl.JoinPath(adminPrefix, "delete-service-account")
|
||||
query := url.Query()
|
||||
query.Set("accessKey", serviceAccount)
|
||||
url.RawQuery = query.Encode()
|
||||
|
||||
// create the request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Get sha256 for request header
|
||||
sum := sha256.Sum256(nil)
|
||||
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
|
||||
|
||||
// Sign request as documented
|
||||
// https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management.html
|
||||
req = signer.SignV4(*req, c.accessKeyId, c.secretAccessKey, "", "")
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status code
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
// expected no content response, unmarshal resp body for error message
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if len(respBody) == 0 {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
errResp := &MinioError{}
|
||||
if err := json.Unmarshal(respBody, &errResp); err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
return errors.New(errResp.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddServiceAccount adds a service account to the minio server
|
||||
func (c *Client) AddServiceAccount(ctx context.Context, in AddServiceAccountReq) (Credential, error) {
|
||||
// construct the url
|
||||
url := c.endpointUrl.JoinPath(adminPrefix, "add-service-account")
|
||||
|
||||
// marshal and encrypt data payload
|
||||
data, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
|
||||
// Minio encrypts streaming data as described https://blog.min.io/frictionless-encryption/
|
||||
payload, err := madmin.EncryptData(c.secretAccessKey, data)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
|
||||
// create the request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Get sha256 for request header
|
||||
sum := sha256.Sum256(payload)
|
||||
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Sign request as documented
|
||||
// https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management.html
|
||||
req = signer.SignV4(*req, c.accessKeyId, c.secretAccessKey, "", "")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// did not get ok, unmarshal resp body for error message
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if len(respBody) == 0 {
|
||||
return Credential{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
errResp := &MinioError{}
|
||||
if err := json.Unmarshal(respBody, &errResp); err != nil {
|
||||
return Credential{}, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
return Credential{}, errors.New(errResp.Message)
|
||||
}
|
||||
|
||||
// got resp lets decrypt it
|
||||
result, err := madmin.DecryptData(c.secretAccessKey, resp.Body)
|
||||
addResp := &AddServiceAccountResp{}
|
||||
if err := json.Unmarshal(result, &addResp); err != nil {
|
||||
return Credential{}, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return addResp.Cred, nil
|
||||
}
|
||||
|
||||
// EnsureServiceAccount true if the account is a service account
|
||||
func (c *Client) EnsureServiceAccount(ctx context.Context, serviceAccount string) error {
|
||||
// construct the url
|
||||
url := c.endpointUrl.JoinPath(adminPrefix, "info-service-account")
|
||||
query := url.Query()
|
||||
query.Set("accessKey", serviceAccount)
|
||||
url.RawQuery = query.Encode()
|
||||
|
||||
// create the request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// get sha256 for request header
|
||||
sum := sha256.Sum256(nil)
|
||||
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
|
||||
|
||||
// sign request as documented
|
||||
// https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management.html
|
||||
req = signer.SignV4(*req, c.accessKeyId, c.secretAccessKey, "", "")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// did not get ok, unmarshal resp body for error message
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if len(respBody) == 0 {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
errResp := &MinioError{}
|
||||
if err := json.Unmarshal(respBody, &errResp); err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
return errors.New(errResp.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
type args struct {
|
||||
endpointUrl string
|
||||
accessKeyId string
|
||||
secretAccessKey string
|
||||
opt []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantUrl string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fully qualified path",
|
||||
args: args{endpointUrl: "https://test.com"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no ssl",
|
||||
args: args{
|
||||
endpointUrl: "test.com",
|
||||
accessKeyId: "username",
|
||||
secretAccessKey: "password",
|
||||
},
|
||||
wantUrl: "http://test.com",
|
||||
},
|
||||
{
|
||||
name: "with ssl",
|
||||
args: args{
|
||||
endpointUrl: "test.com",
|
||||
accessKeyId: "username",
|
||||
secretAccessKey: "password",
|
||||
opt: []Option{WithUseSsl(true)},
|
||||
},
|
||||
wantUrl: "https://test.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.endpointUrl, tt.args.accessKeyId, tt.args.secretAccessKey, tt.args.opt...)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
assert.Equal(tt.wantUrl, got.endpointUrl.String())
|
||||
assert.Equal(tt.args.accessKeyId, got.accessKeyId)
|
||||
assert.Equal(tt.args.secretAccessKey, got.secretAccessKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithUseSsl", func(t *testing.T) {
|
||||
testOpts := getDefaultOptions()
|
||||
assert.Equal(t, false, testOpts.withUseSsl)
|
||||
|
||||
testOpts = getOpts()
|
||||
assert.Equal(t, false, testOpts.withUseSsl)
|
||||
|
||||
testOpts = getOpts(WithUseSsl(true))
|
||||
assert.Equal(t, true, testOpts.withUseSsl)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-minio/internal/client"
|
||||
internaltest "github.com/hashicorp/boundary-plugin-minio/internal/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
server := internaltest.NewMinioServer(t)
|
||||
c, err := client.New(server.ApiAddr, server.RootUsername, server.RootPassword)
|
||||
require.NoError(err)
|
||||
assert.NotNil(c)
|
||||
|
||||
creds, err := c.AddServiceAccount(ctx, client.AddServiceAccountReq{})
|
||||
require.NoError(err)
|
||||
|
||||
// We just added service account it should be valid
|
||||
err = c.EnsureServiceAccount(ctx, creds.AccessKeyId)
|
||||
require.NoError(err)
|
||||
|
||||
err = c.DeleteServiceAccount(ctx, creds.AccessKeyId)
|
||||
require.NoError(err)
|
||||
|
||||
// Now that we have deleted the service account it should be invalid
|
||||
err = c.EnsureServiceAccount(ctx, creds.AccessKeyId)
|
||||
require.Error(err)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/hashicorp/boundary-plugin-minio/internal/client"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/ory/dockertest/v3"
|
||||
@@ -46,8 +46,8 @@ type MinioServer struct {
|
||||
// Client is a MinIO SDK client, created with the default service account
|
||||
// credentials.
|
||||
Client *minio.Client
|
||||
// AdminClient is a Madmin client created with the root account credentials.
|
||||
AdminClient *madmin.AdminClient
|
||||
// AdminClient is a client created with the root account credentials.
|
||||
AdminClient *client.Client
|
||||
}
|
||||
|
||||
type Option func(testing.TB, *options)
|
||||
@@ -165,23 +165,20 @@ func NewMinioServer(t testing.TB, inOpts ...Option) *MinioServer {
|
||||
}
|
||||
|
||||
// Create service account.
|
||||
acl, err := madmin.NewWithOptions(server.ApiAddr, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(server.RootUsername, server.RootPassword, ""),
|
||||
Secure: false,
|
||||
})
|
||||
c, err := client.New(server.ApiAddr, server.RootUsername, server.RootPassword)
|
||||
require.NoError(t, err)
|
||||
server.AdminClient = acl
|
||||
server.AdminClient = c
|
||||
|
||||
creds, err := acl.AddServiceAccount(context.Background(), madmin.AddServiceAccountReq{
|
||||
creds, err := c.AddServiceAccount(context.Background(), client.AddServiceAccountReq{
|
||||
Name: "Boundary MinIO Plugin Test",
|
||||
Description: "MinIO credentials for Boundary MinIO plugin testing",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
server.ServiceAccountAccessKeyId = creds.AccessKey
|
||||
server.ServiceAccountSecretAccessKey = creds.SecretKey
|
||||
server.ServiceAccountAccessKeyId = creds.AccessKeyId
|
||||
server.ServiceAccountSecretAccessKey = creds.SecretAccessKey
|
||||
|
||||
cl, err := minio.New(server.ApiAddr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""),
|
||||
Creds: credentials.NewStaticV4(creds.AccessKeyId, creds.SecretAccessKey, ""),
|
||||
Secure: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in new issue
Block a user