ERS Authorization Architecture - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki

  1. Introduction
  2. Actor identity
  3. Permission checks
  4. Tenant scoping
  5. Audit logging
  6. Correction reasons for sensitive operations
  7. Recent reauthentication
  8. Optional and configurable second-person-approval
  9. Receipt-status enforcement
  10. How engineers should protect a new endpoint
  11. Current transitional headers
  12. Important distinction: authentication vs authorization
  13. Engineering rule of thumb
  14. The Authz UI Button
  15. ERS X authorization headers

0. Introduction

Enterprise Remote Systems uses a backend-enforced authorization architecture designed to protect tenant-scoped business operations, especially sensitive current-account, receipt, payout, correction, and settlement actions.

The architecture has four major goals:

Identify who is performing an operation.
Confirm the actor has permission to perform that operation.
Add extra safeguards for sensitive financial operations.
Create immutable audit records for security and compliance.

1. Actor identity

Every protected backend request must identify an actor.

During the current transitional implementation, actor identity is supplied through request headers, mainly:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Authorized-By identifies the user or system actor attempting the operation.
X-Tenant-ID identifies the tenant context for tenant-scoped operations.

If an endpoint requires authorization and no actor is provided, the backend rejects the request with an authorization error such as:

{
  "error": {
    "code": "missing_actor",
    "message": "Authorization actor is required"
  }
}

This was introduced so sensitive operations are never performed anonymously.

2. Permission checks

Protected operations require specific permissions.

For example, the second-person approval policy endpoints use permissions like:

current_accounts.settings.read
current_accounts.settings.update

Sensitive current-account operations use permissions specific to correction, payout, receipt, or settlement workflows.

During the transitional development phase, permissions may be supplied through a header such as:

X-Actor-Permissions: *

or:

X-Actor-Permissions: current_accounts.settings.read,current_accounts.settings.update

The wildcard permission * is useful for development/admin smoke testing, but it should be treated carefully. Long term, actors should receive permissions through persisted roles/grants, not ad hoc request headers.

The backend authorization flow is conceptually:

Request arrives
β†’ Extract actor
β†’ Extract tenant
β†’ Determine required permission for endpoint/action
β†’ Check actor permission
β†’ Allow or reject
β†’ Write audit event

A request with an identified actor but insufficient permission is rejected with 403 Forbidden.

3. Tenant scoping

ERS is tenant-scoped.

Most business entities belong to a tenant, including:

People
Collaborators
Reference data
Expenses
Ledger entries
Receipts
Journey settlements
Tenant settings
Authorization audit logs

Authorization checks must preserve this boundary.

An actor authorized for one tenant should not automatically operate on another tenant’s data.

The current request tenant is usually resolved from:

X-Tenant-ID: default

Backend handlers and repositories should pass tenant ID into queries and mutations. This is important because duplicate checks, ledger operations, settings, and audit events are tenant-aware.

4. Audit logging

Sensitive authorization decisions are recorded in authz_audit_logs.

Audit events capture information such as:

tenant ID
actor
operation/action
resource
decision
reason
metadata
timestamp

Logs are immutable. That means audit log rows are append-only:

INSERT allowed
UPDATE blocked
DELETE blocked

This is enforced at both levels:

Application/GORM hooks.
SQLite database triggers.

The goal is that once an authorization audit event exists, it cannot be silently modified or deleted by normal application paths.

5. Correction reasons for sensitive operations

Logs include structured correction reasons. Sensitive correction operations must include:

{
  "reasonCode": "DUPLICATE_POSTING",
  "reasonText": "Correcting duplicate expense posting"
}

The backend rejects:

missing reason
blank reason code
blank reason text

Correction reason metadata is persisted where applicable, including ledger/correction metadata and audit metadata.

This applies to sensitive operations such as:

ledger reversal
ledger replacement
manual correction
journey settlement adjustment
receipt backfill/status override
gold zeroing correction
payout correction

The goal is to ensure that every sensitive correction has a structured explanation, not just an untraceable database mutation.

6. Recent reauthentication

The app enforces reauthentication for sensitive operations.

For high-risk operations, the request must include a fresh reauthentication timestamp:

X-Reauthenticated-At: 2026-06-18T15:30:00Z
X-Reauthentication-Method: password

The timestamp must be recent, currently within the configured freshness window used by the backend, such as 15 minutes.

If missing, invalid, stale, or too far in the future, the backend rejects the operation with:

{
  "error": {
    "code": "recent_reauthentication_required",
    "message": "Recent reauthentication is required for this sensitive operation"
  }
}

This protects especially sensitive operations from being executed by a stale session. Examples of operations requiring recent reauthentication include:

ledger reversal
ledger replacement
zero-gold settlement/correction
partial payout correction
close journey settlement
receipt debit-ledger backfill

The current implementation is transitional and header-based. In a future production auth system, this should integrate with the real login/session provider.

7. Optional and configurable second-person approval

The app offers a tenant-scoped configurable optional second-person approval metadata.

A sensitive request may include:

{
  "reasonCode": "DUPLICATE_POSTING",
  "reasonText": "Correcting duplicate expense posting",
  "secondApproval": {
    "approvedBy": "[email protected]",
    "notes": "Reviewed receipt and approved correction"
  }
}

When supplied, the backend validates that:

approvedBy is present
approvedBy is not the same as the primary actor
approval metadata is persisted
approval metadata appears in audit metadata

The policy key is:

current_accounts.require_second_person_approval_for_sensitive_operations

The API endpoints are:

GET /api/v1/current-accounts/settings/second-person-approval
PUT /api/v1/current-accounts/settings/second-person-approval

When the setting is false or missing:

secondApproval omitted        β†’ allowed
valid secondApproval supplied β†’ allowed and audited

When the setting is true:

secondApproval omitted                 β†’ rejected
secondApproval.approvedBy blank        β†’ rejected
secondApproval same as actor           β†’ rejected
valid different secondApproval actor   β†’ allowed and audited

This gives the tenant/application administrator control over whether sensitive operations require dual control.

8. Receipt-status enforcement

Receipt status transitions and required metadata are enforced so the system cannot treat an incomplete receipt as properly returned or finalized.

Examples:

PRINTED receipts require print metadata.
RETURNED receipts require signed/returned metadata.
RETURNED and CANCELLED statuses are terminal.
A second return of the same receipt is rejected.
Blank signed document reference is rejected.

This matters because ERS has a key business rule:

Every debit from a collaborator current account must generate a receipt signed and returned.

Receipt-status enforcement supports that rule at both the application and database level.

9. How engineers should protect a new endpoint

When adding a new backend operation, engineers should ask these questions:

Is the endpoint tenant-scoped? If yes, extract and pass tenant ID through the handler/service/repository path.

tenantID := resolved tenant from request context/header

Do not query or mutate tenant-owned records without tenant filtering.

Does the endpoint require authorization? Most create/update/delete/sensitive operations should require authorization.

The handler should enforce actor extraction and permission checks before doing the mutation.

The permission should be specific enough to describe the operation, for example:

current_accounts.settings.update receipts.backfill ledger_entries.reverse journey_settlements.close

Avoid using broad permissions when a narrow one is appropriate.


Is it a sensitive financial operation?

If yes, it likely needs the following protections:

structured correction reason
recent reauthentication
optional/configurable second-person approval
audit event
receipt lifecycle enforcement, where applicable

A sensitive financial operation should generally not proceed unless:

actor is present
actor is authorized
tenant is known
reasonCode/reasonText are valid
reauthentication is fresh
secondApproval is valid if required
audit event is written
Does it write an audit event?

Sensitive operations should write authorization/audit metadata that includes:

actor
tenant
operation
resource ID
decision
reason code/text
reauthentication metadata
second approval metadata, if supplied

Do not rely on normal application logs for compliance-critical audit trails. Logs are diagnostic; audit events are business/security records.

10. Current transitional headers

For local/dev testing, useful headers include:

Content-Type: application/json
X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *
X-Reauthenticated-At: 2026-06-18T15:30:00Z
X-Reauthentication-Method: password

Example:

BASE_URL="https://dev.enterpriseremotesystems.com"
ACTOR="[email protected]"
TENANT_ID="default"
REAUTH_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"

curl -i \
  -H "Content-Type: application/json" \
  -H "X-Authorized-By: ${ACTOR}" \
  -H "X-Tenant-ID: ${TENANT_ID}" \
  -H "X-Actor-Permissions: *" \
  -H "X-Reauthenticated-At: ${REAUTH_AT}" \
  -H "X-Reauthentication-Method: password" \
  "$BASE_URL/api/v1/healthz"

For non-sensitive endpoints, X-Reauthenticated-At may not be required.

For sensitive endpoints, omit it only when deliberately testing that the backend rejects the request.

11. Important distinction: authentication vs authorization

ERS currently has a transitional, header-based authorization model.

That means:

Authentication = who the user is
Authorization = what the user is allowed to do

The current headers simulate or bridge the authentication layer while backend authorization hardening is being built.

Long term, ERS should replace trusted client-supplied actor/permission headers with a real authenticated session or token. The backend should derive actor identity and permissions from trusted server-side/session claims rather than accepting arbitrary client headers.

The current setup is still valuable because it defines and enforces the backend authorization contract:

actor required
permission required
tenant scope required
audit required
sensitive operation safeguards required

When real authentication is integrated, these same backend authorization checks should remain in place.

12. Engineering rule of thumb

For ERS, engineers should follow this rule:

The frontend may guide the user, but the backend must enforce the rule.

For sensitive operations, never rely only on UI validation.

The backend must reject unsafe requests even if they are sent by curl, a test script, a compromised browser, or a future frontend bug.

13. The Authz UI Button

The Authz button is a development/testing helper for the transitional ERS authorization architecture.

Its purpose is to let you simulate an authorized actor from the UI while ERS is still using header-based authorization instead of a full login/session system.

In practical terms, it helps the frontend attach headers such as:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *

or a more specific permission set.

That matters because after Bite 18 and Bite 19, many backend endpoints no longer allow anonymous requests. They require:

actor identity
tenant context
permissions
sometimes recent reauthentication
sometimes correction reason metadata
sometimes second-person approval metadata

So the Authz button exists to unblock local/dev use of the app before we replace the transitional header model with a real authentication provider.

It is not the final production security model.

The long-term plan should be:

Current temporary model:
Authz button / dev UI state
β†’ frontend sends X-Authorized-By / X-Actor-Permissions headers
β†’ backend enforces authorization

Future production model:
real login/session/token
β†’ backend derives actor and permissions from trusted auth claims
β†’ frontend no longer manually controls actor/permissions

So when explaining it to other engineers:

The Authz button is a temporary developer/operator control used to select or simulate the current authorization actor and permissions so protected backend endpoints can be exercised before full authentication is integrated. The backend still enforces authorization; the button only helps populate the transitional request headers.

14. ERS X authorization headers

ERS currently has a temporary/transitional authorization model. The backend requires authorization data on protected API calls, but we have not yet connected a real login/session provider. So for now, the frontend sends development/testing headers such as:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *

For sensitive operations, the frontend also sends recent reauthentication headers:

X-Reauthenticated-At: <RFC3339 UTC timestamp>
X-Reauthentication-Method: password

This is consistent with the project’s current authorization architecture: the Authz UI button is a development/testing helper that populates transitional authorization headers, not the final production authentication model.

Recommended policy by environment

Environment Who sets the X-* headers? Recommended behavior
Local Frontend dev helper / local fallback The app may auto-send a bootstrap actor so local development is not blocked. You can also use the Authz helper to set a specific test actor.
Development server Authz helper / seeded test actor Use the Authz helper deliberately. Do not rely on silent fallback if you are testing authorization behavior.
Test server Authz helper / seeded test actor Same as development, but more disciplined. Use known test actors and permissions so manual/E2E tests are repeatable.
Production Eventually real authentication/session provider Do not depend on user-controlled X-* headers long term. The backend should derive actor, tenant, permissions, and reauth status from trusted auth/session claims.

Local environment

For local development, the main goal is convenience.

When running:

make local-backend
make local-frontend

the frontend should be able to call protected endpoints without immediately failing on app launch. That is why we added/fixed fallback behavior.

Recommended local fallback:

X-Authorized-By: bootstrap-admin
X-Actor-ID: bootstrap-admin
X-Tenant-ID: default
X-Actor-Permissions: *

Use this for ordinary local development: opening People, Collaborators, Expenses, etc.

For testing a specific actor, use the Authz helper instead and set something like:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *

For sensitive operations, also set:

X-Reauthenticated-At: <current UTC timestamp>
X-Reauthentication-Method: password

The timestamp must be fresh because Bite 19C introduced a 15-minute recent-reauthentication requirement for sensitive operations.

Development environment

For deployed dev, I would avoid invisible magic as much as possible.

Recommended practice:

Use the Authz helper explicitly.
Use seeded test actors.
Use clear permissions.

Example:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *

For permission-specific tests, avoid * and use the real permission string if the backend requires one.

The dev environment should behave like a realistic integration environment, but still allow convenient manual testing.

Test environment

For tst, use an even stricter version of the dev approach.

Recommended practice:

No silent bootstrap assumptions.
Use repeatable seeded actors.
Use repeatable seeded data.
Use the Authz helper or E2E fixtures to set headers.

Example manual test setup:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *
X-Reauthenticated-At: <current UTC timestamp>
X-Reauthentication-Method: password

This is the right place to verify that:

missing actor      β†’ rejected
wrong actor        β†’ rejected
missing permission β†’ rejected
valid actor        β†’ succeeds
stale reauth       β†’ rejected
fresh reauth       β†’ succeeds

Production environment

Production should not rely on browser-controlled X-* headers as the final model.

The long-term production direction should be:

User logs in
β†’ trusted session/token is created
β†’ backend validates the session/token
β†’ backend derives actor, tenant, permissions, and reauth status
β†’ backend enforces authorization

In other words, production should eventually replace this:

X-Authorized-By: [email protected]
X-Tenant-ID: default
X-Actor-Permissions: *

with trusted server-side/session-derived values.

The current headers are acceptable only as a transitional bridge while ERS is still building out the app and authorization architecture.

Practical rule for now

Use this mental model:

Local:
  fallback headers are okay for convenience.

Dev:
  use Authz helper intentionally.

Tst:
  use Authz helper or test automation with seeded actors.

Prod:
  transitional headers are a temporary risk and should be replaced by real authentication/session claims before true go-live.

Why your local app was failing

The error:

Authorization actor is required
code: missing_actor

means the backend received a protected request without a valid actor header. In practice, that means the browser request to:

/api/v1/people

did not include a usable:

X-Authorized-By

or equivalent actor header.

So the correct local fix is not β€œdisable authorization.” The correct fix is:

Make local frontend always send a safe development actor by default,
while still allowing the Authz helper to override it.

That keeps local development convenient without weakening the backend enforcement rule.