Security · Identity · Governance

What Qrly enforces — and what it does not claim

Multi-IdP SSO resolved by hostname. Five collection permission levels, two data access levels and most-permissive-group-wins resolution. A read-only connection pool, a SQL sanitizer and an audit log that records the exact SQL executed. And a plainly worded list of the things Qrly does not do, because you are going to find out anyway and it may as well be from us.

Identity

Eight ways to prove who you are

  • Username / password — BCrypt, form and API login
  • LDAP — per organisation, with group sync
  • Microsoft Active Directory
  • Azure AD (OAuth2/OIDC) — platform-wide and per organisation
  • Google (OAuth2/OIDC) — the same dual model
  • API tokens — scoped, hashed, rate-limited
  • Signed-request JWTs — short-lived, per call
  • Bot Framework tokens — for the Teams integration
Authorisation

Three permission axes, one resolution rule

  • Collection: NO_ACCESS → VIEW → EXPLORE → CURATE → MANAGE
  • Data: NO_ACCESS → RESTRICTED → FULL
  • Project: the same VIEW / EXPLORE / CURATE / MANAGE semantics
  • Most permissive group permission wins
  • No groups resolves to NO_ACCESS
  • Unknown level strings rejected at the boundary
  • Two seeded system groups that cannot be renamed or deleted
Governance

Evidence, limits and encryption

  • Query audit log — the full SQL, row count, duration, status
  • Retention enforced by a daily scheduler
  • Per-user daily query budgets, checked before every query
  • A query governor: per-connection semaphore and FIFO queue
  • Connection credentials AES-encrypted at rest
  • Cache encrypted with AES-256-GCM in both tiers
  • Read-only pool, SQL sanitizer, every parameter bound
Method
Shipped Qrly Spring Security · JWT in an HttpOnly cookie
Scope and detail What is actually configurable
Username and password
BCrypt
Form login and API login, JWT cookie, remember-me
LDAP
Per organisation
URL, base DN, user DN pattern, encrypted bind credentials, user and group search base/filter, group sync
Microsoft Active Directory
Included
URL, base DN, bind credentials, user search base, default domain
Azure AD (OAuth2 / OIDC)
Platform + per org
A platform-wide client and per-organisation registrations, composed at runtime
Google (OAuth2 / OIDC)
Platform + per org
The same dual model as Azure AD
API tokens
Scoped & hashed
qrly_live_<32 hex>, SHA-256 at rest, shown once, per-token rate limiting, per-IP lockout
Signed-request JWTs
Included
Short-lived per-call credentials
Bot Framework tokens
Included
For the Microsoft Teams integration
SSO discovery by hostname
Included
The resolved custom domain decides which buttons the login page shows
Multi-factor authentication
Not implemented
Enforce it at your IdP via Azure AD or Google OIDC
Legend Shipped Workaround / external Not implemented

SSO discovery by hostname

Custom domains are scoped to a tenant or to an organisation, with a primary flag, and they drive two things at once: brand resolution and which sign-in methods the login page offers. The resolved domain decides whether the page shows a password form, LDAP, Active Directory, a per-organisation Azure button, a per-organisation Google button, a global Azure button or a global Google button — narrowing to a single organisation slug when the custom domain resolves to exactly one organisation.

The practical effect: your customer visits their own hostname and sees their own logo and their own identity provider. They never see, and never have to be told to ignore, a button belonging to somebody else on the same installation.

Session and token handling

  1. JWT in an HttpOnly cookie. Not accessible to page JavaScript, which removes the most common exfiltration route for a session credential.
  2. A JWT denylist for logout and revocation. The awkward part of stateless tokens is that logging out is meaningless without one. Qrly keeps a denylist and a validation cache, so signing out and revoking a session actually take effect.
  3. A rotating key provider. Signing keys rotate rather than living forever in a properties file.
  4. Configurable session timeout and maximum sessions per user.
  5. Login-failure tracking alongside the per-IP lockout that guards API tokens.
  6. Password lifecycle endpoints — change, forgot, and reset with a token.
  7. A short-lived session token for the API explorer, superuser-only, with a configurable TTL defaulting to 900 seconds.

Which surfaces are open, and which are locked to a superuser

Unauthenticated, by design: /api/auth/**, /embed/**, shared report links, /setup while the platform is uninitialised, /live, /ready, public branding, the current-domain lookup and static assets. Superuser-only: the API explorer and its spec, the platform console, system APIs, the session-token mint, tenant branding writes and superadmin.

That list is short and worth reading twice, because in most BI platforms the equivalent list is neither published nor short.

Collection levels

  1. NO_ACCESS. The collection is not there as far as this user is concerned.
  2. VIEW. See and open what is in the collection.
  3. EXPLORE. Interrogate it — the level at which a user stops being an audience and starts being an analyst.
  4. CURATE. Organise and maintain the contents.
  5. MANAGE. Including the permissions themselves.

Collections are a materialized-path hierarchy: a child inherits its parent's permissions unless it overrides them. Every user gets a personal collection, and collections can sit outside any project, which is how organisation-wide content works.

Data levels — the axis people forget

Collection access says what content you can reach. Data access says what you can do to a connection, and it is a separate decision with three values.

  1. NO_ACCESS — the connection is unavailable.
  2. RESTRICTED — run saved questions only. The user cannot author SQL. This is the level for the large population who need answers rather than a query editor.
  3. FULL — author and run any SQL. It is also the level required for RPC calls, table writes and applying a Construction. Those three write paths do not have their own separate permission to forget about; they hang off FULL, deliberately.

Project levels

Projects are a workspace above collections, grouping questions, dashboards, reports and collections, and they carry the same VIEW / EXPLORE / CURATE / MANAGE semantics. On creation, Administrators receives MANAGE and every other system group receives EXPLORE — a default that is permissive enough to be usable on day one and explicit enough to be visible in the matrix.

How a level is resolved

  1. Most permissive group permission wins. A user in a VIEW group and an EXPLORE group gets EXPLORE.
  2. A user in no groups resolves to NO_ACCESS. Not to a default, not to a fallback. Nothing.
  3. Unknown level strings are rejected at the boundary rather than silently ranking below no-access. A typo in an integration produces an error instead of an accidental grant — the difference between a bug you find in five minutes and one you find in an audit.
  4. Administrators and superusers bypass to full access, which is stated rather than implied.
  5. Resolution is memoised per request, so the model can be this explicit without costing a query per check.
  6. All Users and Administrators are seeded system groups and cannot be renamed or deleted. Groups otherwise support create, rename, delete, and adding and removing members.
01 / Read-only

The pool hands out read-only connections

Every connection Qrly opens for a query is handed out with setReadOnly(true). All native SQL passes a sanitizer before it reaches a driver, and every parameter is bound rather than interpolated into a string. That is three independent mechanisms guarding the same boundary, which is the right number when the boundary is somebody else's production database.

The only schema write path in the product is Constructions, and it is gated by a three-tier cascade: the tenant AND the organisation AND the connection must each have ddl_enabled, all three defaulting to off. When one of them blocks, the policy service names the outermost blocking tier, so an administrator is told where to go rather than left guessing. On top of the cascade, the caller still needs FULL data access on the target. Constructions also never executes AI-authored SQL: the assistant proposes a design document, which is rebuilt field by field through the DDL generator as a trust boundary.

02 / Audit

The log records the SQL, not a summary of it

The query audit log stores the full SQL that was executed, together with the row count, the duration, the status — SUCCESS, ERROR or TIMEOUT — and the error message where there is one. It is browsable and filterable in the product, and retention is enforced by a daily scheduler rather than by somebody remembering to prune a table.

Recording the actual statement is the difference between an audit trail and a log. When someone asks in six months why a figure moved, "user X ran question Y" does not answer it; the verbatim SQL does. Constructions applies the same principle to schema changes: a run records the verbatim SQL, statements total and run, status, error, duration and who triggered it.

03 / Availability

Budgets and a governor, because a query can be an outage

Every user has a daily query budget: a per-user override, falling back to an organisation default, falling back to 1000. It is checked and reserved atomically before every query, race-safe via an upsert keyed on user and date, with bytes returned and AI tokens recorded asynchronously afterwards. Administrators get a budget page, a per-user editor, a reset and a usage report; users see their own budget on their profile, which stops the whole thing feeling like a trap.

The query governor works on the other axis. Each connection has a semaphore sized to its maxConcurrentQueries and a FIFO wait queue capped at maxQueueSize, returning a queue-full error on overflow and timing out after queueWaitTimeoutSec. The admin page shows live per-connection statistics — active, queued, maximum concurrent, maximum queue, last wait — with a recent-events table. Nobody's ad-hoc query becomes everyone's incident.

04 / Encryption

Credentials and cached rows are both encrypted at rest

Connection credentials are AES-encrypted at rest, including LDAP bind credentials and the extra options JSON that carries SSL settings, SSH tunnel private keys and passphrases, and BigQuery service-account keys. Debezium receives credentials injected after decryption rather than holding its own copy in configuration.

Cached query results are encrypted too, which is the part most platforms skip. Qrly's in-process JVM cache holds encrypted bytes, and the persistent tier — whether it lives in the shared application database or in a per-connection SQLite WAL file laid out by tenant and organisation — is AES-256-GCM encrypted at rest. The cache never stores plaintext rows. Cache failures always degrade to a miss rather than to an error, so the encryption boundary cannot become an availability problem.

MFA is not implemented

Multi-factor authentication is described in the project notes, but there is no TOTP code, no filter and no endpoint in the product. Anyone telling you otherwise has read a planning document rather than the software.

What to do about it today: enforce the second factor at your identity provider. Azure AD and Google OIDC registrations are supported both platform-wide and per organisation, so the factor is applied where your other applications already apply it, by the team that already owns it. That is a legitimate architecture — it is just not the same as Qrly having MFA, and we are not going to write it on a comparison grid as though it were.

Row-level security is not implemented

There is no policy language that filters every query for every user across the whole product. The shipped equivalents are narrower and specific: embed locked params, signed into an embed token so a browser cannot override them, and the embed-agent allow-lists covering agent type, connections, questions, tools, locked params and session caps. Between them they solve the embedded multi-tenant case properly.

Inside the application, access is governed by the collection, data and project levels described above, and OLAP models carry always-applied model filters that act as a security boundary for anything queried through the model. If your requirement is per-user row filtering across arbitrary ad-hoc SQL, Qrly does not do that, and a proof of concept will confirm what this paragraph already told you.

Qrly holds no compliance certification

No SOC 2, no ISO 27001, no attestation of any kind for the software or the company. We are not going to imply one with a badge or a phrase like "built to SOC 2 principles", which means nothing and is designed to be misread.

What Qrly offers instead is self-hosting, and it is a genuinely different answer rather than a consolation prize. The software runs on your infrastructure, against your databases, inside the certification scope you already maintain and the controls your auditors already assess. There is no vendor cloud holding your rows, no sub-processor to add to a register, no data-residency question to negotiate, and no phone-home. The compliance argument is not that we passed an audit — it is that your data never left the environment your audit already covers.

A separate point, so it is not mistaken for a claim about Qrly itself: the product ships a BCBS 239 compliance module. That is a reporting and risk-aggregation capability for institutions subject to BCBS 239. It is not a certification of Qrly.

Does Qrly support multi-factor authentication?

No. MFA is described in the project notes but is not implemented — there is no TOTP code, no filter and no endpoint. If you need a second factor today, put it in front of Qrly at your identity provider: Azure AD and Google OIDC registrations are supported both platform-wide and per organisation, so the factor is enforced where your other applications already enforce it. We would rather state this than let a checkbox on a comparison grid imply otherwise.

Does Qrly have row-level security?

No. Row-level security and data sandboxing are not implemented as a general platform feature. The nearest shipped equivalents are embed locked params — signed into an embed token so a browser cannot override them — and the embed-agent allow-lists. Inside the application, access is governed by collection, data and project permission levels resolved most-permissive-group-wins, and OLAP models carry always-applied model filters that act as a security boundary for model-driven queries.

Is Qrly certified against SOC 2, ISO 27001 or similar?

No. Qrly holds no compliance certification. What Qrly offers instead is self-hosting: the software runs on your infrastructure, against your databases, inside your own control and your own existing certification scope. Your auditors assess an environment you already own rather than a vendor's cloud you cannot inspect. A BCBS 239 compliance module ships in the product, but that is a reporting capability, not a certification of Qrly itself.

What is the difference between RESTRICTED and FULL data access?

RESTRICTED lets a user run saved questions against a connection but not author SQL against it. FULL lets a user author and run any SQL, and it is additionally the level required for RPC calls, table writes and applying a Construction. The distinction is deliberate: giving somebody the ability to read a curated answer is a different decision from giving them the ability to write an arbitrary query.

How are permissions resolved when a user is in several groups?

Most permissive wins. A user in a group with EXPLORE and a group with VIEW on the same collection gets EXPLORE. A user in no groups at all resolves to NO_ACCESS rather than to a default. Unknown level strings are rejected at the boundary rather than silently ranking below no-access, so a typo in an integration fails loudly instead of quietly granting something. Resolution is memoised per request, and Administrators and superusers bypass to full access.

Can a Qrly user damage a source database?

Not through the query path. The connection pool hands out read-only connections, all native SQL passes a sanitizer, and every parameter is bound rather than interpolated. The only write path into schema is Constructions, which requires ddl_enabled on the tenant AND the organisation AND the connection — all three default to off — plus FULL data access on the target. Table writes and RPC are separate API surfaces that also require FULL data access on the connection.

Read the gaps first. Then evaluate us.

Self-hosted, so your data stays inside the environment your auditors already cover. Made in Belgium.