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

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

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/oauthclient"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/pds/identity"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
)

type heldAccount struct {
	open sync.Mutex

	mode v1alpha1.MarketIdentityMode

	ref commonv1.SecretRef

	auth *identity.Authenticator

	lacking string

	account string

	handle string
}

func (h *heldAccount) heldFor(account string) bool {
	h.open.Lock()
	defer h.open.Unlock()
	return h.auth.Session() != nil && h.account == account
}

type heldAccounts struct {
	mu sync.Mutex

	accounts map[string]*heldAccount
}

func (h *heldAccounts) get(workspace string, id *v1alpha1.MarketIdentity) *heldAccount {
	key := qrKey(workspace, id.Name)
	h.mu.Lock()
	defer h.mu.Unlock()
	if h.accounts == nil {
		h.accounts = map[string]*heldAccount{}
	}
	if held, ok := h.accounts[key]; ok && held.mode == id.Spec.Mode {
		return held
	}
	held := &heldAccount{mode: id.Spec.Mode}
	switch id.Spec.Mode {
	case v1alpha1.MarketIdentityModePdsAccount:
		held.auth = identity.NewBearer()
	case v1alpha1.MarketIdentityModeOAuthSessionProxy:
		held.auth = identity.NewProxied()
	default:
		held.auth = identity.NewOAuth()
	}
	if previous, ok := h.accounts[key]; ok {
		previous.auth.Drop()
	}
	h.accounts[key] = held
	return held
}

func (h *heldAccounts) forget(workspace, name string) {
	key := qrKey(workspace, name)
	h.mu.Lock()
	held, ok := h.accounts[key]
	delete(h.accounts, key)
	h.mu.Unlock()
	if ok {
		held.auth.Drop()
	}
}

func (h *heldAccounts) session(workspace, name string) *oauthclient.Session {
	h.mu.Lock()
	held, ok := h.accounts[qrKey(workspace, name)]
	h.mu.Unlock()
	if !ok {
		return nil
	}
	return held.auth.Session()
}

func (a *Accounts) Authenticator(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (*identity.Authenticator, error) {
	held := a.held.get(workspace, id)
	switch id.Spec.Mode {
	case v1alpha1.MarketIdentityModeOAuthBrowser, v1alpha1.MarketIdentityModeOAuthQr, v1alpha1.MarketIdentityModeOAuthDelegated:
		if !held.heldFor(accountKey(id)) {
			status := id.Status
			if _, _, _, err := a.restoreOAuthSession(ctx, workspace, id, &status); err != nil {
				return nil, err
			}
		}
	case v1alpha1.MarketIdentityModePdsAccount:
		if held.auth.Bearer() == nil {
			if err := a.holdBearer(ctx, workspace, id, oauthSessionRef(id, &id.Status), strings.TrimSpace(id.Status.Did),
				firstNonEmpty(id.Status.PdsURL, id.Spec.PdsURL)); err != nil {
				return nil, err
			}
		}
	case v1alpha1.MarketIdentityModeOAuthSessionProxy:
		if err := a.holdProxy(ctx, workspace, id, held); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("marketidentity: mode %q holds no account session", id.Spec.Mode)
	}
	return held.auth, nil
}

func (a *Accounts) holdBearer(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, ref *commonv1.SecretRef, did, pdsURL string) error {
	if ref == nil {
		return errNoSessionRef
	}
	vault := a.vaultFor(workspace)
	if vault == nil {
		return errors.New("marketidentity: no vault reads the pds account session")
	}
	if did == "" {
		return errors.New("marketidentity: the pds account has no DID yet")
	}
	held := a.held.get(workspace, id)
	held.open.Lock()
	defer held.open.Unlock()
	if held.auth.Bearer() != nil && held.ref == *ref && held.auth.DID() == did {
		return nil
	}
	client, err := a.sessionHTTP(ctx, workspace, id)
	if err != nil {
		return err
	}
	held.ref = *ref
	held.auth.HoldBearer(did, identity.NewBearerTokens(secretStore{vault: vault, ref: *ref}, pdsURL, client,
		a.log.With("workspace", workspace, "identity", id.Name)))
	return nil
}

func (a *Accounts) sessionHTTP(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (*http.Client, error) {
	tlsConfig, err := a.tlsFor(ctx, workspace, id)
	if err != nil {
		return nil, err
	}
	base := a.oauthHTTP()
	if tlsConfig == nil {
		return base, nil
	}
	client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
	if base != nil {
		client.Timeout = base.Timeout
	}
	return client, nil
}

func (a *Accounts) holdPdsAccount(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, ref *commonv1.SecretRef, did, pdsURL string) {
	if err := a.holdBearer(ctx, workspace, id, ref, did, pdsURL); err != nil {
		a.log.Warn("the pds account session is not held", "workspace", workspace, "identity", id.Name, "err", err.Error())
	}
}

type secretStore struct {
	vault SecretStore

	ref commonv1.SecretRef
}

var _ identity.Store = secretStore{}

func (s secretStore) Read(ctx context.Context) (map[string]any, error) {
	return s.vault.Get(ctx, s.ref)
}

func (s secretStore) Write(ctx context.Context, data map[string]any) error {
	return s.vault.Put(ctx, s.ref, data)
}