Skip to content

[3.0] Lets mods declare the services they use, and admins withdraw them - #9701

Open
albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/service-access-control
Open

albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/service-access-control

Conversation

@albertlast

Copy link
Copy Markdown
Collaborator

Description

An alternative to #9695, weighted towards the administrator rather than towards how the code reads. Same goals — mods can register services, the container stays an implementation detail — but arrived at from the other end: what does the person running the forum see, and what can they do about it?

The difference in one line: a mod declares its services in package-info.xml instead of registering them through a runtime hook. Everything admin-facing follows from that, because SMF then knows which package every service belongs to, which a hook can never tell it.

<install for="2.1 - 3.0.99">
	<require-file name="MyMod.php" destination="$sourcedir" />
	<service id="MyMod\Greeter" factory="MyMod\Factory::makeGreeter" file="$sourcedir/MyMod.php" />
	<uses-service id="SMF\Services\ErrorHandlerService" />
</install>

What that gives the admin:

  1. They see it before installing. The declarations appear among the install actions, next to the files and hooks, as "Provides: MyMod\Greeter" and "Uses: SMF\Services\ErrorHandlerService". Installing the package is the approval.
  2. They can look it up afterwards. Package Manager → Service Access lists every installed mod with what it provides and uses, plus the services SMF itself provides.
  3. They can withdraw it. One button. The mod stays installed, its services stop being registered, and the page says so. That is a support lever the forum does not have today: the only way to find out whether a mod is at fault is currently to uninstall it.
  4. The access is enforced, not just displayed. Each package's factories are handed an accessor limited to what that package declared. Asking for anything else throws, and the message names the package.

How it works

  • SMF\Infrastructure\Services is the whole API a package sees: has() and get(). It holds the grants for one consumer, and League's exceptions are translated into SMF's own on the way out, so nothing outside SMF\Infrastructure learns which container is in use.
  • SMF\Infrastructure\ServiceRegistry builds the container: SMF's own services from ServicesList.php, then each granted package's. Later registrations of an ID that is taken are refused and logged, rather than a package quietly replacing a service other code is already using.
  • SMF\Infrastructure\PackageServices keeps what each package declared, written by the package manager at install and removed at uninstall.
  • Factories are passed the accessor as an argument rather than having it bound as $this. A mod can then write its factory as any callable, including a static one, which Closure::bind() cannot do.
  • A declared factory file has to be inside the forum directory, or it is refused.

What this does not do

It is a declaration, not a sandbox, and the admin page says so. A mod that runs in the same PHP process can still call Db::$db directly or read Settings.php. What this gives an honest mod is a way to state what it works with, and the admin a way to see and withdraw it. Claiming more than that would be misleading, which is why the wording on the page is "This describes what a modification asks for; it does not stop a badly behaved one from reaching the forum in other ways."

Nothing in core consumes services yet. ErrorHandlerService is still reached the old way. The consumption path — injecting services into actions — is what #9695 is designing, and it fits on top of this: resolve through ServiceRegistry::services() instead of a bare container. The gate is worth landing before the way in, rather than after mods depend on ungated access.

Testing

Unit tests cover the access model: a granted service is handed over, an ungranted one is refused with the package named, an ungranted service reports as absent rather than as present-but-refused (so a package cannot probe for what else is installed), SMF's own accessor reaches everything, and a missing service throws SMF's exception rather than League's. Plus the manifest reading, including a mangled setting leaving everything without access.

Verified in the Docker environment with a real package built for the purpose, installed and uninstalled through the Package Manager over HTTP:

  • the install screen listed "Provides" and "Uses" next to the file actions;
  • after installing, Service Access listed the mod, its service, what it uses and an "Allowed" state;
  • resolving the service ran the mod's factory, which received its accessor and could reach the service it declared, and was refused one it had not;
  • "Withdraw" flipped the page to "Withdrawn", the service stopped being registered, and the entry stayed visible;
  • uninstalling removed the entry;
  • nothing new was logged in log_errors.

Issues References (Fixes|Related|Closes)

Related: #9695

🤖 Generated with Claude Code

A package says in its package-info.xml which services it provides and
which it wants to use. The package manager shows those declarations
among the install actions and records them when the package is
installed, so Package Manager > Service Access can list what every
installed mod does with services and take that access away again
without uninstalling anything.

Each package's factories are handed an accessor limited to what the
package declared, so asking for anything else fails where it happens
and says which package asked. Which container does the work stays
inside SMF\Infrastructure: everything else is given has() and get().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Mathias Albert <mathiaspapealbert@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant