0030. Extend full access to shop connections via a sibling consuming-system connection¶
Date: 2026-09-21 Status: Accepted
Context¶
ADR-0007 splits a PlatformSupport user's
reach inside a reachable tenant in two: connections to a system they operate are fully accessible,
every other connection of the same tenant is status-only.
ADR-0008 fixes which systems can
ever have support members at all — SystemDriver::canHaveSupportUsers() returns false for every
shop adapter (WooCommerce, PrestaShop, Shopify, Shopware) and true for the internal consuming
platforms (rapidmail, User Platform). A shop adapter therefore structurally never has support
members of its own: nobody is ever a member of a System row whose driver is a shop adapter, so
"connections to a system they operate" can never match one.
Combined, the two decisions left every shop connection permanently status-only for every
PlatformSupport user, in every tenant, regardless of which internal platform they support —
including the Bulk Sync action, which
SystemConnectionPolicy gates on SystemConnection::fullyAccessibleBy(). A support user for
rapidmail, looking at the tenant's WooCommerce connection through which their own rapidmail
connection is actually being synced, could see its status but never trigger a resync on it — the
one action the role exists to grant. ADR-0007's own-connections/sibling-connections split was
written with two internal-platform connections in mind; it did not anticipate that one side of that
split (shop adapters) can never be reached by the "operate a system" route at all, no matter how
directly the support relationship runs through it.
Decision¶
SystemConnection::scopeFullyAccessibleBy() now matches a connection when either:
- its own system is one the user directly supports (the existing ADR-0007 rule), or
- (new) its system is a shop adapter (
System::scopeCannotHaveSupportUsers()) and it belongs to a tenant that also has a sibling connection whose system the user directly supports (System::scopeSupportedBy(User $user)).
Both branches consult the same underlying test for "does this user support this system", now
extracted once as System::scopeSupportedBy(User $user) — a canHaveSupportUsers() system with a
matching system_user row — shared by both scopeFullyAccessibleBy()'s own-connection branch and
its new sibling-cascade branch. System::scopeCannotHaveSupportUsers() is the inverse of the
existing scopeCanHaveSupportUsers() (ADR-0008),
added so the cascade branch can state "this is a shop adapter" without restating the driver list.
Both scopes read a single private supportEligibleDriverValues() helper on System.
Tenant::scopeAccessibleBy() is unaffected in behaviour and now calls System::scopeSupportedBy()
instead of restating the same check inline.
The cascade is one hop, and only ever lands on a shop adapter: a connection to a second internal/consuming-platform system is never made fully accessible this way, even when the user supports a third, different internal system in the same tenant. Two shop connections in the same tenant (unusual, but not prevented elsewhere) both become accessible once either is reached through a supported sibling, since the test is "does the tenant contain a supported system", not "is this specific shop paired with the supported one".
The cascade is deliberately scoped to SystemConnection actions only — never to SyncRule
management. A first version of this decision let the cascade flow straight through
SystemConnection::fullyAccessibleBy() into SyncRule::scopeManageableBy(),
SyncRuleForm::counterpartOptions(), and App\Rules\ManageableSyncRulePair, since all three read
that same scope. In manual testing this proved wrong: it let a User-Platform support user create
and manage a WooCommerce ↔ rapidmail sync rule, even though the rapidmail connection itself stays
status-only (read-only) to them per ADR-0007 — a connection that is read-only must keep every sync
rule touching it read-only too, not just the connection record itself. SystemConnection::scopeOperatedBy(User $user)
is the fix: it is exactly the pre-ADR-0030 definition of full access (own system directly
supported, no cascade), and SyncRule::scopeManageableBy(), SyncRuleForm::counterpartOptions(),
and ManageableSyncRulePair all read it instead of scopeFullyAccessibleBy(). Only
SystemConnectionPolicy and the Filament connection resource (view/resync/update/delete on the
connection itself, and the full_access/base_url/external_user_id display split) read the
cascading scopeFullyAccessibleBy().
Consequences¶
A PlatformSupport user gains full access (including Bulk Sync) to a shop connection reached
through a sibling connection to a system they operate, but this does not extend to managing
sync rules that shop participates in unless the other side of the rule is also one they directly
operate — SyncRule::manageableBy() still requires ADR-0012's "at least one directly-operated
side", now read via operatedBy() rather than the cascading fullyAccessibleBy(). A rule between
a cascade-reached shop and a system the user does not operate stays visible but unmanageable, the
same as before this ADR.
Two shop connections belonging to different tenants remain fully isolated from each other; the
cascade never crosses a tenant boundary (ADR-0010
still governs rule pairing). A PlatformSupport user who supports no system in a tenant still
reaches nothing in it, cascade or not — the cascade only widens what is fully accessible inside an
already-reachable tenant, never which tenants are reachable.