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

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"strings"

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

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

type pdsAccountCredential struct {
	handle string

	password string

	pdsURL string
}

var errVaultRead = errors.New("marketidentity: reading the vault")

func (a *Accounts) pdsAccount(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity,
	generation int64, status v1alpha1.MarketIdentityStatus) (Result, error) {
	vault := a.vaultFor(workspace)
	ref := oauthSessionRef(id, &status)
	if ref != nil {
		status.SessionRef = ref
	}
	credential, err := pdsAccountCredentialOf(ctx, vault, id)
	if errors.Is(err, errVaultRead) {
		reason := ReasonVaultUnavailable
		if errors.Is(err, ErrSecretNotFound) {
			reason = ReasonWaitingForCredential
		}
		return pending(status, generation, reason, err.Error()), err
	}
	if err != nil {
		return failed(status, generation, "MissingCredential", err.Error()), nil
	}
	tlsConfig, err := a.tlsFor(ctx, workspace, id)
	if err != nil {
		return pending(status, generation, "TrustUnavailable", err.Error()), err
	}
	if ref != nil {
		if session, ok := a.storedPdsSession(ctx, vault, *ref, credential.pdsURL, tlsConfig); ok {
			a.holdPdsAccount(ctx, workspace, id, ref, session.DID.String(), credential.pdsURL)
			return signedIn(status, generation, session.DID.String(), firstNonEmpty(session.Handle, credential.handle), credential.pdsURL, ref,
				session.AccessJWT), nil
		}
	}
	pds, err := a.remotePDS(credential.pdsURL, nil, tlsConfig)
	if err != nil {
		return failed(status, generation, "PdsUnavailable", err.Error()), nil
	}
	session, err := pds.CreateSession(ctx, credential.handle, credential.password)
	if err != nil {
		message := fmt.Sprintf("com.atproto.server.createSession at %s: %v", credential.pdsURL, err)
		return failed(status, generation, "SignInFailed", message), errors.New(message)
	}
	a.log.Info("pds_session_created", "workspace", workspace, "identity", id.Name, "did", session.DID.String(), "pds", credential.pdsURL)
	if ref != nil {
		if err := savePdsSession(ctx, vault, *ref, credential.pdsURL, session); err != nil {
			return pending(status, generation, "SessionStoreFailed", err.Error()), err
		}
		a.holdPdsAccount(ctx, workspace, id, ref, session.DID.String(), credential.pdsURL)
	}
	return signedIn(status, generation, session.DID.String(), firstNonEmpty(session.Handle, credential.handle), credential.pdsURL, ref,
		session.AccessJWT), nil
}

func pdsAccountCredentialOf(ctx context.Context, vault SecretStore, id *v1alpha1.MarketIdentity) (pdsAccountCredential, error) {
	if id.Spec.KeysRef == nil {
		return pdsAccountCredential{}, errors.New("mode pdsAccount names no spec.keysRef; the password stays in the vault and the controller does not fall back to a local repo")
	}
	if vault == nil {
		return pdsAccountCredential{}, errors.New("mode pdsAccount has no vault to read spec.keysRef")
	}
	data, err := vault.Get(ctx, *id.Spec.KeysRef)
	if err != nil {
		return pdsAccountCredential{}, fmt.Errorf("%w spec.keysRef %s/%s: %w", errVaultRead, id.Spec.KeysRef.Mount, id.Spec.KeysRef.Path, err)
	}
	credential := pdsAccountCredential{
		handle:   firstNonEmpty(id.Spec.Handle, firstString(data, "handle", "identifier", "user", "username")),
		password: firstString(data, "password", "appPassword", "app_password"),
		pdsURL:   firstNonEmpty(id.Spec.PdsURL, firstString(data, "pdsUrl", "pds_url", "pds")),
	}
	switch {
	case credential.handle == "":
		return pdsAccountCredential{}, errors.New("mode pdsAccount has no handle in spec.handle or spec.keysRef")
	case credential.password == "":
		return pdsAccountCredential{}, errors.New("mode pdsAccount has no password in spec.keysRef; it fails closed")
	case credential.pdsURL == "":
		return pdsAccountCredential{}, errors.New("mode pdsAccount has no pdsUrl in spec.pdsUrl or spec.keysRef")
	}
	return credential, nil
}

func (a *Accounts) storedPdsSession(ctx context.Context, vault SecretStore, ref commonv1.SecretRef, pdsURL string, tlsConfig *tls.Config) (pdsclient.Session, bool) {
	if vault == nil {
		return pdsclient.Session{}, false
	}
	data, err := vault.Get(ctx, ref)
	if err != nil {
		return pdsclient.Session{}, false
	}
	access := firstString(data, "accessJwt", "accessToken", "access_token")
	if !oauthclient.AccessTokenLive(access) {
		return pdsclient.Session{}, false
	}
	tokens := pdsclient.TokenSourceFunc(func(ctx context.Context) (string, error) { return access, nil })
	pds, err := a.remotePDS(pdsURL, tokens, tlsConfig)
	if err != nil {
		return pdsclient.Session{}, false
	}
	session, err := pds.GetSession(ctx)
	if err != nil {
		return pdsclient.Session{}, false
	}
	session.AccessJWT = access
	session.RefreshJWT = firstString(data, "refreshJwt", "refreshToken", "refresh_token")
	return session, true
}

func savePdsSession(ctx context.Context, vault SecretStore, ref commonv1.SecretRef, pdsURL string, session pdsclient.Session) error {
	if vault == nil {
		return errors.New("marketidentity: no vault writes the pds account session")
	}
	return vault.Put(ctx, ref, map[string]any{
		"did":           session.DID.String(),
		"handle":        session.Handle,
		"pds":           pdsURL,
		"host_url":      pdsURL,
		"accessJwt":     session.AccessJWT,
		"refreshJwt":    session.RefreshJWT,
		"access_token":  session.AccessJWT,
		"refresh_token": session.RefreshJWT,
	})
}

func (a *Accounts) tlsFor(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (*tls.Config, error) {
	if id.Spec.CARef == nil || id.Spec.CARef.IsZero() {
		return nil, nil
	}
	if a.tls == nil {
		return nil, errors.New("marketidentity: spec.caRef is set but no CA source is configured")
	}
	return a.tls(ctx, workspace, id)
}

func (a *Accounts) remotePDS(endpoint string, tokens pdsclient.TokenSource, tlsConfig *tls.Config) (pdsclient.PDS, error) {
	endpoint = strings.TrimSpace(endpoint)
	if endpoint == "" {
		return nil, errors.New("marketidentity: the remote PDS has no URL")
	}
	if !strings.Contains(endpoint, "://") {
		return nil, fmt.Errorf("marketidentity: the remote PDS %q is not an absolute URL", endpoint)
	}
	cfg := pdsclient.RemoteConfig{BaseURL: endpoint, Tokens: tokens, Timeout: requestTimeout, HTTPClient: a.resolver.HTTPClient,
		TLS: a.resolver.TLS, DPoP: a.resolver.DPoP}
	if a.resolver.Timeout > 0 {
		cfg.Timeout = a.resolver.Timeout
	}
	switch {
	case tlsConfig != nil:
		cfg.HTTPClient, cfg.TLS = nil, tlsConfig
	case cfg.HTTPClient == nil:
		cfg.HTTPClient = a.http
	}
	return pdsclient.NewRemote(cfg)
}