Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Security Bundle Laravel Package

modera/security-bundle

Modera Security Bundle provides reusable security components for Symfony applications, including user authentication, role-based access control, and integration points for managing permissions in Modera-based projects. It helps centralize security configuration and common access-control workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

modera/security-bundle is not an admin UI or a drop-in ACL facade — it is the low-level plumbing: Doctrine-mapped User / Group / Permission entities plus a thin integration with Symfony's security layer (a permission checker and voter). There is effectively no documentation, so the source is the spec. Read three directories first: Entity/ (the domain model and relations), Security/ (the voter/checker — this is where enforcement actually happens), and DependencyInjection/Configuration.php (the only thing you can actually configure).

Reality check before you start. This is a Symfony 2.8/3.x bundle with Doctrine ORM mappings, last released June 2016, under a [READ-ONLY] archived-repo marker, with 3 stars and 0 dependents. If you are on Laravel, there is no seam where this installs: Symfony DI extension vs. service providers, Doctrine entities vs. Eloquent, Symfony voters vs. Gate/Policy. The honest day-to-day use is as a reference implementation — read it, reproduce the Users → Groups → Permissions shape natively, and move on. If you are on legacy Symfony and genuinely need it, the path below is the one.

If you actually install it (Symfony 2.8/3.x):

composer require modera/security-bundle
// app/AppKernel.php
new Modera\SecurityBundle\ModeraSecurityBundle(),

Point the bundle at your concrete entity subclasses (you almost always subclass the shipped User to add your own fields) — check Configuration.php for the exact tree; in Modera bundles it is normally an entities:/target-entity block:

# app/config/config.yml
modera_security:
    entities:
        user: AppBundle\Entity\User
        group: AppBundle\Entity\Group
        permission: AppBundle\Entity\Permission

Then:

app/console doctrine:schema:update --force
app/console doctrine:mapping:info   # confirm the target entities resolved

Wire the provider so the firewall authenticates your subclass:

# app/config/security.yml
providers:
    modera_user:
        entity: { class: AppBundle\Entity\User, property: username }

First use case: protect a controller with is_granted('ROLE_USER'), confirm it works, then immediately graduate to the real value — a fine-grained permission name stored in the Permission table and checked through the bundle's checker/voter (see below). Coarse role checks are table stakes; the whole point of this bundle is the group-scoped permission resolution underneath them.

Laravel first use case (the realistic one): skim the Doctrine annotations, write down the join shape (User ↔ Group many-to-many, Group ↔ Permission many-to-many), and recreate it as migrations + belongsToMany. That is roughly an afternoon of work and gets you the only durable thing this repo offers.

Implementation Patterns

Permission catalog as code, database as storage. The characteristic Modera pattern is a "contributions" provider: permissions are declared in code via a tagged service, and a sync step writes them into the Permission table so groups can reference rows rather than strings. Declare once, deploy, sync.

class PermissionsProvider implements PermissionsProviderInterface
{
    public function getPermissions()
    {
        return [
            'cms.articles.manage'  => ['category' => 'cms', 'label' => 'Manage articles'],
            'cms.articles.publish' => ['category' => 'cms', 'label' => 'Publish articles'],
        ];
    }
}
app.permissions_provider:
    class: AppBundle\Security\PermissionsProvider
    tags:
        - { name: modera_security.permissions_provider }

Then a console command (or a post-install/deploy hook) iterates the tagged providers and upserts rows by machine name. Machine names are the stable contract — never key anything off the row ID or the human label.

Enforcement at the boundary, not in the view. The bundle's checker resolves "does this user have permission X" from group membership; call it from exactly three places:

  • Controllers — a base controller helper or an isGranted-style check before dispatching a command.
  • Services — inject the checker and guard mutating methods, so a permission gap cannot be reached by a new entry point that forgot the controller check.
  • Twig — a small extension exposing the same check, purely to hide/disable UI. Never rely on template checks for security; the controller/service check must exist independently.

Group composition beats per-user grants. Use groups as the unit of administration: "Editors" gets cms.articles.manage, "Publishers" additionally gets cms.articles.publish. The delegated-admin use case (an internal admin panel where a customer admin manages their own team) maps to group assignment — the admin assigns users to groups, never touches permission rows. This keeps the permission catalog owned by engineering and the assignment owned by the customer.

Cache the effective permission set, invalidate on mutation. Permission resolution walks two many-to-many hops per check. Do not do that on every request. Compute the flattened set of permission machine names per user, store it (session, Redis, or a user_effective_permissions pivot), and recompute on: group membership change, group's permission set change, permission catalog sync. For multi-tenant systems, key the cache by (user_id, tenant_id).

Laravel translation — the pattern to actually ship. Same domain model, Laravel idioms:

// migrations: users, groups, permissions, group_user, group_permission
// models
class User extends Authenticatable {
    public function groups() { return $this->belongsToMany(Group::class); }
    public function permissions() { /* resolve via groups, cached */ }
}
// AppServiceProvider::boot()
Gate::before(function (User $user, string $ability) {
    return $user->hasPermission($ability) ? true : null; // null = defer, never false = deny-all
});
// policy usage stays idiomatic
$this->authorize('cms.articles.publish');

Define abilities as permission machine names so the check call sites read the same as they would on Symfony, and back the whole thing with spatie/laravel-permission if you want the pivot tables and cache management for free — it encodes the same Users → Roles → Permissions model and is maintained.

Strangler path for a legacy Symfony install. If a service already runs this bundle and you are moving authorization to a new stack: stand up the new layer in parallel, dual-write group/permission assignments to both stores, cut reads over per-endpoint behind a feature flag, and run a scheduled diff job comparing effective permissions per user between old and new until the cutover completes. Do not attempt to bridge Symfony's TokenInterface/security context into Laravel's — route through a shared data store instead.

Gotchas and Tips

This is an unmaintained security dependency — treat that as a finding, not a footnote. Last release 2016, no maintainer response path, no security disclosure channel, zero downstream consumers exercising edge cases. On the authorization boundary specifically, that is the worst category to carry: you inherit every future PHP minor bump, every framework advisory, and every password-hashing/timing-safety default as internal work. Budget custodianship vs. a one-time native build — the build wins within two upgrade cycles.

It will not install cleanly on PHP 8.x. Expect 2016-era Symfony/Doctrine constraints to fail resolution. If you force them with constraint overrides, breakage surfaces as runtime fatals, not install errors — which is far worse to debug. Do any exploratory spike in an isolated container, never in the application's dependency graph.

doctrine:mapping:info is your first debugging step, always. The most common failure is entity subclassing gone wrong: the bundle is configured with base classes, your mapping uses subclasses, and you get "table not found" or a silently wrong relation. Check that the entities: targets point at your subclasses and that mapping:info lists your class, not the vendor one.

Machine names, not IDs, not labels. Permissions in code, stored in DB, referenced by groups. If you key groups' permission assignments by row ID you break on every environment rebuild; if you key by label you break on every copy edit. Upsert by machine name in the sync step and treat renames as migrations.

N+1 on permission resolution is the default, not the edge case. Two many-to-many hops per is_granted call. Without an effective-permission cache you will see it in the query log the moment a list page renders per-row permission-dependent UI. Design the cache key and invalidation events before wiring the UI, because retrofitting invalidation across an existing group-management surface is the expensive part.

Stale permission cache is a security bug, not a performance bug. A user removed from a group retaining access is the classic failure. Invalidate on membership change, on the group's permission set change, and on catalog sync; add a bounded TTL as a backstop for anything you missed — including direct DB edits.

Fail closed, and prove it. Symfony's voters abstain by default and a missing/abstaining voter can read as permit depending on your access-decision strategy; Laravel's Gate::before returning false denies everything, returning null defers. Pick deny-by-default explicitly and add a test that asserts every route/controller action has an explicit policy. "Unregistered policy silently allows" is the failure mode this architecture invites.

Zero docs means the voter is the truth. When behavior is ambiguous, read Security/ — do not infer from the entity model, because the entity may expose a getPermissions() convenience that the voter does not actually use. Same for anything that looks like a config option: only what is in DependencyInjection/Configuration.php exists; anything else is hardcoded.

Extension points, such as they are. Tagged permission providers (catalog), the entity target-entity configuration (your subclasses), and the voter (override it in your bundle if you need a different resolution strategy — e.g. adding resource/ownership scoping on top of group permissions). Everything else is coupled to Symfony's security context; there is no adapter layer.

Schema drift during any migration. If a legacy store and the new tables both hold assignments, reconcile with a scheduled comparison job and alert on divergence — silent drift between the two authorization sources is invisible until an incident.

Expect to teach the model, not the package. The domain model is legible in an afternoon; the package around it is not worth learning. Ramp-up on the bundle itself yields little, because the durable knowledge is "groups scope permissions, resolve and cache at the boundary, enforce deny-by-default" — which transfers to any stack. Record the decision (extract the model, do not adopt the dependency) somewhere durable so it isn't re-litigated the next time someone links this repo.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console