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