328 lines
7.8 KiB
Go
328 lines
7.8 KiB
Go
package shared
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type VaultKVResult struct {
|
|
Operation string `json:"operation"`
|
|
Mount string `json:"mount"`
|
|
Path string `json:"path"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
Keys []string `json:"keys,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
func HandleVaultKVTool(arguments map[string]any) (string, error) {
|
|
request, err := buildVaultKVRequest(arguments)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
result, err := executeVaultKVRequest(request)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
encoded, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(encoded), nil
|
|
}
|
|
|
|
type vaultKVRequest struct {
|
|
baseURL string
|
|
token string
|
|
namespace string
|
|
operation string
|
|
mount string
|
|
path string
|
|
data map[string]any
|
|
cas int
|
|
}
|
|
|
|
func buildVaultKVRequest(arguments map[string]any) (vaultKVRequest, error) {
|
|
baseURL := strings.TrimSpace(EnvOrDefault("VAULT_SERVER_URL", ""))
|
|
if baseURL == "" {
|
|
return vaultKVRequest{}, errors.New("VAULT_SERVER_URL environment variable not set")
|
|
}
|
|
token := strings.TrimSpace(EnvOrDefault("VAULT_SERVER_ROOT_ACCESS_TOKEN", ""))
|
|
if token == "" {
|
|
return vaultKVRequest{}, errors.New("VAULT_SERVER_ROOT_ACCESS_TOKEN environment variable not set")
|
|
}
|
|
operation := strings.ToLower(strings.TrimSpace(StringArg(arguments, "operation", "")))
|
|
if operation == "" {
|
|
return vaultKVRequest{}, errors.New("operation is required")
|
|
}
|
|
path := normalizeVaultPath(StringArg(arguments, "path", ""))
|
|
if path == "" {
|
|
return vaultKVRequest{}, errors.New("path is required")
|
|
}
|
|
data, err := vaultDataArg(arguments["data"])
|
|
if err != nil {
|
|
return vaultKVRequest{}, err
|
|
}
|
|
return vaultKVRequest{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
token: token,
|
|
namespace: strings.TrimSpace(EnvOrDefault("VAULT_NAMESPACE", "")),
|
|
operation: operation,
|
|
mount: normalizeVaultMount(StringArg(arguments, "mount", "secret")),
|
|
path: path,
|
|
data: data,
|
|
cas: vaultCASArg(arguments["cas"]),
|
|
}, nil
|
|
}
|
|
|
|
func executeVaultKVRequest(request vaultKVRequest) (VaultKVResult, error) {
|
|
switch request.operation {
|
|
case "get", "read":
|
|
return vaultKVRead(request)
|
|
case "put", "write":
|
|
return vaultKVWrite(request)
|
|
case "list":
|
|
return vaultKVList(request)
|
|
case "delete":
|
|
return vaultKVDelete(request)
|
|
default:
|
|
return VaultKVResult{}, fmt.Errorf("unsupported operation: %s", request.operation)
|
|
}
|
|
}
|
|
|
|
func vaultKVRead(request vaultKVRequest) (VaultKVResult, error) {
|
|
response, err := doVaultRequest(
|
|
request,
|
|
http.MethodGet,
|
|
vaultDataURL(request.mount, request.path),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return VaultKVResult{}, err
|
|
}
|
|
dataBlock := mapArg(response["data"])
|
|
return VaultKVResult{
|
|
Operation: "read",
|
|
Mount: request.mount,
|
|
Path: request.path,
|
|
Data: mapArg(dataBlock["data"]),
|
|
Metadata: mapArg(dataBlock["metadata"]),
|
|
}, nil
|
|
}
|
|
|
|
func vaultKVWrite(request vaultKVRequest) (VaultKVResult, error) {
|
|
if len(request.data) == 0 {
|
|
return VaultKVResult{}, errors.New("data is required for write operations")
|
|
}
|
|
payload := map[string]any{"data": request.data}
|
|
if request.cas > 0 {
|
|
payload["options"] = map[string]any{"cas": request.cas}
|
|
}
|
|
response, err := doVaultRequest(
|
|
request,
|
|
http.MethodPost,
|
|
vaultDataURL(request.mount, request.path),
|
|
payload,
|
|
)
|
|
if err != nil {
|
|
return VaultKVResult{}, err
|
|
}
|
|
return VaultKVResult{
|
|
Operation: "write",
|
|
Mount: request.mount,
|
|
Path: request.path,
|
|
Data: request.data,
|
|
Metadata: mapArg(mapArg(response["data"])["metadata"]),
|
|
}, nil
|
|
}
|
|
|
|
func vaultKVList(request vaultKVRequest) (VaultKVResult, error) {
|
|
response, err := doVaultRequest(
|
|
request,
|
|
"LIST",
|
|
vaultMetadataURL(request.mount, request.path),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return VaultKVResult{}, err
|
|
}
|
|
dataBlock := mapArg(response["data"])
|
|
return VaultKVResult{
|
|
Operation: "list",
|
|
Mount: request.mount,
|
|
Path: request.path,
|
|
Keys: stringSliceArg(dataBlock["keys"]),
|
|
}, nil
|
|
}
|
|
|
|
func vaultKVDelete(request vaultKVRequest) (VaultKVResult, error) {
|
|
_, err := doVaultRequest(
|
|
request,
|
|
http.MethodDelete,
|
|
vaultDataURL(request.mount, request.path),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return VaultKVResult{}, err
|
|
}
|
|
return VaultKVResult{
|
|
Operation: "delete",
|
|
Mount: request.mount,
|
|
Path: request.path,
|
|
}, nil
|
|
}
|
|
|
|
func doVaultRequest(
|
|
request vaultKVRequest,
|
|
method string,
|
|
target string,
|
|
payload map[string]any,
|
|
) (map[string]any, error) {
|
|
var body io.Reader
|
|
if payload != nil {
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body = bytes.NewReader(encoded)
|
|
}
|
|
httpRequest, err := http.NewRequest(method, target, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpRequest.Header.Set("X-Vault-Token", request.token)
|
|
if request.namespace != "" {
|
|
httpRequest.Header.Set("X-Vault-Namespace", request.namespace)
|
|
}
|
|
if payload != nil {
|
|
httpRequest.Header.Set("Content-Type", "application/json")
|
|
}
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
response, err := client.Do(httpRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = response.Body.Close()
|
|
}()
|
|
bodyBytes, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf(
|
|
"vault api error %d: %s",
|
|
response.StatusCode,
|
|
strings.TrimSpace(string(bodyBytes)),
|
|
)
|
|
}
|
|
if len(strings.TrimSpace(string(bodyBytes))) == 0 {
|
|
return map[string]any{}, nil
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(bodyBytes, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
return decoded, nil
|
|
}
|
|
|
|
func vaultDataURL(mount, path string) string {
|
|
return fmt.Sprintf("%s/data/%s", vaultBasePath(mount), vaultPathSegments(path))
|
|
}
|
|
|
|
func vaultMetadataURL(mount, path string) string {
|
|
return fmt.Sprintf("%s/metadata/%s", vaultBasePath(mount), vaultPathSegments(path))
|
|
}
|
|
|
|
func vaultBasePath(mount string) string {
|
|
return fmt.Sprintf("%s/v1/%s", strings.TrimRight(strings.TrimSpace(EnvOrDefault("VAULT_SERVER_URL", "")), "/"), url.PathEscape(normalizeVaultMount(mount)))
|
|
}
|
|
|
|
func vaultPathSegments(path string) string {
|
|
segments := strings.Split(normalizeVaultPath(path), "/")
|
|
for index, segment := range segments {
|
|
segments[index] = url.PathEscape(segment)
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
func normalizeVaultMount(raw string) string {
|
|
trimmed := strings.Trim(strings.TrimSpace(raw), "/")
|
|
if trimmed == "" {
|
|
return "secret"
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
func normalizeVaultPath(raw string) string {
|
|
return strings.Trim(strings.TrimSpace(raw), "/")
|
|
}
|
|
|
|
func vaultDataArg(raw any) (map[string]any, error) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
switch typed := raw.(type) {
|
|
case map[string]any:
|
|
return typed, nil
|
|
case string:
|
|
trimmed := strings.TrimSpace(typed)
|
|
if trimmed == "" {
|
|
return nil, nil
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal([]byte(trimmed), &decoded); err != nil {
|
|
return nil, errors.New("data must be a JSON object")
|
|
}
|
|
return decoded, nil
|
|
default:
|
|
return nil, errors.New("data must be an object")
|
|
}
|
|
}
|
|
|
|
func vaultCASArg(raw any) int {
|
|
switch typed := raw.(type) {
|
|
case int:
|
|
return typed
|
|
case int64:
|
|
return int(typed)
|
|
case float64:
|
|
return int(typed)
|
|
case string:
|
|
return IntArg(typed, 0)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func mapArg(raw any) map[string]any {
|
|
switch typed := raw.(type) {
|
|
case map[string]any:
|
|
return typed
|
|
default:
|
|
return map[string]any{}
|
|
}
|
|
}
|
|
|
|
func stringSliceArg(raw any) []string {
|
|
values, ok := raw.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
text := strings.TrimSpace(fmt.Sprint(value))
|
|
if text == "" || text == "<nil>" {
|
|
continue
|
|
}
|
|
result = append(result, text)
|
|
}
|
|
return result
|
|
}
|