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