Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/can.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/can.ts |
| Original SHA-256 | a8a9867a15d85b2dc4fecb8162fdc9c325ccfca519802518897c00b99d8fee8f |
| Git blob SHA | 76a9746655703f336ca544b6818af8693bc73e9d |
| Reading-copy SHA-256 | a8a9867a15d85b2dc4fecb8162fdc9c325ccfca519802518897c00b99d8fee8f |
Recovered source transcript
Download complete source text ↓86 lines · 4.2 KB
import type { SessionPrincipal } from './auth'
// Capability names, mirrored from internal/platform/security. Only the ones the interface actually
// gates on are listed: an unused constant here would imply a permission the UI respects when it does not.
export const CAP = {
// candidateRead gates the bench and the duplicate review, the two shared screens that belong to no tool.
// It is the lowest tier that means anything here: an account holding nothing at all can still sign in, and
// showing it a search over records it cannot read would be a link straight to an empty page.
candidateRead: 'candidate.read',
candidateWrite: 'candidate.write',
requirementEdit: 'requirement.write',
exportRun: 'export.run',
deskReassign: 'desk.reassign',
userInvite: 'user.invite',
userRoleChange: 'user.role.change',
userDisable: 'user.disable',
orgSettings: 'org.settings.write',
auditRead: 'audit.read',
privacyErase: 'privacy.erase',
} as const
// can reports whether the signed in user holds a capability.
//
// Fails closed: no session, or a session from a server that does not send the list, grants nothing. That
// is the safe direction, and it means an older client against a newer server simply offers less rather
// than offering something that will 403.
export function can(principal: SessionPrincipal | null, cap: string): boolean {
return !!principal?.capabilities?.includes(cap)
}
// Tool names, mirrored from internal/platform/security. The vocabulary is closed on both sides.
export const TOOL = {
missionmatch: 'missionmatch',
screenmatch: 'screenmatch',
slatematch: 'slatematch',
recruitmatch: 'recruitmatch',
} as const
// hasTool reports whether the signed in user was granted a product.
//
// Fails closed for the same reason can() does: no session, or a session from a server that does not send
// the list, shows nothing. An older client against a newer server offers less rather than offering a tool
// whose every endpoint will 404.
export function hasTool(principal: SessionPrincipal | null, tool: string): boolean {
return !!principal?.tools?.includes(tool)
}
// orgHasTool reports whether the ORGANIZATION is entitled to a product, which is a different question
// from whether this user holds it. Both are needed to tell the two apart when a control is disabled.
//
// It fails OPEN, treating a missing org_tools list as entitled, and that is deliberate and is the
// opposite of hasTool. An older server does not send the field; failing closed there would claim "your
// organization is not entitled to this product" about every product the tenant actually bought, which is
// a false and alarming statement. The enforcement is the per user grant, which still fails closed, so the
// only thing at stake here is which of two explanations is shown next to a control the server governs.
export function orgHasTool(principal: SessionPrincipal | null, tool: string): boolean {
const list = principal?.org_tools
if (!list) return true
return list.includes(tool)
}
// The two reasons an entitlement control may be disabled, amendment 6 decision 13, NO SILENT GREY. The
// wording mirrors internal/platform/security ToolDisabledReason so the interface and the API say the same
// thing. The organization check comes first because it is the stronger fact: if the tenant never bought
// the product, whether the actor holds it is beside the point.
export const TOOL_DISABLED = {
orgNotEntitled: 'Your organization is not entitled to this product.',
actorLacksTool: 'You do not hold this tool, and you can only grant tools you hold.',
} as const
// toolDisabledReason returns why a GRANT control is disabled, or the empty string when it is usable.
//
// Removal is not disabled by either reason: taking access away is the safe direction, so a tool the actor
// does not hold can still be unticked from someone they outrank. That is why `currentlyOn` is a parameter
// rather than this being a pure function of entitlement.
export function toolDisabledReason(
principal: SessionPrincipal | null,
tool: string,
currentlyOn: boolean,
): string {
if (currentlyOn) return ''
if (!orgHasTool(principal, tool)) return TOOL_DISABLED.orgNotEntitled
if (!hasTool(principal, tool)) return TOOL_DISABLED.actorLacksTool
return ''
}