Roles and Permissions¶
This page explains who can do what in the Conflux admin panel: which roles exist, what each one may do, and why the set of records a person reaches is derived from the connection graph rather than assigned to them.
Two layers of access control¶
Two mechanisms have to agree before anything happens:
- Roles and permissions — which actions a user may perform at all.
- Tenant reach — which records those actions may be applied to.
A PlatformSupport user holds sync_rules.update, but that only lets them change rules belonging
to a tenant they reach — and only rules touching a system they operate themselves.
The first layer is a flat permission list managed by Spatie Laravel Permission. The second is computed on every query; no per-tenant assignment row exists anywhere. See ADR-0007 for why access is derived rather than granted.
Roles¶
| Role | Alias | Who it is for |
|---|---|---|
SuperAdmin |
super_admin |
Platform operators who run Conflux itself. Unscoped. |
PlatformSupport |
platform_support |
Support staff of a consuming platform — rapidmail or User Platform — helping the clients of their platform. |
Viewer |
viewer |
Read-only, scoped the same way as PlatformSupport. |
Roles are the SystemRole backed enum, seeded by RolesAndPermissionsSeeder at deployment time and
not managed at runtime. A user has exactly one.
The shop operators whose WooCommerce store is connected never log into the panel at all. They self-serve inside their shop via the plugin and a per-connection token (ADR-0006).
Permissions¶
| Permission | SuperAdmin | PlatformSupport | Viewer |
|---|---|---|---|
users.create / .view / .update / .delete |
yes | — | — |
tenants.view |
yes | yes | yes |
tenants.create / .update / .delete |
yes | — | — |
systems.view |
yes | yes | yes |
system_connections.view |
yes | yes | yes |
system_connections.create / .update / .delete |
yes | — | — |
system_connections.resync |
yes | yes | — |
sync_rules.view |
yes | yes | yes |
sync_rules.create / .update / .delete |
yes | yes | — |
Names are lowercase {resource}.{action} strings, checked through Spatie's can() helper and the
policies Filament consults automatically. There is no systems.create, systems.update or
systems.delete — see Systems are read-only.
Viewer holds tenants.view because it holds system_connections.view: reaching a connection
without being able to open the tenant that owns it is a dead end.
What each role means¶
SuperAdmin — unrestricted: every tenant, credentials, tokens, user administration. Platform operators.
PlatformSupport — sees the tenants that have a connection to a system they operate, and can act on those connections: trigger a resync, manage the sync rules touching them. No user administration, no credential reads, no creating or deleting tenants and connections.
Viewer — the same reach, read-only. For stakeholders who need visibility without a change path.
Tenant reach is derived¶
A support user is made a member of one or more consuming Systems through the system_user pivot
(User::operatedSystems(), System::supportUsers()). The tenants they reach are then computed:
every tenant that has a connection to one of those systems. Onboarding a tenant requires no
"assign this person to this tenant" step.
Tenant::accessibleBy($user) is that computation, with SuperAdmin short-circuiting it. Only
systems whose driver is an internal consuming platform may have support members at all
(ADR-0008) — a WooCommerce
adapter never does.
Own connections in full, siblings by status¶
Inside a reachable tenant, not every connection is equal:
- Own connections — those pointing at a system the user operates. Full access: view, and trigger a resync. Never credentials.
- Sibling connections — every other connection of the same tenant, e.g. the tenant's WooCommerce store. Status only: is it active, when did it last connect, is it healthy.
Support staff must be able to spot upstream breakage — "the shop stopped syncing" — without gaining reach into a platform they have no relationship with.
The two levels are two model scopes, SystemConnection::accessibleBy() and
SystemConnection::fullyAccessibleBy(). SystemConnectionPolicy reads view from the first and
update, delete and resync from the second.
Changing a sync rule needs one accessible side¶
A SyncRule joins two connections of the same tenant
(ADR-0010). It is visible when its
tenant is reachable, and changeable when at least one of its two connections is fully
accessible.
One side is enough because full access only ever matches internal consuming platforms, and every
derived rule spans shop ↔ internal product: requiring both sides would reduce rule management to
SuperAdmin while presenting itself as a graded permission. See
ADR-0012.
SyncRule::accessibleBy() and SyncRule::manageableBy() are the two scopes; SyncRulePolicy reads
view from the first and update/delete from the second. create receives no rule to inspect,
so there it means only "holds sync_rules.create and has at least one fully accessible
connection" — the pair itself is validated when the rule is saved.
Systems are read-only¶
A System carries the direction matrix that states what an adapter can do in code, and sync rules
are validated against it
(ADR-0005). A matrix editable
through the panel could claim a direction the integration cannot perform, and rule validation would
then enforce that fiction.
So systems are owned by SystemSeeder, and systems.view is the only permission that exists for
them. SystemPolicy denies create, update and delete outright — to SuperAdmin too. The
restriction lives in the policy rather than in the absence of buttons, so a future page cannot
re-open write access by accident.
Where the boundary is enforced¶
Three places, deliberately not one:
- Model scopes —
accessibleBy,fullyAccessibleBy,manageableByeach state one rule once. - Filament resource queries —
getEloquentQuery()applies the scope, so an out-of-reach record is not "forbidden", it is absent; navigating to it directly yields a 404. - Policies — the record-level gate for what a query cannot express, and the source of every action's visibility.
What this model does not cover¶
- Runtime role management. Roles are seeded; changing one is a seeder or database change.
- Per-tenant fine-grained permissions. A role's capabilities are uniform across every tenant it reaches.
- API authentication. This is the human side. Machines authenticate per connection — see API Authentication.
- Policy method contracts. For signatures, resource keys and how to add a policy, see Policies.