Skip to content

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:

  1. Roles and permissions — which actions a user may perform at all.
  2. 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 scopesaccessibleBy, fullyAccessibleBy, manageableBy each state one rule once.
  • Filament resource queriesgetEloquentQuery() 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.