Skip to content
quarry Artifact [7dde3b510d]
Login

Artifact [7dde3b510d]

Artifact 7dde3b510d8e6a903996ae19d12672fd955344edc61bbe875b3e489da89d894e:


package oltp

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

// sameCaps compares two strings as fossil reads them, setup as admin.
func sameCaps(a, b string) bool {
	return Closure(noSetup(a)) == Closure(noSetup(b))
}

// 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 — unless the account's string no longer equals
// what the last sync gave it, which means an owner set it by hand. 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{}
	prior := map[string]string{} // caps the last sync gave each linked login
	q, err := tx.Query(`SELECT login, linked, caps FROM fossil_user WHERE repo = ? AND linked != ''`, repo)
	if err != nil {
		return 0, 0, err
	}
	for q.Next() {
		var login, acct, caps string
		if err := q.Scan(&login, &acct, &caps); err != nil {
			q.Close()
			return 0, 0, err
		}
		linked[login] = acct
		prior[login] = caps
	}
	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
	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++
		// An account follows upstream only while it still holds what the
		// last sync gave it: a string an owner set by hand since then is
		// theirs, and a sync must not downgrade it.
		if acct != "" {
			if cur, err := capsOf(tx, acct, repo); err != nil {
				return 0, 0, err
			} else if sameCaps(cur, prior[u.Login]) && !sameCaps(cur, u.Caps) {
				// The guard a person's change takes, applied to the sync:
				// an upstream downgrade must not leave the repository
				// without an admin. The admin stays, and the log says so.
				before, after, err := d.adminCountIn(tx, repo, acct, noSetup(u.Caps))
				if err != nil {
					return 0, 0, err
				}
				if before > 0 && after == 0 {
					log.Printf("oltp: fossil sync of %s kept %s as admin: upstream %s now holds %q, and nobody else administers it", repo, acct, u.Login, u.Caps)
				} else if err := setCaps(tx, acct, repo, u.Caps); err != nil {
					return 0, 0, err
				}
			}
		}
	}
	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++
	}
	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
}