package oltp
import (
"path/filepath"
"strings"
"testing"
"time"
)
// agentRig is a person, their agent, a stranger and two repositories:
// one public, one private, with the person holding what caps says on
// each.
func agentRig(t *testing.T) *DB {
t.Helper()
db := testDB(t)
for _, who := range []string{"dmestas", "jva"} {
if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
}
if err := db.AddAccount("root", "a-long-enough-password", true); err != nil {
t.Fatal(err)
}
if err := db.NewAgent("dmestas-coder", "dmestas", -1); err != nil {
t.Fatal(err)
}
for _, r := range []string{"pub", "priv"} {
if err := db.AddRepo(r, t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
}
if err := db.SetRepoVisibility("priv", Private); err != nil {
t.Fatal(err)
}
return db
}
func letters(c CapSet) string {
var b strings.Builder
for _, l := range capLetters {
if c.HasAny(string(l)) {
b.WriteRune(l)
}
}
return b.String()
}
// The ceiling: for every (agent grant, operator set) over the presets
// the agent resolves to the intersection plus what every visitor
// holds, with nothing stored.
func TestAgentResolvesToOperatorIntersection(t *testing.T) {
db := agentRig(t)
presets := []Role{NoRole, Read, Write, Maintain, RepoAdmin}
for _, og := range presets {
for _, ag := range presets {
if err := db.Grant("dmestas", "pub", og); err != nil {
t.Fatal(err)
}
if err := db.Grant("dmestas-coder", "pub", ag); err != nil {
t.Fatal(err)
}
op := db.EffectiveCaps("dmestas", "pub")
own := db.personEffectiveIn(db.db, "dmestas-coder", "pub", ag.Caps())
got := letters(db.EffectiveCaps("dmestas-coder", "pub"))
want := letters(CapSet(capUnion(db.CategoryCaps("pub", "nobody"), string(capIntersect(own, op)))))
if got != want {
t.Errorf("FAIL operator %s, agent %s: resolved %q, want %q", og, ag, got, want)
}
for _, l := range got {
if !op.HasAny(string(l)) && !strings.ContainsRune(db.CategoryCaps("pub", "nobody"), l) {
t.Errorf("FAIL operator %s, agent %s: agent holds %c, operator does not", og, ag, l)
}
}
}
}
t.Log("ok: every preset pair resolves to the intersection plus the public set")
}
// The operator losing a letter takes it from the agent on the next
// read, with no write to the agent's row.
func TestOperatorLosingALetterTakesItFromTheAgent(t *testing.T) {
db := agentRig(t)
if err := db.Grant("dmestas", "pub", RepoAdmin); err != nil {
t.Fatal(err)
}
if err := db.Grant("dmestas-coder", "pub", RepoAdmin); err != nil {
t.Fatal(err)
}
if !db.EffectiveCaps("dmestas-coder", "pub").HasAny("a") {
t.Fatal("FAIL: an admin's agent granted admin does not hold a")
}
before, _ := db.CapsOf("dmestas-coder", "pub")
if err := db.Grant("dmestas", "pub", Read); err != nil {
t.Fatal(err)
}
after, _ := db.CapsOf("dmestas-coder", "pub")
eff := db.EffectiveCaps("dmestas-coder", "pub")
if eff.HasAny("a") || eff.HasAny("i") {
t.Errorf("FAIL: the agent kept letters its operator lost: %q", letters(eff))
} else if before != after {
t.Errorf("FAIL: the agent's stored string was rewritten: %q -> %q", before, after)
} else {
t.Log("ok: the operator's demotion reached the agent at read time, nothing written")
}
}
// A site-admin operator yields no s, and a private repository the
// operator cannot see does not exist for the agent.
func TestSiteAdminOperatorYieldsNoSAndPrivateStaysDark(t *testing.T) {
db := agentRig(t)
if err := db.NewAgent("root-bot", "root", -1); err != nil {
t.Fatal(err)
}
if db.EffectiveCaps("root-bot", "").HasAny("s") {
t.Error("FAIL: a site admin's agent holds s at forge scope")
} else {
t.Log("ok: s does not flow from a site-admin operator")
}
if err := db.Grant("root-bot", "priv", RepoAdmin); err != nil {
t.Fatal(err)
}
if db.EffectiveCaps("root-bot", "priv").HasAny("gjoh2r") {
t.Error("FAIL: an agent reads a private repository its operator holds no grant on")
} else {
t.Log("ok: a private repository the operator cannot see does not exist for the agent")
}
if err := db.Grant("dmestas", "priv", Read); err != nil {
t.Fatal(err)
}
if err := db.Grant("dmestas-coder", "priv", Write); err != nil {
t.Fatal(err)
}
eff := db.EffectiveCaps("dmestas-coder", "priv")
if !eff.HasAny("o") || eff.HasAny("i") {
t.Errorf("FAIL: agent on a private repository through a reader operator holds %q", letters(eff))
} else {
t.Log("ok: the agent reaches the private repository through its operator's grant, and no further")
}
// A disabled operator holds nothing.
if err := db.SetDisabled("dmestas", true); err != nil {
t.Fatal(err)
}
if db.EffectiveCaps("dmestas-coder", "priv").HasAny("gjoh2r") {
t.Error("FAIL: a disabled operator's agent still reads the private repository")
}
}
// Granting: agent.grant stores the string clipped to the operator's
// letters and refuses a repository the operator cannot reach.
func TestAgentGrantClipsToTheOperator(t *testing.T) {
db := agentRig(t)
if err := db.Grant("dmestas", "pub", Read); err != nil {
t.Fatal(err)
}
stored, clipped, err := db.AgentGrant("dmestas", "dmestas-coder", "pub", Write.Caps())
if err != nil {
t.Fatalf("FAIL: %v", err)
}
if !clipped {
t.Error("FAIL: developer on a reader's agent was not reported clipped")
}
if CapSet(stored).HasAny("i") || CapSet(stored).HasAny("v") {
t.Errorf("FAIL: stored %q holds check-in the operator lacks", stored)
}
if got, _ := db.CapsOf("dmestas-coder", "pub"); got != stored {
t.Errorf("FAIL: stored %q, row holds %q", stored, got)
}
if err := db.Grant("dmestas", "pub", RepoAdmin); err != nil {
t.Fatal(err)
}
stored, clipped, err = db.AgentGrant("dmestas", "dmestas-coder", "pub", Write.Caps())
if err != nil || clipped || Closure(stored) != Closure(Write.Caps()) {
t.Errorf("FAIL: developer under an admin operator stored as %q (clipped %v, err %v)", stored, clipped, err)
} else {
t.Log("ok: a string within the operator's letters is stored whole; one above is clipped")
}
if _, _, err := db.AgentGrant("dmestas", "dmestas-coder", "priv", Read.Caps()); !IsRefusal(err) {
t.Errorf("FAIL: a grant on a repository the operator cannot reach: %v", err)
} else {
t.Log("ok: refused where the operator cannot reach")
}
if _, _, err := db.AgentGrant("jva", "dmestas-coder", "pub", Read.Caps()); !IsRefusal(err) {
t.Errorf("FAIL: a stranger wrote somebody else's agent: %v", err)
}
}
// The deed: SetOwner refuses an agent, so no path can hand it one.
func TestSetOwnerRefusesAnAgent(t *testing.T) {
db := agentRig(t)
if err := db.SetOwner("pub", "dmestas-coder"); !IsRefusal(err) {
t.Errorf("FAIL: an agent received a deed: %v", err)
}
if owner, _ := db.OwnerOf("pub"); owner != "" {
t.Errorf("FAIL: the deed moved to %q", owner)
}
if err := db.SetOwner("pub", "dmestas"); err != nil {
t.Errorf("FAIL: a person refused a deed: %v", err)
}
if n, _ := db.OwnedRepoCount("dmestas-coder"); n != 0 {
t.Errorf("FAIL: the agent owns %d repositories", n)
}
}
// The two a guards, keyed on the caller being an agent only.
func TestAgentWithAdminCannotLowerOperatorOrDeedHolderNorEscalate(t *testing.T) {
db := agentRig(t)
for _, who := range []string{"dmestas", "dmestas-coder", "jva"} {
if err := db.Grant(who, "pub", RepoAdmin); err != nil {
t.Fatal(err)
}
}
if err := db.AddAccount("owner", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
if err := db.Grant("owner", "pub", Maintain); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("pub", "owner"); err != nil {
t.Fatal(err)
}
for _, target := range []string{"dmestas", "owner"} {
if _, err := db.GrantGuarded("dmestas-coder", target, "pub", Read); !IsRefusal(err) {
t.Errorf("FAIL: the agent lowered %s: %v", target, err)
}
if _, err := db.GrantGuarded("dmestas-coder", target, "pub", NoRole); !IsRefusal(err) {
t.Errorf("FAIL: the agent revoked %s: %v", target, err)
}
}
// Raising the deed holder is not lowering.
if _, err := db.GrantGuarded("dmestas-coder", "owner", "pub", RepoAdmin); err != nil {
t.Errorf("FAIL: the agent could not raise the deed holder: %v", err)
}
// A third account may be lowered, and a person may lower anyone.
if _, err := db.GrantGuarded("dmestas-coder", "jva", "pub", Read); err != nil {
t.Errorf("FAIL: the agent could not lower a third account: %v", err)
}
if _, err := db.GrantGuarded("jva", "dmestas", "pub", Read); err != nil {
t.Errorf("FAIL: a person with a was held to the agent's guard: %v", err)
}
// Escalation: the operator is a reader now, so the agent resolves
// to reader and cannot write developer on anyone.
if _, err := db.SetCapsGuarded("dmestas-coder", "jva", "pub", Write.Caps()); !IsRefusal(err) {
t.Errorf("FAIL: the agent granted letters it does not resolve to: %v", err)
} else {
t.Log("ok: an agent writes at most what it resolves to")
}
if _, err := db.SetCapsGuarded("owner", "jva", "pub", Write.Caps()); err != nil {
t.Errorf("FAIL: a person was held to the agent's ceiling: %v", err)
}
}
// Marking clears the password and ends the sessions in one
// transaction; SetOperator refuses self and an agent as operator.
func TestMarkingClearsThePasswordAndSetOperatorRefusesSelf(t *testing.T) {
db := agentRig(t)
tok, _, ok := db.NewSession("jva", "a-long-enough-password")
if !ok {
t.Fatal("no session")
}
if err := db.SetOperator("jva", "dmestas"); err != nil {
t.Fatalf("FAIL: %v", err)
}
if _, ok := db.Authenticate("jva", "a-long-enough-password"); ok {
t.Error("FAIL: a marked account still signs in with its password")
}
if _, ok := db.SessionAccount(tok); ok {
t.Error("FAIL: a marked account's session survived")
}
if a, _ := db.AccountByName("jva"); a.Pending {
t.Error("FAIL: an agent reads as an unclaimed invite")
}
if err := db.SetOperator("jva", "jva"); !IsRefusal(err) {
t.Errorf("FAIL: an account became its own operator: %v", err)
}
if err := db.SetOperator("jva", "dmestas-coder"); !IsRefusal(err) {
t.Errorf("FAIL: an agent became an operator: %v", err)
}
if err := db.NewAgent("x", "x", -1); !IsRefusal(err) {
t.Errorf("FAIL: NewAgent accepted self as operator: %v", err)
}
if err := db.NewAgent("root-bot", "root", -1); err != nil {
t.Fatal(err)
}
if a, _ := db.AccountByName("root-bot"); a.Admin {
t.Error("FAIL: a site admin's agent was made a site admin")
}
// The one-time-link ops refuse an agent by name.
if _, err := db.InviteAccount("jva", "", false); !IsRefusal(err) {
t.Errorf("FAIL: an agent was invited: %v", err)
}
if _, err := db.ResetPassword("jva"); !IsRefusal(err) {
t.Errorf("FAIL: an agent's password was reset: %v", err)
}
// Clearing the mark leaves a passwordless person an invite can reach.
if err := db.SetOperator("jva", ""); err != nil {
t.Fatal(err)
}
if _, err := db.InviteAccount("jva", "", false); err != nil {
t.Errorf("FAIL: a cleared account cannot be invited: %v", err)
} else {
t.Log("ok: marking clears the password; the mark cleared, the invite path is open again")
}
}
// Lifecycle: disabling and deleting an operator suspends their agents;
// reopening and reassignment lift it; day thirty-one deletes.
func TestOperatorDisableAndDeleteSuspendAgents(t *testing.T) {
db := agentRig(t)
value, _, err := db.MintToken("dmestas-coder", TokenSpec{Name: "s", Scopes: []string{ScopeAPI}})
if err != nil {
t.Fatal(err)
}
if err := db.SetDisabled("dmestas", true); err != nil {
t.Fatal(err)
}
a, _ := db.AccountByName("dmestas-coder")
if a.Disabled == "" || a.Suspended == "" {
t.Errorf("FAIL: the agent was not suspended with its operator: %+v", a)
}
if _, ok := db.ResolveToken("dmestas-coder", value, ScopeAPI); ok {
t.Error("FAIL: a suspended agent's token still resolves")
}
if err := db.SetDisabled("dmestas", false); err != nil {
t.Fatal(err)
}
if a, _ := db.AccountByName("dmestas-coder"); a.Disabled != "" || a.Suspended != "" {
t.Errorf("FAIL: reopening the operator left the agent shut: %+v", a)
}
if _, ok := db.ResolveToken("dmestas-coder", value, ScopeAPI); !ok {
t.Error("FAIL: the token did not come back with the operator")
}
// An agent a site admin shut on purpose stays shut.
if err := db.SetDisabled("dmestas-coder", true); err != nil {
t.Fatal(err)
}
if err := db.SetDisabled("dmestas", true); err != nil {
t.Fatal(err)
}
if err := db.SetDisabled("dmestas", false); err != nil {
t.Fatal(err)
}
if a, _ := db.AccountByName("dmestas-coder"); a.Disabled == "" {
t.Error("FAIL: reopening the operator reopened an agent shut on purpose")
}
if err := db.SetDisabled("dmestas-coder", false); err != nil {
t.Fatal(err)
}
// Deletion suspends, reassignment lifts, the sweep deletes.
if err := db.DeleteAccount("dmestas"); err != nil {
t.Fatal(err)
}
a, _ = db.AccountByName("dmestas-coder")
if a.Suspended == "" || a.Operator != "dmestas" {
t.Errorf("FAIL: deleting the operator did not suspend the agent under its name: %+v", a)
}
if n, err := db.SweepSuspendedAgents(time.Now().Add(29 * 24 * time.Hour)); err != nil || n != 0 {
t.Errorf("FAIL: the sweep took a day-29 agent (%d, %v)", n, err)
}
if err := db.SetOperator("dmestas-coder", "jva"); err != nil {
t.Fatal(err)
}
if a, _ := db.AccountByName("dmestas-coder"); a.Disabled != "" || a.Suspended != "" {
t.Errorf("FAIL: reassignment did not lift the suspension: %+v", a)
}
if err := db.DeleteAccount("jva"); err != nil {
t.Fatal(err)
}
if n, err := db.SweepSuspendedAgents(time.Now().Add(31 * 24 * time.Hour)); err != nil || n != 1 {
t.Errorf("FAIL: the sweep did not take the day-31 agent (%d, %v)", n, err)
}
if _, ok := db.AccountByName("dmestas-coder"); ok {
t.Error("FAIL: the swept agent still exists")
} else if !db.Tombstoned("dmestas-coder") {
t.Error("FAIL: the swept agent's name was not retired")
} else {
t.Log("ok: suspended with the operator, lifted by reassignment, deleted on day thirty-one")
}
}
// revoke_all leaves no live token or session across two agents.
func TestRevokeAllAgentTokens(t *testing.T) {
db := agentRig(t)
if err := db.NewAgent("dmestas-release", "dmestas", -1); err != nil {
t.Fatal(err)
}
var values []string
for _, agent := range []string{"dmestas-coder", "dmestas-release"} {
v, _, err := db.MintToken(agent, TokenSpec{Name: "s", Scopes: []string{ScopeAPI}})
if err != nil {
t.Fatal(err)
}
values = append(values, v)
}
mine, _, err := db.MintToken("dmestas", TokenSpec{Name: "mine", Scopes: []string{ScopeAPI}})
if err != nil {
t.Fatal(err)
}
n, err := db.RevokeAgentTokens("dmestas")
if err != nil || n != 2 {
t.Fatalf("FAIL: revoked %d, %v", n, err)
}
for i, agent := range []string{"dmestas-coder", "dmestas-release"} {
if _, ok := db.ResolveToken(agent, values[i], ScopeAPI); ok {
t.Errorf("FAIL: %s's token survived revoke_all", agent)
}
}
if _, ok := db.ResolveToken("dmestas", mine, ScopeAPI); !ok {
t.Error("FAIL: revoke_all took the operator's own token")
} else {
t.Log("ok: every agent token gone, the person's own kept")
}
}
// The owner backfill (ownerBackfill, run on every open) is the one deed
// write that does not go through SetOwner: an unclaimed repository
// whose sole admin is an agent stays forge-held across a reopen.
func TestOwnerBackfillNeverDeedsAnAgent(t *testing.T) {
path := filepath.Join(t.TempDir(), "o.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddAccount("dmestas", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
if err := db.NewAgent("coder", "dmestas", -1); err != nil {
t.Fatal(err)
}
for _, r := range []string{"r1", "r2"} {
if err := db.AddRepo(r, t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
}
if err := db.Grant("coder", "r1", RepoAdmin); err != nil {
t.Fatal(err)
}
if err := db.Grant("dmestas", "r2", RepoAdmin); err != nil {
t.Fatal(err)
}
db.Close()
db, err = Open(path)
if err != nil {
t.Fatal(err)
}
defer db.Close()
if owner, _ := db.OwnerOf("r1"); owner != "" {
t.Errorf("FAIL: the backfill deeded r1 to %q, an agent", owner)
}
if owner, _ := db.OwnerOf("r2"); owner != "dmestas" {
t.Errorf("FAIL: the backfill left r2 with %q, want dmestas", owner)
} else {
t.Log("ok: the backfill adopts a person's repository and leaves an agent's forge-held")
}
}
// Marking refuses an account that operates agents (a chain would put
// them past their operator's operator), holds a deed, or alone owns a
// team, and names what it holds; and it clears the site-admin flag.
func TestMarkingRefusesHoldingsAndClearsTheAdminFlag(t *testing.T) {
db := agentRig(t)
if err := db.AddAccount("bob", "a-long-enough-password", true); err != nil {
t.Fatal(err)
}
if err := db.NewAgent("bobbot", "bob", -1); err != nil {
t.Fatal(err)
}
err := db.SetOperator("bob", "dmestas")
if !IsRefusal(err) || !strings.Contains(err.Error(), "bobbot") {
t.Errorf("FAIL: an operator of agents was marked, or the refusal did not name them: %v", err)
}
if db.Operator("bobbot") != "bob" || db.Operator("bob") != "" {
t.Error("FAIL: a chain formed")
}
if err := db.DeleteAccount("bobbot"); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("pub", "bob"); err != nil {
t.Fatal(err)
}
if err := db.SetOperator("bob", "dmestas"); !IsRefusal(err) || !strings.Contains(err.Error(), "pub") {
t.Errorf("FAIL: a deed holder was marked, or the refusal did not name the repository: %v", err)
}
if err := db.SetOwner("pub", ""); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("crew", "Crew", "", "bob"); err != nil {
t.Fatal(err)
}
if err := db.SetOperator("bob", "dmestas"); !IsRefusal(err) || !strings.Contains(err.Error(), "crew") {
t.Errorf("FAIL: a team's only owner was marked, or the refusal did not name the team: %v", err)
}
if _, err := db.SetTeamMemberGuarded("bob", "crew", "dmestas", "", true, true); err != nil {
t.Fatal(err)
}
if err := db.SetOperator("bob", "dmestas"); err != nil {
t.Fatalf("FAIL: with the holdings moved, marking was still refused: %v", err)
}
if a, _ := db.AccountByName("bob"); a.Admin {
t.Error("FAIL: marking kept the site-admin flag")
} else if db.EffectiveCaps("bob", "").HasAny("s") {
t.Error("FAIL: a newly marked agent still resolves to s")
} else {
t.Log("ok: marking refuses agents, deeds and sole-owned teams by name, and clears the flag")
}
}
// forge is reserved for accounts and teams alike, and is never an
// operator a person's path can name; an agent never gets a password
// by any write; the cap is counted under the lock.
func TestForgeIsReservedAndAgentsNeverGetAPassword(t *testing.T) {
db := agentRig(t)
if err := db.AddAccount("forge", "a-long-enough-password", false); !IsRefusal(err) {
t.Errorf("FAIL: an account called forge: %v", err)
}
if err := db.AddAccount("Forge", "a-long-enough-password", false); !IsRefusal(err) {
t.Errorf("FAIL: an account called Forge: %v", err)
}
if err := db.AddTeam("forge", "Forge", "", "dmestas"); !IsRefusal(err) {
t.Errorf("FAIL: a team called forge: %v", err)
}
if err := db.NewAgent("bot", "forge", -1); !IsRefusal(err) {
t.Errorf("FAIL: NewAgent took forge as operator: %v", err)
}
if err := db.SetPassword("dmestas-coder", "a-long-enough-password"); !IsRefusal(err) {
t.Errorf("FAIL: SetPassword gave an agent a password: %v", err)
}
if err := db.SetPasswordSelf("dmestas-coder", "a-long-enough-password", ""); !IsRefusal(err) {
t.Errorf("FAIL: SetPasswordSelf gave an agent a password: %v", err)
}
if _, ok := db.Authenticate("dmestas-coder", "a-long-enough-password"); ok {
t.Error("FAIL: the agent signed in with a password")
}
if err := db.NewAgent("dmestas-second", "dmestas", 1); !IsRefusal(err) {
t.Errorf("FAIL: the cap of one let a second agent in: %v", err)
}
if err := db.NewAgent("dmestas-second", "dmestas", 2); err != nil {
t.Errorf("FAIL: the cap of two refused the second agent: %v", err)
} else {
t.Log("ok: forge reserved everywhere, no password write reaches an agent, the cap holds under the lock")
}
}