~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
503 lines · 17.9 KB Raw History
package compose

import (
	"cmp"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"slices"
	"strings"

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

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	pdsv1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/pds/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/bus"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo"
)

const (
	EnvPdsAccountPassword = "ATPROTO_PASSWORD"

	AssociationWatch = "association"

	AssociateConfirmNSID = "com.publicdomainrelay.temp.requester.associateConfirm"
	BadgeBlueKeysNSID    = "com.publicdomainrelay.temp.badgeBlueKeys"
	BidderAssociate      = "bidder_associate"
	RequesterAssociate   = "requester_associate"
)

const (
	OptionIdentityMode = "atproto-identity-mode"
	OptionHandle       = "atproto-handle"
	OptionPdsURL       = "atproto-pds-url"
)

var NetworkRequesterIdentityOptions = []string{OptionIdentityMode, OptionHandle, OptionPdsURL}

func NetworkRequesterIdentity(options []v1alpha1.ApplicationOption, networkPdsURL string) v1alpha1.ApplicationIdentity {
	id := v1alpha1.ApplicationIdentity{Association: v1alpha1.AssociationSkip}
	if v1alpha1.MarketIdentityMode(OptionValue(options, OptionIdentityMode)) != v1alpha1.MarketIdentityModePdsAccount {
		return id
	}
	id.Mode = v1alpha1.MarketIdentityModePdsAccount
	id.Handle = strings.TrimSpace(OptionValue(options, OptionHandle))
	id.PdsURL = cmp.Or(strings.TrimSpace(OptionValue(options, OptionPdsURL)), networkPdsURL)
	return id
}

var ErrMissingPdsAccountPassword = errors.New("compose: mode pdsAccount needs the account password in " +
	EnvPdsAccountPassword + " or --atproto-password; no password is available")

type AccountVault struct {
	Server string

	Client string

	Mount string
}

func (v AccountVault) MountOr(fallback string) string {
	return cmp.Or(strings.Trim(strings.TrimSpace(v.Mount), "/"), fallback)
}

type AccountPlan struct {
	Mode v1alpha1.MarketIdentityMode

	Local bool

	Identity v1alpha1.MarketIdentitySpec

	PdsRef *commonv1.ObjectRef

	RemotePDS *pdsv1alpha1.RemotePDS

	PdsURL string

	KeysPath string

	ComponentMode string

	MarketExposure *commonv1.LocalObjectRef

	PdsDID string

	Vault AccountVault
}

func (p AccountPlan) ComponentPdsRef() *commonv1.ObjectRef {
	if p.Local {
		return ComponentRefFor(ComponentKindPds, PdsComponent)
	}
	return p.PdsRef
}

func (p AccountPlan) UsesAssociation() bool {
	return p.Local
}

type accountBinding struct {
	DID string

	PDSRef *commonv1.ObjectRef

	RemotePDS *pdsv1alpha1.RemotePDS
}

func (p AccountPlan) binding(did, pdsURL string) (accountBinding, error) {
	binding := accountBinding{DID: strings.TrimSpace(did)}
	if p.Local {
		binding.PDSRef = &commonv1.ObjectRef{Kind: ComponentKindPds, Name: PdsComponent}
		return binding, nil
	}
	if p.RemotePDS != nil && p.RemotePDS.Name != "" {
		binding.RemotePDS = p.RemotePDS
	} else if p.PdsRef != nil && strings.EqualFold(p.PdsRef.Kind, KindRemotePDS) && p.PdsRef.Name != "" {
		binding.RemotePDS = &pdsv1alpha1.RemotePDS{ObjectMeta: metav1.ObjectMeta{Name: p.PdsRef.Name}}
	}
	if u := FirstNonEmpty(p.PdsURL, p.Identity.PdsURL, pdsURL); binding.RemotePDS == nil && u != "" {
		binding.RemotePDS = &pdsv1alpha1.RemotePDS{ObjectMeta: metav1.ObjectMeta{Name: deterministicPdsName(u)},
			Spec: pdsv1alpha1.RemotePDSSpec{URL: u, DID: pdsServiceDID(u)}}
	}
	if binding.RemotePDS != nil {
		binding.PDSRef = &commonv1.ObjectRef{Kind: KindRemotePDS, Name: binding.RemotePDS.Name}
		return binding, nil
	}
	return accountBinding{}, errors.New("compose: the existing-account repo names no PDS")
}

func deterministicPdsName(u string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(u)))
	return "pds-" + hex.EncodeToString(sum[:8])
}

func (c *Composer) PlanAccount(ctx context.Context, ws string, id v1alpha1.ApplicationIdentity,
	suffix string, vault AccountVault, defaultAssociation v1alpha1.AssociationMode) (AccountPlan, error) {
	mode := cmp.Or(id.Mode, v1alpha1.MarketIdentityModeLocalRepo)
	mount := vault.MountOr(suffix)
	sessionPath := "identity/" + suffix + "/pds-session"
	keysPath := "identity/" + suffix + "/keys"
	plan := AccountPlan{Mode: mode, KeysPath: keysPath, Vault: vault}
	approved := slices.Clone(id.ApprovedAssociations)
	association := cmp.Or(id.Association, defaultAssociation, v1alpha1.AssociationRequireApproval)
	switch mode {
	case v1alpha1.MarketIdentityModeLocalRepo:
		plan.Local = true
		plan.ComponentMode = "local"
		plan.Identity = v1alpha1.MarketIdentitySpec{Mode: mode, Handle: id.Handle, Did: id.Did, PdsURL: id.PdsURL, Association: association,
			ApprovedAssociations: approved, AssociationRef: id.AssociationRef.DeepCopy()}
		plan.KeysPath = ""
		return plan, nil
	case v1alpha1.MarketIdentityModePdsAccount:
		plan.ComponentMode = "callback"
		plan.Identity = v1alpha1.MarketIdentitySpec{Mode: mode, Handle: id.Handle, Did: id.Did, PdsURL: id.PdsURL,
			KeysRef:              &commonv1.SecretRef{Mount: mount, Path: keysPath},
			SessionRef:           &commonv1.SecretRef{Mount: mount, Path: sessionPath},
			Association:          association,
			ApprovedAssociations: approved,
			AssociationRef:       id.AssociationRef.DeepCopy()}
		u, remote, err := c.resolveAccountPds(ctx, ws, id)
		if err != nil {
			return AccountPlan{}, err
		}
		plan.PdsURL = u
		plan.RemotePDS = remote
		plan.Identity.PdsURL = u
		if remote != nil {
			plan.PdsRef, plan.PdsDID = &commonv1.ObjectRef{Kind: KindRemotePDS, Name: remote.Name}, remote.Spec.DID
		}
		return plan, nil
	case v1alpha1.MarketIdentityModeOAuthBrowser, v1alpha1.MarketIdentityModeOAuthQr, v1alpha1.MarketIdentityModeOAuthDelegated:
		if mode == v1alpha1.MarketIdentityModeOAuthQr && suffix == "bidder" && strings.TrimSpace(id.Did) == "" && strings.TrimSpace(id.Handle) == "" {
			return AccountPlan{}, errors.New("compose: a bidder in mode oauthQr needs spec.identity.did or spec.identity.handle naming its account (run bidder --handle); a transferred QR session is checked against it")
		}
		plan.ComponentMode = "oauth"
		plan.Identity = v1alpha1.MarketIdentitySpec{Mode: mode, Handle: id.Handle, Did: id.Did, PdsURL: id.PdsURL,
			SessionRef: &commonv1.SecretRef{Mount: mount, Path: sessionPath}, Association: association, ApprovedAssociations: approved,
			AssociationRef: id.AssociationRef.DeepCopy()}
		plan.PdsURL = strings.TrimSpace(id.PdsURL)
		remote, err := c.remotePDSRef(ctx, ws, id.PdsRef)
		if err != nil {
			return AccountPlan{}, err
		}
		if remote != nil {
			plan.RemotePDS = remote
			plan.PdsRef, plan.PdsDID = &commonv1.ObjectRef{Kind: KindRemotePDS, Name: remote.Name}, remote.Spec.DID
			plan.PdsURL = cmp.Or(plan.PdsURL, remote.Spec.URL)
			plan.Identity.PdsURL = cmp.Or(plan.Identity.PdsURL, remote.Spec.URL)
		}
		return plan, nil
	}
	return AccountPlan{}, fmt.Errorf("compose: MarketIdentity mode %q: want one of localRepo, pdsAccount, oauthBrowser, oauthQr, oauthDelegated", mode)
}

func (c *Composer) SeedPdsAccountPassword(ctx context.Context, r Root, plan AccountPlan) error {
	if plan.Mode != v1alpha1.MarketIdentityModePdsAccount {
		return nil
	}
	password := strings.TrimSpace(os.Getenv(EnvPdsAccountPassword))
	if plan.Identity.KeysRef == nil || strings.TrimSpace(plan.KeysPath) == "" {
		return errors.New("compose: mode pdsAccount names no keysRef for its password")
	}
	if plan.Vault.Server == "" {
		return errors.New("compose: mode pdsAccount names no vault for its password")
	}
	mount := plan.Vault.MountOr(plan.Vault.Client)
	kv, err := c.KV(ctx, r, plan.Vault.Server, mount+"/identity/*")
	if err != nil {
		return err
	}
	data, err := readKV(ctx, kv, mount, plan.KeysPath)
	if err != nil {
		return err
	}
	existing, _ := data["password"].(string)
	switch {
	case password == "" && strings.TrimSpace(existing) != "":
		return nil
	case password == "":
		return ErrMissingPdsAccountPassword
	case existing == password:
		return nil
	}
	data["password"] = password
	if err := kv.Write(ctx, mount, plan.KeysPath, data); err != nil {
		return err
	}
	c.log.Info("the pdsAccount password is in its vault keysRef", "vault", plan.Vault.Server, "path", plan.KeysPath)
	return nil
}

func (c *Composer) resolveAccountPds(ctx context.Context, ws string, id v1alpha1.ApplicationIdentity) (string, *pdsv1alpha1.RemotePDS, error) {
	remote, err := c.remotePDSRef(ctx, ws, id.PdsRef)
	if err != nil {
		return "", nil, err
	}
	if u := strings.TrimSpace(id.PdsURL); u != "" {
		return u, remote, nil
	}
	if remote == nil {
		return "", nil, fmt.Errorf("compose: mode pdsAccount names no pdsUrl and no RemotePDS ref; it fails closed instead of using a local repo")
	}
	if strings.TrimSpace(remote.Spec.URL) == "" {
		return "", nil, fmt.Errorf("compose: RemotePDS %s names no url; mode pdsAccount fails closed", remote.Name)
	}
	return strings.TrimSpace(remote.Spec.URL), remote, nil
}

func (c *Composer) remotePDSRef(ctx context.Context, ws string, ref *commonv1.ObjectRef) (*pdsv1alpha1.RemotePDS, error) {
	if ref == nil || !strings.EqualFold(strings.TrimSpace(ref.Kind), KindRemotePDS) || strings.TrimSpace(ref.Name) == "" {
		return nil, nil
	}
	target := cmp.Or(strings.TrimSpace(ref.Workspace), ws)
	object, err := Get[pdsv1alpha1.RemotePDS](ctx, c, target, RemotePDSGVR, strings.TrimSpace(ref.Name))
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, fmt.Errorf("compose: RemotePDS %s is absent in workspace %s; the existing-account mode fails closed", ref.Name, target)
		}
		return nil, err
	}
	return object, nil
}

func (c *Composer) DeleteAssociationObjects(ctx context.Context, ws string) {
	for _, gvr := range []schema.GroupVersionResource{AtprotoRecordGVR, RecordWatchGVR} {
		ri, err := c.Client(ws, gvr)
		if err != nil {
			continue
		}
		if err := ri.Delete(ctx, AssociationWatch, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
			c.log.Warn("removing an association object", "resource", gvr.Resource, "err", err.Error())
		}
	}
}

func pdsServiceDID(raw string) string {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || parsed.Host == "" || net.ParseIP(parsed.Hostname()) != nil {
		return ""
	}
	return "did:web:" + strings.ReplaceAll(strings.ToLower(parsed.Host), ":", "%3A")
}

func (c *Composer) Identity(ctx context.Context, ws, name string) (*v1alpha1.MarketIdentity, error) {
	return Get[v1alpha1.MarketIdentity](ctx, c, ws, MarketIdentityGVR, name)
}

func (c *Composer) IdentityDid(ctx context.Context, ws, name string) string {
	id, err := c.Identity(ctx, ws, name)
	if err != nil {
		return ""
	}
	return id.Status.Did
}

func OperatorWatched(s v1alpha1.MarketIdentitySpec, operator, service string) v1alpha1.MarketIdentitySpec {
	if s.Mode == v1alpha1.MarketIdentityModeLocalRepo && s.Association != v1alpha1.AssociationSkip {
		s.AssociateWith, s.AssociationWatch = operator, AssociationWatch
		s.AssociationService = service
	}
	return s
}

func (c *Composer) ApplyIdentitySpec(ctx context.Context, r Root, ws, name string, spec v1alpha1.MarketIdentitySpec) error {
	if spec.Mode == v1alpha1.MarketIdentityModeLocalRepo && spec.Did == "" {
		if live, err := c.Identity(ctx, ws, name); err == nil {
			spec.Did = live.Spec.Did
			if spec.Did != "" {
				spec.PdsURL = live.Spec.PdsURL
			}
		}
	}
	if spec.Mode == v1alpha1.MarketIdentityModeLocalRepo {
		probe := RefLink(AppRef(KindMarketIdentity, name, ws), AppRef(ComponentKindPds, PdsComponent, r.Workspace),
			Cap(bus.CapabilityXRPC, "com.atproto.server.describeServer"))
		if err := c.ApplyLinks(ctx, r, "identity-"+name, []Link{probe}); err != nil {
			return err
		}
	}
	obj := &v1alpha1.MarketIdentity{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: spec}
	return c.ApplyTyped(ctx, r, ws, MarketIdentityGVR, v1alpha1.GroupVersion.String(), KindMarketIdentity, obj)
}

func (c *Composer) ApplyOAuthIdentityEndpoints(ctx context.Context, r Root, snap Snapshot, ws, identity, exposure, role string, configured v1alpha1.ApplicationIdentity) error {
	clientID, redirectURI := strings.TrimSpace(configured.OAuthClientID), strings.TrimSpace(configured.OAuthRedirectURI)
	if clientID == "" || redirectURI == "" {
		raw, _ := snap.ExposureURL(exposure)
		base := strings.TrimRight(raw, "/")
		if base == "" {
			return nil
		}
		clientID, redirectURI = cmp.Or(clientID, base+"/oauth-client-metadata.json"), cmp.Or(redirectURI, base+"/oauth/callback/"+role)
	}
	id, err := c.Identity(ctx, ws, identity)
	if err != nil {
		return err
	}
	switch id.Spec.Mode {
	case v1alpha1.MarketIdentityModeOAuthBrowser, v1alpha1.MarketIdentityModeOAuthQr, v1alpha1.MarketIdentityModeOAuthDelegated:
	default:
		return nil
	}
	u := NewDoc(v1alpha1.GroupVersion.String(), KindMarketIdentity, identity)
	u["spec"] = map[string]any{"oauthClientID": clientID, "oauthRedirectURI": redirectURI}
	return c.applyAs(ctx, r, r.IdentityManager(), ws, MarketIdentityGVR, u)
}

func ClaimOpen(id *v1alpha1.MarketIdentity) bool {
	if id == nil || strings.TrimSpace(id.Status.Did) == "" {
		return false
	}
	for _, cond := range id.Status.Conditions {
		if cond.Type == v1alpha1.ConditionAssociated && cond.Reason == "Revoked" {
			return false
		}
	}
	if id.Spec.Mode == v1alpha1.MarketIdentityModeLocalRepo {
		if id.Spec.Association == v1alpha1.AssociationSkip || AssociatedClaim(id) || len(id.Status.Associations) > 0 {
			return false
		}
		if id.Status.Phase == v1alpha1.MarketIdentityPhaseAwaitingApproval {
			return true
		}
		return id.Status.Phase == v1alpha1.MarketIdentityPhaseReady && len(id.Spec.ApprovedAssociations) == 0
	}
	return id.Status.Phase == v1alpha1.MarketIdentityPhaseAwaitingScan && id.Status.PendingAction != nil
}

func AssociatedClaim(id *v1alpha1.MarketIdentity) bool {
	if id == nil {
		return false
	}
	for _, cond := range id.Status.Conditions {
		if cond.Type == v1alpha1.ConditionAssociated && cond.Status == metav1.ConditionTrue {
			return true
		}
	}
	return false
}

func associatedMessage(id *v1alpha1.MarketIdentity) string {
	for _, cond := range id.Status.Conditions {
		if cond.Type == v1alpha1.ConditionAssociated {
			return cond.Reason + ": " + cond.Message
		}
	}
	return "no Associated condition yet"
}

func associationRequired(plan AccountPlan, id *v1alpha1.MarketIdentity) bool {
	if id == nil {
		return false
	}
	if plan.Identity.AssociationRef != nil {
		return true
	}
	if plan.Mode != v1alpha1.MarketIdentityModeLocalRepo || plan.Identity.Association == v1alpha1.AssociationSkip {
		return false
	}
	return strings.TrimSpace(id.Spec.AssociationWatch) != "" || strings.TrimSpace(id.Spec.AssociateWith) != ""
}

func AssociationWait(plan AccountPlan, id *v1alpha1.MarketIdentity, service string) string {
	if !associationRequired(plan, id) {
		return ""
	}
	if ref := plan.Identity.AssociationRef; ref != nil {
		if id.Spec.AssociationRef == nil || *id.Spec.AssociationRef != *ref || id.Status.AssociationRef == nil || *id.Status.AssociationRef != *ref ||
			!AssociatedClaim(id) || id.Status.Phase != v1alpha1.MarketIdentityPhaseReady {
			return "waiting for the MarketIdentity to verify the association record " + ref.URI + " (cid " + ref.CID + "): " + associatedMessage(id)
		}
		return ""
	}
	if len(plan.Identity.ApprovedAssociations) > 0 {
		return ""
	}
	if id.Status.Phase == v1alpha1.MarketIdentityPhaseReady && (AssociatedClaim(id) || len(id.Status.Associations) > 0) {
		return ""
	}
	keyID := "the requester DID"
	if id != nil {
		if did := strings.TrimSpace(id.Status.Did); did != "" {
			keyID = did
		}
	}
	return fmt.Sprintf("waiting for the operator's %s claim record (challenge=operator, keyId=%s, service=%s)", service, keyID, service)
}

func OperatorDID(declared []string, id *v1alpha1.MarketIdentity) string {
	for _, did := range declared {
		if did = strings.TrimSpace(did); did != "" {
			return did
		}
	}
	if id == nil {
		return ""
	}
	for _, did := range id.Status.Associations {
		if did = strings.TrimSpace(did); did != "" {
			return did
		}
	}
	for _, did := range id.Spec.ApprovedAssociations {
		if did = strings.TrimSpace(did); did != "" {
			return did
		}
	}
	return ""
}

func (c *Composer) ApplyRemotePDS(ctx context.Context, r Root, ws string, desired *pdsv1alpha1.RemotePDS) (*pdsv1alpha1.RemotePDS, error) {
	if desired == nil || strings.TrimSpace(desired.Name) == "" {
		return nil, nil
	}
	obj := &pdsv1alpha1.RemotePDS{ObjectMeta: metav1.ObjectMeta{Name: desired.Name, Labels: desired.Labels}, Spec: desired.Spec}
	if err := c.ApplyTyped(ctx, r, ws, RemotePDSGVR, pdsv1alpha1.GroupVersion.String(), KindRemotePDS, obj); err != nil {
		return nil, err
	}
	return obj, nil
}

func (c *Composer) AccountRepo(ctx context.Context, r Root, ws, identityName string, plan AccountPlan, did, pdsURL string) ([]Link, error) {
	if plan.Local || strings.TrimSpace(did) == "" {
		return nil, nil
	}
	binding, err := plan.binding(did, pdsURL)
	if err != nil {
		return nil, err
	}
	if binding.RemotePDS != nil {
		ensured, err := c.ApplyRemotePDS(ctx, r, ws, binding.RemotePDS)
		if err != nil {
			return nil, err
		}
		if ensured != nil {
			binding.PDSRef = &commonv1.ObjectRef{Group: pdsv1alpha1.GroupName, Kind: KindRemotePDS, Name: ensured.Name}
		}
	}
	if !plan.UsesAssociation() {
		c.DeleteAssociationObjects(ctx, ws)
	}
	if err := c.PruneLocalAccounts(ctx, r, ws, LocalAccount{}); err != nil {
		return nil, err
	}
	name := atprotorepo.RepoName(binding.DID)
	identity := AppRef(KindMarketIdentity, identityName, ws)
	spec := v1alpha1.RemoteRepoSpec{DID: binding.DID, PdsRef: *binding.PDSRef,
		CredentialsRef: commonv1.CredentialsRef{ObjectRef: commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: KindMarketIdentity, Name: identityName}}}
	if err := c.ApplyRepo(ctx, r, ws, name, KindRemoteRepo, v1alpha1.RemoteRepoGVR, &v1alpha1.RemoteRepo{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: spec}); err != nil {
		return nil, err
	}
	remote := AppRef(KindRemoteRepo, name, ws)
	links := []Link{RepoBackendLink(AppRef(KindAtprotoRepo, name, ws), remote)}
	links = append(links, RemoteRepoLinks(remote, RemotePDSRefIn(binding.PDSRef.Name, ws), identity, binding.DID)...)
	return links, nil
}