Repository navigation
Route Authorization Coverage Audit
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
}No authorization headers:
check_status 200 "$API/healthz"
check_status 200 http://localhost:8080/healthzBoth must return 200.
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" } }
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.
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.
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.
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_goldThis verifies that authorizationHandledByHandler() did not accidentally make the route public and that denied sensitive operations still reach the audit layer.
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