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(ctx, 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)
}