Skip to content
quarry Artifact [0e07d35c04]
Login

Artifact [0e07d35c04]

Artifact 0e07d35c044cdf60ac59b41006bec8fc5ae8bc99da24cbbfbef29e005abb686b:


// syncmint.go bridges the two credentials a person on this forge holds
// without ever being told they hold two: the account password typed
// into a page, and the secret `zit push` proves itself with.
//
// They cannot be one value. RFC §6.3 fixes the push login as
// SHA1(project-code/user/password), which the server has to be able to
// RECOMPUTE, while an account password is verified against argon2id and
// deliberately cannot be recovered (password.go says so, and is right
// to). So the sync secret can only be derived at the instants the
// plaintext is legitimately in hand: signing in, and setting a
// password.
//
// Until now it was derived at none of them. The only writer of a
// sync_user row was an admin running the CLI on the server with the
// password in their hand, so a gated repository admitted exactly the
// people somebody had SSH'd in for — two accounts, on one repository.
// That was survivable while "no sync users" meant "anonymous push
// allowed". When that default closed (correctly — it was
// unauthenticated write), it meant twenty-three of this forge's
// twenty-four repositories had no writer at all, their owners
// included, and fossil's summary line reports a refused push as
// "Artifacts sent: 0" with exit 0, so the work looked landed and was
// not.
//
// The lesson worth keeping: closing a door is only half a change. The
// other half is checking that the people who belong inside still have
// one, and the test that proves it has to push on both.
package oltp

import "craftdesign.group/quarry/internal/xfer"

// reposForSync lists every repository the forge knows, so minting can
// ask the letters about each. Ordered for a stable, testable sweep.
func (d *DB) reposForSync() ([]string, error) {
	rows, err := d.db.Query(`SELECT name FROM repo ORDER BY name`)
	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()
}

// DeleteSyncUser drops one repo user's stored secret.
func (d *DB) DeleteSyncUser(repo, user string) error {
	_, err := d.db.Exec(`DELETE FROM sync_user WHERE repo = ? AND user = ?`, repo, user)
	return err
}

// MintSyncSecrets brings one account's push credentials in line with
// the letters, for every repository on the forge.
//
// Where the account may check in it gets a current secret; where it may
// not, any secret it still holds is dropped, so a revoked grant stops
// being a live credential rather than lingering as one. That second
// half matters more than it looks: without it, the only way to take
// push away from somebody would be a table no page displays.
//
// Best-effort by design, and silent. It runs on the sign-in path, and a
// repository whose project code cannot be read must never turn a good
// password into a failed login. The cost of skipping one is a push that
// asks the person to sign in again; the cost of failing loudly is a
// forge nobody can get into.
func (d *DB) MintSyncSecrets(name, password string) {
	if d.SyncProjectCode == nil || name == "" || password == "" {
		return
	}
	repos, err := d.reposForSync()
	if err != nil {
		return
	}
	for _, repo := range repos {
		if !d.EffectiveCaps(name, repo).HasAny("i") {
			_ = d.DeleteSyncUser(repo, name)
			continue
		}
		code, ok := d.SyncProjectCode(repo)
		if !ok || code == "" {
			continue
		}
		_ = d.SetSyncUser(repo, name, xfer.SharedSecret(code, name, password))
	}
}