Follows #345, which is otherwise done: a plugin declares its permission keys in registry.json (#360), and those keys appear in the inventory an operator grants from (#361). A plugin that declares correctly is fully usable.
What is left is the plugin that does not declare correctly.
The remaining case
A route requiring a key that is neither a host key nor declared by its plugin:
[CalloraRoute("POST", "/dialer/campaigns", Permission = "communication.trunk.updat")]
// ^ typo
The manifest is valid, the plugin installs and activates, the route is served — and answers 403 forever. Nothing says why. It is the original defect of #345 in miniature, surviving as a typo.
The information to catch it exists at both ends: PluginApiEndpointDataSource.BuildEndpoints sees route.Permission and owned.PluginId; PluginDeclaredPermissionCatalog knows what that plugin declared.
Why it was not done with the rest
BuildEndpoints is synchronous and has no service scope; the catalog reads manifests asynchronously. Bridging that is a design decision of its own — an eagerly-populated runtime registry (as RuntimeCapabilityRegistry does), or a check moved into activation where the lifecycle already has both — and it does not block anything, which is why it was cut rather than rushed.
Proposal
Refuse the route rather than failing the install. BuildEndpoints already does exactly this for reserved-prefix collisions:
logger.LogWarning(
"Rejected plugin route {Method} {Path} on {ControllerType}: it collides with a reserved host route namespace.");
Follow that. An install-time failure would take down an otherwise working plugin over one mistyped route, and leave an operator with no move except uninstalling it. A refused route with a named reason is repairable and costs only the broken part.
Done when
Not in scope
Anything about granting. That works — see #361.
Follows #345, which is otherwise done: a plugin declares its permission keys in
registry.json(#360), and those keys appear in the inventory an operator grants from (#361). A plugin that declares correctly is fully usable.What is left is the plugin that does not declare correctly.
The remaining case
A route requiring a key that is neither a host key nor declared by its plugin:
The manifest is valid, the plugin installs and activates, the route is served — and answers
403forever. Nothing says why. It is the original defect of #345 in miniature, surviving as a typo.The information to catch it exists at both ends:
PluginApiEndpointDataSource.BuildEndpointsseesroute.Permissionandowned.PluginId;PluginDeclaredPermissionCatalogknows what that plugin declared.Why it was not done with the rest
BuildEndpointsis synchronous and has no service scope; the catalog reads manifests asynchronously. Bridging that is a design decision of its own — an eagerly-populated runtime registry (asRuntimeCapabilityRegistrydoes), or a check moved into activation where the lifecycle already has both — and it does not block anything, which is why it was cut rather than rushed.Proposal
Refuse the route rather than failing the install.
BuildEndpointsalready does exactly this for reserved-prefix collisions:Follow that. An install-time failure would take down an otherwise working plugin over one mistyped route, and leave an operator with no move except uninstalling it. A refused route with a named reason is repairable and costs only the broken part.
Done when
plugin.executeBuildEndpointsis documented where the next person will look for itNot in scope
Anything about granting. That works — see #361.