// agent.go — which accounts are machine-operated, and who answers for
// them.
//
// A forge that cannot say this has a record it cannot read back. On
// this one, in a single day: eleven tickets filed by an automated
// campaign under a person's name, permanent in every clone, indexed
// forever as that person's words; a sync credential minted in the
// operator's name by a machine; and two agent sessions writing all day
// with nothing on any page to say either was a machine.
//
// The mark is on the ACCOUNT, not the write. A write-level mark is more
// honest and much larger — it would have to reach the artifact format —
// and the account-level one is what makes the write-level one worth
// building later: you cannot ask "which of this account's writes were
// machine-made" until you can ask "is this account a machine".
//
// RFC 0021 makes the mark worth more: an agent is an account with no
// password (an empty pwhash, an operator named), holds tokens instead, and
// never holds more than its operator (caps.go's intersection). What
// follows is the agent's lifecycle: creation, the operator's grant, the
// suspension that follows its operator's disabling or deletion, and the
// deed it may never hold (SetOwner, repo.go).
package oltp
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
)
// forgeOperator is the reserved operator of a forge agent (RFC 0021,
// Forge agents): one the forge itself runs, with no operator ceiling.
// Everything here that cuts an agent down to its operator leaves a
// forge agent alone; the rest of its rules are its own section's.
const forgeOperator = "forge"
// agentSuspendedAfter is how long a suspended agent waits for a new
// operator before it is deleted as its operator was (RFC 0021,
// Lifecycle).
const agentSuspendedAfter = 30 * 24 * time.Hour
// personsAgent reports whether a is a person's agent: marked, and not
// the forge's own.
func personsAgent(a Account) bool {
return a.Operator != "" && a.Operator != forgeOperator
}
// IsAgent reports whether name is a person's agent (operator set, and
// not the forge's). False for a person, a forge agent and an unknown
// name alike.
func (d *DB) IsAgent(name string) bool {
a, ok := d.AccountByName(name)
return ok && personsAgent(a)
}
// Operator names the person answerable for a machine-operated account.
// Empty means the account is a person's own.
func (d *DB) Operator(name string) string {
var op string
if err := d.db.QueryRow(`SELECT operator FROM account WHERE name = ?`, name).Scan(&op); err != nil {
return ""
}
return op
}
// SetOperator marks an account as machine-operated and records who runs
// it; an empty operator clears the mark.
//
// The GATE is the caller's, not this function's — see api's account.agent
// for the asymmetry: declaring yourself an agent is always allowed and
// undeclaring is a site admin's call. Stated there rather than here
// because it is a policy about callers, and this is a table.
//
// Marking clears the password and ends the sessions in the same
// transaction (RFC 0021, Creating an agent): a token is the one
// credential an agent has, and an account that kept its password while
// marked would be an agent with the credential an agent must not have.
// Marking a suspended agent with a new operator lifts the suspension,
// which is how a site admin reassigns one (Lifecycle).
func (d *DB) SetOperator(name, operator string) error {
if name == "" {
return errors.New("oltp: empty account name")
}
if operator != "" && !validLogin(operator) {
return fmt.Errorf("oltp: operator %q is not an account name", operator)
}
// An agent reassigned to itself names nobody who can be asked, and
// an agent answering for an agent names somebody who cannot be
// either: the operator is a person.
if operator != "" && operator == name {
return Refusal{fmt.Sprintf("%s cannot be its own operator; name the person who runs it", name)}
}
return d.guarded(func(tx *sql.Tx) error {
if operator != "" {
op, ok := accountByName(tx, operator)
if !ok {
// An operator who is not an account is a name nobody can
// be asked about, which is the thing this exists to end.
return fmt.Errorf("oltp: no account named %q to answer for %q", operator, name)
}
if personsAgent(op) {
return Refusal{fmt.Sprintf("%s is itself an agent; name the person who runs %s", operator, name)}
}
}
res, err := tx.Exec(`UPDATE account SET operator = ? WHERE name = ?`, operator, name)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return fmt.Errorf("oltp: no such account %q", name)
}
if operator == "" {
// A person again: whatever suspension it carried as an agent
// ends with the mark. Its password is still gone; the caller
// mints the invite that lets somebody set one.
_, err := tx.Exec(`UPDATE account SET suspended = '', disabled = '' WHERE name = ? AND suspended <> ''`, name)
return err
}
if _, err := tx.Exec(`UPDATE account SET pwhash = '', legacy_pw = '', legacy_code = '',
suspended = '', disabled = CASE WHEN suspended <> '' THEN '' ELSE disabled END
WHERE name = ?`, name); err != nil {
return err
}
return endAccountSessions(tx, name)
})
}
// NewAgent creates a person's agent: an account with no password whose
// operator is set from the start (RFC 0021, Creating an agent). The
// name comes from the account namespace under every rule AddAccount
// applies. Never a site admin: the flag is a person's.
func (d *DB) NewAgent(name, operator string) error {
if err := d.checkNewName(name); err != nil {
return err
}
if operator == "" {
return errors.New("oltp: an agent needs an operator")
}
if operator == name {
return Refusal{fmt.Sprintf("%s cannot be its own operator; name the person who runs it", name)}
}
return d.guarded(func(tx *sql.Tx) error {
op, ok := accountByName(tx, operator)
if !ok {
return fmt.Errorf("oltp: no account named %q to answer for %q", operator, name)
}
if personsAgent(op) {
return Refusal{fmt.Sprintf("%s is itself an agent; name the person who runs %s", operator, name)}
}
_, err := tx.Exec(`INSERT INTO account (name, pwhash, admin, created, operator) VALUES (?, '', 0, ?, ?)`,
name, now(), operator)
if err != nil && strings.Contains(err.Error(), "UNIQUE") {
return fmt.Errorf("oltp: account %q already exists", name)
}
return err
})
}
// Agents lists operator's agents, name-ordered. A forge agent's
// operator is forge, so Agents(forge) is the fleet.
func (d *DB) Agents(operator string) ([]Account, error) {
if operator == "" {
return nil, nil
}
rows, err := d.db.Query(`SELECT name, admin, created, email, email_pending, disabled, last_login, last_seen, operator, suspended,
pwhash = '' AND legacy_pw = '' AND operator = ''
FROM account WHERE operator = ? ORDER BY name`, operator)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Account
for rows.Next() {
var a Account
var admin int
if err := rows.Scan(&a.Name, &admin, &a.Created, &a.Email, &a.EmailPending, &a.Disabled, &a.LastLogin, &a.LastSeen,
&a.Operator, &a.Suspended, &a.Pending); err != nil {
return nil, err
}
a.Admin = admin != 0
out = append(out, a)
}
return out, rows.Err()
}
// AgentCount is how many agents operator has, for the per-person cap.
func (d *DB) AgentCount(operator string) (int, error) {
var n int
err := d.db.QueryRow(`SELECT count(*) FROM account WHERE operator = ?`, operator).Scan(&n)
return n, err
}
// suspendAgents is the cascade an operator's disabling or deletion
// runs on their agents (RFC 0021, Lifecycle): sessions gone, tokens
// refused (a disabled account's are, token.go), the rows kept and
// marked with when. An agent a site admin had already shut stays shut
// and is not marked, so reopening the operator does not reopen it.
func suspendAgents(tx *sql.Tx, operator string, at time.Time) error {
when := at.UTC().Format(time.RFC3339)
if _, err := tx.Exec(`DELETE FROM session WHERE account IN (SELECT name FROM account WHERE operator = ?)`, operator); err != nil {
return err
}
_, err := tx.Exec(`UPDATE account SET suspended = ?, disabled = ? WHERE operator = ? AND disabled = ''`, when, when, operator)
return err
}
// liftAgents undoes suspendAgents for operator's agents: the ones the
// cascade shut, and only those.
func liftAgents(tx *sql.Tx, operator string) error {
_, err := tx.Exec(`UPDATE account SET suspended = '', disabled = '' WHERE operator = ? AND suspended <> ''`, operator)
return err
}
// SweepSuspendedAgents deletes the agents whose operator was deleted
// more than thirty days ago and that nobody reassigned since, as the
// person was deleted: the same cascade, the same tombstone. An agent
// suspended because its operator is merely disabled waits for the
// operator, however long.
func (d *DB) SweepSuspendedAgents(now time.Time) (int, error) {
cut := now.Add(-agentSuspendedAfter).UTC().Format(time.RFC3339)
rows, err := d.db.Query(`SELECT name FROM account a WHERE suspended <> '' AND suspended < ?
AND NOT EXISTS (SELECT 1 FROM account o WHERE o.name = a.operator)`, cut)
if err != nil {
return 0, err
}
var names []string
for rows.Next() {
var n string
if err := rows.Scan(&n); err != nil {
rows.Close()
return 0, err
}
names = append(names, n)
}
rows.Close()
if err := rows.Err(); err != nil {
return 0, err
}
for _, n := range names {
if err := d.DeleteAccount(n); err != nil {
return 0, err
}
}
return len(names), nil
}
// RevokeAgentTokens is the operator's one button (RFC 0021, Lifecycle:
// revoke all): every live token of every agent of theirs revoked and
// every session of theirs ended, in one transaction. It is what a
// person reaches for when a laptop goes missing, and it is one button
// because there is no safe answer to "which of my twelve tokens was on
// that machine".
func (d *DB) RevokeAgentTokens(operator string) (int, error) {
if operator == "" {
return 0, errors.New("oltp: empty operator name")
}
var n int64
err := d.guarded(func(tx *sql.Tx) error {
res, err := tx.Exec(`UPDATE token SET revoked = ? WHERE revoked = '' AND account <> ''
AND account IN (SELECT name FROM account WHERE operator = ?)`, time.Now().UTC().Format(time.RFC3339), operator)
if err != nil {
return err
}
n, _ = res.RowsAffected()
_, err = tx.Exec(`DELETE FROM session WHERE account IN (SELECT name FROM account WHERE operator = ?)`, operator)
return err
})
return int(n), err
}
// AgentGrant writes operator's own agent's string on a repository
// (RFC 0021, Granting): any repository the operator can reach, with the
// string clipped to the operator's effective letters there before it is
// stored. The clipped string is returned, and clipped says whether the
// clip took anything. Refused on a repository the operator cannot read,
// and for an agent that is not the operator's.
//
// Clipping at write time is what the page shows; the intersection in
// effectiveIn runs on every read regardless, so a letter the operator
// loses later is gone from this string's effect without a rewrite.
func (d *DB) AgentGrant(operator, agent, repo, caps string) (stored string, clipped bool, err error) {
if repo == "" {
return "", false, errors.New("oltp: empty repository name")
}
err = d.guarded(func(tx *sql.Tx) error {
a, ok := accountByName(tx, agent)
if !ok || !personsAgent(a) || a.Operator != operator {
return Refusal{fmt.Sprintf("%s is not an agent of %s's", agent, operator)}
}
opOwn, _ := capsOf(tx, operator, repo)
opEff := d.personEffectiveIn(tx, operator, repo, opOwn)
if !opEff.HasAny("gjoh2r") {
return Refusal{fmt.Sprintf("%s cannot reach %s, so %s cannot be put to work there", operator, repo, agent)}
}
// Within the operator's letters the string is stored as given,
// so a preset reads back as itself; above them, the letters
// both hold, spelled out.
want := d.expandCeiling(repo, caps)
stored = string(want)
if cut := capIntersect(want, opEff); letterSet(cut) != letterSet(want) {
stored, clipped = Minimal(string(cut)), true
} else {
stored = noSetup(caps)
}
return setCaps(tx, agent, repo, stored)
})
return stored, clipped, err
}
// letterSet is the letters a set answers for, spelled out in a fixed
// order, so two sets compare as sets.
func letterSet(c CapSet) string {
var b strings.Builder
for _, l := range capLetters {
if c.HasAny(string(l)) {
b.WriteRune(l)
}
}
return b.String()
}