package security // Role is the tenant role enum already enforced by the users table CHECK constraint. The five 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 ( RoleOwner Role = "owner" RoleAdmin Role = "admin" RoleRecruiter Role = "recruiter" 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" CapAuditRead Capability = "audit.read" CapComplianceRead Capability = "compliance.read" // adverse impact report, AEDT inventory and model registry, retention ) // 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, RoleAdmin: 3, RoleOwner: 4, } // 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, RoleRecruiter, RoleMember, RoleViewer}, CapSearchRun: {RoleOwner, RoleAdmin, RoleRecruiter, RoleMember, RoleViewer}, CapCandidateWrite: {RoleOwner, RoleAdmin, RoleRecruiter}, CapRequirementEdit: {RoleOwner, RoleAdmin, RoleRecruiter}, CapSlateEdit: {RoleOwner, RoleAdmin, RoleRecruiter}, CapExportRun: {RoleOwner, RoleAdmin, RoleRecruiter}, CapSourcingRead: {RoleOwner, RoleAdmin, RoleRecruiter}, CapPrivacyErase: {RoleOwner, RoleAdmin}, CapUserInvite: {RoleOwner, RoleAdmin}, CapUserRoleChange: {RoleOwner, RoleAdmin}, CapUserDisable: {RoleOwner, RoleAdmin}, CapAuditRead: {RoleOwner, RoleAdmin}, CapComplianceRead: {RoleOwner, RoleAdmin}, CapOrgSettings: {RoleOwner}, } // capabilitySet is the matrix indexed for O(1) lookup, built once at init. var capabilitySet = func() map[Role]map[Capability]bool { set := map[Role]map[Capability]bool{ RoleOwner: {}, RoleAdmin: {}, RoleRecruiter: {}, RoleMember: {}, RoleViewer: {}, } for cap, roles := range capabilityRoles { for _, role := range roles { set[role][cap] = true } } return set }() // ValidRole reports whether role is one of the five 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 { 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 } // CanInvite reports whether an actor may invite a user with the given role. The invited role must be // strictly below the actor's own role, so an admin can invite recruiter, member, and viewer but // never another admin or an owner, and owners are created only through provisioning, never an invite. func CanInvite(actor, invited Role) bool { return OutRanks(actor, invited) }