Skip to main content

Authorization and Segregation of Duties

Merlon authenticates interactive users with JWT sessions and non-interactive clients with API keys. Both paths apply the same role model. Deployments must map these roles to their own access-control and change-management policy.

The first administrator​

A new deployment has no accounts, so no login credential exists. POST /api/v1/setup, reached in the browser at /setup, creates the sole initial Admin account and nothing else.

  • It is reachable without authentication, because at that moment there is no credential that could authenticate anyone.
  • It succeeds exactly once. Once any user row exists it returns 409, so it cannot be replayed to mint a second administrator later.
  • The password minimum is 12 characters.
  • The login screen links to it, since a first-run operator would otherwise have to know the URL. The link is unconditional: probing the server for whether any account exists would disclose that to anyone who can reach the login page.

After setup, an Admin manages local accounts through User management or the /api/v1/admin/users API:

ActionEndpoint
List accountsGET /api/v1/admin/users
Create an accountPOST /api/v1/admin/users
Change role or active statePATCH /api/v1/admin/users/{id}
Set a replacement passwordPOST /api/v1/admin/users/{id}/reset-password
Force all sessions to endPOST /api/v1/admin/users/{id}/revoke-sessions

Passwords must contain at least 12 characters. Role and active-state changes, password resets, and explicit session revocation end every session that was active for the affected account. A later login starts a new session under the current role. An inactive account cannot log in. The last active Admin cannot be disabled or demoted.

Account mutations and their audit entry commit together. Audit details include authority changes but never passwords, password hashes, tokens, or refresh- session family identifiers.

The consequence for deployment is that the window between first start and first administrator is the one moment where an unauthenticated caller can create a privileged account. Do not expose a fresh instance to an untrusted network before completing setup.

MERLON_AUTH_ENABLED=false disables authentication entirely. It exists for the demo topology and for local development. It must never be set in production; see Configuration Reference.

Session lifecycle​

Each successful login starts an independent refresh-token family and issues an access token with both a token identifier and that family identifier. Refresh rotates the refresh token within the same family. Reuse of a rotated token revokes the family as a compromise signal.

Logout revokes the current access token and refresh-token family. Other concurrent sessions for the same user remain active, and an immediate new login starts a different family that is not affected by the logout. A user can have up to five active families; starting another session evicts the oldest one. In PostgreSQL deployments, each authenticated request verifies that the persisted refresh-token family remains active. Logout and forced revocation are therefore visible across API replicas and survive an API process restart. Access tokens issued by an older build without a session-family identifier are rejected after the upgrade; affected users must log in again. Each refresh-token family also records the role that started the session. If an administrator changes or disables that user, existing access tokens and refresh tokens are rejected; the user must log in again under the new authority. Refresh and logout are state-changing cookie-authenticated requests. Clients must echo the csrf_token cookie in the X-CSRF-Token header. A missing or mismatched token is rejected with 403 before session state changes. If persistent server-side revocation cannot be confirmed, the response is an error rather than a successful logout; browser cookies are still cleared and the failed attempt is audited. A failure in the process-local revocation cache does not override a revocation already committed to PostgreSQL.

An Admin can call POST /api/v1/admin/users/{id}/revoke-sessions to invalidate all currently active families for one user. This is the user-wide path for an intentional forced logout. It does not deny a later successful login. Login success, refresh, logout, current-session revocation, and user-wide revocation are recorded as distinct audit actions; token values and refresh-family identifiers are not written to the audit log.

Roles​

PermissionAdminAnalystViewer
Request a whitelist entry (whitelist:request)YesYesNo
Approve a whitelist entry (whitelist:approve)YesNoNo
Read audit records (audit:read)YesNoNo
Manage local users (user:manage)YesNoNo
Create, update, import, activate, or deactivate rules (rule:write)YesNoNo
Confirm a bulk run covering a large target population (batch:execute:large)YesNoNo
Re-score a customer (cdd:score)YesYesNo
Approve a proposed risk-tier override (cdd:override:approve)YesNoNo

This table is the whole grant model. GET /api/v1/system/capabilities derives its answers from it rather than from a second, separately maintained matrix, so a permission added here reaches the operator interface without further wiring.

Capability discovery​

GET /api/v1/system/capabilities reports, for each administrative or permission-gated function, whether it is available, not_configured, forbidden, unsupported, degraded or unavailable, which permission it needs, whether it is offered as a screen or only as an endpoint, and where it is documented. GET /api/v1/auth/me carries the same session's auth_mode, roles and permissions.

The interface uses this to hide or explain a control. It is not authorization: every capability named there is enforced again on the route that performs the action. A control that is hidden by the client is still refused by the server.

auth_mode distinguishes three deployments:

auth_modeMeaning
sessionUsers log in; API keys also work
api_key_onlyAPI keys authenticate, but no JWT signing key is configured, so no user can log in
disabledMERLON_AUTH_ENABLED=false: no role reaches any request

With authentication disabled every capability whose dependency is configured reports available, because no role exists that could refuse one. A function this deployment never configured still reports not_configured — an absent permission and an absent feature are different facts (ADR-0024).

Functions supported through the API only​

Not every backend capability has a screen, and an intentional omission is recorded rather than left to be discovered:

CapabilityEndpointsWhy there is no screen
retention.manageGET/PUT /api/v1/admin/retention-policiesRetention periods are set once per deployment by an administrator and are audited; see Retention and purge
accounts.managePOST /api/v1/accounts, GET /api/v1/accounts/{id}, POST/GET /api/v1/accounts/{id}/customersAccount linkage is established at ingestion by the adapter layer, not by an analyst; see Adapter guide

The whitelist workflow enforces that a requester cannot approve their own request. This provides a separate first-line request and second-line approval control for whitelist decisions.

Rule activation and deactivation enforce the same separation in the application, inside the repository transaction rather than as a pre-check (ADR-0014). The exact target version is locked, and the request is rejected with 403 when the approver and the creator of that version are the same identity, or when either identity is missing — the control fails closed rather than guessing. Rules and rule versions created through the API are always inactive, so activation is always a second, separately attributable act.

Each state change writes a rule_activation_events row in the same transaction, recording the target version, creator, approver, requested state, and timestamp. The serving role holds only SELECT and INSERT on that ledger, so the approval evidence cannot be altered after the fact.

For authentication design and credential guidance, see Configuration Reference.