Skip to content
quarry Artifact [abc8733d6a]
Login

Artifact [abc8733d6a]

Artifact abc8733d6a75d242d7238b99ceb5e02d0f6b35dc647a984ca951dcae7c0a2072:


package api

import (
	"context"
	"fmt"
	"time"

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

// A Git mirror of an upstream that is not public fetches with an access
// token (a GitHub fine-grained token with read access to the
// repository's contents). It is given once, when the mirror is made or
// replaced here, sealed in the mirror row, sent only to that upstream as
// a request header, and never returned by any op.

// MirrorTokenStore is the mirror row's token half.
type MirrorTokenStore interface {
	// SetMirrorGitToken seals token for repo's Git mirror; "" clears it.
	SetMirrorGitToken(repo, token string) error
}

// gitTokenProblem is why token cannot be one, "" when it can: printable
// ASCII without spaces, since it rides in a request header.
func gitTokenProblem(token string) string {
	if len(token) > 1024 {
		return "an access token is at most 1024 characters"
	}
	for _, c := range token {
		if c <= ' ' || c > '~' {
			return "an access token has no spaces or other special characters; paste just the token"
		}
	}
	return ""
}

type mirrorTokenIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
	// Token replaces the one held; Clear removes it, and the mirror
	// fetches anonymously again.
	Token string `json:"token" validate:"max=1024"`
	Clear bool   `json:"clear"`
}

type mirrorTokenOut struct {
	Mirror Mirror `json:"mirror"`
}

func registerMirrorToken(r *ops.Registry, d Deps) error {
	return ops.Register(r, ops.Op[mirrorTokenIn, mirrorTokenOut]{
		Name:  "mirror.token",
		Caps:  "a",
		Scope: ops.ScopeRepo,
		Doc: "Replace or clear the access token a Git mirror fetches its upstream with, for an upstream that is not " +
			"public. The token is sealed, sent only to that upstream, and never returned; the mirror says only whether it holds one.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/mirrors/token",
		Run: func(ctx context.Context, in mirrorTokenIn) (mirrorTokenOut, error) {
			if d.Mirrors == nil || d.MirrorTokens == nil {
				return mirrorTokenOut{}, fmt.Errorf("mirroring is not configured: %w", ops.ErrUnavailable)
			}
			if _, err := d.repoFor(ctx, in.Repo); err != nil {
				return mirrorTokenOut{}, err
			}
			if err := d.refuseWithout(ctx, in.Repo, "a", false); err != nil {
				return mirrorTokenOut{}, err
			}
			if in.Clear == (in.Token != "") {
				return mirrorTokenOut{}, fmt.Errorf("give a token to replace the one held, or clear: %w", ops.ErrInvalid)
			}
			if why := gitTokenProblem(in.Token); why != "" {
				return mirrorTokenOut{}, fmt.Errorf("%s: %w", why, ops.ErrInvalid)
			}
			m, ok, err := d.Mirrors.MirrorFor(in.Repo)
			if err != nil {
				return mirrorTokenOut{}, err
			}
			if !ok || !m.Git {
				return mirrorTokenOut{}, fmt.Errorf("%s does not mirror a Git repository: %w", in.Repo, ops.ErrNotFound)
			}
			if err := d.MirrorTokens.SetMirrorGitToken(in.Repo, in.Token); err != nil {
				return mirrorTokenOut{}, err
			}
			if m, _, err = d.Mirrors.MirrorFor(in.Repo); err != nil {
				return mirrorTokenOut{}, err
			}
			m.State = mirrorState(m, time.Now())
			return mirrorTokenOut{Mirror: m}, nil
		},
	})
}