Define the data boundary before writing tests

Create a compact matrix with four columns:

Actor Data scope Allowed actions Explicitly denied actions
North user North records Read and update assigned records Read, update, or export South records
South user South records Read and update assigned records Read, update, or export North records
Scoped operator Documented North and South support scope Read or repair under an attributable procedure Any action outside that procedure
Unauthenticated request None None Every company-data action

Replace these example scopes with the application's actual departments, customers, projects, practices, or permission groups. List reads, updates, exports, invitations, deletions, and administrative operations separately.

The result should say where each authorization decision is made. If the answer is “the React component hides the action,” stop. The interface can adapt to a user's capabilities, but the trusted backend must decide whether the request is allowed.

Enforce authorization on every request

The OWASP Authorization Cheat Sheet recommends least privilege, deny-by-default behavior, permission validation on every request, and authorization tests. Apply those principles to the application's own data model rather than assuming a framework or platform label supplies the correct rules.

For a record request, the trusted code should resolve:

  1. the authenticated actor;
  2. the requested action;
  3. the authoritative record and its scope;
  4. the policy that connects actor, action, and scope; and
  5. the allowed or denied outcome before returning or changing data.

Do not fetch the record, send it to the browser, and then decide whether to display it. Do not trust a scope identifier supplied by the browser without checking it against the authenticated actor and authoritative record.

If the selected platform generates client and server code, inspect the final enforcement location. Lovable's current security guidance also distinguishes frontend behavior from server-side and database controls; the exact setup still needs to be verified in the chosen project.

Test direct object references

After the allowed path works, keep Alice signed in and replace a North record identifier with a South record identifier. Send the request directly rather than relying on available interface controls.

Repeat that test for:

  • read by identifier;
  • update or status transition;
  • download or attachment access;
  • bulk export and report filters;
  • invitation or membership changes; and
  • any endpoint that accepts a customer, project, or organization identifier.

The OWASP IDOR guidance explains why complex identifiers do not replace object-level authorization. An unguessable ID can reduce casual discovery while a copied link, log entry, browser history, export, or support message still reveals a valid identifier.

For simple record reads, consider returning the same bounded response for an unknown record and a record the actor may not access. Do not adopt that pattern blindly: incident logging and legitimate user feedback still need enough detail for responsible operation without revealing another scope's existence.

Run the two-scope fixture

The checked-in P22 fixture uses synthetic data only:

  • Alice belongs to North;
  • Bob belongs to South;
  • Olivia is a scoped operator;
  • each organization has one document; and
  • the service calls an authorization function before read, update, and export.

Eight tests passed on Node.js 22.18.0 with no failures. They show:

  1. Alice can read the North document.
  2. Alice receives no South document content from a direct-ID request.
  3. A forbidden ID and an unknown ID return the same bounded read response.
  4. A denied South update preserves the record.
  5. A denied South export returns no partial records.
  6. An unauthenticated actor is rejected.
  7. Olivia's allowed cross-scope read is attributed in the fixture audit record.
  8. The denial assertion fails when an intentionally permissive authorization function replaces the policy.

The last case matters because a test that stays green after the authorization check is removed is weak evidence. Introduce a safe mutation in a disposable fixture and confirm the denial test detects it.

Record denied behavior without leaking data

An operator needs to see attempted access and investigate failures, but the evidence should not copy the protected record into an error or test log. Record the actor, requested action, protected-object reference, policy outcome, environment, and attributable time where appropriate.

Avoid credentials, session tokens, full document contents, and unnecessary personal information. Test the logging path itself so a denied request does not become a secondary disclosure.

Understand what the fixture does not prove

The P22 result is an in-memory policy demonstration. It does not establish:

  • secure sessions or token validation;
  • correct database row-level security;
  • isolation across multiple processes or caches;
  • safe streaming of a large export;
  • durable or tamper-resistant audit history;
  • complete coverage of every route and background job; or
  • security or compliance for a real application.

A production review must enumerate the application's actual endpoints, jobs, storage paths, files, integrations, and administrative operations. Use the fixture as a repeatable pattern, not a certificate.

Verification checklist

  • Every protected object has an authoritative scope.
  • Every actor and role has explicit allowed and denied actions.
  • The trusted server or database checks each request.
  • Direct-ID read, update, download, and export cases are tested.
  • Separate synthetic identities exercise both sides of the boundary.
  • Denied actions preserve data and return no partial result.
  • A removed authorization rule makes the denial test fail.
  • Operator exceptions are narrow and attributable.
  • Logs preserve useful evidence without copying protected content.

Frequently asked questions

Is login enough for an internal app?

No. Authentication identifies an actor. Authorization decides which records and actions that actor may access after signing in.

Can I enforce the rule by hiding links and buttons?

Use hidden or disabled controls to improve the interface, but enforce the same decision at the trusted backend for every protected operation.

Does this fixture prevent every data leak?

No. It demonstrates a small authorization boundary with synthetic data. Other failure paths—including credentials, logs, exports, backups, integrations, and runtime configuration—need their own review and evidence.