Test Database access policies
Test access with the same site, app, table, action, and rows your application uses. Authentication alone does not prove that a caller can act on a Database resource.
Prepare test data
In a development environment, create:
- two sites with different hostnames;
- two apps in the primary site;
- the same logical table name in both apps;
- an app with the same slug and logical table name in the second site;
- an allowed identity with an app-scoped test role in the primary app only;
- a denied-action identity whose test role omits the action under test;
- no matching app membership for either identity in the cross-app or cross-site target;
- a row policy that separates the allowed rows from the protected rows by an attribute your policy already uses;
- one row allowed to the test caller; and
- one row that must remain outside the caller's policy.
Use synthetic records whose identifiers make the expected scope clear. Do not run authorization tests against customer data. Before each route case, record the app-role assignment, table-policy revision, and row attributes that produce the expected allow or deny result.
Run the route matrix
Repeat this matrix for every read, create, update, and delete route the application uses.
| Case | Credential and target | Expected result |
|---|---|---|
| Allowed | Valid credential, correct site/app, permitted row | Operation succeeds |
| Denied action | Valid credential, correct scope, disallowed action | Denied according to the approved route policy; no protected data is returned or changed |
| Row outside policy | Valid credential, correct table, disallowed row | No row data is returned or changed |
| Anonymous request | No credential | Request is denied and no record data is returned |
| Invalid credential | Malformed or expired supplied credential | 401 Unauthorized |
| Cross-app | Valid credential, same table name in another app | No data crosses the app boundary |
| Cross-site | Valid credential sent to another site hostname | No data crosses the tenant boundary |
| Unknown resource | Correct scope, unknown table or record | No resource data is disclosed |
Assert the response body as well as the status. A denial test fails if it returns a record identifier, relationship payload, SQL detail, or policy diagnostic that the caller should not see.
Use this acceptance rule for every denied case: the response can follow the
service's 401, 403, or resource-hiding 404 policy, but it must never
contain or modify the protected record. Record the expected status for each
application route so later changes are deliberate.
Run a repeatable read probe
Run the same request once for every row in the matrix. Set the CASE_
variables to the site, app, record, and credential for that case:
curl --silent --show-error \
--output "${CASE_NAME}.json" \
--write-out "${CASE_NAME} %{http_code}\n" \
--request GET \
"${CASE_SITE_URL}/api/apps/${CASE_APP_SLUG}/datatables/tasks/data/${CASE_TASK_ID}/" \
--header "Authorization: Api-Key ${CASE_API_KEY}"
Use these substitutions:
| Case | Site | App | Record | Credential |
|---|---|---|---|---|
| Allowed | Primary site | Primary app | Allowed task | Allowed identity |
| Denied row | Primary site | Primary app | Protected task | Allowed identity |
| Cross-app | Primary site | Second app | Protected task in that app | Primary-app identity |
| Cross-site | Second site | App with the same slug | Protected task in that site | Primary-site identity |
| Invalid credential | Primary site | Primary app | Allowed task | Deliberately invalid test value |
For the anonymous case, run the same command without the Authorization
header. Store the response body only in the isolated test workspace, verify
that it contains no protected fields, then remove the captured files after
the test run.
Verify filtered collections
For collection reads:
- Insert at least one allowed and one denied row.
- Query without an application filter.
- Add each application filter and sort used in production.
- Verify the denied row never appears and is not included in
total. - Repeat across page boundaries with stable ordering.
Authorization filters and application filters must narrow the result together. A malformed or rejected application filter must not broaden access.
Verify every mutation action
Test create, update, and delete independently. A caller allowed to read a
row may still be denied permission to change it.
For updates and deletes, attempt both an allowed ID and a denied ID. Verify the denied record is unchanged after the request.
Verify per-record action hints
Request allowed_actions=update,delete for a page containing rows with
different permissions. Check that each row has an _allowed_actions array
containing only the actions permitted for that caller and row.
Action hints help a UI choose which controls to show. They do not authorize a later write, so submit the mutation normally and verify its independent authorization result.
Extend the matrix to related data
Add a permitted and a protected row to every related table. Run the matrix for the root read and for each explicit related-table read. Verify the root and every related resource separately.
Use the explicit two-query pattern in Relationships for application workflows covered by this documentation.
Record the results
For each case, record the route, the kind of credential, the policy version, the test record IDs, and the expected and actual results. Re-run the tests when a route, policy, provider, SDK, site, or app changes.
Delete the synthetic records and all three test tables after the matrix passes: the two tables in the primary site's apps and the same-slug table in the second site. Confirm cleanup from each app and site so a reused logical table name cannot hide a scope mistake.
Continue with: