Files
fs/cmd/admin_client.go
2026-03-11 00:40:51 +01:00

282 lines
7.1 KiB
Go

package cmd
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
type adminUserListItem struct {
AccessKeyID string `json:"accessKeyId"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
type adminUserListResponse struct {
Items []adminUserListItem `json:"items"`
NextCursor string `json:"nextCursor,omitempty"`
}
type adminPolicyStatement struct {
Effect string `json:"effect"`
Actions []string `json:"actions"`
Bucket string `json:"bucket"`
Prefix string `json:"prefix"`
}
type adminPolicy struct {
Principal string `json:"principal,omitempty"`
Statements []adminPolicyStatement `json:"statements"`
}
type adminUserResponse struct {
AccessKeyID string `json:"accessKeyId"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
Policy *adminPolicy `json:"policy,omitempty"`
SecretKey string `json:"secretKey,omitempty"`
}
type createUserRequest struct {
AccessKeyID string `json:"accessKeyId"`
SecretKey string `json:"secretKey,omitempty"`
Status string `json:"status,omitempty"`
Policy adminPolicy `json:"policy"`
}
type setStatusRequest struct {
Status string `json:"status"`
}
type setPolicyRequest struct {
Policy adminPolicy `json:"policy"`
}
type adminErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId,omitempty"`
}
type adminAPIError struct {
StatusCode int
Code string
Message string
RequestID string
}
func (e *adminAPIError) Error() string {
if e == nil {
return ""
}
if e.Code == "" {
return fmt.Sprintf("admin API request failed: status=%d", e.StatusCode)
}
if e.RequestID == "" {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
return fmt.Sprintf("%s: %s (requestId=%s)", e.Code, e.Message, e.RequestID)
}
type adminAPIClient struct {
baseURL *url.URL
region string
accessKey string
secretKey string
client *http.Client
}
func newAdminAPIClient(opts *adminOptions, requireCredentials bool) (*adminAPIClient, error) {
if opts == nil {
return nil, errors.New("admin options are required")
}
if err := opts.resolve(requireCredentials); err != nil {
return nil, err
}
baseURL, err := url.Parse(opts.Endpoint)
if err != nil {
return nil, err
}
return &adminAPIClient{
baseURL: baseURL,
region: opts.Region,
accessKey: opts.AccessKey,
secretKey: opts.SecretKey,
client: &http.Client{
Timeout: opts.Timeout,
},
}, nil
}
func (c *adminAPIClient) CreateUser(ctx context.Context, request createUserRequest) (*adminUserResponse, error) {
var out adminUserResponse
if err := c.doJSON(ctx, http.MethodPost, "/_admin/v1/users", nil, request, &out, http.StatusCreated); err != nil {
return nil, err
}
return &out, nil
}
func (c *adminAPIClient) ListUsers(ctx context.Context, limit int, cursor string) (*adminUserListResponse, error) {
query := make(url.Values)
if limit > 0 {
query.Set("limit", strconv.Itoa(limit))
}
if strings.TrimSpace(cursor) != "" {
query.Set("cursor", strings.TrimSpace(cursor))
}
var out adminUserListResponse
if err := c.doJSON(ctx, http.MethodGet, "/_admin/v1/users", query, nil, &out, http.StatusOK); err != nil {
return nil, err
}
return &out, nil
}
func (c *adminAPIClient) GetUser(ctx context.Context, accessKeyID string) (*adminUserResponse, error) {
var out adminUserResponse
path := "/_admin/v1/users/" + url.PathEscape(strings.TrimSpace(accessKeyID))
if err := c.doJSON(ctx, http.MethodGet, path, nil, nil, &out, http.StatusOK); err != nil {
return nil, err
}
return &out, nil
}
func (c *adminAPIClient) DeleteUser(ctx context.Context, accessKeyID string) error {
path := "/_admin/v1/users/" + url.PathEscape(strings.TrimSpace(accessKeyID))
return c.doJSON(ctx, http.MethodDelete, path, nil, nil, nil, http.StatusNoContent)
}
func (c *adminAPIClient) SetUserStatus(ctx context.Context, accessKeyID, status string) (*adminUserResponse, error) {
var out adminUserResponse
path := "/_admin/v1/users/" + url.PathEscape(strings.TrimSpace(accessKeyID)) + "/status"
if err := c.doJSON(ctx, http.MethodPut, path, nil, setStatusRequest{Status: status}, &out, http.StatusOK); err != nil {
return nil, err
}
return &out, nil
}
func (c *adminAPIClient) SetUserPolicy(ctx context.Context, accessKeyID string, policy adminPolicy) (*adminUserResponse, error) {
var out adminUserResponse
path := "/_admin/v1/users/" + url.PathEscape(strings.TrimSpace(accessKeyID)) + "/policy"
if err := c.doJSON(ctx, http.MethodPut, path, nil, setPolicyRequest{Policy: policy}, &out, http.StatusOK); err != nil {
return nil, err
}
return &out, nil
}
func (c *adminAPIClient) Health(ctx context.Context) (string, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/healthz", nil, nil)
if err != nil {
return "", err
}
resp, err := c.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
text := strings.TrimSpace(string(body))
if resp.StatusCode != http.StatusOK {
if text == "" {
text = http.StatusText(resp.StatusCode)
}
return text, fmt.Errorf("health check failed: status=%d", resp.StatusCode)
}
if text == "" {
text = "ok"
}
return text, nil
}
func (c *adminAPIClient) doJSON(
ctx context.Context,
method string,
path string,
query url.Values,
body any,
out any,
expectedStatus int,
) error {
var payload []byte
var err error
if body != nil {
payload, err = json.Marshal(body)
if err != nil {
return err
}
}
req, err := c.newRequest(ctx, method, path, query, payload)
if err != nil {
return err
}
if len(payload) > 0 {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
if err := signSigV4Request(req, payload, c.accessKey, c.secretKey, c.region, "s3"); err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if readErr != nil {
return readErr
}
if resp.StatusCode != expectedStatus {
apiErr := &adminAPIError{StatusCode: resp.StatusCode}
parsed := adminErrorResponse{}
if len(raw) > 0 && json.Unmarshal(raw, &parsed) == nil {
apiErr.Code = parsed.Code
apiErr.Message = parsed.Message
apiErr.RequestID = parsed.RequestID
}
if apiErr.Message == "" {
apiErr.Message = strings.TrimSpace(string(raw))
}
return apiErr
}
if out == nil || len(raw) == 0 {
return nil
}
return json.NewDecoder(bytes.NewReader(raw)).Decode(out)
}
func (c *adminAPIClient) newRequest(
ctx context.Context,
method string,
path string,
query url.Values,
payload []byte,
) (*http.Request, error) {
u := *c.baseURL
u.Path = strings.TrimRight(c.baseURL.Path, "/") + path
u.RawQuery = ""
if len(query) > 0 {
u.RawQuery = query.Encode()
}
req, err := http.NewRequestWithContext(ctx, method, u.String(), bytes.NewReader(payload))
if err != nil {
return nil, err
}
return req, nil
}