~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
128 lines · 6.1 KB Raw History
package atproto

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/xrpc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/pds/identity"
)

type SessionsFunc func(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (vault.AtprotoSessions, error)

var ErrSignInAgain = errors.New("marketidentity: the owner must sign in to the web app again")

func (a *Accounts) sessionProxy(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, generation int64,
	status v1alpha1.MarketIdentityStatus) (Result, error) {
	did := strings.TrimSpace(id.Spec.Did)
	ref := id.Spec.SessionRef
	if did == "" || ref == nil || ref.Vault == nil {
		return failed(status, generation, "InvalidSpec",
			"mode oauthSessionProxy needs spec.did and a spec.sessionRef naming the vault, the atproto auth mount and session/<did>"), nil
	}
	session, err := a.proxiedSession(ctx, workspace, id)
	switch {
	case errors.Is(err, vault.ErrNoAtprotoSession):
		return a.signInAgain(status, generation, id, "NoSession",
			fmt.Sprintf("the web app holds no session for %s in %s; the owner must sign in to the web app again", did, ref.Mount)), ErrSignInAgain
	case errors.Is(err, vault.ErrAtprotoSessionInactive):
		return a.signInAgain(status, generation, id, "SessionInactive",
			fmt.Sprintf("the web app session of %s is not active (%v); the owner must sign in to the web app again", did, err)), ErrSignInAgain
	case err != nil:
		return pending(status, generation, "SessionProxyUnavailable", "reading the web app session of "+did+" through the vault: "+err.Error()), err
	case session.DID != did:
		return failed(status, generation, "SubjectMismatch", fmt.Sprintf("the web app session at %s/%s is for %s, not %s", ref.Mount, ref.Path, session.DID, did)), nil
	case session.Status != vault.AtprotoSessionActive:
		return a.signInAgain(status, generation, id, "SessionInactive",
			fmt.Sprintf("the web app session of %s is %s (%s); the owner must sign in to the web app again", did, session.Status, session.StatusReason)), ErrSignInAgain
	case strings.TrimSpace(session.PDS) == "":
		return pending(status, generation, "SessionProxyUnavailable", "the web app session of "+did+" names no PDS"), nil
	}
	held := a.held.get(workspace, id)
	held.open.Lock()
	err = a.scopeCheck(workspace, id, held, strings.Fields(session.Scope))
	held.open.Unlock()
	if changed := (*scopeChangedError)(nil); errors.As(err, &changed) {
		held.auth.Drop()
		status.Did, status.PdsURL = did, strings.TrimRight(session.PDS, "/")
		return a.signInAgain(status, generation, id, "ScopeChanged",
			fmt.Sprintf("the web app session of %s lacks scopes the gateway web app now requests (%s); sign in to the web app again",
				did, strings.Join(changed.subjects(), ", "))), ErrSignInAgain
	}
	status.Phase = v1alpha1.MarketIdentityPhaseReady
	status.ObservedGeneration = generation
	status.Did = did
	status.Handle = firstNonEmpty(session.Handle, id.Spec.Handle)
	status.PdsURL = strings.TrimRight(session.PDS, "/")
	status.SessionRef = ref
	status.Associations = nil
	status.PendingAction = nil
	status.Message = ""
	conds.Set(&status.Conditions, generation, v1alpha1.ConditionSignedIn, true, "SignedIn",
		"the web app session in "+ref.Mount+" is active; the atproto auth plugin refreshes it and signs each request")
	setReady(&status, generation, true, "SignedIn", did)
	return Result{Status: status}, nil
}

func (a *Accounts) signInAgain(status v1alpha1.MarketIdentityStatus, generation int64, id *v1alpha1.MarketIdentity, reason, message string) Result {
	status.Phase = v1alpha1.MarketIdentityPhaseSessionExpired
	status.ObservedGeneration = generation
	status.SessionRef = id.Spec.SessionRef
	status.PendingAction = &v1alpha1.MarketIdentityPendingAction{Kind: v1alpha1.PendingActionSignInAgain, Handle: id.Spec.Handle}
	status.Message = message
	conds.Set(&status.Conditions, generation, v1alpha1.ConditionSignedIn, false, reason, message)
	setReady(&status, generation, false, reason, message)
	return Result{Status: status}
}

func (a *Accounts) proxiedSession(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (vault.AtprotoSession, error) {
	if a.sessions == nil {
		return vault.AtprotoSession{}, errors.New("marketidentity: this core resolves no vault atproto session handle")
	}
	sessions, err := a.sessions(ctx, workspace, id)
	if err != nil {
		return vault.AtprotoSession{}, err
	}
	return sessions.Session(ctx, id.Spec.SessionRef.Mount, id.Spec.SessionRef.Path)
}

func (a *Accounts) holdProxy(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, held *heldAccount) error {
	ref := id.Spec.SessionRef
	did, pdsURL := strings.TrimSpace(id.Status.Did), strings.TrimSpace(id.Status.PdsURL)
	if ref == nil || ref.Vault == nil || did == "" || pdsURL == "" || id.Status.Phase != v1alpha1.MarketIdentityPhaseReady {
		return fmt.Errorf("%w: the web app session of %s is not ready (%s)", ErrSignInAgain, firstNonEmpty(did, id.Spec.Did), id.Status.Message)
	}
	if a.sessions == nil {
		return errors.New("marketidentity: this core resolves no vault atproto session handle")
	}
	held.open.Lock()
	defer held.open.Unlock()
	if p := held.auth.Proxied(); p != nil && held.ref == *ref && held.auth.DID() == did && p.PdsURL == pdsURL {
		return nil
	}
	client, err := a.sessionHTTP(ctx, workspace, id)
	if err != nil {
		return err
	}
	mount, path := ref.Mount, ref.Path
	snapshot := id.DeepCopy()
	prover := xrpc.DPoPProverFunc(func(ctx context.Context, method, target, _, nonce string) (xrpc.DPoPProof, error) {
		sessions, err := a.sessions(ctx, workspace, snapshot)
		if err != nil {
			return xrpc.DPoPProof{}, err
		}
		proof, err := sessions.Proof(ctx, mount, path, method, target, nonce)
		if err != nil {
			return xrpc.DPoPProof{}, err
		}
		return xrpc.DPoPProof{AccessToken: proof.AccessToken, Proof: proof.DPoP}, nil
	})
	held.ref = *ref
	held.auth.HoldProxy(did, &identity.Proxy{PdsURL: pdsURL, HTTP: client, Prover: prover})
	return nil
}