Skip to main content

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.

CaseCredential and targetExpected result
AllowedValid credential, correct site/app, permitted rowOperation succeeds
Denied actionValid credential, correct scope, disallowed actionDenied according to the approved route policy; no protected data is returned or changed
Row outside policyValid credential, correct table, disallowed rowNo row data is returned or changed
Anonymous requestNo credentialRequest is denied and no record data is returned
Invalid credentialMalformed or expired supplied credential401 Unauthorized
Cross-appValid credential, same table name in another appNo data crosses the app boundary
Cross-siteValid credential sent to another site hostnameNo data crosses the tenant boundary
Unknown resourceCorrect scope, unknown table or recordNo 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:

CaseSiteAppRecordCredential
AllowedPrimary sitePrimary appAllowed taskAllowed identity
Denied rowPrimary sitePrimary appProtected taskAllowed identity
Cross-appPrimary siteSecond appProtected task in that appPrimary-app identity
Cross-siteSecond siteApp with the same slugProtected task in that sitePrimary-site identity
Invalid credentialPrimary sitePrimary appAllowed taskDeliberately 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:

  1. Insert at least one allowed and one denied row.
  2. Query without an application filter.
  3. Add each application filter and sort used in production.
  4. Verify the denied row never appears and is not included in total.
  5. 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.

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: