Skip to content
quarry Artifact [dfc4d2a970]
Login

Artifact [dfc4d2a970]

Artifact dfc4d2a9704b8d7cb6ca5b85e2c1fc0755f6c9c6751729558ccad552e673857a:


package api

import (
	"context"
	"fmt"
	"log"
	"reflect"

	"craftdesign.group/quarry/internal/oltp"

	"craftdesign.group/quarry/internal/ops"
)

// ForgeActs records and reads what the forge's operator did to
// repositories they do not hold.
type ForgeActs interface {
	RecordForgeAct(actor, op, repo, owner string) error
	ForgeActs(repo string, limit int) ([]oltp.ForgeAct, error)
}

// repoOf digs the repository name out of an op's input.
//
// By reflection, deliberately, because the alternative is asking every
// op to declare it and trusting 188 of them to keep doing so. A field
// named Repo is the convention across the op inputs; the repo.* family
// names it Name instead, since the repository IS their subject.
//
// A miss means no receipt for that act, which is a gap in the journal
// rather than a refusal — see RecordForgeAct.
func repoOf(in any) string {
	v := reflect.ValueOf(in)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return ""
	}
	for _, name := range []string{"Repo", "Name"} {
		f := v.FieldByName(name)
		if f.IsValid() && f.Kind() == reflect.String && f.String() != "" {
			return f.String()
		}
	}
	return ""
}

// WitnessOperator returns the observer that writes a receipt whenever a
// site admin changes a repository somebody else holds.
//
// The three conditions are the whole policy, and each excludes a case
// that should NOT be journalled. The actor must be a site admin, since
// an ordinary member acting within their letters is not the operator
// reaching in. The repository must have an owner, since acting on a
// store nobody holds is administering the forge, which is the job. And
// the owner must not be the actor, since nobody needs a receipt for
// what they did to their own repository.
//
// All three are read BEFORE the act. A transfer changes the owner, so
// reading after it named the recipient as the person reached into, and
// a delete purges the row the owner is read from (#318). The receipt
// itself is written after, so a delete's survives the purge.
func (d Deps) WitnessOperator() ops.Observed {
	return func(ctx context.Context, op string, in any) func() {
		if d.Acts == nil || d.Owners == nil {
			return nil
		}
		actor := ops.User(ctx)
		if actor == "" || !d.isSiteAdmin(actor) {
			return nil
		}
		repo := repoOf(in)
		if repo == "" {
			return nil
		}
		owner, err := d.Owners.OwnerOf(repo)
		if err != nil || owner == "" || owner == actor {
			return nil
		}
		return func() {
			if err := d.Acts.RecordForgeAct(actor, op, repo, owner); err != nil {
				// Never fails the act it describes: a forge that stops
				// working because it cannot journal is worse than one
				// whose journal has a hole, and the hole is in the log.
				logForgeActFailure(op, repo, err)
			}
		}
	}
}

type forgeActLogIn struct {
	Repo  string `json:"repo" validate:"required,max=64"`
	Limit int    `json:"limit" validate:"min=0,max=500"`
}

type forgeActLogOut struct {
	Acts []oltp.ForgeAct `json:"acts"`
}

// registerForgeActLog is the receipt, read by the people it is for.
//
// Gated on `a` for the repository rather than on being the operator:
// the whole point is that the person whose repository was reached into
// can see it. A site admin reads it only with a role there (#318).
func registerForgeActLog(r *ops.Registry, d Deps) error {
	return ops.Register(r, ops.Op[forgeActLogIn, forgeActLogOut]{
		Name:  "repo.forgelog",
		Caps:  "a",
		Doc:   "What the forge's operator did to this repository while somebody else held it. Written whenever a site admin changes a repository they do not own.",
		Kind:  ops.Query,
		Auth:  ops.Token,
		Route: "GET /api/v1/r/{repo}/forgelog",
		Run: func(ctx context.Context, in forgeActLogIn) (forgeActLogOut, error) {
			if d.Acts == nil {
				return forgeActLogOut{}, fmt.Errorf("this deployment keeps no forge journal: %w", ops.ErrUnavailable)
			}
			acts, err := d.Acts.ForgeActs(in.Repo, in.Limit)
			if err != nil {
				return forgeActLogOut{}, err
			}
			if acts == nil {
				acts = []oltp.ForgeAct{}
			}
			return forgeActLogOut{Acts: acts}, nil
		},
	})
}

func logForgeActFailure(op, repo string, err error) {
	log.Printf("forge journal: %s on %s: %v", op, repo, err)
}