package api
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// A sign-off's secret is what an approved call minted (a token's value)
// held for the asking agent to collect once. It is sealed at rest with
// the forge's secret key, the box CI secrets use, so it is never in the
// database or a backup in the clear; and it lapses: uncollected after
// signOffSecretLife it is cleared, and a worker token it held is
// revoked.
const (
signOffSecretLife = oltp.SignOffSecretLife
sealedPrefix = "sealed:"
)
// signOffAsk says, inside a run of an approved request, who asked and
// which request: the settings log names them beside the approver.
type signOffAsk struct{ agent, id string }
type signOffAskKey struct{}
func withSignOffAsk(ctx context.Context, s SignOffRequest) context.Context {
return context.WithValue(ctx, signOffAskKey{}, signOffAsk{agent: s.Agent, id: s.ID})
}
func signOffAskOf(ctx context.Context) (signOffAsk, bool) {
a, ok := ctx.Value(signOffAskKey{}).(signOffAsk)
return a, ok
}
func (d Deps) sealSignOffSecret(id, secret string) (string, error) {
if secret == "" {
return "", nil
}
if !d.SecretBox.Ready() {
return "", fmt.Errorf("this forge has no secret key, so it cannot hold a credential for collection: %w", ops.ErrUnavailable)
}
sealed, err := d.SecretBox.Seal("signoff", id, "secret", secret)
if err != nil {
return "", err
}
return sealedPrefix + base64.StdEncoding.EncodeToString(sealed), nil
}
// openSignOffSecret reads what sealSignOffSecret stored. A row written
// before secrets were sealed holds plain JSON; it is handed over once
// like any other and the sweep clears the rest after the same limit.
func (d Deps) openSignOffSecret(id, stored string) (string, error) {
raw, ok := strings.CutPrefix(stored, sealedPrefix)
if !ok {
return stored, nil
}
sealed, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return "", err
}
return d.SecretBox.Open("signoff", id, "secret", sealed)
}
// lapseRef names one credential an approved call minted, not secret.
type lapseRef struct {
Repo string `json:"repo"`
Token string `json:"token"`
}
// lapseRefOf is the credential an op's answer minted, when it is one the
// sweep knows how to revoke.
func lapseRefOf(op, repo string, out any) []lapseRef {
if op != "ci.worker.token.new" {
return nil
}
if o, ok := out.(workerTokenNewOut); ok && o.Token.ID != "" {
return []lapseRef{{Repo: repo, Token: o.Token.ID}}
}
return nil
}
// holdSignOffSecret records what an approved request answered: the
// credentials it minted (not secret), then the answer and the sealed
// secret. A forge that cannot seal revokes what was minted rather than
// hold it in the clear.
func (d Deps) holdSignOffSecret(ctx context.Context, s SignOffRequest, result, secret string, refs []lapseRef) error {
sealed, err := d.sealSignOffSecret(s.ID, secret)
if err != nil {
for _, ref := range refs {
d.revokeLapsed(ref, s, "no secret key to hold it")
}
return err
}
if len(refs) > 0 {
raw, _ := json.Marshal(refs)
if err := d.SignOffs.SetSignOffRefs(s.ID, string(raw)); err != nil {
return err
}
}
return d.SignOffs.FinishSignOff(s.ID, result, sealed)
}
// LapseSignOffSecrets clears the secrets nobody collected within their
// limit, revoking the worker tokens they held and logging each. It is
// the periodic upkeep's call; a second pass finds nothing.
func (d Deps) LapseSignOffSecrets(now time.Time) (int, error) {
if d.SignOffs == nil {
return 0, nil
}
lapsed, err := d.SignOffs.LapseSignOffSecrets(now.Add(-signOffSecretLife))
for _, l := range lapsed {
var refs []lapseRef
if l.Refs != "" {
if jerr := json.Unmarshal([]byte(l.Refs), &refs); jerr != nil {
log.Printf("signoff %s: lapse refs: %v", l.ID, jerr)
}
}
for _, ref := range refs {
d.revokeLapsed(ref, SignOffRequest{ID: l.ID, Agent: l.Agent}, "lapsed uncollected")
}
}
return len(lapsed), err
}
// revokeLapsed revokes one token its secret never reached anybody for,
// and says so in the repository's settings log.
func (d Deps) revokeLapsed(ref lapseRef, s SignOffRequest, why string) {
if d.WorkerTokens == nil || ref.Repo == "" {
return
}
own, err := d.ownWorkerTokens(ref.Repo)
if err != nil {
log.Printf("signoff %s: listing tokens to revoke: %v", s.ID, err)
return
}
for _, t := range own {
if t.ID != ref.Token || t.RevokedAt != "" {
continue
}
if err := d.WorkerTokens.RevokeWorkerToken(t.ID); err != nil {
log.Printf("signoff %s: revoking %s: %v", s.ID, t.ID, err)
return
}
if d.SettingLog != nil {
d.SettingLog.LogSetting(ref.Repo, "forge", "", "worker-token", tokenWords(t, ""),
fmt.Sprintf("revoked: %s (request %s, asked by %s)", why, s.ID, s.Agent))
}
}
}