Permissions Component

softspring/permissions-bundle is a small security component packaged as a Symfony bundle.

Its purpose is to make attributes that start with PERMISSION_ work like hierarchical security checks in Symfony.

That sounds small, but it creates a consistent authorization model across the Softspring ecosystem:

  • application roles stay as ROLE_*
  • action-level permissions use PERMISSION_*
  • reusable packages can publish stable permission names in role_hierarchy
  • applications can aggregate those permissions into business roles
  • custom voters can still deny or refine access for concrete subjects

In practice, this is what makes checks such as is_granted('PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE', $media) behave as expected.

Why Treat It As A Component

This package is better understood as a component than as an application-facing bundle because it does not provide:

  • screens
  • routes
  • controllers
  • persistence
  • admin UI
  • end-user workflows

It contributes one infrastructure capability to Symfony Security and is meant to be reused by higher-level bundles and applications.

Installation

composer require softspring/permissions-bundle:^6.0

If your application does not use Symfony Flex, enable it manually:

<?php

return [
    // ...
    Softspring\PermissionsBundle\SfsPermissionsBundle::class => ['all' => true],
];

What The Component Adds

The component registers one service:

  • a RoleHierarchyVoter
  • configured with the prefix PERMISSION_
  • under the service id sfs_permissions.role_hierarchy.permission

That is the whole core.

It does not add:

  • a permission database
  • user-to-permission persistence
  • ACL tables
  • a UI to assign permissions
  • a custom configuration tree

How It Works

Symfony already has a role hierarchy voter for ROLE_*.

This component registers a second hierarchy voter for the PERMISSION_ prefix.

That means a check like:

$this->denyAccessUnlessGranted('PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE');

can be granted through role_hierarchy, even if the user does not literally have that string in the stored roles array.

Example:

security:
    role_hierarchy:
        ROLE_SFS_MEDIA_ADMIN_MEDIAS_RW:
            - ROLE_SFS_MEDIA_ADMIN_MEDIAS_RO
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_CREATE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_DELETE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_MIGRATE

With the component enabled, a user with ROLE_SFS_MEDIA_ADMIN_MEDIAS_RW is also granted PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE.

No Custom Configuration

There is no sfs_permissions: config block to define.

The component works through standard Symfony Security configuration:

  • security.role_hierarchy
  • is_granted()
  • denyAccessUnlessGranted()
  • custom voters

Why Use PERMISSION_* Instead Of ROLE_*

Using PERMISSION_* creates a clean separation between:

  • business or user roles
    • ROLE_ADMIN
    • ROLE_EDITOR
    • ROLE_SFS_MEDIA_ADMIN_MEDIAS_RW
  • atomic allowed actions
    • PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_LIST
    • PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE
    • PERMISSION_SFS_USER_ADMIN_USERS_PROMOTE

That separation gives practical benefits:

  • reusable packages can ship stable permission names without deciding your final user roles
  • applications can compose roles from permissions
  • controllers and templates can check the exact action they need
  • subject-specific voters can deny one permission without redefining the whole role model

Main Usage Pattern

The standard Softspring pattern has three layers:

  1. each package publishes atomic PERMISSION_* names
  2. each package groups them into convenience ROLE_* roles when it makes sense
  3. the application grants those roles to users or aggregates them into broader business roles

Package-Level Permission Groups

For example, media-bundle defines:

security:
    role_hierarchy:
        ROLE_SFS_MEDIA_ADMIN_MEDIAS_RO:
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_LIST
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_DETAILS
        ROLE_SFS_MEDIA_ADMIN_MEDIAS_RW:
            - ROLE_SFS_MEDIA_ADMIN_MEDIAS_RO
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_CREATE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_DELETE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE
            - PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_MIGRATE

Application-Level Aggregation

Projects such as armonic-standalone then aggregate package roles into final application roles:

security:
    role_hierarchy:
        ROLE_ADMIN:
            - ROLE_SFS_USER_ADMIN_USERS_RW
            - ROLE_SFS_MEDIA_ADMIN_MEDIAS_RW
            - ROLE_SFS_CMS_ADMIN_BLOCKS_RW
            - ROLE_SFS_CMS_ADMIN_CONTENTS_RW

This lets packages stay decoupled from your final business role model.

Real Usage Cases

Declarative CRUD Configuration

Reusable controller config often checks permissions directly:

list:
    is_granted: 'PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_LIST'
create:
    is_granted: 'PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_CREATE'
update:
    is_granted: 'PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_UPDATE'
delete:
    is_granted: 'PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_DELETE'

This is a good default pattern for reusable packages because it keeps permissions stable and lets each application decide how to grant them.

Direct Controller Checks

When one action does not fit a generic CRUD flow, check the permission directly:

$this->denyAccessUnlessGranted('PERMISSION_SFS_USER_ADMIN_USERS_PROMOTE', $user);

This keeps the controller explicit while still allowing hierarchy-based grants and subject-specific denials.

Twig Templates

Templates can use the same permission names:

{% if is_granted('PERMISSION_SFS_USER_ADMIN_USERS_UPDATE', user) %}
    <a href="...">Update</a>
{% endif %}

The template does not need to know whether the grant came from:

  • a direct role
  • role hierarchy
  • a custom voter

Declarative Menus

The same permissions work well in config-driven menus:

users:
    route: 'sfs_user_admin_users_list'
    role: PERMISSION_SFS_USER_ADMIN_USERS_LIST

This is useful when visibility should automatically follow authorization rules.

Combining Permissions With Custom Voters

This component grants permission attributes through hierarchy, but it does not stop you from adding more specific voters.

That is how several Softspring packages work.

For example, one voter can say the user is broadly allowed to recompile content, while another voter denies recompilation for one specific subject because it is disabled in the current state.

This gives you a layered model:

  1. role hierarchy says the user is broadly allowed
  2. a domain voter says this concrete subject is still not allowed right now

Why unanimous Works Well

Several Softspring applications use:

security:
    access_decision_manager:
        strategy: unanimous

That fits well with PERMISSION_* checks because:

  • the hierarchy voter can grant the permission
  • a subject-specific voter can deny it
  • under unanimous, the deny wins

Naming Convention

The common naming pattern is:

PERMISSION_<VENDOR OR APP>_<AREA>_<RESOURCE>_<ACTION>

Examples:

  • PERMISSION_SFS_MEDIA_ADMIN_MEDIAS_LIST
  • PERMISSION_SFS_ACCOUNT_ADMIN_ACCOUNTS_UPDATE
  • PERMISSION_SFS_USER_ADMIN_USERS_PROMOTE
  • PERMISSION_SFS_CMS_ADMIN_SECTION_VERSION_DELETE

This matters because permission names become part of the reusable API exposed by each package.

Limitations

These limits are deliberate:

  • there is no persistence layer
  • there is no admin UI
  • there is no object-level logic by itself
  • only the PERMISSION_ prefix is handled

If you need dynamic permission assignment or object-specific rules, build that in your application and combine it with Symfony voters.