Skip to content

Decouple admin from webware-acl: abstract ACL usage behind a neutral contract #12

Description

@tyrsson

Goal

Remove the hard dependency webware/webware-admin -> webware/webware-acl. Admin should only suggest webware-acl. Admin's ACL usage should be abstracted around a neutral contract so consumers (host applications) can alias the ACL implementation to the common interface via their own ConfigProvider. Inverted direction: webware-acl will require webware-admin (its admin UI subsystem and dashboard widget are built on top of admin).

Current coupling (admin side)

composer.json requires webware/webware-acl: 0.1.x-dev (plus its VCS repo entry). Four source locations reference Webware\Acl\AclInterface:

  • src/ConfigProvider.phpgetAclConfig() returns roles / resources / allow (Administrator => ['Member'], admin.dashboard.read), exported under the AclInterface::class config key. This role/resource/allow config should move to webware-acl's ConfigProvider.
  • src/Middleware/DashboardMiddleware.php — dispatches RegisterWidgetEvent, then filters the widget iterator through the ACL using the current user's roles. Also imports Webware\UserManager\UserInterface (reads the webware attribute key).
  • src/Container/DashboardMiddlewareFactory.php — resolves $container->get(Webware\Acl\AclInterface::class).
  • src/Widget/AclWidgetFilterIterator.php — wraps the widget iterator and calls $acl->isAllowed($user, $widget->resourceId, $widget->privilege).

test/unit/AclWidgetFilterIteratorTest.php exists but all tests are markTestSkipped('Blocked on webware-usermanager split-out (UserInterface typing).').

The acl -> admin direction is already event-based and not a problem: admin dispatches RegisterWidgetEvent (PSR-14 event); webware-acl's ConfigProvider registers RegisterWidgetListener under listeners for that event.

Research: abstraction options

Option A — Laminas\Permissions\Acl\AclInterface

  • Admin already requires laminas/laminas-permissions-acl: ^2.16 (its WidgetInterface extends laminas ResourceInterface), so zero new packages.
  • Webware\Acl\Acl already extends Laminas\Permissions\Acl\Acl implements Laminas\Permissions\Acl\AclInterface — webware's ACL is a drop-in implementation.
  • isAllowed($role, $resource, $privilege) is object-friendly: webware's UserInterface implements laminas RoleInterface, so $acl->isAllowed($user, $resourceId, $privilege) works against both a stock laminas ACL and webware's ACL.
  • Preserves webware's object-centric design: UserRoleIterator requires the UserInterface aggregate object (not string role names), and assertions (e.g. OwnershipAssertion / ProprietaryInterface) can only be satisfied by objects. See docblock on Webware\Acl\AclInterface::isAllowedRoute().
  • App wiring becomes a one-line alias in the host app ConfigProvider:
    'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].
  • Admin changes: swap the AclInterface import in the 4 locations above; drop getAclConfig() and the AclInterface::class config key; move the Administrator/admin.dashboard.read ACL config into webware-acl; move AclWidgetFilterIterator (+ test) into webware-acl; suggest webware-acl in composer.json and remove the VCS repo entry.
  • Also drop the Webware\UserManager\UserInterface import in DashboardMiddleware — read the standard Mezzio\Authentication\UserInterface::class request attribute (mezzio-authentication is already an admin dependency) and pass the value through opaquely. The app pipeline owns populating that attribute.

Option B — mezzio-authorization / mezzio-authorization-acl

  • Adds mezzio/mezzio-authorization + mezzio/mezzio-authorization-acl dependencies to admin.
  • AuthorizationInterface::isGranted(string $role, string $resource, ?string $privilege) — the role is a string. This fights webware's ACL design:
    • UserRoleIterator requires the UserInterface object (laminas RoleInterface implementation) to iterate roles.
    • Assertions (ProprietaryInterface) cannot be satisfied by a plain string role.
    • Webware would need a custom adapter that recovers the user object from the request context — added friction for a weaker contract in this use case.
  • Chosen over Option A only if the goal is to support non-laminas ACL implementations; webware's own ACL is laminas-based.

Decision (settled)

Option A is adopted. Admin's ACL usage will type-hint against Laminas\Permissions\Acl\AclInterface — purely as a decoupling mechanism; in practice the bound implementation will always be webware-acl's Acl (which implements the laminas interface). The abstraction implementation will be done by the maintainer; this issue records the research.

Dependency direction (final)

  • webware-admin: suggest webware/webware-acl; code references Laminas\Permissions\Acl\AclInterface only (no Webware\Acl\* references). Registrations may be guarded as optional wiring where appropriate.
  • webware-acl: require webware/webware-admin (0.1.x-dev) — its management UI (CRUD handlers, overview middleware, dashboard widget, templates) is built on webware-admin and is an intrinsic part of the package; consumers must not be expected to roll their own UI.
  • Host app ConfigProvider: 'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].
  • webware-acl owns the admin ACL config (roles/resources/allow — Administrator => ['Member'], admin.dashboard.read), the AclWidgetFilterIterator (moved from admin, typed to the laminas interface), and the widget listener.

Related notes

  • webware-acl currently has a bogus suggest entry (webware/admin with a mention of a nonexistent RegisterAclWidgetListener) that should be replaced by the real require.
  • Related docs:
    • webware-acl/docs/planning/todo-2026-05-31.md — "webware-admin resources/allow config" TODO item.
    • webware-acl/docs/admin-ui-workflows.md — "Redundant Administrator Parent" (admin's ConfigProvider currently contributes Administrator => ['Member']; after the move this config lives in webware-acl).
    • webware-acl/docs/integration-guide.md — acl <-> usermanager circular-dependency notes (adjacent concern).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions