~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
122 lines · 3.8 KB Raw History
package computevm

import (
	"context"
	"errors"
	"fmt"
	"regexp"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

const (
	GuestLoginTokenTTL     = "5m"
	GuestLoginTokenNumUses = 50
	GuestAliasPrefix       = "guest-"
)

var (
	sha256Hex  = regexp.MustCompile(`^[0-9a-f]{64}$`)
	mountName  = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
	errNoLogin = errors.New("no guest login")
	errBadSpec = errors.New("spec.guestLogin")
)

func guestLoginRole(o v1alpha1.ComputeVM) (string, error) {
	g := o.Spec.GuestLogin
	if g == nil {
		return "", errNoLogin
	}
	if len(g.Mount) > 63 || !mountName.MatchString(g.Mount) {
		return "", fmt.Errorf("%w: mount %q is not an auth mount name", errBadSpec, g.Mount)
	}
	if !sha256Hex.MatchString(g.BindSecretSha256) {
		return "", fmt.Errorf("%w: bindSecretSha256 is not a hex sha256", errBadSpec)
	}
	return "auth/" + g.Mount + "/role/" + o.Name, nil
}

func (h *Host) EnsureGuestLogin(ctx context.Context, o v1alpha1.ComputeVM) error {
	path, err := guestLoginRole(o)
	if errors.Is(err, errNoLogin) {
		return nil
	}
	if err != nil {
		return err
	}
	client, err := h.vaultFor(ctx, v1alpha1.VaultServer(o.Spec.UserData.Vault.Vault, ""))
	if err != nil {
		return err
	}
	held, err := client.Read(ctx, path)
	switch {
	case errors.Is(err, openbao.ErrNotFound):
	case err != nil:
		return fmt.Errorf("reading the guest login role %s: %w", path, err)
	case held["bound"] == true:
		return nil
	}
	body := map[string]any{
		"bind_secret_hash": o.Spec.GuestLogin.BindSecretSha256,
		"alias_name":       GuestAliasPrefix + o.Name,
		"token_ttl":        GuestLoginTokenTTL,
		"token_num_uses":   GuestLoginTokenNumUses,
		"token_policies":   []string{GuestAliasPrefix + o.Name},
	}
	if err := client.Write(ctx, path, body); err != nil {
		return fmt.Errorf("writing the guest login role %s: %w", path, err)
	}
	h.c.log().Info("the guest's login role is in the bidder's OpenBao; the guest binds its ssh host key to it once",
		"computevm", o.Name, "role", path, "alias", GuestAliasPrefix+o.Name)
	return nil
}

func (h *Host) DropGuestLogin(ctx context.Context, o v1alpha1.ComputeVM) {
	path, err := guestLoginRole(o)
	if err != nil {
		return
	}
	server := v1alpha1.VaultServer(o.Spec.UserData.Vault.Vault, "")
	client, err := h.vaultFor(ctx, server)
	if err == nil {
		err = client.Delete(ctx, path)
	}
	if err != nil {
		h.c.log().Warn("the guest login role stays in the bidder's OpenBao", "computevm", o.Name, "role", path, "err", err.Error())
		return
	}
	h.c.log().Info("the guest login role is gone with its ComputeVM", "computevm", o.Name, "role", path)
}

func (h *Host) DropGuestGrants(ctx context.Context, name string) {
	list, err := kube.List[v1alpha1.GuestIdentityGrant](ctx, h.grants, metav1.ListOptions{})
	if err != nil {
		if !apierrors.IsNotFound(err) {
			h.c.log().Warn("listing the GuestIdentityGrants of a released ComputeVM", "computevm", name, "err", err.Error())
		}
		return
	}
	for _, it := range list {
		if it.Spec.ComputeVMRef.Name != name {
			continue
		}
		if err := h.grants.Delete(ctx, it.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
			h.c.log().Warn("deleting a GuestIdentityGrant of a released ComputeVM", "computevm", name, "grant", it.Name, "err", err.Error())
			continue
		}
		h.c.log().Info("a GuestIdentityGrant goes with its ComputeVM", "computevm", name, "grant", it.Name)
	}
}

func RefusedLogin(err error) bool {
	var re *openbao.ResponseError
	if errors.As(err, &re) {
		return re.Status >= 400 && re.Status < 500 && re.Status != 403
	}
	return !errors.Is(err, errNoLogin) && errors.Is(err, errBadSpec)
}