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

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"slices"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	exposurev1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/exposure/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/trust"
)

const (
	VouchNSID = "sh.tangled.graph.vouch"

	OperatorRepoName = "operator"

	OperatorVouchPrefix = "operator-vouch-"

	OperatorRecordLabel = "apps.socialweb.computer/record-repo"

	OperatorSessionPath = "identity/operator/pds-session"

	operatorRecordLinkSet = "operator-records"
)

type OwnerSession struct {
	Service    string `json:"service"`
	Identifier string `json:"identifier"`
	Password   string `json:"password"`
}

func ReadOwnerSessions(path string) (map[string]OwnerSession, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var sessions map[string]OwnerSession
	if err := json.Unmarshal(b, &sessions); err != nil {
		return nil, fmt.Errorf("compose: the owner sessions file %s: %w", path, err)
	}
	return sessions, nil
}

func FirstOwnerSession(path string) (string, OwnerSession, error) {
	sessions, err := ReadOwnerSessions(path)
	if err != nil {
		return "", OwnerSession{}, err
	}
	owners := make([]string, 0, len(sessions))
	for did := range sessions {
		owners = append(owners, did)
	}
	if len(owners) == 0 {
		return "", OwnerSession{}, fmt.Errorf("compose: the owner sessions file %s holds no session", path)
	}
	slices.Sort(owners)
	return owners[0], sessions[owners[0]], nil
}

func OperatorRepoRef() *commonv1.ObjectRef {
	return &commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: KindAtprotoRepo, Name: OperatorRepoName}
}

func (c *Composer) ApplyOperatorVouch(ctx context.Context, r Root, ws, app, subject, at string) error {
	record, err := json.Marshal(map[string]any{"$type": VouchNSID, "subject": subject, "createdAt": at})
	if err != nil {
		return err
	}
	obj := &v1alpha1.AtprotoRecord{ObjectMeta: metav1.ObjectMeta{Name: OperatorVouchPrefix + app, Labels: map[string]string{OperatorRecordLabel: OperatorRepoName}},
		Spec: v1alpha1.AtprotoRecordSpec{RepoRef: OperatorRepoRef(), Collection: VouchNSID, Rkey: subject, Record: record}}
	return c.ApplyTyped(ctx, r, ws, AtprotoRecordGVR, v1alpha1.GroupVersion.String(), "AtprotoRecord", obj)
}

const AssociationRecordName = trust.MirrorRKey

func (c *Composer) ApplyAssociationRecord(ctx context.Context, r Root, ws string, repo commonv1.ObjectRef, identity, service string) error {
	from := func(path string) map[string]any {
		return map[string]any{"valueFrom": map[string]any{"kind": KindMarketIdentity, "name": identity, "fieldPath": path}}
	}
	proof := map[string]any{"valueFrom": map[string]any{"kind": KindMarketIdentity, "name": identity, "fieldPath": "status.associationRef", "optional": true}}
	record, err := json.Marshal(map[string]any{"challenge": from("status.associations[0]"), "keyId": from("status.did"),
		"service": service, "name": identity, "createdAt": from("metadata.creationTimestamp"), "proof": proof})
	if err != nil {
		return err
	}
	obj := &v1alpha1.AtprotoRecord{ObjectMeta: metav1.ObjectMeta{Name: AssociationRecordName},
		Spec: v1alpha1.AtprotoRecordSpec{RepoRef: &repo, Collection: BadgeBlueKeysNSID, Rkey: AssociationRecordName, Record: record,
			OnMissingReference: v1alpha1.MissingReferenceDelete}}
	return c.ApplyTyped(ctx, r, ws, AtprotoRecordGVR, v1alpha1.GroupVersion.String(), "AtprotoRecord", obj)
}

const AssociationRequestRecordName = "association-request"

func (c *Composer) ApplyAssociationRequestRecord(ctx context.Context, r Root, ws string, repo commonv1.ObjectRef, identity, service string,
	req v1alpha1.AssociationRequest) error {
	authority, err := syntax.ParseDID(strings.TrimSpace(req.Authority))
	if err != nil || authority.String() != req.Authority {
		return fmt.Errorf("compose: spec.identity.associationRequest.authority %q is not a DID", req.Authority)
	}
	contract, err := syntax.ParseATURI(strings.TrimSpace(req.Contract))
	if err != nil || contract.Authority().String() != req.Authority || contract.RecordKey() == "" {
		return fmt.Errorf("compose: spec.identity.associationRequest.contract %q is not a record of %s", req.Contract, req.Authority)
	}
	if nonce := strings.TrimSpace(req.Nonce); nonce == "" || len(nonce) > 128 || nonce != req.Nonce {
		return fmt.Errorf("compose: spec.identity.associationRequest.nonce is not 1 to 128 characters")
	}
	created := map[string]any{"valueFrom": map[string]any{"kind": KindMarketIdentity, "name": identity, "fieldPath": "metadata.creationTimestamp"}}
	record, err := json.Marshal(map[string]any{"$type": market.AssociationRequestNSID, "authority": req.Authority, "service": service,
		"contract": req.Contract, "nonce": req.Nonce, "createdAt": created})
	if err != nil {
		return err
	}
	obj := &v1alpha1.AtprotoRecord{ObjectMeta: metav1.ObjectMeta{Name: AssociationRequestRecordName},
		Spec: v1alpha1.AtprotoRecordSpec{RepoRef: &repo, Collection: market.AssociationRequestNSID, Rkey: market.ContractScopedRKey(service, req.Authority, req.Contract),
			Record: record}}
	return c.ApplyTyped(ctx, r, ws, AtprotoRecordGVR, v1alpha1.GroupVersion.String(), "AtprotoRecord", obj)
}

func OperatorRecordLinks(ws string, remotePDS commonv1.ObjectRef, did string, caRef *commonv1.ObjectRef) []Link {
	repo := AppRef(KindAtprotoRepo, OperatorRepoName, ws)
	remote := AppRef(KindRemoteRepo, OperatorRepoName, ws)
	records := exposurev1alpha1.ComponentLinkSelector{Group: v1alpha1.GroupName, Kind: "AtprotoRecord", Workspace: ws,
		MatchLabels: map[string]string{OperatorRecordLabel: OperatorRepoName}}
	links := []Link{
		SelectorLink(LinkName("records", OperatorRepoName), records, repo, Cap(atprotorepo.CapabilityWrite, VouchNSID)),
		RepoBackendLink(repo, remote),
	}
	identity := AppRef(KindMarketIdentity, OperatorRepoName, ws)
	if caRef != nil {
		links = append(links, TrustLink(identity, *caRef))
	}
	return append(links, RemoteRepoLinks(remote, remotePDS, identity, did)...)
}

func (c *Composer) ApplyOperatorRepo(ctx context.Context, r Root, ws string, op Operator) error {
	ri, err := c.Client(ws, AtprotoRecordGVR)
	if err != nil {
		return err
	}
	records, err := kube.List[v1alpha1.AtprotoRecord](ctx, ri, metav1.ListOptions{LabelSelector: OperatorRecordLabel + "=" + OperatorRepoName})
	if apierrors.IsNotFound(err) {
		return nil
	}
	if err != nil {
		return err
	}
	if len(records) == 0 {
		return nil
	}
	did := ""
	if id, err := c.Identity(ctx, ws, OperatorRepoName); err == nil {
		did = strings.TrimSpace(id.Status.Did)
	} else if !apierrors.IsNotFound(err) {
		return err
	}
	if did == "" {
		if _, did, err = c.OperatorRepo(ctx, r, op); err != nil {
			return err
		}
	} else if strings.TrimSpace(op.PdsURL) != "" {
		if err := c.applyOperatorPDS(ctx, r, op); err != nil {
			return err
		}
	}
	return c.applyOperatorRecordRepo(ctx, r, ws, op, did, operatorAccountIdentity(r, op))
}

func operatorVault(r Root, op Operator) *commonv1.ObjectRef {
	return &commonv1.ObjectRef{Kind: v1alpha1.KindOpenBaoServer, Name: op.Vault, Workspace: r.Workspace}
}

func operatorAccountIdentity(r Root, op Operator) v1alpha1.MarketIdentitySpec {
	identity := v1alpha1.MarketIdentitySpec{Mode: v1alpha1.MarketIdentityModePdsAccount, Handle: strings.TrimSpace(op.Identifier),
		PdsURL: strings.TrimRight(strings.TrimSpace(op.PdsURL), "/"), Association: v1alpha1.AssociationSkip,
		KeysRef:    &commonv1.SecretRef{Vault: operatorVault(r, op), Mount: op.Mount, Path: OperatorKeysPath},
		SessionRef: &commonv1.SecretRef{Vault: operatorVault(r, op), Mount: op.Mount, Path: OperatorSessionPath}}
	if op.CARef != nil && op.tls() {
		ca := *op.CARef
		identity.CARef = &ca
	}
	return identity
}

func (c *Composer) applyOperatorRecordRepo(ctx context.Context, r Root, ws string, op Operator, did string, identity v1alpha1.MarketIdentitySpec) error {
	var caRef *commonv1.ObjectRef
	if op.CARef != nil && op.tls() {
		ca := *op.CARef
		caRef = &ca
	}
	links := OperatorRecordLinks(ws, op.ref(r.Workspace), did, caRef)
	if err := c.AddLinks(ctx, r, operatorRecordLinkSet, links); err != nil {
		return err
	}
	if err := c.ApplyIdentitySpec(ctx, r, ws, OperatorRepoName, identity); err != nil {
		return err
	}
	pdsRef := op.ref(r.Workspace)
	remote := &v1alpha1.RemoteRepo{ObjectMeta: metav1.ObjectMeta{Name: OperatorRepoName}, Spec: v1alpha1.RemoteRepoSpec{DID: did, PdsRef: pdsRef,
		CredentialsRef: commonv1.CredentialsRef{ObjectRef: commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: KindMarketIdentity, Name: OperatorRepoName}}}}
	if err := c.ApplyRepo(ctx, r, ws, OperatorRepoName, KindRemoteRepo, v1alpha1.RemoteRepoGVR, remote); err != nil {
		return err
	}
	return c.ApplyLinks(ctx, r, operatorRecordLinkSet, links, ws)
}