Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/security/rbac.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/security/rbac.go |
| Original SHA-256 | db902528dbd028aa39d980230a304a07ca8d5d60691c35662e635234b5d49e72 |
| Git blob SHA | fff129bcdcb0b59a97fbd7fbbc4b7ffe443b4977 |
| Reading-copy SHA-256 | db902528dbd028aa39d980230a304a07ca8d5d60691c35662e635234b5d49e72 |
Recovered source transcript
Download complete source text ↓287 lines · 15.3 KB
package security
import "sort"
// Role is the tenant role enum already enforced by the users table CHECK constraint. The six roles
// form an ordered hierarchy for the RequireRole comparison, but capabilities are assigned explicitly
// per role below, not implied by rank alone.
type Role string
const (
// RoleOperator is the PLATFORM PLANE, above and outside the tenant ladder (amendment 6 decision 1). It
// holds every platform capability and every tenant capability in every tenant, every action is audited,
// and MFA is mandatory for it from the day the role exists. Superuser is consequently demoted in meaning
// to the tenant top seat, the client's own seat, and its holds everything by rule now reads within its
// tenant. Adding this value EXTENDS the ladder; no existing value is renamed or removed.
RoleOperator Role = "operator"
// RoleSuperuser sits above owner and is the only role permitted to create another of its own kind.
// It exists so the top of the ladder can survive its holders: owner cannot mint an owner, by the same
// separation of duties rule that stops an admin minting an admin, which means without a tier above it
// an organization that loses its owners cannot recover without direct database access. Superuser is
// that tier, held by a named few, and every other rule about it is ordinary.
RoleSuperuser Role = "superuser"
RoleOwner Role = "owner"
RoleAdmin Role = "admin"
RoleRecruiter Role = "recruiter"
// RoleManager is a recruiting team lead: they see the whole team's book and move work between
// people, and they hold none of the account or privacy authority an admin does.
RoleManager Role = "manager"
RoleMember Role = "member"
RoleViewer Role = "viewer"
)
// Capability is a fine grained permission checked at the API. Handlers gate on a Capability rather
// than a role so the policy lives in one table and the UI never has to hide a control to enforce
// access.
type Capability string
const (
CapCandidateRead Capability = "candidate.read"
CapCandidateWrite Capability = "candidate.write"
CapRequirementEdit Capability = "requirement.write"
CapSlateEdit Capability = "slate.write"
CapSearchRun Capability = "search.run"
CapExportRun Capability = "export.run"
CapSourcingRead Capability = "sourcing.read" // see sourced candidate contact fields and the sourcing log
CapPrivacyErase Capability = "privacy.erase" // hard delete a sourced candidate and their PII
CapUserInvite Capability = "user.invite"
CapUserRoleChange Capability = "user.role.change"
CapUserDisable Capability = "user.disable"
CapOrgSettings Capability = "org.settings.write"
// CapDeskReassign moves an account or a job order between recruiters. It is the reason the manager
// tier exists: reassigning a desk is the most common thing a team lead does, and before this it
// required admin, which also carries PII deletion.
CapDeskReassign Capability = "desk.reassign"
CapAuditRead Capability = "audit.read"
CapComplianceRead Capability = "compliance.read" // adverse impact report, AEDT inventory and model registry, retention reads
CapComplianceWrite Capability = "compliance.write" // register an AEDT tool, run retention: governance writes
CapStaffingRecommend Capability = "staffing.recommend" // generate an AI staffing recommendation run; this WRITES recommendation rows, so it is a write capability, not a read
CapBrainQuery Capability = "brain.query" // ask the AI brain a natural language question (read only augmented generation over the tenant's own data)
CapCurationCurate Capability = "curation.curate" // approve a teaching curation proposal and verify the staged ontology edge, the two step WS8 human in the loop gate; a governance write held at the compliance tier
// CapOpportunityManage gates the tenant opportunity capture layer: creating and updating a tracked
// opportunity, uploading its solicitation documents, and moving it through the pursuit pipeline. It is a
// recruiting workflow write, so it sits at the same recruiter and above tier as slate.write and
// requirement.write, not at the admin tier.
CapOpportunityManage Capability = "opportunity.manage"
// CapScopeReview gates the scope review queue: accepting, editing, rejecting, splitting, and merging the
// scope items extraction proposed. It is review work, so it sits at recruiter and above with the other
// recruiting writes.
CapScopeReview Capability = "scope.review"
// CapRequirementApprove gates the ONE transition that lets anything a machine derived become visible to
// the scoring engine. It is deliberately held HIGHER than the review capabilities, at manager and above,
// because approving a requirement is the AEDT decision point: the approver's name is recorded on the row
// and the schema refuses an approval that names nobody.
CapRequirementApprove Capability = "requirement.approve"
)
// roleRank orders the hierarchy for AtLeast and for bounding invited roles. Higher is more
// privileged.
var roleRank = map[Role]int{
RoleViewer: 0,
RoleMember: 1,
RoleRecruiter: 2,
RoleManager: 3,
RoleAdmin: 4,
RoleOwner: 5,
RoleSuperuser: 6,
RoleOperator: 7, // the platform plane, above the tenant ladder
}
// capabilityRoles is the authoritative permission matrix. owner holds every capability. admin manages
// recruiter, member, and viewer accounts and runs the recruiting workflow, but cannot change org
// settings or manage other admins and owners (those are owner only and are further guarded by the
// separation of duties checks in the auth service). The two privacy capabilities gate the sourced
// candidate contact fields, the sourcing log, and erasure: sourcing.read goes to the roles that
// actually source (owner, admin, recruiter) and the destructive privacy.erase is held tighter
// (owner, admin).
var capabilityRoles = map[Capability][]Role{
CapCandidateRead: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter, RoleMember, RoleViewer},
CapSearchRun: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter, RoleMember, RoleViewer},
CapBrainQuery: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter, RoleMember, RoleViewer}, // same tier as search.run: a read only question over the tenant's own data
CapCandidateWrite: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter},
CapRequirementEdit: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter},
CapSlateEdit: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter},
CapStaffingRecommend: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter}, // same roles that hold slate.write: generating staffing recommendations is the same authority tier
CapOpportunityManage: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter}, // recruiter and above: capturing and pursuing an opportunity is a recruiting workflow write
CapScopeReview: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter}, // recruiter and above: reviewing extracted scope is review work
// Approving a requirement is the AEDT decision point, so it is held one tier above reviewing the scope
// that produced it. A recruiter may shape the scope; a manager decides it becomes live.
CapRequirementApprove: {RoleOwner, RoleAdmin, RoleManager},
CapExportRun: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter},
CapSourcingRead: {RoleOwner, RoleAdmin, RoleManager, RoleRecruiter},
// A team lead moves work between people. This is the whole reason the tier exists, and it is
// deliberately the ONLY authority manager holds that recruiter does not, besides reading the audit
// trail for the team they are accountable for.
CapDeskReassign: {RoleOwner, RoleAdmin, RoleManager},
CapAuditRead: {RoleOwner, RoleAdmin, RoleManager},
// Everything below stays at admin and above. A manager who could erase a candidate or change an
// account's role would be an admin with a different name, and the tier would buy nothing.
CapPrivacyErase: {RoleOwner, RoleAdmin},
CapUserInvite: {RoleOwner, RoleAdmin},
CapUserRoleChange: {RoleOwner, RoleAdmin},
CapUserDisable: {RoleOwner, RoleAdmin},
CapComplianceRead: {RoleOwner, RoleAdmin},
CapComplianceWrite: {RoleOwner, RoleAdmin},
// Curation writes the GLOBAL skill ontology, which every tenant's scoring reads. It is therefore a
// platform operator action, not a tenant administrator one, and superuser is the only tier that is
// platform wide.
//
// It used to be {RoleOwner, RoleAdmin}, held by the owner and admin of EVERY tenant, while
// curation_proposals carries no organization_id and the approve and verify path reads proposals by
// bare primary key on the BYPASSRLS migrator pool. Any tenant's admin could therefore approve a
// proposal derived from another tenant's data, and a second admin IN THE SAME TENANT could verify it,
// since the separation of duties check only compares two user ids and never requires them to be in
// different organizations. The result is a live skill_relationships edge that alters match scores for
// everyone. Where tenants are competing primes bidding the same solicitations, that is a cross tenant
// integrity primitive, not a permissions nicety.
// Amendment 6 moved the platform surfaces, including Curation, behind the operator plane. They answer
// 404 to everyone else, tenant superusers included.
CapCurationCurate: {RoleOperator},
CapOrgSettings: {RoleOwner},
}
// platformCapabilities are the capabilities that are NOT tenant scoped: they act across every tenant at
// once. They are held by operator alone, and they are the exact set that superuser's within its tenant by
// rule grant must exclude. Amendment 6 section 2 item 2.
var platformCapabilities = map[Capability]bool{
CapCurationCurate: true,
}
// capabilitySet is the matrix indexed for O(1) lookup, built once at init.
var capabilitySet = func() map[Role]map[Capability]bool {
// Built from roleRank rather than from a second hand written role list. The literal that used to be
// here omitted superuser, so the first capability assigned to that role wrote to a nil map and PANICKED
// AT INIT, taking the binary down at boot rather than failing a request. Deriving the keys from the
// rank table means a role added there can never be missing here.
set := make(map[Role]map[Capability]bool, len(roleRank))
for role := range roleRank {
set[role] = map[Capability]bool{}
}
for cap, roles := range capabilityRoles {
for _, role := range roles {
set[role][cap] = true
}
}
return set
}()
// ValidRole reports whether role is one of the six known roles.
func ValidRole(role Role) bool {
_, ok := roleRank[role]
return ok
}
// Has reports whether role holds the capability.
func Has(role Role, cap Capability) bool {
// Superuser holds every capability, resolved here rather than by adding RoleSuperuser to all
// nineteen rows of the matrix. Enumerating it would mean a capability added later silently omits the
// tier that is supposed to hold everything, and the omission would present as an unexplained refusal
// for the one account expected never to be refused. Owner remains explicitly enumerated, so the
// matrix still reads as the authority on every tier that is not the top.
// Operator is the platform plane and holds every capability everywhere, resolved by rule so a capability
// added later cannot silently omit the tier that is supposed to hold everything.
if role == RoleOperator {
return cap != ""
}
// Superuser holds everything by rule too, but amendment 6 narrowed that rule to WITHIN ITS TENANT. A
// PLATFORM capability is not tenant scoped by definition, so the tenant top seat does not hold it: the
// skill ontology curation writes a graph every tenant is scored against, and a client's own top seat
// changing what a competitor's candidates score is a cross tenant integrity problem, not a permission
// nicety. This is the same reasoning that previously made curation superuser only rather than admin
// only, applied one tier further now that a real platform plane exists to hold it.
if role == RoleSuperuser {
return cap != "" && !platformCapabilities[cap]
}
caps, ok := capabilitySet[role]
if !ok {
return false
}
return caps[cap]
}
// AtLeast reports whether role is at or above min in the hierarchy. Unknown roles are never at least
// anything.
func AtLeast(role, min Role) bool {
r, ok := roleRank[role]
if !ok {
return false
}
m, ok := roleRank[min]
if !ok {
return false
}
return r >= m
}
// OutRanks reports whether actor is strictly more privileged than other. It is the core separation of
// duties predicate: a user may only invite, role change, disable, or reset the MFA of someone they
// outrank, never a peer or a superior, and never themselves.
func OutRanks(actor, other Role) bool {
a, ok := roleRank[actor]
if !ok {
return false
}
o, ok := roleRank[other]
if !ok {
return false
}
return a > o
}
// CanGrantRole reports whether an actor may put another account into the given role, whether by invite
// or by changing an existing user's role. Both paths MUST use this, or the two disagree and the weaker
// one becomes the way around the stronger.
//
// The rule is: strictly below your own, with exactly one exception.
//
// The exception is superuser granting superuser. Every other tier is deliberately unable to reproduce
// itself, which is what stops an admin quietly building a bench of admins. Applied to the top of the
// ladder that same rule is a trap rather than a control: if no role can create a superuser, the tier can
// only ever shrink, and an organization that loses its last holder is locked out of its own access
// control with no in application recovery. Someone has to be able to mint the top tier, and the least
// dangerous answer is that only the top tier can. It is a small, named group granting to each other, it
// is audited like any other role change, and an actor still cannot change their OWN role, so no single
// superuser can act alone on their own account.
func CanGrantRole(actor, granted Role) bool {
if actor == RoleSuperuser && granted == RoleSuperuser {
return true
}
return OutRanks(actor, granted)
}
// CanInvite reports whether an actor may invite a user with the given role. It defers to CanGrantRole so
// an invitation can never confer a role that a direct role change would refuse.
func CanInvite(actor, invited Role) bool { return CanGrantRole(actor, invited) }
// CapabilitiesOf returns every capability a role holds, sorted, for sending to a client.
//
// Derived from the same matrix Has consults, so the list a browser receives and the checks the server
// performs cannot disagree. Sorted so the response is stable and diffable, and so a test can compare it
// without ordering noise.
func CapabilitiesOf(role Role) []string {
// The by rule tiers report their vocabulary rather than a matrix row they have none of. Derived from
// capabilityRoles, the same source Has consults, so the two cannot drift. Superuser's list excludes the
// platform capabilities for the same reason Has does: its rule is within its tenant.
if role == RoleOperator || role == RoleSuperuser {
out := make([]string, 0, len(capabilityRoles))
for c := range capabilityRoles {
if role == RoleSuperuser && platformCapabilities[c] {
continue
}
out = append(out, string(c))
}
sort.Strings(out)
return out
}
caps, ok := capabilitySet[role]
if !ok {
return []string{}
}
out := make([]string, 0, len(caps))
for c := range caps {
out = append(out, string(c))
}
sort.Strings(out)
return out
}