QDG Knowledge Base Read-only viewer QWebHub
general

Roles, Permissions & Multi-Tenancy

Version 1 · Roles/permissions model and an explicit, honest comparison against Keycloak-style multi-tenancy — requested by ClickUp task 86d40a7qe

Historical version

Roles, Permissions & Multi-Tenancy

Written for the "official developer and integration guide" requested in ClickUp task 86d40a7qe, which frames QDBAuth as "similar to Keycloak." This page documents where that comparison holds and where it doesn't — most importantly for multi-tenancy, which QDBAuth does not actually implement the way Keycloak does.

Roles

Every account has exactly one role string (users.role, VARCHAR(20)) plus an is_admin flag derived from it. There is no separate fine-grained permission system (no per-action permission grants) — role is the whole permission model, and consuming apps are expected to interpret the role string returned to them for their own authorization decisions.

Global roles

Default set: admin, developer, user (ROLE_CHOICES in apps/auth/users.py). admin sets is_admin=1 and is the only role that can access QDBAuth's own dashboard (dashboard.require_admin matches the literal string "ADMIN" after uppercasing — nothing else can match it).

Per-site custom roles

A site can define its own role list instead of the global one (ROLE_GROUPS in apps/auth/users.py). Currently only TD (Troyen Data) does: Developer, Support, Dev Lead, Support Lead, Super Admin. TD's Super Admin is that group's admin_equivalent (sets is_admin=1) but — critically — still cannot access QDBAuth's own dashboard, because that check only matches the literal "admin" role, never a site's custom equivalent. Per-site roles are meant to be consumed by the site itself (via the role field in /api/verify-2fa/'s response, or user_permissions from /api/whoami/), not to grant any QDBAuth-level privilege.

role_group_for_sites(approved_sites) resolves which group applies at creation/edit time: the first granted site with its own entry in ROLE_GROUPS wins. Known limitation: a user granted two sites that each define different custom role groups can't have two independent role values — role is one column. This hasn't come up yet (only one site customizes roles today) but would need a design decision if a second site ever adds its own role list.

Where roles are enforced

  • QDBAuth's own dashboard: dashboard.require_admin/require_login decorators, checked against the session's dash_permissions value.
  • Everywhere else: the consuming app receives the role string (/api/verify-2fa/'s user object, or /api/whoami/'s user_permissions) and is responsible for interpreting it. QDBAuth does not gate access to any consuming app's own features — it only vouches for who the person is and what role string they hold.

Multi-tenancy — the actual answer

QDBAuth does not implement multi-tenancy in the Keycloak sense. There is one global users table, shared across every consuming application — a person's email is unique across the entire ecosystem, not scoped per tenant/realm. There's no data isolation between "tenants"; every site's admin can (in principle) see every user via the same dashboard.

What QDBAuth actually has, that's closest to a Keycloak concept, is the "site" model (site table + approved_sites) — this maps to Keycloak's "client" concept (a registered consuming application), not to a "realm" (an isolated tenant with its own user pool). Concretely:

Keycloak concept QDBAuth equivalent Notes
Realm (tenant, isolated user pool) Not implemented All users are global; no tenant boundary
Client (a registered app) site table entry site_name + site_url
Client-specific role mapping ROLE_GROUPS (only for sites that opt in) Only TD uses this today
Per-client credentials site_credentials.py (opt-in, new accounts only) See Overview

If genuine tenant isolation (separate user pools per customer/organization) is a real future requirement, that's a substantial architectural change, not a configuration option — flag it as its own project decision rather than assuming today's "site" model already provides it.

Related pages

Updated by Claude on Aug. 11, 2026, 8:31 a.m.