What We’re Testing
ACL rules can be modified and deleted through two interfaces:
-
Dashboard (
https://login.quickztna.com/acls) — the Edit button opens a form pre-filled with the rule’s current values. The Delete button (trash icon) removes the rule after confirmation. Both operations use the generic CRUD endpoint. -
REST API —
PATCH /api/db/acl_rulesfor updates,DELETE /api/db/acl_rulesfor removal.
Key implementation details from the source code:
- Editable columns (from
db-crud.tsWRITABLE_COLUMNS):name,source,destination,ports,protocol,action,priority,enabled,updated_at,created_by,expires_at,time_start,time_end - PATCH filtering: The CRUD handler uses
_filtersin the body for WHERE conditions (URL query params are ignored for PATCH). The dashboard uses.eq("org_id", orgId).eq("id", ruleId).update({...})which translates to body filters. - DELETE requirements: Requires
Content-Type: application/jsonand a body (even if empty{}). The handler parses the body for non-GET methods. - Cache invalidation: When
acl_rulesare modified, the keyacl-rules:{org_id}is invalidated (seeCACHE_INVALIDATION_MAPindb-crud.ts). The evaluation engine caches rules for 300 seconds, but CRUD writes force immediate invalidation. - Audit logging: The dashboard logs
acl_rule.updated,acl_rule.deleted, andacl_rule.reorderedevents via thelogAudit()client function.
Write operations on acl_rules require admin or owner role (ADMIN_WRITE_TABLES).
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | Browser + curl for edit/delete operations |
| 🐧 Linux-C | Verification via ztna acl list and ztna acl test |
Prerequisite: At least two ACL rules should exist (create them per Chapter 26 or 27 instructions).
ST1 — Edit Rule via API (Change Ports and Action)
What it verifies: An existing rule can be updated via the CRUD PATCH endpoint, and the changes take effect in the evaluation engine.
Steps:
- First, list rules to get the ID of the rule to edit:
curl -s "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID" `
-H "Authorization: Bearer YOUR_TOKEN" | python -m json.tool
Pick a rule ID (e.g., an allow rule on port 443).
- Update the rule to change ports from
443to80,443,8080and name:
curl -s -X PATCH "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"_filters": {
"org_id": "YOUR_ORG_ID",
"id": "RULE_ID"
},
"ports": "80,443,8080",
"name": "Allow-Web-Extended"
}'
Important: PATCH uses _filters in the body for the WHERE clause. URL query params alone are not sufficient for PATCH.
Expected response:
{
"success": true,
"data": { "changes": 1 }
}
- Verify the update:
curl -s "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" | python -m json.tool
Expected: The rule now has "ports": "80,443,8080" and "name": "Allow-Web-Extended".
- Verify the evaluation reflects the change. Test port 8080 (newly added):
curl -s -X POST "https://login.quickztna.com/api/acl-evaluate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"org_id": "YOUR_ORG_ID",
"source_machine_id": "LINUX_C_MACHINE_ID",
"destination_machine_id": "SOME_DEST_ID",
"port": 8080,
"protocol": "tcp"
}'
Expected: allowed: true, matched rule name is "Allow-Web-Extended".
- Change the action from allow to deny:
curl -s -X PATCH "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"_filters": {
"org_id": "YOUR_ORG_ID",
"id": "RULE_ID"
},
"action": "deny"
}'
Re-evaluate port 8080:
Expected: allowed: false, matched rule shows "action": "deny".
Pass: PATCH updates the specified fields. The evaluation engine immediately reflects the changes due to cache invalidation. Both field values (ports, action) update correctly.
Fail / Common issues:
changes: 0— the_filtersdid not match any row. Verify theorg_idand ruleidare correct.- Evaluation still shows old values — the ACL cache (300s TTL) should be invalidated on CRUD write. If not, wait up to 5 minutes or restart the API container.
ST2 — Edit Rule via Dashboard
What it verifies: The dashboard edit dialog pre-fills current values and saves changes correctly.
Steps:
-
On ⊞ Win-A , open
https://login.quickztna.com/acls. -
Find a rule in the list. Click the Edit button (pencil icon) on the rule card.
-
The edit dialog opens with fields pre-filled:
- Name, Source, Destination, Protocol, Ports, Action should match the current rule values.
-
Change the Source field from
*totag:engineering. -
Change the Ports field from
80,443,8080to443. -
Click Save.
Expected behavior:
- Success toast: “Rule updated”
- The rule card updates immediately:
- Source now shows
tag:engineering - Ports now shows
443 - All other fields unchanged
- Source now shows
- An audit log entry is created with action
acl_rule.updated
-
Verify by reloading the page — the changes persist.
-
Test the toggle (enable/disable): Click the switch toggle on the rule card.
Expected:
- The rule becomes disabled (grayed out, “Disabled” badge appears)
- The evaluation engine skips disabled rules (the SQL query filters
WHERE enabled = true)
Pass: Edit dialog shows correct pre-filled values. Saving updates the rule. Toggle switches enabled/disabled state. Audit log entry is written.
Fail / Common issues:
- Edit button not visible — only admin and owner roles see the edit/delete buttons. Members see a read-only view.
- “Failed” toast on save — check browser Network tab for the PATCH request response. Common cause: trying to set a column not in the writable whitelist.
ST3 — Delete Rule via API
What it verifies: A rule can be deleted via the CRUD DELETE endpoint, and the deletion takes effect immediately in the evaluation engine.
Steps:
- Create a temporary rule to delete:
curl -s -X POST "https://login.quickztna.com/api/db/acl_rules" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"org_id": "YOUR_ORG_ID",
"name": "Temp-Delete-Test",
"source": "*",
"destination": "*",
"ports": "9999",
"protocol": "tcp",
"action": "allow",
"priority": 500
}'
Note the rule ID from the response (data.data.id).
- Verify the rule exists:
ztna acl list
Should show Temp-Delete-Test in the output.
- Delete the rule:
curl -s -X DELETE "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{}'
Important: The DELETE request MUST include Content-Type: application/json and a body (even an empty {}). The handler parses the body for non-GET methods.
Expected response:
{
"success": true,
"data": { "changes": 1 }
}
- Verify deletion:
ztna acl list
The Temp-Delete-Test rule should no longer appear.
- Evaluate port 9999 (previously allowed by the deleted rule):
curl -s -X POST "https://login.quickztna.com/api/acl-evaluate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"org_id": "YOUR_ORG_ID",
"source_machine_id": "LINUX_C_MACHINE_ID",
"destination_machine_id": "SOME_DEST_ID",
"port": 9999,
"protocol": "tcp"
}'
Expected: allowed: false (the rule is gone, so default deny applies, assuming no other rule matches port 9999).
Pass: DELETE returns changes: 1. The rule disappears from ztna acl list. The evaluation engine no longer matches the deleted rule.
Fail / Common issues:
changes: 0— incorrect rule ID or org_id in the query params.- 400 or parse error — missing the empty JSON body
{}. DELETE requires it. - Rule still appears — the cache may not have been invalidated. The
CACHE_INVALIDATION_MAPforacl_rulesshould clearacl-rules:{org_id}.
ST4 — Verify Traffic Behavior Changes After Edit/Delete
What it verifies: Editing or deleting a rule causes an immediate change in traffic evaluation results (within the cache invalidation window).
Steps:
- Start with a known state. Create an allow rule:
curl -s -X POST "https://login.quickztna.com/api/db/acl_rules" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"org_id": "YOUR_ORG_ID",
"name": "Behavior-Test",
"source": "*",
"destination": "*",
"ports": "5000",
"protocol": "tcp",
"action": "allow",
"priority": 75
}'
- Evaluate — should be allowed:
ztna acl test --src Linux-C --dst Win-A --port 5000 --proto tcp
Expected: ALLOWED: Linux-C -> Win-A:5000/tcp
- Edit the rule to change action to
deny:
curl -s -X PATCH "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"_filters": { "org_id": "YOUR_ORG_ID", "id": "RULE_ID" },
"action": "deny"
}'
- Re-evaluate immediately:
ztna acl test --src Linux-C --dst Win-A --port 5000 --proto tcp
Expected: DENIED: Linux-C -> Win-A:5000/tcp — the change is immediate because CRUD writes invalidate the ACL cache.
- Delete the rule entirely:
curl -s -X DELETE "https://login.quickztna.com/api/db/acl_rules?org_id=YOUR_ORG_ID&id=eq.RULE_ID" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{}'
- Re-evaluate:
ztna acl test --src Linux-C --dst Win-A --port 5000 --proto tcp
Expected: DENIED: Linux-C -> Win-A:5000/tcp — still denied, but now by default deny (no matching rule) rather than an explicit deny rule.
- Verify via the API that the matched_rule is now
null(default deny) rather than the deleted rule:
curl -s -X POST "https://login.quickztna.com/api/acl-evaluate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"org_id": "YOUR_ORG_ID",
"source_machine_id": "LINUX_C_MACHINE_ID",
"destination_machine_id": "WIN_A_MACHINE_ID",
"port": 5000,
"protocol": "tcp"
}'
Expected: matched_rule: null (no rule matched, default deny).
Pass: Allow-to-deny edit changes the evaluation result immediately. Deletion removes the rule from evaluation. Cache invalidation works correctly.
Fail / Common issues:
- Results unchanged after edit — the cache key
acl-rules:{org_id}may not be invalidated. Check thatCACHE_INVALIDATION_MAPindb-crud.tsincludes theacl_rulesentry. - Results change after a delay (up to 300 seconds) — cache invalidation failed but the TTL expired. This indicates a Valkey connectivity issue.
ST5 — Verify Audit Log Entries
What it verifies: Creating, editing, and deleting ACL rules generates audit log entries visible in the audit log.
Steps:
-
On ⊞ Win-A , open
https://login.quickztna.com/audit-login the dashboard. -
Look for recent entries with these action types:
acl_rule.created— from rule creation in previous testsacl_rule.updated— from edits (including toggle and reorder)acl_rule.deleted— from deletionsacl_rule.reordered— from priority reorder (arrow buttons on dashboard)
-
Each audit entry should include:
- Action: the event type (e.g.,
acl_rule.updated) - Resource type:
acl_rule - Resource ID: the rule ID or name
- Timestamp: when the action occurred
- Details: metadata such as the changed fields
- Action: the event type (e.g.,
-
Verify a reorder audit entry. On the ACLs page, click the up/down arrow buttons to reorder two rules. Then check the audit log for
acl_rule.reorderedwith{ "direction": "up" }or{ "direction": "down" }. -
Verify audit entries via the API:
curl -s "https://login.quickztna.com/api/db/audit_logs?org_id=YOUR_ORG_ID&resource_type=eq.acl_rule" `
-H "Authorization: Bearer YOUR_TOKEN" | python -m json.tool
Expected: At least one entry for each action type from this test session.
Note: Audit logs in QuickZTNA are stored in Loki (not PostgreSQL). The dashboard’s audit log page reads from Loki via the services/loki.ts service. The logAudit() function in the frontend sends audit events to the backend, which forwards them to Loki.
Pass: All ACL CRUD operations (create, update, delete, reorder, toggle) produce corresponding audit log entries with correct action types, resource IDs, and timestamps.
Fail / Common issues:
- No audit entries visible — the
logAudit()call is client-side (fire-and-forget). If the audit API endpoint is down or Loki is unreachable, entries may be lost. Check the backend logs for Loki connection errors. - Entries from API-only operations (curl) may not appear because
logAudit()is called from the frontend JavaScript, not the backend handler. Only dashboard operations generate frontend audit logs.
Summary
| Sub-test | What it proves |
|---|---|
| ST1 | Rules can be updated via API PATCH with _filters in the body |
| ST2 | Dashboard edit dialog pre-fills values correctly and saves changes |
| ST3 | Rules can be deleted via API DELETE (requires empty JSON body) |
| ST4 | Traffic evaluation results change immediately after edit/delete (cache invalidation works) |
| ST5 | All ACL CRUD operations generate audit log entries in Loki |