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

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

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

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

const signedOutMessage = "signed out through spec.signOutGeneration; sign in again"

func SignOutRequested(id *v1alpha1.MarketIdentity, status v1alpha1.MarketIdentityStatus) bool {
	return id.Spec.SignOutGeneration > status.SignedOutGeneration
}

func SignOutRefusal(mode v1alpha1.MarketIdentityMode) error {
	switch mode {
	case v1alpha1.MarketIdentityModeOAuthBrowser, v1alpha1.MarketIdentityModeOAuthQr, v1alpha1.MarketIdentityModeOAuthDelegated:
		return nil
	case v1alpha1.MarketIdentityModeOAuthSessionProxy:
		return errors.New("marketidentity: sign out is refused: mode oauthSessionProxy uses the web app's session, which the web app owns; sign out in the web app")
	}
	return fmt.Errorf("marketidentity: sign out is refused: mode %q holds no oauth session to sign out of", mode)
}

func (a *Accounts) Reconcile(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, status v1alpha1.MarketIdentityStatus) (Result, error) {
	if !SignOutRequested(id, status) {
		return a.reconcile(ctx, workspace, id, status)
	}
	if refusal := SignOutRefusal(id.Spec.Mode); refusal != nil {
		result, err := a.reconcile(ctx, workspace, id, status)
		message := strings.TrimPrefix(refusal.Error(), "marketidentity: ")
		if current := strings.TrimSpace(result.Status.Message); strings.Contains(current, message) {
			message = current
		} else if current != "" {
			message = current + "; " + message
		}
		result.Status.Message = message
		return result, err
	}
	next, err := a.signOut(ctx, workspace, id, status)
	if err != nil {
		return pending(status, id.Generation, "SignOutFailed", err.Error()), err
	}
	return a.reconcile(ctx, workspace, id, next)
}

func (a *Accounts) signOut(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, status v1alpha1.MarketIdentityStatus) (v1alpha1.MarketIdentityStatus, error) {
	key := qrKey(workspace, id.Name)
	if issue, ok := a.browser.Get(key); ok && !issue.spent {
		if _, _, err := a.statesFor(workspace).Take(ctx, issue.state); err != nil {
			a.log.Warn("the pending oauth callback state is not removed at sign out", "workspace", workspace, "identity", id.Name, "err", err.Error())
		}
	}
	a.browser.Delete(key)
	a.qr.Delete(key)
	ref := oauthSessionRef(id, &status)
	a.revoke(ctx, workspace, id, ref)
	a.held.forget(workspace, id.Name)
	if ref != nil {
		vault := a.vaultFor(workspace)
		if vault == nil {
			return status, errors.New("marketidentity: no vault deletes the oauth session at sign out")
		}
		if err := vault.Delete(ctx, *ref); err != nil && !errors.Is(err, ErrSecretNotFound) {
			return status, fmt.Errorf("marketidentity: deleting the oauth session at sign out: %w", err)
		}
	}
	generation := id.Generation
	status.SignedOutGeneration = id.Spec.SignOutGeneration
	status.SignedOutAt = time.Now().UTC().Format(time.RFC3339)
	status.Did, status.Handle, status.PdsURL = "", "", ""
	status.PendingAction = nil
	status.Message = signedOutMessage
	conds.Set(&status.Conditions, generation, v1alpha1.ConditionSignedIn, false, v1alpha1.ReasonSignedOut, signedOutMessage)
	setReady(&status, generation, false, v1alpha1.ReasonSignedOut, signedOutMessage)
	a.log.Info("the identity is signed out", "workspace", workspace, "identity", id.Name, "mode", id.Spec.Mode,
		"signOutGeneration", id.Spec.SignOutGeneration)
	return status, nil
}

func (a *Accounts) revoke(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, ref *commonv1.SecretRef) {
	log := a.log.With("workspace", workspace, "identity", id.Name)
	session := a.held.session(workspace, id.Name)
	if session == nil && ref != nil {
		session = a.storedSession(ctx, workspace, id, *ref)
	}
	if session == nil {
		log.Info("no oauth session is held or stored; nothing to revoke at sign out")
		return
	}
	revocation, err := session.Revoke(ctx)
	switch {
	case errors.Is(err, oauthclient.ErrNoRevocationEndpoint):
		log.Info("the authorization server names no revocation endpoint; the oauth session is only forgotten", "did", session.DID())
	case err != nil:
		log.Warn("the oauth session is not revoked at the authorization server", "did", session.DID(), "endpoint", revocation.Endpoint,
			"revoked", revocation.Revoked, "err", err.Error())
	default:
		log.Info("the oauth session is revoked at the authorization server", "did", session.DID(), "endpoint", revocation.Endpoint,
			"revoked", revocation.Revoked)
	}
}

func (a *Accounts) storedSession(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, ref commonv1.SecretRef) *oauthclient.Session {
	vault := a.vaultFor(workspace)
	if vault == nil {
		return nil
	}
	stored, err := (&vaultSessionStore{vault: vault, ref: ref}).List(ctx)
	if err != nil || len(stored) == 0 {
		return nil
	}
	client, err := a.sessionHTTP(ctx, workspace, id)
	if err != nil {
		return nil
	}
	data := stored[0]
	if clientID := a.pinnedClientID(id); clientID != "" {
		data.ClientID = clientID
	}
	session, err := oauthclient.New(data, oauthclient.Options{ClientID: a.sessionClientID(id), HTTPClient: client, Log: a.log})
	if err != nil {
		return nil
	}
	return session
}

func signedOut(status *v1alpha1.MarketIdentityStatus) bool {
	for _, c := range status.Conditions {
		if c.Type == v1alpha1.ConditionSignedIn {
			return c.Reason == v1alpha1.ReasonSignedOut
		}
	}
	return false
}