Approval workflow
Roles, connection grants, and the lifecycle of a query request from submission through approval to masked, audited results.
Updated:
Everything in QueryProxy revolves around one object: the query request. This guide walks through who can do what, and what happens to a request at each step.
Roles
| Role | Can do |
|---|---|
| Admin | Everything, system-wide: teams, users, all settings. Set per user (is_admin). |
| DBA | Manage connections and masking rules, grant access, approve/reject requests; can also submit queries on any of the team’s connections. |
| Developer | Submit queries through granted connections, view own (masked) results. |
| Auditor | Read-only access to the audit log (and its CSV export) and to the team’s Requests list and request detail (SQL, status, requester, reviewer, approval trace, DDL flag); cannot view or download result rows, and has no access to connections, approvals, masking or ChatOps settings. |
Admin is global; DBA, Developer and Auditor are per team. A user can belong to several teams with different roles. Connections, requests, masking rules and audit logs never cross team boundaries.
The audit log is reserved to the Auditor and the Admin; a DBA has no access to it.
Suspending and deleting users
An Admin can suspend a user on the Users screen. A suspended user cannot sign
in, open sessions end, and their Slack and Teams identities stop working; their
pending requests stay and are decided under the normal rules. Resume restores
access. Deleting a user is a soft delete with the same effect, and it also
cancels their pending requests (audited as request.cancelled with actor
system); approved and queued requests are left alone. Request history is kept:
the foreign keys refuse a hard delete, and a deleted user’s e-mail address stays
reserved. Nobody can suspend or delete their own account.
Connections and grants
DBAs register target databases under Connections. Credentials (host, port, database, username, password) are encrypted at rest with AES-256. The password is never shown again after saving — the edit form leaves it blank, and leaving it blank keeps the stored one — and credentials never appear in logs or error messages either.
A developer sees a connection only after a DBA grants it to them (Connections → Grants). Ungranted connections are invisible in Query Studio.
The request lifecycle
pending ──▶ approved ──▶ queued ──▶ running ──▶ completed
│ │
├──▶ rejected (reason required) └──▶ failed
└──▶ cancelled (by the requester or an admin)
- Submit. The developer writes SQL in Query Studio. The SQL guards validate it before submission — requests that violate a guard never enter the queue.
- Pending. The request waits indefinitely until someone decides; there is
no auto-expiry. Team DBAs are notified in the portal and, if configured, in
Slack / Teams. After
QUERYPROXY_PENDING_REMIND_AFTERhours (default4;0turns it off) the team’s DBAs and admins get one reminder, and the chat card is posted again with a fresh action token. The requester may add an optional note when submitting; DBAs see it with the request. - Sensitive-filter badge. When a request filters (
WHERE,JOIN,HAVING,GROUP BY,ORDER BY) by a column that the team masks with a column rule, the approval screen and the request detail show a badge with the column names. A masked column can still leak a value by inference when the query tests candidates, so look at it before you approve. It shows column names only. It does not resolve column aliases or derived tables, ignores regex and validator rules, and requests submitted before the upgrade carry no badge. - Approve / Reject. Any team DBA can decide — except on their own requests: self-approval is blocked. Admins may override that block, and the override is flagged in the audit log. Rejection requires a written reason. An override needs a written reason (up to 1000 characters) on every channel; it is stored in the audit log. Deciding from chat follows the same rules, and each Approve / Reject control there carries a single-use token bound to that one request and valid for 24 hours, so a decision taken in chat can only answer a message QueryProxy actually posted, and only once. The resolved approver must also be a member of the integration’s team.
- Execute. Approved requests are queued and picked up by the worker. Reads
stream through a database cursor into a result file, with
masking applied to every row before it is written.
Writes report affected row counts; multi-statement requests run atomically
and roll back entirely if any statement fails. Reads run in a read-only session
on the target database with a statement and a lock timeout (see
Configuration). At most
QUERYPROXY_CONNECTION_SLOTSrequests (default2) run on one connection at a time; the others stayqueueduntil a slot frees up. - Stop. The requester, the team’s DBAs and administrators can press Stop
query on a
runningrequest whose database session is known (not on SQLite). The database cancels the statement and rolls back what it can; changes that were already committed stay. The request becomescancelledand the audit log recordsexecution.cancelled. SQL Server needs extra privileges for this; see Configuration. - Results. The requester (and the team’s DBAs) can browse results page-by-page or download them as CSV. Auditors see request metadata, history and approval traces — not the result data itself. Result files are pruned automatically after the configured retention period.
Notifications
Requesters are notified when their request is decided and when execution finishes; DBAs are notified of new submissions. Notifications appear in the portal’s bell menu. New submissions and decisions are also announced in your chat channel when it is configured; the execution-finished notification goes to the requester in the portal only, not to Slack or Teams.
The audit trail
Every step above writes an audit entry: submission, decision (with channel and
override flag), execution start/finish (with duration and affected rows),
result downloads, plus logins, grants and configuration changes. Audit rows are
immutable — the model layer and database triggers refuse updates and deletes, and
the records form a hash chain that php artisan audit:verify checks. Auditors
can filter the trail and export it as CSV. The audit screen and the CSV show the
SQL with literals redacted (sql_redacted), never the raw statement. Events can
also be exported to a SIEM: see
Audit chain and SIEM export.
Entries record the client IP as QueryProxy sees it. Behind a reverse proxy that
is the proxy’s own address unless you list it in TRUSTED_PROXIES — see
Configuration.