Skip to content
quarry Artifact [c401c0e912]
Login

Artifact [c401c0e912]

Artifact c401c0e912b815f855a12316d5bd5b73c401868742df6acf8d382967eee7eaa4:


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)
);`

// 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
}

// 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 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); 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.
func (d *DB) CheckReporterToken(repo, token string) bool {
	var n int
	if err := d.db.QueryRow(`SELECT count(*) FROM ci_reporter_token WHERE repo = ? AND hash = ?`,
		repo, tokenHash(token)).Scan(&n); err != nil {
		return false
	}
	return n > 0
}

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
}

// 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()
}