svcrole: a run role that only an exact set reaches, never Admin (PRO-6214)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+101
−4
internal/svcrole/svcrole.go+33 −4internal/svcrole/svcrole_test.go+68 −0
diff --git a/internal/svcrole/svcrole.go b/internal/svcrole/svcrole.go| index 9c0820a95..a6f4da0b7 100644 |
| --- a/internal/svcrole/svcrole.go |
| +++ b/internal/svcrole/svcrole.go |
| @@ -55,14 +55,22 @@ const ( |
| // TestInstanceRoleMatchesTodaysAppTokenReach pins Instance to exactly the |
| // doors it reaches today, and this role exists so that pin can hold. |
| Service Role = "service" |
| - // Admin is the union of the five above plus ledgers, store and place. |
| - // It is NOT "everything harmless": store grant mints a path-scoped IAM |
| - // user, which is a real handover. |
| + // Run is a credential that starts `carlos run` runs: one-off commands on |
| + // the platform's own hosts. It is deliberately outside the union Admin |
| + // stands for. Allow never adds it to a set, and nothing that lists |
| + // Admin reaches it; a door admits it only by being registered through |
| + // Exactly (spec 2026-10-24-service-runs-design.md § 1). |
| + Run Role = "run" |
| + // Admin is the union of every role except Run, plus ledgers, store and |
| + // place. It is NOT "everything harmless": store grant mints a |
| + // path-scoped IAM user, which is a real handover. Run is reachable only |
| + // through Exactly, so an Admin principal cannot start a run by being |
| + // Admin. |
| Admin Role = "admin" |
| ) |
| // catalogue is the whole vocabulary, in the order All returns it. |
| -var catalogue = []Role{Publish, Deploy, Instance, Operate, Service, Admin} |
| +var catalogue = []Role{Publish, Deploy, Instance, Operate, Service, Run, Admin} |
| // All returns the catalogue. The slice is copied so a caller cannot edit the |
| // vocabulary by writing through it. |
| @@ -127,6 +135,27 @@ func Allow(roles ...Role) Set { |
| return s |
| } |
| +// Exactly builds the Set for a door that admits ONLY the roles listed. Unlike |
| +// Allow it does not add Admin: this is how a role outside Admin's union (Run) |
| +// is granted to a door, and how a door says "no administrator either". |
| +// |
| +// Like Allow, it panics on an unparseable role, for the same reason: a |
| +// dropped role would be a door silently narrowed. Called with no arguments it |
| +// returns the empty, closed Set. |
| +func Exactly(roles ...Role) Set { |
| + if len(roles) == 0 { |
| + return Set{} |
| + } |
| + s := Set{roles: make(map[Role]bool, len(roles))} |
| + for _, r := range roles { |
| + if _, ok := Parse(string(r)); !ok { |
| + panic("svcrole: Exactly called with an unknown role " + strconv.Quote(string(r))) |
| + } |
| + s.roles[r] = true |
| + } |
| + return s |
| +} |
| + |
| // Permits reports whether a principal holding r may pass this door. |
| func (s Set) Permits(r Role) bool { |
| if s.roles == nil { |
diff --git a/internal/svcrole/svcrole_test.go b/internal/svcrole/svcrole_test.go| index a5775141f..180566e06 100644 |
| --- a/internal/svcrole/svcrole_test.go |
| +++ b/internal/svcrole/svcrole_test.go |
| @@ -32,6 +32,74 @@ func TestAdminPermitsEveryNonEmptySet(t *testing.T) { |
| t.Errorf("Allow(%v).Permits(Admin) = false; want true", s.Roles()) |
| } |
| } |
| + // Admin is the union of every role EXCEPT Run, so an Allow set never |
| + // carries Run on Admin's account either. |
| + if Allow(Publish).Permits(Run) { |
| + t.Error("Allow(Publish).Permits(Run) = true; want false") |
| + } |
| + // An Exactly set admits only its listed roles, Admin included. |
| + for _, s := range []Set{Exactly(Run), Exactly(Run, Publish), Exactly(Publish)} { |
| + for _, r := range All() { |
| + listed := false |
| + for _, l := range s.Roles() { |
| + listed = listed || l == r |
| + } |
| + if s.Permits(r) != listed { |
| + t.Errorf("Exactly set %v: Permits(%q) = %v; want %v", s.Roles(), r, s.Permits(r), listed) |
| + } |
| + } |
| + } |
| +} |
| + |
| +func TestRunIsInTheCatalogue(t *testing.T) { |
| + got, ok := Parse("run") |
| + if !ok || got != Run { |
| + t.Fatalf(`Parse("run") = %q, %v; want %q, true`, got, ok, Run) |
| + } |
| + found := false |
| + for _, r := range All() { |
| + found = found || r == Run |
| + } |
| + if !found { |
| + t.Errorf("All() = %v; want it to contain run", All()) |
| + } |
| +} |
| + |
| +func TestExactlyDoesNotAddAdmin(t *testing.T) { |
| + s := Exactly(Run) |
| + if !s.Permits(Run) { |
| + t.Error("Exactly(Run).Permits(Run) = false; want true") |
| + } |
| + for _, r := range All() { |
| + if r != Run && s.Permits(r) { |
| + t.Errorf("Exactly(Run).Permits(%q) = true; want false", r) |
| + } |
| + } |
| + if s.Permits(Admin) { |
| + t.Error("Exactly(Run).Permits(Admin) = true; want false") |
| + } |
| + if got := s.Roles(); !reflect.DeepEqual(got, []Role{Run}) { |
| + t.Errorf("Exactly(Run).Roles() = %v; want [run]", got) |
| + } |
| +} |
| + |
| +func TestExactlyPanicsOnAnUnknownRole(t *testing.T) { |
| + for _, bad := range []Role{"", "rnu", "Run"} { |
| + func() { |
| + defer func() { |
| + if recover() == nil { |
| + t.Errorf("Exactly(%q) did not panic", bad) |
| + } |
| + }() |
| + Exactly(bad) |
| + }() |
| + } |
| + defer func() { |
| + if recover() == nil { |
| + t.Error("Exactly(Run, invalid) did not panic") |
| + } |
| + }() |
| + Exactly(Run, Role("rnu")) |
| } |
| func TestAllowIsExactForNonAdmin(t *testing.T) { |