Selecting the owner view makes financial information visible. Does switching back to staff make that information secure? I chose not to describe screen configuration and access control as the same feature.
The current experience changes menus for synthetic records. A new independent module exposes a policy that combines company, assignment and action. It does not reproduce a client’s accounts or authorization rules.
A hidden menu does not prevent access to records
The public experience lets visitors choose staff, coordinator or owner and see a different explanation of useful menus and fields. That helps them imagine a company-specific configuration. It neither changes an authenticated identity nor grants a server permission. The page states that actual access control requires authentication and API enforcement, and contains no sensitive records.
A real service must assume someone can construct a request without using the visible controls. The server first establishes the requesting identity, then checks which company’s records that identity may act on. I separate the interface decision that makes a view understandable from the enforcement decision that permits an operation. Neither implementation substitutes for the other.
An owner label does not have to include every action
An owner who reviews company results may not need to edit order quantities. In this teaching policy, staff can read their assigned records and edit draft quantities. Coordinators can read company records and edit or approve drafts. Owners can read and export company records but cannot edit or approve. This is an explicit example decision, not a claim about how every company should authorize work.
No role receives restore permission by default. Reading a backup and replacing current records have different consequences. A real recovery operator or approval procedure should be designed as a separate authority. Describing permitted actions by job can be more useful than arranging role names in a hierarchy that automatically grants every operation to the highest title.
Check company scope before the role-specific rule
An owner role must not imply access to another company’s records. The new module checks company equality before its action rules and checks assignment for staff. Inactive users and unknown roles or actions are denied by default. A denied result does not include another company’s data or a detailed explanation of which private record exists.
The principal supplied to this function must come from a trusted server context. Passing through a role from the browser would let a caller simply claim to be the owner. Selecting a role in the demo is a synthetic experiment for comparing conditions. Neither the page nor its pure function can be deployed on its own as the security boundary; real identity verification and server integration remain separate requirements.
Even an authorized row needs an explicit response shape
People reading the same order may not need the same financial fields or internal notes. The shared module selects response fields after allowing a read. Its base projection contains identifier, status and quantity, with amount added only for the owner example. It does not copy the whole input object, including fields that may be introduced later.
Editable fields are likewise limited to quantity. An operational server must enforce that list against the actual request body and validate its values. The module does not save edit requests. Bulk export also needs its own correctly scoped query and record checks: permission for one example row must never authorize exporting the entire dataset. Lists, details, mutations and exports all need the same company boundary.
Explore the implementation
Independent module field projection; depends on decideAccess
export function projectOrder(principal,record) {
if(!decideAccess(principal,'read',record).allowed)return null;
if(!Number.isSafeInteger(record.quantity)||record.quantity<1||record.quantity>99)return null;
const result={id:record.id,status:record.status,quantity:record.quantity};
if(principal.role==='owner'){
if(!Number.isSafeInteger(record.amount)||record.amount<0)return null;
result.amount=record.amount;
}
return result;
}Design UI availability and server rejection together
Removing unavailable buttons makes a screen easier to use, but it remains an interface convenience. The server makes the final authorization decision. If a document has already been sent, a stale screen must not retain the authority to edit it merely because the button is still visible. Current state, authorization and the actual write need an appropriate transaction boundary.
This site’s content-management API has a separate authentication boundary. That does not mean the public ERP experience has a multi-role operational backend. I do not combine the existing CMS authentication and this new synthetic policy into a claim of a finished ERP security system. The module isolates decisions and projections so readers can inspect how changing a role affects an explicit rule.
Test the cases that must be denied
A successful request from one ordinary user is not enough. Tests cover another company, another assignee, inactive users, unknown roles and actions, and sent documents. I compared 120 combinations across three roles, five actions, two states, company equality and assignment equality. Separate assertions cover owner amounts, limited staff projections and restore authority denied to every role.
Those results validate the synthetic policy functions. They are not an audit of a real identity service, permission revocation, every API endpoint or production security. For a company implementation, begin by documenting who needs to do what and why. The shared module leaves those choices and their tests visible as a starting point for a different business policy.