~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
234 lines · 6.6 KB Raw History
package openbao

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
)

var ErrOutOfScope = errors.New("openbao: the path is outside the handle's scope")

type ClientFor func(ctx context.Context, mount string) (*Client, error)

type Handle struct {
	clients ClientFor

	mount string

	scope []string
}

var (
	_ vault.KV = (*Handle)(nil)

	_ vault.Leases = (*Handle)(nil)

	_ vault.PKI = (*Handle)(nil)

	_ vault.Keys = (*Handle)(nil)
)

func NewHandle(clients ClientFor, defaultMount string, scope []string) (*Handle, error) {
	if clients == nil {
		return nil, errors.New("openbao: a handle needs a client source")
	}
	cleaned := make([]string, 0, len(scope))
	for _, s := range scope {
		if s = strings.Trim(strings.TrimSpace(s), "/"); s != "" {
			cleaned = append(cleaned, s)
		}
	}
	if len(cleaned) == 0 {
		return nil, fmt.Errorf("%w: a handle needs a scope", ErrOutOfScope)
	}
	return &Handle{clients: clients, mount: strings.Trim(defaultMount, "/"), scope: cleaned}, nil
}

func (h *Handle) allows(target string) bool {
	target = strings.Trim(target, "/")
	for _, s := range h.scope {
		if s == "*" || s == target {
			return true
		}
		if prefix, ok := strings.CutSuffix(s, "*"); ok && strings.HasPrefix(target, prefix) {
			return true
		}
		if strings.HasPrefix(target, s+"/") {
			return true
		}
	}
	return false
}

func (h *Handle) kvTarget(ctx context.Context, mount, path string) (*Client, string, string, error) {
	mount = strings.Trim(strings.TrimSpace(mount), "/")
	if mount == "" {
		mount = h.mount
	}
	path = strings.Trim(strings.TrimSpace(path), "/")
	path = strings.TrimPrefix(strings.TrimPrefix(path, mount+"/data/"), mount+"/metadata/")
	if mount == "" || path == "" {
		return nil, "", "", errors.New("openbao: a KV call needs a mount and a path")
	}
	if !h.allows(mount + "/" + path) {
		return nil, "", "", fmt.Errorf("%w: %s/%s", ErrOutOfScope, mount, path)
	}
	client, err := h.clients(ctx, mount)
	if err != nil {
		return nil, "", "", err
	}
	return client, mount, path, nil
}

func (h *Handle) Read(ctx context.Context, mount, path string, version int) (map[string]any, error) {
	client, mount, path, err := h.kvTarget(ctx, mount, path)
	if err != nil {
		return nil, err
	}
	target := mount + "/data/" + path
	if version > 0 {
		target = fmt.Sprintf("%s?version=%d", target, version)
	}
	data, err := client.GetKV(ctx, target)
	var re *ResponseError
	if errors.As(err, &re) && re.Status == http.StatusNotFound && strings.Contains(re.Body, "no handler for route") {
		return nil, mountNotReady{re}
	}
	return data, err
}

type mountNotReady struct {
	re *ResponseError
}

func (e mountNotReady) Error() string { return vault.ErrMountNotReady.Error() + ": " + e.re.Error() }

func (e mountNotReady) Unwrap() []error { return []error{vault.ErrMountNotReady, e.re} }

func (h *Handle) Write(ctx context.Context, mount, path string, data map[string]any) error {
	client, mount, path, err := h.kvTarget(ctx, mount, path)
	if err != nil {
		return err
	}
	if data == nil {
		data = map[string]any{}
	}
	return client.PutKV(ctx, mount+"/data/"+path, data)
}

func (h *Handle) Delete(ctx context.Context, mount, path string) error {
	client, mount, path, err := h.kvTarget(ctx, mount, path)
	if err != nil {
		return err
	}
	return client.Delete(ctx, mount+"/metadata/"+path)
}

func (h *Handle) Acquire(ctx context.Context, req vault.LeaseRequest) (vault.Lease, error) {
	role := strings.Trim(strings.TrimSpace(req.Role), "/")
	if role == "" || !h.allows(role) {
		return vault.Lease{}, fmt.Errorf("%w: lease role %q", ErrOutOfScope, req.Role)
	}
	client, err := h.clients(ctx, "")
	if err != nil {
		return vault.Lease{}, err
	}
	body := map[string]any{}
	if req.TTL > 0 {
		body["ttl"] = req.TTL.String()
	}
	_, raw, err := client.authCall(ctx, "auth/token/create/"+role, body)
	if err != nil {
		return vault.Lease{}, err
	}
	return vault.Lease{ID: raw.accessor, Role: role, Renewable: raw.renewable, ExpiresAt: leaseUntil(raw.lease)}, nil
}

func (h *Handle) Renew(ctx context.Context, id string, increment time.Duration) (vault.Lease, error) {
	client, err := h.clients(ctx, "")
	if err != nil {
		return vault.Lease{}, err
	}
	body := map[string]any{"accessor": id}
	if increment > 0 {
		body["increment"] = increment.String()
	}
	_, raw, err := client.authCall(ctx, "auth/token/renew-accessor", body)
	if err != nil {
		return vault.Lease{}, err
	}
	return vault.Lease{ID: id, Renewable: raw.renewable, ExpiresAt: leaseUntil(raw.lease)}, nil
}

func (h *Handle) Revoke(ctx context.Context, id string) error {
	client, err := h.clients(ctx, "")
	if err != nil {
		return err
	}
	return client.Write(ctx, "auth/token/revoke-accessor", map[string]any{"accessor": id})
}

func (h *Handle) Issue(ctx context.Context, mount, role string, req vault.CertRequest) (vault.Cert, error) {
	mount, role = strings.Trim(mount, "/"), strings.Trim(role, "/")
	if mount == "" || role == "" || strings.TrimSpace(req.CommonName) == "" {
		return vault.Cert{}, errors.New("openbao: issuing a certificate needs a PKI mount, a role and a common name")
	}
	if !h.allows(mount+"/"+role) && !h.allows(role) {
		return vault.Cert{}, fmt.Errorf("%w: PKI role %s/%s", ErrOutOfScope, mount, role)
	}
	client, err := h.clients(ctx, "")
	if err != nil {
		return vault.Cert{}, err
	}
	return Backend{Client: client}.Issue(ctx, "", mount, role, req)
}

func (h *Handle) CAChain(ctx context.Context, mount string) (string, error) {
	client, err := h.clients(ctx, "")
	if err != nil {
		return "", err
	}
	return client.CAChainPEM(ctx, mount)
}

func (h *Handle) CA(ctx context.Context, mount string) (string, error) {
	client, err := h.clients(ctx, "")
	if err != nil {
		return "", err
	}
	return client.CAPEM(ctx, mount)
}

func (h *Handle) RotateKey(ctx context.Context, mount, name string) error {
	name = strings.Trim(strings.TrimSpace(name), "/")
	target := "identity/oidc/key/" + name
	if name == "" || !h.allows(target) {
		return fmt.Errorf("%w: OIDC key %q", ErrOutOfScope, name)
	}
	mount = strings.Trim(strings.TrimSpace(mount), "/")
	if mount == "" {
		mount = h.mount
	}
	client, err := h.clients(ctx, mount)
	if err != nil {
		return err
	}
	return client.Write(ctx, target+"/rotate", nil)
}

func (c *Client) authCall(ctx context.Context, path string, body map[string]any) (int, loginAuth, error) {
	status, raw, err := c.request(ctx, http.MethodPost, path, body)
	if err != nil {
		return status, loginAuth{}, err
	}
	if status < 200 || status > 299 {
		return status, loginAuth{}, &ResponseError{Method: http.MethodPost, Path: path, Status: status, Body: strings.TrimSpace(string(raw))}
	}
	auth, err := decodeAuth(path, raw, false)
	return status, auth, err
}