quorate/packages/versitygw-terraform-provider/internal/client/client.go

246 lines
6 KiB
Go
Raw Permalink Normal View History

package client
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)
type Config struct {
Endpoint string
Region string
AdminAccessKey string
AdminSecretKey string
InsecureTLS bool
}
type Client struct {
endpoint *url.URL
region string
creds aws.Credentials
http *http.Client
signer *v4.Signer
}
type Account struct {
Access string `xml:"Access"`
Secret string `xml:"Secret,omitempty"`
Role string `xml:"Role"`
UserID int64 `xml:"UserID"`
GroupID int64 `xml:"GroupID"`
ProjectID int64 `xml:"ProjectID"`
}
type MutableProps struct {
XMLName xml.Name `xml:"MutableProps"`
Secret *string `xml:"Secret,omitempty"`
Role string `xml:"Role,omitempty"`
UserID *int64 `xml:"UserID,omitempty"`
GroupID *int64 `xml:"GroupID,omitempty"`
ProjectID *int64 `xml:"ProjectID,omitempty"`
}
type ListUserAccountsResult struct {
XMLName xml.Name `xml:"ListUserAccountsResult"`
Accounts []Account `xml:"Accounts"`
}
type ErrorResponse struct {
XMLName xml.Name `xml:"Error"`
Code string `xml:"Code"`
Message string `xml:"Message"`
RequestID string `xml:"RequestId"`
HostID string `xml:"HostId"`
}
type APIError struct {
Status int
Code string
Message string
RequestID string
HostID string
RawBody string
}
func (e *APIError) Error() string {
if e.Message == "" {
return fmt.Sprintf("admin API status %d: %s", e.Status, e.Code)
}
return fmt.Sprintf("admin API status %d: %s: %s", e.Status, e.Code, e.Message)
}
func IsErrorCode(err error, code string) bool {
var apiErr *APIError
if !errors.As(err, &apiErr) {
return false
}
return apiErr.Code == code
}
func New(cfg Config) (*Client, error) {
if cfg.Endpoint == "" {
return nil, fmt.Errorf("endpoint is required")
}
if cfg.Region == "" {
cfg.Region = "us-east-1"
}
if cfg.AdminAccessKey == "" {
return nil, fmt.Errorf("admin access key is required")
}
if cfg.AdminSecretKey == "" {
return nil, fmt.Errorf("admin secret key is required")
}
endpoint, err := url.Parse(cfg.Endpoint)
if err != nil {
return nil, fmt.Errorf("parse endpoint: %w", err)
}
if endpoint.Scheme == "" || endpoint.Host == "" {
return nil, fmt.Errorf("endpoint must include scheme and host")
}
return &Client{
endpoint: endpoint,
region: cfg.Region,
creds: aws.Credentials{
AccessKeyID: cfg.AdminAccessKey,
SecretAccessKey: cfg.AdminSecretKey,
},
http: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureTLS}, //nolint:gosec
},
},
signer: v4.NewSigner(),
}, nil
}
func (c *Client) CreateUser(ctx context.Context, account Account) error {
body, err := xml.Marshal(account)
if err != nil {
return fmt.Errorf("marshal create-user request: %w", err)
}
_, err = c.do(ctx, http.MethodPatch, "/create-user", nil, body)
return err
}
func (c *Client) UpdateUser(ctx context.Context, access string, props MutableProps) error {
body, err := xml.Marshal(props)
if err != nil {
return fmt.Errorf("marshal update-user request: %w", err)
}
query := url.Values{"access": []string{access}}
_, err = c.do(ctx, http.MethodPatch, "/update-user", query, body)
return err
}
func (c *Client) DeleteUser(ctx context.Context, access string) error {
query := url.Values{"access": []string{access}}
_, err := c.do(ctx, http.MethodPatch, "/delete-user", query, nil)
return err
}
func (c *Client) GetUser(ctx context.Context, access string) (Account, bool, error) {
accounts, err := c.ListUsers(ctx)
if err != nil {
return Account{}, false, err
}
for _, account := range accounts {
if account.Access == access {
return account, true, nil
}
}
return Account{}, false, nil
}
func (c *Client) ListUsers(ctx context.Context) ([]Account, error) {
body, err := c.do(ctx, http.MethodPatch, "/list-users", nil, nil)
if err != nil {
return nil, err
}
var result ListUserAccountsResult
if err := xml.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("parse list-users response: %w", err)
}
return result.Accounts, nil
}
func (c *Client) do(ctx context.Context, method, path string, query url.Values, body []byte) ([]byte, error) {
endpoint := *c.endpoint
endpoint.Path = strings.TrimRight(c.endpoint.Path, "/") + path
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
hashedPayload := sha256.Sum256(body)
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
if len(body) > 0 {
req.Header.Set("Content-Type", "application/xml")
}
if err := c.signer.SignHTTP(ctx, c.creds, req, hexPayload, "s3", c.region, time.Now()); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, parseError(resp.StatusCode, respBody)
}
return respBody, nil
}
func parseError(status int, body []byte) error {
var apiErr ErrorResponse
if err := xml.Unmarshal(body, &apiErr); err != nil {
return &APIError{
Status: status,
RawBody: strings.TrimSpace(string(body)),
Message: strings.TrimSpace(string(body)),
}
}
if apiErr.Code == "" && apiErr.Message == "" {
return &APIError{
Status: status,
RawBody: strings.TrimSpace(string(body)),
Message: strings.TrimSpace(string(body)),
}
}
return &APIError{
Status: status,
Code: apiErr.Code,
Message: apiErr.Message,
RequestID: apiErr.RequestID,
HostID: apiErr.HostID,
RawBody: strings.TrimSpace(string(body)),
}
}