# Stable actor resolution audit

## Root cause

The old shared bootstrap called Dolibarr `/status`, which proved that a token was accepted but did not identify its owner. `AuthContextService` then looked up a SHA-256 fingerprint of the current token in `api_actor_identities`. A missing row or token rotation therefore produced `ACTOR_MAPPING_REQUIRED` even for a valid user.

The replacement calls `GET /api/index.php/users/info?includepermissions=1`, validates the returned stable user ID, login, and active state, and maps `dolibarr_user_id` through `application_actors`. Raw tokens and token hashes are never persisted or logged by the new resolver.

## Database findings and migration

The inspected local schema has no separate application-user master. Existing identity-shaped values are in `assigned_module.user_id`, K-form/task audit columns, and the legacy token-fingerprint table. A name/login backfill cannot be proven safely from those values.

Apply `009_stable_actor_mapping.up.sql` after migrations 001-008. It:

- preserves the old fingerprint table as `api_actor_identities_legacy`, which is not consulted;
- creates `application_actors` with a unique `dolibarr_user_id`;
- creates `actor_mapping_exceptions` and records unverified legacy IDs without guessing.

Run `009_stable_actor_mapping.report.sql` after migration. Every open exception requires administrator verification. For a verified exceptional mapping, use a database administrator operation such as:

```sql
INSERT INTO application_actors
  (application_user_id, dolibarr_user_id, login, is_active, mapping_source)
VALUES
  (:application_user_id, :verified_dolibarr_user_id, :verified_login, 1, 'administrator');
```

Do not use a token in any value. In this schema, safe just-in-time provisioning uses the stable Dolibarr user ID as both IDs, and only for an active user with a configured UMSB module (or a Dolibarr administrator).

## Protected mutation coverage

`global_function/global2.php` now resolves and authorizes the actor before controller input is processed. Coverage includes:

- K-form and workflow mutations;
- Operations and Operations task mutations;
- Transport Manager and Transport Driver mutations;
- HR, commission, and rate-management mutations;
- fleet, driver, vehicle, permit, supplier, maintenance, tyre, and tyre-history mutations.

Legacy audit inputs (`created_by`, `updated_by`, `remarks_by`, `task_assigner`, `assigned_by`, and the legacy fleet `user_id`) are replaced at their consuming controllers. Business target fields such as `task_assignee`, `driver_id`, and `port_op_user_id` remain request data.

## Readiness and redacted tests

```bash
curl -i \
  -H "DOLAPIKEY: <redacted-valid-token>" \
  https://umsbapiv2.ferwan.com/controller/auth/context.php
```

```bash
curl -i -X POST \
  -H "Content-Type: application/json" \
  -H "DOLAPIKEY: <redacted-valid-token>" \
  -d '{"created_by":999999,"k_form_type":"<test-value>"}' \
  https://umsbapiv2.ferwan.com/controller/k-form/create.php
```

The second request is only a forged-field test template; supply a complete non-production K-form payload. Verify the stored `created_by` equals `data.application_user_id` from the readiness endpoint and is not `999999`.

Expected authentication outcomes:

- missing, invalid, or expired DOLAPIKEY: HTTP 401 JSON;
- valid active identity without an eligible mapping/module: HTTP 403 `ACTOR_MAPPING_REQUIRED` JSON;
- inactive Dolibarr or application actor: HTTP 403 JSON;
- valid mapped/JIT actor with the required module: controller response, with audit actor derived server-side.

Local verification performed: all PHP files passed `php -l`; the foundation suite passed 32/32 checks; `git diff --check` reported no whitespace errors. Database-backed and live HTTP tests remain deployment checks because this checkout has no connected disposable database or production credentials.
