Important

You are browsing upcoming documentation for version 6.0 of OroCommerce, OroCRM, and OroPlatform, scheduled for release in 2024. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

Introduction to Security in Oro Applications 

The OroSecurityBundle sits on top of the Symfony security layer to protect your resources. Each application user is granted access to a particular subset of your company’s resources. Coincidentally, they have to be prevented from accessing resources when access was not granted to them.

Access Control Lists 

Access Control Lists are an essential part of the Symfony Security Components. The OroSecurityBundle leverages them to fulfill the requirements of companies in the business context.

Hint

You can find detailed information about Symfony ACL-based security model in the Symfony documentation.

Access Levels 

Access can be granted to a user for a particular resource on several levels. The lowest level is the User level. Being on this level means that users can only access resources assigned to them. At the other end of the hierarchy is the Global level. Users at this level can access all records within the system without exception. The security bundle comes with the following five levels (ordered up from the bottom of the hierarchy):

Level

Constant

Description

User

BASIC_LEVEL

The user is granted access to their own records.

Business Unit

LOCAL_LEVEL

The user is given access to the records in all business units they are assigned to.

Division

DEEP_LEVEL

This is the same as the Business Unit level except that the user can also access all resources that are owned by subordinate units of the business units they are assigned to.

Organization

GLOBAL_LEVEL

The user is given access to all records within the organization, regardless of the business unit the object belongs to or the user is assigned to.

Global

SYSTEM_LEVEL

The user can access all objects within the system.

Note

This level is available only in Enterprise editions. Global access level makes sense if the user works in the scope of a global organization. If the user works in the scope of an ordinary organization, global level equals organization.

Each record is associated with an owning organization. When a user logs into the system, they work in the scope of one of their organizations.

Note

For all access levels, a class constant is defined in the Oro\Bundle\SecurityBundle\Acl\AccessLevel class. Its value is shown in the Constant column.

There are two special constants AccessLevel::UNKNOWN (unknown access level, should not be assigned to a user) and AccessLevel::NONE_LEVEL (globally deny access for the user).

Permissions 

A user can be assigned different modes to a resource. These modes describe what they are allowed to do with the resource. Namely, the following permissions are supported for entities:

Permission

VIEW

Whether or not a user is allowed to view a record.

CREATE

Whether or not a user is allowed to create a record. The access level set for this permission limits the number of owners that can be assigned to a created record.

EDIT

Whether or not a user is allowed to modify a record.

DELETE

Whether or not a user is allowed to delete a record.

ASSIGN

Whether or not a user is allowed to assign a record to another user. This permission is only evaluated when an entity is edited.

SHARE

Whether or not a user is allowed to assign a record to another user. Only works on entities’ view pages if the feature is enabled.

Note

This permission is available only in Enterprise editions.

See also

Read the official documentation for a first insight into the usage of ACLs examples.

The following permissions are supported for fields:

Permission

VIEW

Whether or not a user is allowed to view a field.

EDIT

Whether or not a user is allowed to modify a field.

Ownership Type 

Each ACL-protected entity must have an ownership type. Various entities can act as one, such as user business unit, organization. If the type of owner is not specified, but the entity is ACL-protected, this type is called None. The set of access levels available for permissions of this entity changes depending on the ownership type.

The following table shows what access levels can be assigned depending on the entity’s ownership type:

Ownership type

Possible access levels for an entity with this ownership type

User

None, User, Business Unit, Division, Organization, Global

Business Unit

None, Business Unit, Division, Organization, Global

Organization

None, Organization, Global

None

None, Global

Although ownership types uses the same concepts as an access level, their impact is different. For example:

  • The None ownership type gives the broadest access to entity records. It means this record does not belong to any particular organization, business unit, or user. Therefore, all users can access it, or no one at all.’

  • The None access level completely restricts access to entity records, so no one can perform this action on the entity.

Every record of a security-protected entity with ownership type User, Business Unit, and Organization has an organization.

Remember that once the entity is created, you can no longer change its ownership type. Consequently, you cannot change the predefined ownership types of system entities (such as an account or a business unit).

Configuring Permissions for Entities 

To be able to protect access to your entities, you first have to configure which permissions can be granted to a user to them. Use the security scope in the defaultValues section of the #[Config] attribute:

src/Acme/Bundle/DemoBundle/Entity/Favorite.php 
#[Config(
    defaultValues: [
        'security' => ['type' => 'ACL', 'permissions' => 'All', 'group_name' => '', 'category' => ''],
        'dataaudit' => ['auditable' => true]
    ]
)]

Note

After changing ACL in the Config annotation, run the oro:entity-config:update command in the console to apply changes.

The permissions parameter is used to specify the access list for the entity. This parameter is optional. If it is not specified, or is All, it is considered that the entity access to all available security permissions.

You can create your list of accesses. For example, string VIEW;EDIT will set viewing and editing permissions parameters for the entity.

The group_name parameter is used to group entities by applications. It is used to split security into application scopes.

The category parameter is used to categorize an entity. It is used to split entities by section on the role privileges edit page.

By default (or when using the special ALL value for the permissions property as in the example above), any available permission can be granted to a user on an entity. If you want to restrict the available permissions for an entity, you can list them separated. For example, you limit it to the VIEW and EDIT permissions:

...
    'security' => [
        'type' => 'ACL',
        'permissions' => 'VIEW;EDIT',
        'group_name' => 'DemoGroup',
    ]
...

Once an entity is marked as ACL-protected, you need to specify its ownership type. It is done with the help of the ownership scope in the defaultValues section.

In this config, you should specify the ownership type that will be used for the entity, as well as the names of the columns in the database and fields that will be used to store the link to the owner of the record and the organization where this record was created.

For example, the config will be the following for the USER owner type:

src/Acme/Bundle/DemoBundle/Entity/Favorite.php 
#[Config(
    defaultValues: [
        'ownership' => [
            'owner_type' => 'USER',
            'owner_field_name' => 'owner',
            'owner_column_name' => 'user_owner_id',
            'organization_field_name' => 'organization',
            'organization_column_name' => 'organization_id'
        ],
    ]
)]

For the business unit owner type:

#[Config(
    defaultValues: [
        ...
        'ownership' => [
            'owner_type' => 'BUSINESS_UNIT',
            'owner_field_name' => 'owner',
            'owner_column_name' => 'owner_id',
            'organization_field_name' => 'organization',
            'organization_column_name' => 'organization_id'
        ]
    ]
)]

For an Organization owner type, you can specify only the owner_field_name and owner_column_name:

#[Config(
    defaultValues: [
        ...
        'ownership' => [
            'owner_type' => 'ORGANIZATION',
            'owner_field_name' => 'owner',
            'owner_column_name' => 'owner_id'
        ]
    ]
)]

Important

For the User and Business Unit ownership types, organization fields are mandatory.

Protecting Resources 

After configuring which permissions a user can be granted to a particular entity, you have to ensure that the permissions are considered when checking if a user has access to a resource. Such checks must be placed in the right places in the code.

Restricting Access to Controller Methods 

Suppose you have configured an entity to be protectable via ACLs. You have granted some of its objects to a set of users. Now you can control who can enter specific resources through the controller method. Restricting access can be done in two different ways:

  1. Use the #[Acl] attribute on a controller method, providing the entity class name and the permission to check for:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
<?php

namespace Acme\Bundle\DemoBundle\Controller;

use Acme\Bundle\DemoBundle\Entity\Favorite;
use Oro\Bundle\EntityBundle\ORM\DoctrineHelper;
use Oro\Bundle\SecurityBundle\Attribute\Acl;
use Oro\Bundle\SecurityBundle\Attribute\AclAncestor;
use Oro\Bundle\SecurityBundle\Attribute\CsrfProtection;
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Contains CRUD actions for Favorite
 */
#[Route(path: '/favorite', name: 'acme_demo_favorite_')]
class FavoriteController extends AbstractController
{
    #[Route(path: '/', name: 'index')]
    #[Template]
    #[AclAncestor('acme_demo_favorite_index')]
    public function indexAction(): array
    {
        return ['entity_class' => Favorite::class];
    }
}
  1. When you need to perform a particular check repeatedly, write #[Acl] repeatedly. This, however, is tedious, especially when your requirements change and you have to change a lot of ACLs.

    The ACL configuration from the example above looks like this:

src/Acme/Bundle/DemoBundle/config/oro/acls.yml 
acls:
    favorites_edit:
        type: entity
        class: Acme\Bundle\DemoBundle\Entity\Favorite
        permission: EDIT

Attribute #[AclAncestor] enables you to reuse ACL resources defined with the ACL attribute or described in the acls.yml file. The name of the ACL resource is used as the parameter of this attribute:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
<?php

namespace Acme\Bundle\DemoBundle\Controller;

use Acme\Bundle\DemoBundle\Entity\Favorite;
use Oro\Bundle\EntityBundle\ORM\DoctrineHelper;
use Oro\Bundle\SecurityBundle\Attribute\Acl;
use Oro\Bundle\SecurityBundle\Attribute\AclAncestor;
use Oro\Bundle\SecurityBundle\Attribute\CsrfProtection;
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Contains CRUD actions for Favorite
 */
#[Route(path: '/favorite', name: 'acme_demo_favorite_')]
class FavoriteController extends AbstractController
{
    #[Route(path: '/new-edit', name: 'new_edit')]
    #[Template('@AcmeDemo/Favorite/index.html.twig')]
    #[AclAncestor('acme_demo_favorite_new_edit')]
    public function newEditAction()
    {
        $entity = $this->getUser();
        if (!$this->isGranted('VIEW', $entity)) {
            throw new AccessDeniedException();
        }
        // check access to the given entity field
        $authorizationChecker = $this->container->get('security.authorization_checker');
        if (!$authorizationChecker->isGranted('VIEW', new FieldVote($entity, '_field_name_'))) {
            throw new AccessDeniedException('Access denied');
        }

        return ['entity_class' => Favorite::class];
    }
}

Sometimes you want to protect a controller method from code you do not control. Therefore, you cannot add the #[AclAncestor] attribute to it. Use the bindings key in the YAML configuration of your ACL to define which method(s) should be protected:

src/Acme/Bundle/DemoBundle/Resources/config/oro/acls.yml 
acls:
    favorites_edit:
        type: entity
        class: Acme\Bundle\DemoBundle\Entity\Favorite
        permission: EDIT
        bindings:
            -   class: Acme\Bundle\DemoBundle\Controller\FavoritesController
                method: newEditAction

You can read detailed explanations for all available YAML configuration options in the reference section.

Using Param Converters

  • If the parameters of the method have an entity that is also used in the parameters of the ACL annotation, then access is checked directly on this object.

  • If the parameters of the method have no type that is used in the annotation, then a check is performed at the class level when it is checked whether the user has access to this type of an entity, rather than to a specific instance of an entity.

See also

It is also possible to protect Doctrine queries.

Data Grids 

You can protect a datasource with ACL by adding the acl_resource parameter under the source node in the datagrid configuration:

src/Acme/Bundle/DemoBundle/Resources/config/oro/datagrids.yml 
datagrids:
    acme-demo-question-grid-base:
        extended_entity_name: Acme\Bundle\DemoBundle\Entity\Question
        acl_resource: acme_demo_question_view

Protecting Custom DQL Queries 

When building custom DQL queries, reduce the result set being returned to the set of domain objects to which the user is granted access. To achieve this, use the ACL helper provided by the OroSecurityBundle:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
<?php

namespace Acme\Bundle\DemoBundle\Controller;

use Acme\Bundle\DemoBundle\Entity\Favorite;
use Oro\Bundle\EntityBundle\ORM\DoctrineHelper;
use Oro\Bundle\SecurityBundle\Attribute\Acl;
use Oro\Bundle\SecurityBundle\Attribute\AclAncestor;
use Oro\Bundle\SecurityBundle\Attribute\CsrfProtection;
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Contains CRUD actions for Favorite
 */
#[Route(path: '/favorite', name: 'acme_demo_favorite_')]
class FavoriteController extends AbstractController
{
    #[Route(path: '/protected', name: 'protected')]
    #[CsrfProtection]
    #[Template('@AcmeDemo/Favorite/index.html.twig')]
    #[Acl(id: 'acme_demo_favorite_protected_action', type: 'action')]
    public function protectedAction()
    {
        $repository = $this->container->get(DoctrineHelper::class)
            ->getEntityManager(Favorite::class)
            ->getRepository(Favorite::class);
        $queryBuilder = $repository
            ->createQueryBuilder('f')
            ->where('f.viewCount > :viewCount')
            ->orderBy('f.viewCount', 'ASC')
            ->setParameter('viewCount', 6);
        $aclHelper = $this->container->get(AclHelper::class);
        $query = $aclHelper->apply($queryBuilder, 'VIEW');

        return [
            'data' => $query->getResult()
        ];
    }
}

In this example, a query is built that selects all products from the database that cost more than 19.99. Then, the query builder is passed to the apply() method of the oro_security.acl_helper service. This service, an instance of the Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper class modifies the query only to the return entities to which the user has access.

Manual Access Checks 

Sometimes it is impossible to do an ACL check in the controller using annotations due to additional conditions.

In this case, you can use the isGranted function:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
<?php

namespace Acme\Bundle\DemoBundle\Controller;

use Acme\Bundle\DemoBundle\Entity\Favorite;
use Oro\Bundle\EntityBundle\ORM\DoctrineHelper;
use Oro\Bundle\SecurityBundle\Attribute\Acl;
use Oro\Bundle\SecurityBundle\Attribute\AclAncestor;
use Oro\Bundle\SecurityBundle\Attribute\CsrfProtection;
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Contains CRUD actions for Favorite
 */
#[Route(path: '/favorite', name: 'acme_demo_favorite_')]
class FavoriteController extends AbstractController
{
    #[Route(path: '/new-edit', name: 'new_edit')]
    #[Template('@AcmeDemo/Favorite/index.html.twig')]
    #[AclAncestor('acme_demo_favorite_new_edit')]
    public function newEditAction()
    {
        $entity = $this->getUser();
        if (!$this->isGranted('VIEW', $entity)) {
            throw new AccessDeniedException();
        }
        // check access to the given entity field
        $authorizationChecker = $this->container->get('security.authorization_checker');
        if (!$authorizationChecker->isGranted('VIEW', new FieldVote($entity, '_field_name_'))) {
            throw new AccessDeniedException('Access denied');
        }

        return ['entity_class' => Favorite::class];
    }
}

If you need to carry out an ACL check on an object not in the controller, use the isGranted method of the security.authorization_checker service.

The security.authorization_checker service is a public service used to check whether access to a resource is granted or denied. This service represents the Authorization Checker. The implementation of the Platform specific attributes and objects is in AuthorizationChecker class.

The main entry point is the isGranted method:

isGranted($attribute, $subject = null)

$attribute can be a role name, permission name, an ACL annotation id, a string in format permission;descriptor (e.g., VIEW;entity:Acme\DemoBundle\Entity\AcmeEntity or EXECUTE;action:acme_action) or some other identifiers depending on registered security voters.

$object can be an entity type descriptor (e.g., entity:Acme/Bundle/DemoBundle/Entity/AcmeEntity or action:some_action), an entity object, instance of ObjectIdentity, DomainObjectReference or DomainObjectWrapper

Examples

Checking access to some ACL annotation resource

$this->authorizationChecker->isGranted('some_resource_id')

Checking VIEW access to the entity by class name

$this->authorizationChecker->isGranted('VIEW', 'entity:' . MyEntity::class);

Checking VIEW access to the entity’s field

$this->authorizationChecker->isGranted('VIEW', new FieldVote($entity, $fieldName));

Checking ASSIGN access to the entity object

$this->authorizationChecker->isGranted('ASSIGN', $myEntity);

Checking access is performed in the following way: Object-Scope->**Class-Scope**->**Default Permissions**.

For example, we are checking View permission to $myEntity object of MyEntity class. When we call

$this->authorizationChecker->isGranted('VIEW', $myEntity);

The first ACL for $myEntity object is checked; if nothing is found, it checks the ACL for the MyEntity class. It checks the Default(root) permissions if no records are found.

Two additional authorization checkers can also be helpful:

Restricting Access to Non-Entity Resources 

Sometimes, you only want to allow or deny access to a specific part of your application without protecting an entity. To achieve this, use a particular action type for an ACL:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
    #[Route(path: '/protected', name: 'protected')]
    #[CsrfProtection]
    #[Template('@AcmeDemo/Favorite/index.html.twig')]
    #[Acl(id: 'acme_demo_favorite_protected_action', type: 'action')]
    public function protectedAction()
    {
        $repository = $this->container->get(DoctrineHelper::class)
            ->getEntityManager(Favorite::class)
            ->getRepository(Favorite::class);
        $queryBuilder = $repository
            ->createQueryBuilder('f')
            ->where('f.viewCount > :viewCount')
            ->orderBy('f.viewCount', 'ASC')
            ->setParameter('viewCount', 6);
        $aclHelper = $this->container->get(AclHelper::class);
        $query = $aclHelper->apply($queryBuilder, 'VIEW');

        return [
            'data' => $query->getResult()
        ];
    }
src/Acme/Bundle/DemoBundle/Resources/config/oro/acls.yml 
acls:
    protected_action:
        type: action

Manual Access Check on an Object Field 

The developer can check access to the given entity field by passing the instance FieldVote class to the isGranted method of the Authorization Checker:

src/Acme/Bundle/DemoBundle/Controller/FavoriteController.php 
    #[Route(path: '/new-edit', name: 'new_edit')]
    #[Template('@AcmeDemo/Favorite/index.html.twig')]
    #[AclAncestor('acme_demo_favorite_new_edit')]
    public function newEditAction()
    {
        $entity = $this->getUser();
        if (!$this->isGranted('VIEW', $entity)) {
            throw new AccessDeniedException();
        }
        // check access to the given entity field
        $authorizationChecker = $this->container->get('security.authorization_checker');
        if (!$authorizationChecker->isGranted('VIEW', new FieldVote($entity, '_field_name_'))) {
            throw new AccessDeniedException('Access denied');
        }

        return ['entity_class' => Favorite::class];
    }

Check ACL for Search Queries 

When collecting entities to search, information about the owner and the organization is automatically added to the search index.

Every search query is ACL protected with Search ACL helper. This helper limits data with the current access levels for entities used in the query.

Organization Context 

As mentioned previously, each record is associated with an owning organization. When a user logs into the system, they work in the scope of one of their organizations.

In Enterprise editions, a user can be assigned to multiple organizations.

During the login, if the security token supports organizations, then the current organization in the token is replaced with the preferred one. The first available organization is used if a user logs into the system for the first time.

After the login, the user can switch their current organization.

For the security token to ignore the preferable organization, for example, an API token, add its class name to the ignore_preferred_organization_tokens parameter of the OrganizationPro bundle in the app.yml file of your bundle:

src/Acme/Bundle/DemoBundle/Resources/config/oro/app.yml 
oro_organization_pro:
    ignore_preferred_organization_tokens:
        - cme\Bundle\DemoBundle\Security\AcmeWsseToken

Business Tip

Discover how digital transformation in manufacturing improves operations, customer experiences, and sales by reading our guide.