246 lines
6 KiB
Go
246 lines
6 KiB
Go
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)),
|
|
}
|
|
}
|