Connect by JBRH Open Connect

Authorisation models

An authorisation model decides who may do what. The four common shapes — allowlist, role, scope and row policy — differ less in what they permit than in what they do when they refuse: a closed door, a missing button, a narrower rule winning, or a row that simply is not there. Connect layers three of them, deliberately, over every request.

Status
Reference What this means
Audience
both, developer
Last verified
Product version
6.3.2

Four shapes, and the refusal each produces#

Reading an authorisation model by its permit rule tells you little. Reading it by its refusal tells you how a fault will present to the person who reports it, which is the part that costs time.

ModelDecides onWhat a refusal looks like
AllowlistThe path or operation being calledA hard 403 at the entrance; nothing downstream runs
RoleWho the caller isA capability absent from the screen, or refused on use
ScopeWhich rule is narrowest for this piece of workBehaviour changes rather than stopping — a stricter rule won
Row policyWhich rows this caller may see or writeData appears missing; a refused write can surface as a server error

The last row is the dangerous one. A row policy that hides a record produces a fault that reads as data loss, not as a permission problem, and people look in the wrong place for hours.

Connect's three layers over one request#

A customer request is rewritten by tenantAdapt in the browser before it leaves — an Owner path becomes a /api/customer/ui/… one, and anything the rewriter does not recognise becomes /api/customer/ui/blocked, which answers 403. That is the first gate and it is a routing decision, not yet an authorisation one.

  1. The allowlist. customer_safe in backend/app/main.py refuses any /api/* a customer session is not permitted to call. This is an operation-level decision made before any handler runs.
  2. The kernel. workspace_kernel.py filters every SQLAlchemy query by workspace_id, using SCOPED_TABLES as the list of what is scoped. This is a row-level decision made inside the application.
  3. Row-level security. PostgreSQL applies the same restriction a third time, in the database, where no application bug can skip it.

Three layers is not belt and braces for its own sake. Each catches a different class of mistake: the allowlist catches a route nobody thought about, the kernel catches a query written without a filter, and row-level security catches both of those failing at once.

Narrowest scope wins#

Connect's autonomy rules are a scope model rather than a role model. The four scopes, narrowest first, are contact, endpoint (one mailbox or one number), channel and workspace, and the narrowest applicable one decides. Memory resolves over the same four tiers in the same order.

The practical consequence is that a rule set against one contact overrides the channel's, which is how a workspace holds every reply to one account for review without slowing down anything else. The failure mode is equally characteristic: a setting appears not to apply, and the reason is a narrower rule you have forgotten about at another scope.

Roles do appear, but in a smaller place than people expect. A mailbox has a role, and the Connect Assistant's rights are narrower than a person's — it cannot set pricing and cannot clear a do-not-contact entry, regardless of who is asking it to.

Does Connect use these models?#

Yes — an allowlist, an in-application row filter, a database row policy, and a four-level scope model for behaviour. Not a general role-based access control system with editable role definitions; there is no screen for inventing a role and attaching permissions to it.

Allowlist
customer_safe, plus the tenantAdapt rewrite that fails closed to /api/customer/ui/blocked
Row filtering in the application
workspace_kernel.py and SCOPED_TABLES
Row policies in the database
PostgreSQL row-level security, forced on every scoped table
Scopes
autonomy.py — contact, endpoint, channel, workspace, narrowest first
Enumerated parity
tools/test_audience_parity_v1.py compares every Owner capability with its customer equivalent, so a one-audience capability fails the suite rather than shipping quietly

Two failures this design actually produced#

A row stamped with an empty workspace_id matched no scope and no row policy, so it was invisible to every list while every action addressing it by id still worked. Two connected mail identities can disappear from a screen that way, with no error and no duplicate warning. Repair belongs to the schema owner at boot, not to a request, because stamping the row from a request is refused by the policy's WITH CHECK — which turns a missing row into a server error.

Separately, four mailbox routes took an id from the browser and fetched it with a bare lookup that the kernel's query filter can miss when the row is already in the session's identity map. They go through the console's own row lookup now. Both faults are the same lesson: an authorisation layer you can bypass by accident is a layer you do not have.

Questions#

Why does the same action work for one account and 403 for another?

Because the two audiences reach it by different paths. A customer session's request is rewritten to a /api/customer/ui/… path and then checked against the customer_safe allowlist; an unrecognised path becomes blocked and answers 403. The capability is the same code underneath — what differs is whether the route was added on both doors.

Is row-level security enough on its own?

It is the layer that cannot be skipped by an application bug, which makes it the most valuable single layer. It is not enough on its own because it only applies to tables that have a policy: a new scoped table gets created at boot, but its policy comes from an operational script, so a table missing from that step is scoped by nothing at all.

Can I define my own roles and permissions?

No. Connect does not offer editable role definitions. What is configurable is autonomy — what Connect may do without asking, per channel and at four scopes — which controls the actions people care about rather than the screens they can open.