Skip to content
quarry Artifact [6229894e28]
Login

Artifact [6229894e28]

Artifact 6229894e2879532c453b8ad7a38027c5ec16ca634a2df0dd0df941379cdf8864:


package oltp

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"strings"
	"time"
)

// External CI (RFC 0009 §external-ci): checks quarry did not run, and
// the reporter tokens that may write them. A token authenticates; it
// holds no capability letters and reaches exactly one op — a
// compromised one can lie about a check and do nothing else.

const extCISchema = `
CREATE TABLE IF NOT EXISTS ci_reporter_token (
	repo    TEXT NOT NULL,
	hash    TEXT NOT NULL,           -- sha256 of the token, hex
	label   TEXT NOT NULL DEFAULT '',
	created TEXT NOT NULL,
	PRIMARY KEY (repo, hash)
);
CREATE TABLE IF NOT EXISTS external_check (
	repo    TEXT NOT NULL,
	subject TEXT NOT NULL,           -- check-in hash, or pair:<branch>:<target>
	context TEXT NOT NULL,           -- the check's name
	state   TEXT NOT NULL,           -- pending | success | failure | error
	url     TEXT NOT NULL DEFAULT '',
	summary TEXT NOT NULL DEFAULT '',
	updated TEXT NOT NULL,
	PRIMARY KEY (repo, subject, context)
);`

// extCIMigrations widen older stores. A token that has never been
// used and one that reported an hour ago look identical in a list of
// hashes, and "which of these can I revoke?" is the question an
// administrator actually has.
var extCIMigrations = []string{
	`ALTER TABLE ci_reporter_token ADD COLUMN used TEXT NOT NULL DEFAULT ''`,
}

// ReporterToken is one credential, as the settings page lists it. The
// token itself is shown once at mint and never stored.
type ReporterToken struct {
	Hash    string
	Label   string
	Created string
	// Used is when this token last reported, empty for one that never
	// has. Written on each accepted report, not on each presentation:
	// a wrong token is not this token being used.
	Used string
}

// MintReporterToken creates a credential for one repository and
// returns the token — the only time it exists in the clear.
func (d *DB) MintReporterToken(repo, label string) (string, error) {
	raw := make([]byte, 24)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	token := "qcr_" + hex.EncodeToString(raw)
	_, err := d.db.Exec(`INSERT INTO ci_reporter_token (repo, hash, label, created)
		VALUES (?, ?, ?, ?)`,
		repo, tokenHash(token), strings.TrimSpace(label), time.Now().UTC().Format(time.RFC3339))
	if err != nil {
		return "", err
	}
	return token, nil
}

// ReporterTokens lists a repository's credentials — hashes and labels,
// never tokens.
func (d *DB) ReporterTokens(repo string) ([]ReporterToken, error) {
	rows, err := d.db.Query(`SELECT hash, label, created, used FROM ci_reporter_token
		WHERE repo = ? ORDER BY created DESC`, repo)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []ReporterToken
	for rows.Next() {
		var t ReporterToken
		if err := rows.Scan(&t.Hash, &t.Label, &t.Created, &t.Used); err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// RevokeReporterToken removes one credential by its stored hash.
func (d *DB) RevokeReporterToken(repo, hash string) error {
	_, err := d.db.Exec(`DELETE FROM ci_reporter_token WHERE repo = ? AND hash = ?`, repo, hash)
	return err
}

// CheckReporterToken says whether a presented token may report here,
// and remembers that it did. The write is best-effort: a store that
// cannot record the use must not refuse the report over it.
func (d *DB) CheckReporterToken(repo, token string) bool {
	hash := tokenHash(token)
	var n int
	if err := d.db.QueryRow(`SELECT count(*) FROM ci_reporter_token WHERE repo = ? AND hash = ?`,
		repo, hash).Scan(&n); err != nil {
		return false
	}
	if n == 0 {
		return false
	}
	_, _ = d.db.Exec(`UPDATE ci_reporter_token SET used = ? WHERE repo = ? AND hash = ?`,
		time.Now().UTC().Format(time.RFC3339), repo, hash)
	return true
}

func tokenHash(token string) string {
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

// ExternalCheck is one reported state, the commit-status contract
// quarry-shaped.
type ExternalCheck struct {
	Subject string
	Context string
	State   string
	URL     string
	Summary string
	Updated string
}

var checkStates = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}

// UpsertExternalCheck records a state; the newest report per
// (subject, context) wins, which is the whole contract.
func (d *DB) UpsertExternalCheck(repo string, c ExternalCheck) error {
	if !checkStates[c.State] {
		return errors.New("oltp: check state must be pending, success, failure or error")
	}
	if strings.TrimSpace(c.Context) == "" || strings.TrimSpace(c.Subject) == "" {
		return errors.New("oltp: a check needs a subject and a context")
	}
	_, err := d.db.Exec(`INSERT INTO external_check (repo, subject, context, state, url, summary, updated)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT (repo, subject, context) DO UPDATE SET
			state = excluded.state, url = excluded.url,
			summary = excluded.summary, updated = excluded.updated`,
		repo, c.Subject, c.Context, c.State, c.URL, c.Summary,
		time.Now().UTC().Format(time.RFC3339))
	return err
}

// CheckNamesSeen is every check name reported on this repository since
// a moment, for the settings page to offer where a rule names its
// required checks.
func (d *DB) CheckNamesSeen(repo string, since time.Time) ([]string, error) {
	// A check has two sources: an external reporter's status, and the
	// forge's own CI, whose workflow name is what a rule requires. The
	// settings page warned that nothing had reported "quarry" while
	// quarry's own runner reported it every hour, because only the
	// first source was read.
	cut := since.UTC().Format(time.RFC3339)
	rows, err := d.db.Query(`SELECT context FROM external_check WHERE repo = ? AND updated >= ?
		UNION SELECT workflow FROM ci_run WHERE repo = ? AND started >= ?
		ORDER BY 1`, repo, cut, repo, cut)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var n string
		if err := rows.Scan(&n); err != nil {
			return nil, err
		}
		out = append(out, n)
	}
	return out, rows.Err()
}

// ExternalChecks lists what was reported for one subject.
func (d *DB) ExternalChecks(repo, subject string) ([]ExternalCheck, error) {
	rows, err := d.db.Query(`SELECT subject, context, state, url, summary, updated
		FROM external_check WHERE repo = ? AND subject = ? ORDER BY context`, repo, subject)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []ExternalCheck
	for rows.Next() {
		var c ExternalCheck
		if err := rows.Scan(&c.Subject, &c.Context, &c.State, &c.URL, &c.Summary, &c.Updated); err != nil {
			return nil, err
		}
		out = append(out, c)
	}
	return out, rows.Err()
}