fedale/access-control-bundle
Composer 安装命令:
composer require fedale/access-control-bundle
包简介
Symfony bundle for dynamic access control and authorization management
README 文档
README
Rule-based HTTP access control for Symfony. The bundle inspects every incoming request and allows or denies it according to a set of rules matched on path, host, HTTP method, client IP and user roles. Rules can come from any source — Doctrine (built-in), a YAML/array, a remote API, MongoDB… — because the lookup sits behind a single interface. An optional PSR-6 cache layer keeps rule loading cheap.
Features
- Allow/deny rules matched on path (regex), host (regex), HTTP methods, client IP / CIDR and roles.
- First-match-wins evaluation, ordered by an explicit
sortfield. - Source-agnostic: rules are read through
RuleProviderInterface. Doctrine is the default; any service can replace it. - Super-admin bypass and configurable anonymous-access gating.
- Default policy (
allow/deny) applied when no rule matches. - Optional PSR-6 caching of the rule set, configurable from YAML.
- Built on Symfony's native
ChainRequestMatcher— no deprecated APIs.
Requirements
- PHP 8.2+
- Symfony 6.4 or 7.x (
http-kernel,http-foundation,security-bundle,security-core) psr/cache^3.0- Doctrine ORM is only required when you use the default
doctrinerule provider (doctrine/orm,doctrine/doctrine-bundle). It is arequire-dev/suggestdependency, not a hard one.
Installation
composer require fedale/access-control-bundle
Not on Packagist yet. Until the package is published, point Composer at the repository directly in your application's
composer.json:{ "repositories": [ { "type": "vcs", "url": "https://github.com/Fedale/FedaleAccessControlBundle" } ] }then run
composer require fedale/access-control-bundle:dev-main.
If you use Symfony Flex, the bundle is registered automatically. Otherwise, add it to
config/bundles.php:
// config/bundles.php return [ // ... Fedale\AccessControlBundle\FedaleAccessControlBundle::class => ['all' => true], ];
Quick start
- Create the configuration file:
# config/packages/fedale_access_control.yaml fedale_access_control: enabled: true super_admin_role: ROLE_SUPER_ADMIN default_policy: deny # block anything no rule matched provider: doctrine # read rules from the database
- Create the database table and add a rule (see Doctrine provider).
For example, a rule that blocks
/adminfor everyone exceptROLE_ADMIN:
| name | path | roles | allow | sort |
|---|---|---|---|---|
| admin-area | ^/admin |
["ROLE_ADMIN"] |
1 |
0 |
- That's it. A request to
/adminfrom an anonymous user now receives:
HTTP/1.1 403 Forbidden
Access denied
while a user with ROLE_ADMIN (or ROLE_SUPER_ADMIN) passes through.
Configuration reference
All options with their defaults:
# config/packages/fedale_access_control.yaml fedale_access_control: # Master switch. When false the listener allows every request. enabled: true # When false, requests from unauthenticated users that match no rule are denied, # regardless of `default_policy`. See "Super admin & anonymous access". anonymous_access: false # Role that bypasses all rules. Set to an empty string to disable the bypass. super_admin_role: ROLE_SUPER_ADMIN # Policy applied when no rule matches the request: allow | deny default_policy: deny cache: # Enable/disable the PSR-6 cache decorator around the rule provider. enabled: true # Id of the PSR-6 cache pool used to store the rules. pool: cache.app # Time-to-live in seconds; null = no expiration (invalidate the pool manually). ttl: null # 'doctrine' (built-in) or the service id of a custom RuleProviderInterface # implementation (e.g. a YAML, API or Mongo source). provider: doctrine
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Turn the whole access control on/off. |
anonymous_access |
bool | false |
Allow unauthenticated users through on an unmatched request. |
super_admin_role |
string | ROLE_SUPER_ADMIN |
Role that bypasses every rule (empty string disables it). |
default_policy |
allow|deny |
deny |
Decision when no rule matches. |
cache.enabled |
bool | true |
Wrap the provider with a caching decorator. |
cache.pool |
string | cache.app |
PSR-6 pool service id. |
cache.ttl |
int|null | null |
Cache lifetime in seconds; null means no expiration. |
provider |
string | doctrine |
doctrine or a custom provider service id. |
How it works
The bundle registers a listener on the kernel.request event (priority -10). For every main
request it calls AccessDecisionManager::decide(), which evaluates:
- Bundle disabled? (
enabled: false) → allow. - Super admin? (current user is granted
super_admin_role) → allow. - Find the first matching rule (rules are evaluated in
sortorder, first match wins):- rule found and
allow = false→ deny; - rule found,
allow = true, no roles required → allow; - rule found,
allow = true, roles required → allow if the user is granted any of them, otherwise deny.
- rule found and
- No rule matched (fallback):
- user is not authenticated and
anonymous_access = false→ deny; - otherwise apply
default_policy(allow→ allow,deny→ deny).
- user is not authenticated and
When the decision is deny, the listener dispatches an AccessDeniedEvent and then
short-circuits the request with a 403 Forbidden response. If no listener provides its own
response, the body is content-negotiated: clients that send Accept: application/json get
{"error":"Access denied"}, everyone else gets the plain text Access denied.
The bundle stays outside Symfony's firewall: a denied request always yields a
403, it is never redirected to the login page. Authentication-driven redirects remain the firewall's job — this layer only answers "may this request proceed?".
Reacting to a denial (AccessDeniedEvent)
To log denials, emit metrics, or return a custom response (a branded error page, a redirect, a
different JSON shape), listen for AccessDeniedEvent. Setting a response on the event overrides
the default one:
use Fedale\AccessControlBundle\Event\AccessDeniedEvent; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\Response; #[AsEventListener] final class AccessDeniedSubscriber { public function __invoke(AccessDeniedEvent $event): void { // $event->getRequest() is available for inspection/logging. $event->setResponse(new Response('Custom forbidden page', Response::HTTP_FORBIDDEN)); } }
Defining rules
A rule is described by the AccessRule DTO. Whatever the source, it produces objects with these
fields:
| Field | Type | Meaning |
|---|---|---|
id |
int | Identifier (from the source). |
name |
string | Human-readable label. |
reason |
?string | Optional explanation (e.g. why access is blocked); null when unset. |
path |
string | Regular expression matched against the request path (e.g. ^/admin). |
host |
?string | Optional regular expression matched against the host. |
roles |
string[] | Required roles; empty means "no role required". |
methods |
string[] | Allowed HTTP methods (e.g. ["GET", "POST"]); empty means any. |
ips |
string[] | Allowed IPs or CIDR ranges (e.g. ["10.0.0.0/8"]); empty means any. |
allow |
bool | true = allow (subject to roles), false = deny. |
sort |
int | Evaluation order (ascending). Lower runs first. |
active |
bool | Inactive rules are ignored. |
Matching is delegated to Symfony's ChainRequestMatcher: path and host are treated as
regular expressions, while methods and ips only constrain the request when non-empty.
Invalid path/host regular expressions are rejected as soon as the rule is built, with an
InvalidArgumentException naming the offending rule.
Client IP behind a proxy. The
ipsfield matches againstRequest::getClientIp(). If your app sits behind a reverse proxy or load balancer, configure Symfony's trusted proxies (framework.trusted_proxies/trusted_headers) — otherwisegetClientIp()returns the proxy's address and your IP rules will match the wrong client.
Rule sources (providers)
The decision logic never talks to a database directly — it depends only on
RuleProviderInterface:
namespace Fedale\AccessControlBundle\Contract; use Fedale\AccessControlBundle\Dto\AccessRule; interface RuleProviderInterface { /** @return iterable<AccessRule> */ public function getRules(): iterable; }
The provider option selects which implementation is wired.
Doctrine (default provider)
With provider: doctrine the rules are read from the access_control table, mapped by the
AccessControlEntity. Only active rules are returned, ordered by sort.
The bundle registers its own Doctrine ORM mapping automatically (via prependExtension)
when provider: doctrine and DoctrineBundle is installed — you do not need to add a
doctrine.orm.mappings entry pointing into vendor/ in your application. The mapping is
skipped automatically if you switch to a custom provider.
Create the schema with a migration (recommended):
php bin/console make:migration php bin/console doctrine:migrations:migrate
or, in development:
php bin/console doctrine:schema:update --force
Add rules like any other entity:
use Fedale\AccessControlBundle\Bridge\Doctrine\Entity\AccessControlEntity; $rule = (new AccessControlEntity()) ->setName('admin-area') ->setReason('Admins only') ->setPath('^/admin') ->setRoles(['ROLE_ADMIN']) ->setMethods([]) // any method ->setIps([]) // any IP ->setAllow(true) ->setSort(0) ->setActive(true); $entityManager->persist($rule); $entityManager->flush();
The setters normalise input for you (trim strings, de-duplicate roles/IPs, upper-case methods).
Custom provider (YAML, API, Mongo, …)
To read rules from anywhere else, implement RuleProviderInterface and point provider at your
service id. Here is a provider that returns rules defined in plain PHP:
// src/Security/StaticRuleProvider.php namespace App\Security; use Fedale\AccessControlBundle\Contract\RuleProviderInterface; use Fedale\AccessControlBundle\Dto\AccessRule; final class StaticRuleProvider implements RuleProviderInterface { public function getRules(): iterable { yield new AccessRule( id: 1, name: 'admin-area', reason: 'Admins only', path: '^/admin', host: null, roles: ['ROLE_ADMIN'], methods: [], ips: [], allow: true, sort: 0, active: true, ); yield new AccessRule( id: 2, name: 'block-internal', reason: 'Internal API is private', path: '^/internal', host: null, roles: [], methods: [], ips: [], allow: false, sort: 10, active: true, ); } }
If autowiring/autoconfiguration is on (the Symfony default), the service id equals the class name. Select it:
# config/packages/fedale_access_control.yaml fedale_access_control: provider: 'App\Security\StaticRuleProvider'
With a custom provider you do not need Doctrine at all.
The selected provider must resolve to a real service; otherwise the
RuleProviderInterfacealias is missing and the container fails to boot.
Caching
When cache.enabled is true, the selected provider is transparently wrapped in a
CachedRuleProvider that stores the materialised rule list in a PSR-6 pool. This works for
any source (Doctrine, custom, …) because it decorates the interface, not the implementation.
fedale_access_control: cache: enabled: true pool: cache.app # any PSR-6 pool service ttl: 3600 # refresh hourly; null = never expires
Invalidation. With ttl: null the cache never expires, so after changing your rules you
must clear it. Either clear the configured pool, or delete the single cache key used by the
bundle:
use Fedale\AccessControlBundle\Cache\CachedRuleProvider; $pool->deleteItem(CachedRuleProvider::CACHE_KEY);
To turn caching off entirely, set cache.enabled: false.
Super admin & anonymous access
- Super admin — any user granted
super_admin_role(defaultROLE_SUPER_ADMIN) bypasses every rule. Setsuper_admin_role: ''to remove the bypass. - Anonymous access — only affects requests that match no rule. With
anonymous_access: false, unauthenticated users are denied on an unmatched request even ifdefault_policy: allow. Authenticated users instead fall through todefault_policy.
Example — site that is open by default, but never to anonymous users on unknown routes:
fedale_access_control: anonymous_access: false default_policy: allow
Explicit allow rules (e.g. for ^/login) are always evaluated first, so public routes keep
working.
Architecture / extension points
Everything is wired through small contracts under src/Contract/, so you can override any piece:
| Interface | Responsibility |
|---|---|
RuleProviderInterface |
Where rules come from (Doctrine, YAML, API, …). |
RuleRequestMatcherInterface |
Whether a single rule matches a request. |
AccessMatcherInterface |
Pick the first matching rule for a request. |
AccessDecisionManagerInterface |
Turn a request into an allow/deny decision. |
On every denial the listener dispatches AccessDeniedEvent, so you can hook in logging, metrics
or a custom response without replacing any of the above (see Reacting to a denial).
Default implementations live in src/Matcher/, src/Security/ and src/Cache/. To replace
one, register your own service aliased to the relevant interface.
Console
List the effective rules as the bundle sees them (read-only, works with any provider):
php bin/console fedale:access-control:list
sort id name path host methods ips roles policy active
0 1 admin-area ^/admin * * * ROLE_ADMIN allow yes
10 2 block-int ^/internal * * 10.0.0.0/8 - deny yes
With the Doctrine provider only active rules are listed; with caching enabled you see the materialised (cached) list. Enabling/disabling rules is plain entity manipulation — see Doctrine provider.
A Symfony Profiler data collector (showing which rule matched and the resulting decision) is a natural next extension but is not bundled yet, to keep the dependency surface small.
Testing
composer install vendor/bin/phpunit
The suite covers unit (matchers, decision manager, cache), DI wiring, a functional test booting
a real HttpKernel, and ORM mapping validation.
License & contributing
Released under the MIT License. Issues and pull requests are welcome.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-15