Problem/Motivation
Role revocation in SubscriptionEntity::updateUserRoles() and preDelete() resolves which roles to remove by calling getPlan() at revocation time. If that lookup fails, no roles are removed and the user retains paid access indefinitely. The code acknowledges this: updateUserRoles() contains the comment "There's a bug here. If a plan is deleted, then an expiring subscription will fail to remove the corresponding user roles."
Lookup failure is not exotic. It happens when the plan entity was deleted locally (including by plan sync removing plans that no longer exist upstream), when the remote plan was renamed so plan_id no longer matches, or when #3616776: SubscriptionEntity::getPlan() ignores connector_plugin_id, allowing cross-connector plan_id collisions's cross-connector collision resolves to a different plan (which removes the wrong roles, equally bad). The same live-lookup pattern exists in subscription_manager_user_presave(), which rebuilds required roles from each active subscription's plan.
Net effect: access granted by a paid subscription can outlive the subscription. For sites selling access, this is revenue-relevant broken access control. (Filed in the public queue per policy, as the module has no stable release.)
Steps to reproduce
- Create a plan granting a role; subscribe a user (role granted).
- Delete the plan entity (or change its
plan_id). - Let the subscription expire via cron, or set
statusto 0 and save. updateUserRoles()logs "Could not find local plan matching remote plan id" and returns; the user keeps the role permanently.
Proposed resolution
- Add a
granted_rolesmulti-value string base field to thesubscriptionentity. Write it whenever roles are granted (inaddRoles()), recording exactly what this subscription conferred. - Revoke from the snapshot, not from
getPlan():updateUserRoles()(deactivation path) andpreDelete()remove the snapshotted roles. Keep the plan lookup only as a fallback for pre-existing rows with an empty snapshot. - Update
subscription_manager_user_presave()to compute required roles as the union of snapshots of the user's other active subscriptions, with plan-lookup fallback, so its re-add logic can't resurrect roles from a wrong plan match. - Update hook: install the field and backfill snapshots from current plan lookups where they still resolve.
Remaining tasks
Patch; kernel tests for the reproduce case above, for deletion with a missing plan, and for the presave union logic; change record for the new field.
User interface changes
None.
API changes
None; internal role-handling behavior only.
Data model changes
New granted_roles base field on the subscription entity, installed via update hook.
Issue fork subscription_manager-3616929
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
colanComment #4
colanMR opened, implementing the proposed resolution:
granted_rolesmulti-value string base field on the subscription entity.preSave()snapshots the plan's roles whenever an active subscription is saved while its plan still resolves — grant time is the moment the lookup is known-good. On a plan change it first revokes from the old row's snapshot, then clears it so the new plan's roles are recorded.updateUserRoles()(deactivation) andpreDelete()now revoke via a newgetRevocableRoleIds()helper — the snapshot, with the live plan lookup kept only as a fallback for rows saved before the field existed.GrantedRolesSnapshotTest): the issue's reproduce case (deactivation after plan deletion now revokes), deletion with a missing plan, the presave union (deactivating one of two subscriptions removes only its role, and a plain user save resurrects nothing), and the backfill. The page-attachments query-budget expectation grows from 2 to 3 statements — the multi-value field's dedicated-table fetch joins the same logical load.The "There's a bug here" comment this issue quotes is gone with the bug. Change record for the new field to follow before commit.
Comment #6
colan