Skip to content
quarry Artifact [bab3554cb2]
Login

Artifact [bab3554cb2]

Artifact bab3554cb263d6e051aabb5048897fb87abdb760bbe2f8ce214014fa7842366d:


package oltp

import (
	"database/sql"
	"errors"
	"fmt"
)

// A fossil repository's user table, kept as a reference rather than as
// accounts. An admin syncs it from the fossil server; each login is a
// row an admin may link to a quarry account, and only a link hands out
// capabilities. Fossil logins are per repository and quarry accounts
// span the forge, so a matching name is a suggestion and never a grant.
// No password hash is stored: nobody signs in as a row.
const fossilUserSchema = `
CREATE TABLE IF NOT EXISTS fossil_user (
	repo      TEXT NOT NULL,
	login     TEXT NOT NULL,
	caps      TEXT NOT NULL DEFAULT '',
	info      TEXT NOT NULL DEFAULT '',
	linked    TEXT NOT NULL DEFAULT '', -- the quarry account it was linked to
	synced_at TEXT NOT NULL,
	PRIMARY KEY (repo, login)
);`

// FossilUser is one row of a repository's synced fossil user table.
type FossilUser struct {
	Login    string
	Caps     string
	Info     string
	Linked   string
	SyncedAt string
	// Suggest is true when an unlinked row's login names an existing
	// quarry account — offered to the admin, never acted on.
	Suggest bool
}

// ErrNoFossilUser and ErrNoAccount are a link's two refusals.
var (
	ErrNoFossilUser = errors.New("no such fossil user on this repository")
	ErrNoAccount    = errors.New("no such account")
)

var fossilCategories = map[string]bool{
	"nobody": true, "anonymous": true, "reader": true, "developer": true,
}

// ReplaceFossilUsers records a fresh sync of repo's fossil user table.
//
// The four category rows are applied as this repository's categories —
// that is what lets a fossil repository need no further setup — and a
// category the peer did not send keeps its current string. Every other
// row replaces the list. A login that is still present keeps its link,
// and its linked account is given the fresh capabilities, because that
// is what syncing means. A login that has gone loses its row and link;
// the account it was linked to keeps what it holds, since nothing here
// revokes access an admin has not looked at.
func (d *DB) ReplaceFossilUsers(repo string, users []FossilUser) (rows, categories int, err error) {
	tx, err := d.w.Begin()
	if err != nil {
		return 0, 0, err
	}
	defer tx.Rollback()
	linked := map[string]string{}
	q, err := tx.Query(`SELECT login, linked FROM fossil_user WHERE repo = ? AND linked != ''`, repo)
	if err != nil {
		return 0, 0, err
	}
	for q.Next() {
		var login, acct string
		if err := q.Scan(&login, &acct); err != nil {
			q.Close()
			return 0, 0, err
		}
		linked[login] = acct
	}
	q.Close()
	if _, err := tx.Exec(`DELETE FROM fossil_user WHERE repo = ?`, repo); err != nil {
		return 0, 0, err
	}
	at := now()
	var cats [][2]string
	var relink [][2]string
	for _, u := range users {
		if u.Login == "" {
			continue
		}
		if fossilCategories[u.Login] {
			cats = append(cats, [2]string{u.Login, u.Caps})
			continue
		}
		acct := linked[u.Login]
		if _, err := tx.Exec(`INSERT INTO fossil_user (repo, login, caps, info, linked, synced_at)
			VALUES (?, ?, ?, ?, ?, ?)`, repo, u.Login, u.Caps, u.Info, acct, at); err != nil {
			return 0, 0, err
		}
		rows++
		if acct != "" {
			relink = append(relink, [2]string{acct, u.Caps})
		}
	}
	if err := tx.Commit(); err != nil {
		return 0, 0, err
	}
	for _, c := range cats {
		if err := d.SetCategoryCaps(repo, c[0], c[1]); err != nil {
			return rows, categories, err
		}
		categories++
	}
	for _, l := range relink {
		if err := d.SetCaps(l[0], repo, l[1]); err != nil {
			return rows, categories, err
		}
	}
	return rows, categories, nil
}

// FossilUsers lists repo's synced fossil users by login.
func (d *DB) FossilUsers(repo string) ([]FossilUser, error) {
	q, err := d.db.Query(`SELECT f.login, f.caps, f.info, f.linked, f.synced_at,
			f.linked = '' AND EXISTS (SELECT 1 FROM account a WHERE a.name = f.login)
		FROM fossil_user f WHERE f.repo = ? ORDER BY f.login`, repo)
	if err != nil {
		return nil, err
	}
	defer q.Close()
	var out []FossilUser
	for q.Next() {
		var u FossilUser
		if err := q.Scan(&u.Login, &u.Caps, &u.Info, &u.Linked, &u.SyncedAt, &u.Suggest); err != nil {
			return nil, err
		}
		out = append(out, u)
	}
	return out, q.Err()
}

// LinkFossilUser links a fossil login to a quarry account, which then
// holds that login's capabilities on repo. An empty account unlinks,
// leaving the account's capabilities as they are.
//
// Linking replaces the account's string with the login's, so it is a
// caps write like any other and takes the same guard, for the actor,
// under the lock it writes with: linking a repository's last admin, or
// oneself, to a login without a would otherwise leave it with none.
func (d *DB) LinkFossilUser(actor, repo, login, account string) (warning string, err error) {
	err = d.guarded(func(tx *sql.Tx) error {
		var caps string
		if err := tx.QueryRow(`SELECT caps FROM fossil_user WHERE repo = ? AND login = ?`,
			repo, login).Scan(&caps); err != nil {
			return fmt.Errorf("%s: %w", login, ErrNoFossilUser)
		}
		if account != "" {
			if _, ok := accountByName(tx, account); !ok {
				return fmt.Errorf("%s: %w", account, ErrNoAccount)
			}
			if warning, err = d.guardCaps(tx, actor, account, repo, caps); err != nil {
				return err
			}
		}
		if _, err := tx.Exec(`UPDATE fossil_user SET linked = ? WHERE repo = ? AND login = ?`,
			account, repo, login); err != nil {
			return err
		}
		if account == "" {
			return nil
		}
		return setCaps(tx, account, repo, caps)
	})
	return warning, err
}