Route Authorization Coverage Audit - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki

Start the local application

Use your normal local stack. Confirm:

curl -i http://localhost:8080/api/v1/healthz
```bash

Expected: 200.

Set up this helper in another terminal:

```bash
API=http://localhost:8080/api/v1

check_status() {
  expected="$1"
  shift

  actual="$(curl -sS \
    -o /tmp/bite27a-response.json \
    -w "%{http_code}" \
    "$@")"

  printf "Expected %s, received %s\n" "$expected" "$actual"

  if [ "$actual" != "$expected" ]; then
    cat /tmp/bite27a-response.json
    echo
    return 1
  fi
}

Confirm health checks remain public

No authorization headers:

check_status 200 "$API/healthz"
check_status 200 http://localhost:8080/healthz

Both must return 200.

Confirm newly protected routes reject a missing actor

Run without authorization headers:

check_status 401 "$API/reference-data/sector"
check_status 401 "$API/tenants/current"
check_status 401 "$API/authz/roles"
check_status 401 "$API/receipts/outstanding"
check_status 401 "$API/current-accounts/settings/second-person-approval"

Each response should contain an error similar to:

{ "error": { "code": "missing_actor" } }

Confirm an actor with the wrong permission receives 403

Use a temporary, non-persisted actor carrying only people.read:

check_status 403 \
  -H "X-Actor-ID: bite27a-wrong-permission" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  "$API/reference-data/sector"

check_status 403 \
  -H "X-Actor-ID: bite27a-wrong-permission" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  "$API/tenants/current"

check_status 403 \
  -H "X-Actor-ID: bite27a-wrong-permission" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  "$API/authz/roles"

check_status 403 \
  -H "X-Actor-ID: bite27a-wrong-permission" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  "$API/receipts/outstanding"

check_status 403 \
  -H "X-Actor-ID: bite27a-wrong-permission" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  "$API/current-accounts/settings/second-person-approval"

Each must return 403 with error code forbidden.

Confirm each narrow read permission succeeds

check_status 200 \
  -H "X-Actor-ID: bite27a-reference-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: reference_data.read" \
  "$API/reference-data/sector"

check_status 200 \
  -H "X-Actor-ID: bite27a-tenant-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: tenants.read" \
  "$API/tenants/current"

check_status 200 \
  -H "X-Actor-ID: bite27a-authz-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: authz.read" \
  "$API/authz/roles"

check_status 200 \
  -H "X-Actor-ID: bite27a-receipt-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: ledger.receipts.read" \
  "$API/receipts/outstanding"

check_status 200 \
  -H "X-Actor-ID: bite27a-settings-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: current_accounts.settings.read" \
  "$API/current-accounts/settings/second-person-approval"

This verifies both the route-level guards and the handler-owned authorization marker.

Test Reference Data write separation

First verify that read permission cannot create Reference Data:

check_status 403 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-reference-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: reference_data.read" \
  -d '{
    "code": "BITE27A_DENIED",
    "label": "Bite 27A denied test",
    "sortOrder": 9999
  }' \
  "$API/reference-data/sector"

Now create a temporary item with reference_data.manage:

REF_CODE="BITE27A_$(date +%s)"

check_status 201 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-reference-manager" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: reference_data.manage" \
  -d "{
    \"code\": \"$REF_CODE\",
    \"label\": \"Bite 27A manual authorization test\",
    \"description\": \"Temporary local manual test record\",
    \"sortOrder\": 9999
  }" \
  "$API/reference-data/sector"

REF_ID="$(python3 -c \
  'import json; print(json.load(open("/tmp/bite27a-response.json"))["data"]["id"])')"

echo "$REF_ID"

Update it:

check_status 200 \
  -X PUT \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-reference-manager" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: reference_data.manage" \
  -d "{
    \"code\": \"$REF_CODE\",
    \"label\": \"Bite 27A updated authorization test\",
    \"description\": \"Updated during manual testing\",
    \"sortOrder\": 9999
  }" \
  "$API/reference-data/sector/$REF_ID"

Deactivate, reactivate, and finally leave it inactive:

check_status 200
-X PATCH
-H "X-Actor-ID: bite27a-reference-manager"
-H "X-Tenant-ID: default"
-H "X-Actor-Permissions: reference_data.manage"
"$API/reference-data/sector/$REF_ID/deactivate"

check_status 200
-X PATCH
-H "X-Actor-ID: bite27a-reference-manager"
-H "X-Tenant-ID: default"
-H "X-Actor-Permissions: reference_data.manage"
"$API/reference-data/sector/$REF_ID/reactivate"

check_status 200
-X PATCH
-H "X-Actor-ID: bite27a-reference-manager"
-H "X-Tenant-ID: default"
-H "X-Actor-Permissions: reference_data.manage"
"$API/reference-data/sector/$REF_ID/deactivate"


# Test Authz read/manage separation

An authz.read actor may list roles:

```bash
check_status 200 \
  -H "X-Actor-ID: bite27a-authz-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: authz.read" \
  "$API/authz/roles"

But it may not create actors:

check_status 403 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-authz-reader" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: authz.read" \
  -d '{}' \
  "$API/authz/actors"

An authz.manage actor should pass the authorization guard. The empty request must then fail normal request validation with 400, not 403:

check_status 400 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-authz-manager" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: authz.manage" \
  -d '{}' \
  "$API/authz/actors"

This creates no actor.

Confirm sensitive handler authorization remains active and audited

Use a denied actor against a settlement operation. The dummy collaborator ID prevents any real financial mutation:

check_status 403 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Actor-ID: bite27a-sensitive-denied" \
  -H "X-Tenant-ID: default" \
  -H "X-Actor-Permissions: people.read" \
  -d '{}' \
  "$API/collaborators/00000000-0000-0000-0000-000000000000/zero-gold"

Then open:

http://localhost:5173/admin/audit-logs

Confirm a denied authorization event exists for:

Actor: bite27a-sensitive-denied
Operation: current_accounts.zero_gold
Decision: denied
Permission: journey.settlements.zero_gold

This verifies that authorizationHandledByHandler() did not accidentally make the route public and that denied sensitive operations still reach the audit layer.

Browser smoke tests as bootstrap-admin

Open these pages and verify they load without authorization errors:

http://localhost:5173/admin/reference-data
http://localhost:5173/admin/authorization
http://localhost:5173/admin/current-account-settings
http://localhost:5173/receipts/outstanding

Also:

  • Open a Collaborator.
  • Open their Current Account.
  • Confirm balances and ledger entries load.
  • Open a printable receipt, when one exists.
  • Confirm the Authz page lists:
  • reference_data.read
  • reference_data.manage