ERS Authorization Architecture - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki
- Introduction
- Actor identity
- Permission checks
- Tenant scoping
- Audit logging
- Correction reasons for sensitive operations
- Recent reauthentication
- Optional and configurable second-person-approval
- Receipt-status enforcement
- How engineers should protect a new endpoint
- Current transitional headers
- Important distinction: authentication vs authorization
- Engineering rule of thumb
- The Authz UI Button
- 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.