~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
221 lines · 5.6 KB Raw History
package ownersession

import (
	"context"
	"crypto/subtle"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/gateway"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/gatewayhttp"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/sshtransport"
)

const DefaultListLimit = 50

var ErrNoSession = errors.New("sshserver: no account session is configured")

type Session = sshtransport.OwnerSession

type Repo interface {
	atprotorepo.RecordReader

	atprotorepo.RecordWriter
}

type Open func(did string, session Session) (Repo, error)

type Config struct {
	Sessions map[string]Session
}

type Deps struct {
	Open Open
}

type Owners struct {
	sessions map[string]Session

	open Open

	mu sync.Mutex

	repos map[string]Repo
}

var (
	_ sshtransport.Authenticator = (*Owners)(nil)
	_ sshtransport.AgentProvider = (*Owners)(nil)
	_ gateway.WebAccountAgent    = (*agent)(nil)
	_ gateway.WebAccountAgent    = Unavailable{}
)

func New(cfg Config, deps Deps) *Owners {
	sessions := map[string]Session{}
	for did, session := range cfg.Sessions {
		if strings.TrimSpace(session.Service) == "" {
			continue
		}
		sessions[did] = session
	}
	return &Owners{sessions: sessions, open: deps.Open, repos: map[string]Repo{}}
}

func (o *Owners) Len() int {
	return len(o.sessions)
}

func (o *Owners) Authenticate(r *http.Request) (*gatewayhttp.WebCaller, error) {
	if r == nil {
		return nil, nil
	}
	identifier, password, ok := r.BasicAuth()
	if !ok {
		return nil, nil
	}
	for did, session := range o.sessions {
		sameIdentifier := subtle.ConstantTimeCompare([]byte(session.Identifier), []byte(identifier)) == 1
		samePassword := subtle.ConstantTimeCompare([]byte(session.Password), []byte(password)) == 1
		if !sameIdentifier || !samePassword {
			continue
		}
		return &gatewayhttp.WebCaller{WebUser: gatewayhttp.WebUser{DID: did, Handle: session.Identifier, PDS: session.Service}}, nil
	}
	return nil, nil
}

func (o *Owners) AgentFor(caller *gatewayhttp.WebCaller) gatewayhttp.WebAccountAgent {
	if caller == nil {
		return Unavailable{}
	}
	if _, ok := o.sessions[caller.DID]; !ok {
		return Unavailable{}
	}
	return &agent{owners: o, did: caller.DID}
}

func (o *Owners) repo(did string) (Repo, error) {
	o.mu.Lock()
	defer o.mu.Unlock()
	if repo := o.repos[did]; repo != nil {
		return repo, nil
	}
	session, ok := o.sessions[did]
	if !ok || o.open == nil {
		return nil, ErrNoSession
	}
	repo, err := o.open(did, session)
	if err != nil {
		return nil, err
	}
	o.repos[did] = repo
	return repo, nil
}

type agent struct {
	owners *Owners

	did string
}

func (a *agent) bound(did string) (Repo, error) {
	if did != a.did {
		return nil, fmt.Errorf("sshserver: the owner session of %s cannot write the repo %s", a.did, did)
	}
	return a.owners.repo(a.did)
}

func (a *agent) GetServiceAuth(ctx context.Context, aud, lxm string) (string, error) {
	return "", errors.New("sshserver: the owner session agent does not mint service auth tokens")
}

func (a *agent) ListRecords(ctx context.Context, did, collection string, limit int) ([]gatewayhttp.Record, error) {
	repo, err := a.owners.repo(a.did)
	if err != nil {
		return nil, err
	}
	page, err := repo.ListRecords(ctx, did, collection, atprotorepo.ListOptions{Limit: opt.Positive(limit, DefaultListLimit)})
	if err != nil {
		return nil, err
	}
	out := make([]gatewayhttp.Record, 0, len(page.Records))
	for _, record := range page.Records {
		var value map[string]any
		if err := json.Unmarshal(record.Value, &value); err != nil {
			continue
		}
		out = append(out, gatewayhttp.Record{URI: record.URI, CID: record.CID, Value: value})
	}
	return out, nil
}

func (a *agent) CreateRecord(ctx context.Context, did, collection, rkey string, record map[string]any) (gatewayhttp.RecordRef, error) {
	repo, err := a.bound(did)
	if err != nil {
		return gatewayhttp.RecordRef{}, err
	}
	raw, err := json.Marshal(record)
	if err != nil {
		return gatewayhttp.RecordRef{}, err
	}
	ref, err := repo.CreateRecord(ctx, collection, rkey, raw)
	if err != nil {
		return gatewayhttp.RecordRef{}, err
	}
	return gatewayhttp.RecordRef{URI: ref.URI, CID: ref.CID}, nil
}

func (a *agent) ApplyWrites(ctx context.Context, did string, writes []gatewayhttp.Write) error {
	repo, err := a.bound(did)
	if err != nil {
		return err
	}
	for _, write := range writes {
		switch write.Action {
		case "delete":
			if err := repo.DeleteRecord(ctx, write.Collection, write.Rkey); err != nil {
				return err
			}
		case "create", "update":
			raw, err := json.Marshal(write.Record)
			if err != nil {
				return err
			}
			if write.Action == "create" {
				_, err = repo.CreateRecord(ctx, write.Collection, write.Rkey, raw)
			} else {
				_, err = repo.PutRecord(ctx, write.Collection, write.Rkey, raw)
			}
			if err != nil {
				return err
			}
		default:
			return fmt.Errorf("sshserver: unknown write action %q", write.Action)
		}
	}
	return nil
}

type Unavailable struct{}

func (Unavailable) GetServiceAuth(ctx context.Context, aud, lxm string) (string, error) {
	return "", ErrNoSession
}

func (Unavailable) ListRecords(ctx context.Context, did, collection string, limit int) ([]gatewayhttp.Record, error) {
	return nil, ErrNoSession
}

func (Unavailable) CreateRecord(ctx context.Context, did, collection, rkey string, record map[string]any) (gatewayhttp.RecordRef, error) {
	return gatewayhttp.RecordRef{}, ErrNoSession
}

func (Unavailable) ApplyWrites(ctx context.Context, did string, writes []gatewayhttp.Write) error {
	return ErrNoSession
}