Extension points
phil/route-access-checkThe library has three ports, interfaces of Phil\RouteAccessCheck\Contract. In Symfony,
phil/route-access-check-bundle tags a service implementing AccessRuleDetector or ReportFormatter by
autoconfiguration.
AccessRuleDetector
A source of access rules of the application's own: a voter that guards a whole controller namespace, a signed-URL listener, a gateway in front of some routes.
interface AccessRuleDetector
{
/**
* @return iterable<AccessRule>
*/
public function detect(AnalysedRoute $route): iterable;
}
- The detector is asked once per route, in any order, and keeps no state between routes.
- It reads
$route->route(the Symfony route: options, defaults, requirements),$route->methods(the labels to cover),$route->controller(its class, action, file, line and attributes, read by name withattributesNamed()), and the sample path and host. - Each rule says its
source, itsAccessEffect, itsRuleScope, adescriptiona reader recognises, and the method labels it applies to —nullfor all of them. A protected, public or unsecured rule covers; an unchecked or conditional one only informs. - A rule written on the route (
RuleScope::Route) that is public conflicts with a check on the same method; a rule of the configuration does not.
A detector for the routes whose option signed is true, checked by a listener of the application:
use Phil\RouteAccessCheck\AccessEffect;
use Phil\RouteAccessCheck\AccessRule;
use Phil\RouteAccessCheck\AnalysedRoute;
use Phil\RouteAccessCheck\Contract\AccessRuleDetector;
use Phil\RouteAccessCheck\RuleScope;
final readonly class SignedUrlDetector implements AccessRuleDetector
{
public function detect(AnalysedRoute $route): iterable
{
if (true === $route->route->getOption('signed')) {
yield new AccessRule('signed_url', AccessEffect::Protected, RuleScope::Route, 'a signed URL, checked by SignedUrlListener');
}
}
}
A detector of attributes on the controller:
foreach ($route->controller?->attributesNamed(RequiresSubscription::class) ?? [] as [$attribute, $onAction]) {
yield new AccessRule('subscription', AccessEffect::Protected, RuleScope::Route, '#[RequiresSubscription] on '.($onAction ? $route->controller->label() : $route->controller->class));
}
When the attribute takes the arguments of #[IsGranted], SecurityAttributeDetector reads it without a detector of
its own: give it the attribute's class name.
RuleInventory
A detector whose rules are declared ahead of the routes implements it too, so that the rules no route reaches are reported:
interface RuleInventory
{
/**
* @return array<string, string> each declared rule's description, by its ruleId
*/
public function declaredRules(): array;
}
The detector gives each AccessRule it returns the ruleId of the declared rule it applied. SecurityConfigDetector
declares the access_control entries as access_control#<position>.
ReportFormatter
A format of the application's own, under a name the console command's --format takes:
interface ReportFormatter
{
public function name(): string;
public function format(Report $report, bool $onlyProblems = false): string;
}
A format that lists the failures as CSV for a spreadsheet:
final readonly class CsvFormatter implements ReportFormatter
{
public function name(): string
{
return 'csv';
}
public function format(Report $report, bool $onlyProblems = false): string
{
$lines = "route,status,problem\n";
foreach ($report->failures() as $verdict) {
$lines .= sprintf("%s,%s,\"%s\"\n", $verdict->route->name, $verdict->status->value, str_replace('"', '""', $verdict->problem()));
}
return $lines;
}
}
The ignore list
IgnoreList is not an interface: it takes the prefixes of the route names and paths to leave aside.
IgnoreList::FRAMEWORK_ROUTE_NAMES and IgnoreList::FRAMEWORK_PATHS are its defaults — the profiler, the web debug
toolbar and the error previews —, to spread into a list of the application's own. An empty prefix is refused: it
would leave every route aside.