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