package domain import "testing" // Invariants of the policy table itself. No database: these catch the mistakes // that would otherwise only show up as a missing 403 in an integration test, or // not at all. // Every resource must say who may reach it. A resource added to the schema and // left out of policies.go is unreachable — which is the safe direction, and // still a mistake worth failing on rather than discovering in production. func TestEveryResourceHasAPolicy(t *testing.T) { for _, r := range AllResources { if r.Policy == nil { t.Errorf("resource %q (%s) has no policy: it permits nothing, which is safe but almost certainly unintended", r.Name, r.Path) } } } // A nil policy denies everything. This is the property the test above relies on // being true, so it is asserted rather than assumed. func TestNilPolicyDeniesEverything(t *testing.T) { var p *Policy for _, op := range []Op{OpList, OpGet, OpCreate, OpUpdate, OpDelete} { for _, role := range []Role{RoleAdmin, RoleEmployer, RoleTalent} { if p.Allows(op, role) { t.Errorf("a nil policy allowed op %d for %s", op, role) } } } if got := p.ScopeFor(RoleTalent); got.Kind != ScopeNone { t.Error("a nil policy returned a scope") } } // A policy must not grant an operation the resource does not expose. Such a // grant is dead — no route is registered — but it reads as permission and would // become real the moment the operation is added. func TestPolicyGrantsNothingWithoutARoute(t *testing.T) { ops := []struct { op Op name string }{ {OpList, "List"}, {OpGet, "Get"}, {OpCreate, "Create"}, {OpUpdate, "Update"}, {OpDelete, "Delete"}, } for _, r := range AllResources { if r.Policy == nil { continue } for _, o := range ops { granted := len(r.Policy.rolesFor(o.op)) > 0 if granted && !r.Supports(o.op) { t.Errorf("%s: policy grants %s but the resource has no such route", r.Path, o.name) } } } } // An unrecognised role authorizes nothing, whatever the policy says. func TestUnknownRoleIsDenied(t *testing.T) { if _, ok := ParseRole("superuser"); ok { t.Fatal("ParseRole accepted a role outside the users_role_check constraint") } if _, ok := ParseRole(""); ok { t.Fatal("ParseRole accepted an empty role") } for _, r := range AllResources { if r.Policy.Allows(OpList, Role("superuser")) { t.Errorf("%s allows an unknown role", r.Path) } } // The three real ones parse. for _, want := range []Role{RoleAdmin, RoleEmployer, RoleTalent} { if got, ok := ParseRole(string(want)); !ok || got != want { t.Errorf("ParseRole(%q) = %q, %v", want, got, ok) } } } // Every column the server derives must also be ReadOnly, or a request body // could still set it on a path the derivation does not cover. func TestDerivedColumnsAreReadOnlyOrTalentScoped(t *testing.T) { for _, r := range AllResources { if r.Policy == nil { continue } for _, d := range r.Policy.Derived { col, ok := r.Column(d.Column) if !ok { t.Errorf("%s: derives %q, which is not a column", r.Path, d.Column) continue } // A TalentOnly derivation intentionally leaves the column writable // for operators — an admin filing a candidate's application must be // able to say whose it is. The unconditional ones must be sealed. if !d.TalentOnly && !col.ReadOnly { t.Errorf("%s.%s is derived unconditionally but is not ReadOnly: a request body could still set it", r.Path, d.Column) } } } } // The six columns Phase 3D closed. Named explicitly, so that regenerating the // descriptors without the SERVER_OWNED map in gen_resources.py fails loudly // rather than silently reopening the holes. func TestServerOwnedColumnsAreReadOnly(t *testing.T) { sealed := map[string][]string{ "worker-profiles": {"user_id"}, "user-activity": {"user_id", "user_email", "user_name", "account_type"}, "job-postings": {"created_by"}, } for path, cols := range sealed { res, ok := ResourceByPath[path] if !ok { t.Fatalf("resource %s is missing", path) } for _, name := range cols { col, ok := res.Column(name) if !ok { t.Errorf("%s has no column %s", path, name) continue } if !col.ReadOnly { t.Errorf("%s.%s is not ReadOnly — a client could supply it", path, name) } } } // And org_id everywhere, which predates Phase 3D and must stay that way. for _, r := range AllResources { if col, ok := r.Column("org_id"); ok && !col.ReadOnly { t.Errorf("%s.org_id is not ReadOnly", r.Path) } } } // Talent is the only scoped role. If a scope ever applied to an operator the // admin console would start losing rows, which is a failure mode worth pinning. func TestOnlyTalentIsRowScoped(t *testing.T) { for _, r := range AllResources { for _, role := range []Role{RoleAdmin, RoleEmployer} { if got := r.Policy.ScopeFor(role); got.Kind != ScopeNone { t.Errorf("%s scopes rows for %s: operators see the whole organization", r.Path, role) } } } } // Every talent scope must name a column the resource actually has. func TestTalentScopesNameRealColumns(t *testing.T) { for _, r := range AllResources { scope := r.Policy.ScopeFor(RoleTalent) if scope.Kind == ScopeNone { continue } if scope.Column == "" { t.Errorf("%s has a talent scope with no column", r.Path) continue } if _, ok := r.Column(scope.Column); !ok { t.Errorf("%s scopes on %q, which is not one of its columns", r.Path, scope.Column) } } } // Talent must not reach an operator resource by having a scope but no grant, // or a grant but no scope where one is required. This pins the shape of the // contract: wherever talent may list a resource that also holds other people's // rows, a scope must narrow it. func TestTalentGrantsHaveScopesWhereRowsAreShared(t *testing.T) { // Resources whose rows are the organization's rather than any one person's: // a talent grant here is deliberate and needs no ownership predicate. shared := map[string]bool{ "courses": true, "learning-paths": true, "role-categories": true, "certifications": true, } for _, r := range AllResources { if !r.Policy.Allows(OpList, RoleTalent) { continue } if shared[r.Path] { continue } if r.Policy.ScopeFor(RoleTalent).Kind == ScopeNone { t.Errorf("%s: talent may list it but no ownership scope narrows the rows", r.Path) } } }