mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-04 20:36:25 +00:00
282 lines
7.1 KiB
Go
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
|
|
}
|