Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/security/matrix_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/security/matrix_test.go |
| Original SHA-256 | 44a6dbf46c4820440451535b143a33e3aa594c8b7c3e3309e6ef62c9a142161b |
| Git blob SHA | 721b3dcc16b46a5bde2297ecdee9194744227b1c |
| Reading-copy SHA-256 | 44a6dbf46c4820440451535b143a33e3aa594c8b7c3e3309e6ef62c9a142161b |
Recovered source transcript
Download complete source text ↓310 lines · 13.0 KB
package security
import (
"sort"
"testing"
)
// allRoles and allCaps are the closed vocabularies. Adding either without updating the matrix below
// fails the completeness test, which is the point: a new capability that nobody has explicitly assigned
// is a permission decision made by omission.
// Every role, superuser included. Leaving the top tier out of this fixture would make the matrix tests
// pass vacuously for the one role that holds everything, which is the role whose mistakes cost most.
var allRoles = []Role{RoleViewer, RoleMember, RoleRecruiter, RoleManager, RoleAdmin, RoleOwner, RoleSuperuser, RoleOperator}
var allCaps = []Capability{
CapCandidateRead, CapCandidateWrite, CapRequirementEdit, CapSlateEdit, CapSearchRun,
CapExportRun, CapSourcingRead, CapPrivacyErase, CapUserInvite, CapUserRoleChange,
CapUserDisable, CapOrgSettings, CapAuditRead, CapComplianceRead, CapComplianceWrite,
CapStaffingRecommend, CapBrainQuery, CapCurationCurate, CapDeskReassign, CapOpportunityManage,
CapScopeReview, CapRequirementApprove,
}
// wantMatrix is the entire permission model written out by hand.
//
// Every cell is asserted, so widening a role is impossible without editing this table, and a reviewer
// comparing a diff here is comparing the actual authority granted rather than inferring it from a list
// of role names buried in a map literal.
var wantMatrix = map[Role][]Capability{
RoleViewer: {CapCandidateRead, CapSearchRun, CapBrainQuery},
RoleMember: {CapCandidateRead, CapSearchRun, CapBrainQuery},
RoleRecruiter: {
CapCandidateRead, CapSearchRun, CapBrainQuery,
CapCandidateWrite, CapRequirementEdit, CapSlateEdit, CapStaffingRecommend, CapOpportunityManage,
CapScopeReview,
CapExportRun, CapSourcingRead,
},
// Manager is recruiter plus exactly two things: moving work between people, and reading the audit
// trail for the team they are accountable for. Nothing else. If this list ever grows to include
// user or privacy capabilities, the tier has become an admin with a different name.
RoleManager: {
CapCandidateRead, CapSearchRun, CapBrainQuery,
CapCandidateWrite, CapRequirementEdit, CapSlateEdit, CapStaffingRecommend, CapOpportunityManage,
CapScopeReview,
CapExportRun, CapSourcingRead,
CapDeskReassign, CapAuditRead, CapRequirementApprove,
},
RoleAdmin: {
CapCandidateRead, CapSearchRun, CapBrainQuery,
CapCandidateWrite, CapRequirementEdit, CapSlateEdit, CapStaffingRecommend, CapOpportunityManage,
CapScopeReview,
CapExportRun, CapSourcingRead,
CapDeskReassign, CapAuditRead, CapRequirementApprove,
CapPrivacyErase, CapUserInvite, CapUserRoleChange, CapUserDisable,
CapComplianceRead, CapComplianceWrite,
// NOT CapCurationCurate: curating the global ontology is a platform operator action, superuser only.
},
// Owner holds everything EXCEPT global ontology curation, which is platform wide and therefore
// superuser only. Owner is the top of one organization; it is not the top of the platform.
RoleOwner: withoutCap(allCaps, CapCurationCurate),
// Superuser also holds everything. It is written out here rather than skipped so the exhaustive test
// actually covers the top tier: Has resolves it by rule instead of by matrix row, and this row is what
// proves the rule and the intended model agree.
// Superuser holds everything EXCEPT the platform capabilities, because amendment 6 narrowed its by rule
// grant to within its tenant and curation writes a graph shared by every tenant.
RoleSuperuser: withoutCap(allCaps, CapCurationCurate),
// Operator is the platform plane and holds every capability in every tenant, by rule. It is written out
// here rather than skipped so the exhaustive test actually covers the tier whose mistakes cost most.
RoleOperator: allCaps,
}
// TestPermissionMatrixExhaustively asserts every role against every capability.
func TestPermissionMatrixExhaustively(t *testing.T) {
for _, role := range allRoles {
granted := map[Capability]bool{}
for _, c := range wantMatrix[role] {
granted[c] = true
}
for _, cap := range allCaps {
want := granted[cap]
if got := Has(role, cap); got != want {
t.Errorf("Has(%s, %s) = %v, want %v", role, cap, got, want)
}
}
}
}
// TestEveryCapabilityIsAssignedToSomeone. A capability nobody holds is dead weight that reads as a
// working permission, and one nobody remembered to assign is a decision made by omission.
func TestEveryCapabilityIsAssigned(t *testing.T) {
for _, cap := range allCaps {
held := false
for _, role := range allRoles {
if Has(role, cap) {
held = true
break
}
}
if !held {
t.Errorf("capability %s is held by no role", cap)
}
}
// And the declared matrix covers every capability the code defines, so a new capability cannot be
// added without appearing in the test table above.
if len(capabilityRoles) != len(allCaps) {
t.Errorf("the code defines %d capabilities but the test enumerates %d; a new one is untested",
len(capabilityRoles), len(allCaps))
}
}
// TestManagerCannotReachAdminAuthority is the containment property that justifies the tier existing.
func TestManagerCannotReachAdminAuthority(t *testing.T) {
forbidden := []Capability{
CapPrivacyErase, CapUserInvite, CapUserRoleChange, CapUserDisable,
CapOrgSettings, CapComplianceWrite, CapCurationCurate,
}
for _, cap := range forbidden {
if Has(RoleManager, cap) {
t.Errorf("manager holds %s, which makes the tier an admin under another name", cap)
}
}
// And it must actually be MORE than a recruiter, or it buys nothing.
if !Has(RoleManager, CapDeskReassign) {
t.Error("manager cannot reassign a desk, which is the only reason the tier exists")
}
if Has(RoleRecruiter, CapDeskReassign) {
t.Error("recruiter can reassign a desk, so the manager tier grants nothing")
}
}
// TestOrgSettingsIsOwnerOnly. Changing the organization's timezone moves what "today" means for every
// count in the product, so it stays at the top.
func TestOrgSettingsIsOwnerOnly(t *testing.T) {
for _, role := range allRoles {
// The by rule tiers hold every capability, so they are exempt here for the same reason owner is.
if role == RoleOwner || role == RoleSuperuser || role == RoleOperator {
continue
}
if Has(role, CapOrgSettings) {
t.Errorf("%s can change org settings", role)
}
}
}
// TestRankOrderAfterInsertingManager. The tier sits between recruiter and admin, and the ordering is
// what every separation of duties check consults.
func TestRankOrder(t *testing.T) {
order := []Role{RoleViewer, RoleMember, RoleRecruiter, RoleManager, RoleAdmin, RoleOwner}
for i := 0; i+1 < len(order); i++ {
lower, higher := order[i], order[i+1]
if !OutRanks(higher, lower) {
t.Errorf("%s does not outrank %s", higher, lower)
}
if OutRanks(lower, higher) {
t.Errorf("%s outranks %s, the hierarchy is inverted", lower, higher)
}
if !AtLeast(higher, lower) {
t.Errorf("AtLeast(%s, %s) is false", higher, lower)
}
}
// Nobody outranks themselves, which is what stops a peer acting on a peer.
for _, r := range allRoles {
if OutRanks(r, r) {
t.Errorf("%s outranks itself, so a peer could act on a peer", r)
}
}
}
// TestOnlySuperuserOutranksOwner. Owner used to be the ceiling, and nothing could reach it; migration
// 0062 puts exactly one tier above, held by a named few.
//
// The lockout concern that motivated the original rule still holds, and is now answered differently: an
// owner cannot be demoted by an admin or by another owner, so ordinary operation cannot strand the
// organization, while a superuser exists precisely so a stranded organization has a recovery path that
// does not require direct database access.
func TestOnlySuperuserOutranksOwner(t *testing.T) {
for _, r := range allRoles {
// Superuser is the tenant recovery path, and operator is the platform plane that creates tenant top
// seats in the first place (amendment 6: operator manages everything including creating superusers).
// Both legitimately outrank owner; nothing below them may.
if r == RoleSuperuser || r == RoleOperator {
if !OutRanks(r, RoleOwner) {
t.Errorf("%s must outrank owner, or the tier cannot administer one", r)
}
continue
}
if OutRanks(r, RoleOwner) {
t.Errorf("%s outranks owner; only superuser and operator may", r)
}
if CanInvite(r, RoleOwner) {
t.Errorf("%s can invite an owner, which would let authority be created sideways", r)
}
}
}
// TestPrivilegeEscalationIsRefused. Every role attempting to grant every role at or above itself.
func TestPrivilegeEscalationIsRefused(t *testing.T) {
for _, actor := range allRoles {
for _, target := range allRoles {
canInvite := CanInvite(actor, target)
shouldInvite := roleRank[actor] > roleRank[target]
// The single documented exception: superuser may reproduce its own tier, so the ladder can
// survive losing its holders. Every other self grant stays refused, asserted just below.
if actor == RoleSuperuser && target == RoleSuperuser {
shouldInvite = true
}
if canInvite != shouldInvite {
t.Errorf("CanInvite(%s, %s) = %v, want %v", actor, target, canInvite, shouldInvite)
}
// The consequence worth naming: an admin cannot create another admin. Only an owner can.
if actor == RoleAdmin && target == RoleAdmin && canInvite {
t.Error("an admin can create another admin, so admin authority self propagates")
}
}
}
}
// TestUnknownRolesHoldNothing. A role string from a stale token or a hand edited row must fail closed.
func TestUnknownRolesHoldNothing(t *testing.T) {
// "superuser" was a bogus value here until it became a real tier in migration 0062. Case variants stay
// in the list because role comparison is exact: "Admin" must never be treated as admin.
for _, bogus := range []Role{"", "root", "Admin", "OWNER", "superadmin"} {
for _, cap := range allCaps {
if Has(bogus, cap) {
t.Errorf("unknown role %q holds %s", bogus, cap)
}
}
if ValidRole(bogus) {
t.Errorf("unknown role %q passed validation", bogus)
}
if AtLeast(bogus, RoleViewer) {
t.Errorf("unknown role %q is at least a viewer", bogus)
}
if OutRanks(bogus, RoleViewer) {
t.Errorf("unknown role %q outranks a viewer", bogus)
}
}
}
// TestCapabilitiesOfMatchesHas. The list sent to a browser and the checks the server performs come from
// the same matrix, so they cannot disagree about what a role may do.
func TestCapabilitiesOfMatchesHas(t *testing.T) {
for _, role := range allRoles {
got := CapabilitiesOf(role)
if !sort.StringsAreSorted(got) {
t.Errorf("CapabilitiesOf(%s) is not sorted, so the response is not stable", role)
}
set := map[string]bool{}
for _, c := range got {
set[c] = true
}
for _, cap := range allCaps {
if set[string(cap)] != Has(role, cap) {
t.Errorf("CapabilitiesOf(%s) and Has disagree about %s", role, cap)
}
}
}
// An unknown role receives an empty list, never nil, so the JSON is [] and the client can iterate.
if got := CapabilitiesOf("nobody"); got == nil || len(got) != 0 {
t.Errorf("CapabilitiesOf on an unknown role = %v, want an empty slice", got)
}
}
// TestRoleVocabularyMatchesMigration pins the Go roles against the CHECK constraint, now 0078, which
// EXTENDED 0062 with the operator platform role (amendment 6). The two must move together: a role the code
// knows but the constraint refuses fails at the INSERT with a constraint violation rather than a clear
// refusal, and a role the constraint permits but the code does not know ranks nowhere and silently outranks
// nothing. This test caught exactly that drift when operator was added to the code, which is why the
// migration exists.
func TestRoleVocabularyMatchesMigration(t *testing.T) {
inMigration := []string{"operator", "superuser", "owner", "admin", "manager", "recruiter", "member", "viewer"}
if len(roleRank) != len(inMigration) {
t.Fatalf("the code defines %d roles, migration 0078 permits %d", len(roleRank), len(inMigration))
}
for _, r := range inMigration {
if !ValidRole(Role(r)) {
t.Errorf("role %q is in migration 0078 but not valid in code", r)
}
}
}
// withoutCap returns allCaps minus one, so the owner row states its single exclusion rather than
// re listing eighteen capabilities that would then drift from allCaps.
func withoutCap(caps []Capability, drop Capability) []Capability {
out := make([]Capability, 0, len(caps))
for _, c := range caps {
if c != drop {
out = append(out, c)
}
}
return out
}
// TestGlobalOntologyCurationIsOperatorOnly. The skill graph this writes is shared by every tenant, so no
// TENANT role may change what another tenant's candidates score, and that now includes the tenant top seat.
// Amendment 6 moved curation behind the operator platform plane: superuser's holds everything by rule was
// narrowed to within its tenant, and curation is by definition not tenant scoped.
func TestGlobalOntologyCurationIsOperatorOnly(t *testing.T) {
for _, r := range allRoles {
if r == RoleOperator {
continue
}
if Has(r, CapCurationCurate) {
t.Errorf("%s holds curation.curate and can rewrite the ontology every tenant is scored against", r)
}
}
if !Has(RoleOperator, CapCurationCurate) {
t.Error("operator must hold curation.curate, or nobody can curate the ontology at all")
}
}