Like most larger PHP frameworks, Symfony2 contains a component that handles all kinds of security topics. Its main two capabilities are authentication and authorization. While making it easy to implement these concepts, the component separates them and executes one after the other. In the first step, the framework determines who the current user is or whether he is who he claims to be respectively (Authentication). Secondly, it evaluates whether he is allowed to perform a certain operation (Authorization).
This article focuses on authorization or – more accurately – Symfony2’s Access Control Lists (ACLs) and how they support the implementation of complex access right models.
The Theoretical Background
During authorization, a system decides whether the user may perform an operation on a specific object. All these permissions can be modelled on access control matrices. An Access Control Matrix (ACM) is a formalized notation for what I just described:
- There are subjects, that can perform operations.
- There are objects, which allow operations to be performed on them.
- There be multiple operations per object, which can be invoked by subjects.
The easiest way to think of ACMs is: each subject is represented by a row in a table, each object by a column. The actions a subject is allowed to perform on an object are listed in the table where the subject’s row crosses the object’s column.
The following table is an example of an ACM that describes the access rights of users in a message board.
Thread X | Thread Y | Thread Z | User A | User B | User C | |
---|---|---|---|---|---|---|
User A | read | read write |
read | read write |
read | read |
User B | read write |
read | read write moderate |
read | read write |
read |
User C | read write moderate |
read write moderate |
read | read write delete |
read write delete |
read write delete |
Columns can have different types: both threads and users are objects in this ACM, moreover, users are also subjects. This implies that users can have access rights on other users. User C for example has a higher security clearance than User A and User B which gives him the right to administrate their accounts.
To implement access right models based on this scheme, there are two major approaches: Access Control Lists (ACLs) and Capabilities.
Access Control Lists (ACLs)
An ACL is basically just one column of the ACM. Hence, there is one ACL for each object that is subject to access right restrictions. Every entry (Access Control Entry – ACE) of an ACL refers to one subject and one or more operations. When a subject wants to perform a specific operation on an object, the security system just has to iterate over the object’s ACL and look for an entry associated with the user (computational effort: O(N)
, N equals the number of subjects in the system). If there is none, or the entry does not contain the desired operation, access is denied. If there is an ACE which refers to the user and contains the operation, the user is allowed to continue.
The biggest advantage of this model is that you can easily change access to an object by just altering its ACL. This will only take O(N)
operations. On the other hand, it is „expensive“ to remove all access rights of one specific user or deny a certain action in the system completely. In these cases you would have to iterate over the whole ACM which leads to a computational complexity of O(M×N)
(with M being the number of objects in the system).
Capabilities
The other popular approach to represent access rights are capability lists. In a capability-based system, not the columns of an ACM are stored with the ACM’s objects, but its rows are stored with the subjects. If an access control system needs to provide subjects with access rights, this model introduces some advantages over ACLs regarding performance (O(N)
). In a capability-based system it is easier to pass capabilities between subjects or even groups of subjects. On the other hand, it is harder to change access rights of multiple subjects for one object (O(M×N)
). If that’s the case, you have to search for all subjects that have capabilities associated with the object.
Symfony2’s Access Control Mechanism
In Symfony2’s security component, there are several classes that enable developers to implement ACLs easily. When using the full-stack framework with the SecurityBundle, it is even easier to use ACLs, since the bundle registers ACL related services in the service container. In the following, I explain how to manage ACLs. Furthermore I demonstrate how access rights can be tested through ACLs.
For creating and altering ACLs, a MutableAclProvider
is required. In the service container, this object is identified by the ID security.acl.provider
. Once you have retrieved a MutableAclProvider
, you can use its methods to find, create, update, and delete ACLs. Each of these methods requires an ObjectIdentity
to identify the correct ACL. ACLs are not associated with objects directly but via an identity object, (of type ObjectIdentity
) which decouples the ACL implementation from the actual application logic.
Retrieving an object’s ACL
To retrieve the ACL of a Thread
object, the method findAcl
is used:
get('security.acl.provider'); $acl = $provider->findAcl(ObjectIdentity::fromDomainObject($thread));
Creating new ACLs
For objects, which do not have ACLs, you can create new ACLs using the same MutableAclProvider
:
get('security.acl.provider'); $acl = $provider->createAcl(ObjectIdentity::fromDomainObject($thread));
In most cases, you will end up creating ACLs right after creating the corresponding objects. (If your objects are Doctrine entities, make sure to persist the newly created objects and flush the EntityManager
before trying to create ACLs for them!) Sometimes, this is not possible and some objects already have ACLs associated while others don’t. In this case, you might want to use the following code to retrieve ACLs:
get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($thread); try { $acl = $provider->findAcl($objectIdentity); } catch (AclNotFoundException $e) { $acl = $provider->createAcl($objectIdentity); }
Modifying ACLs
To grant access to an operation of an object, you have to add an Access Control Entry to the object’s ACL. Each ACE refers to one instance of a class that implements the SecurityIdentityInterface
. This object references a subject and contains a bitmask that specifies which rights the subject has. In Symfony2, there are two default implementations of SecurityIdentityInterface
: UserSecurityInterface
and RoleSecurityInterface
. As you might have guessed, the first one identifies a user object and the latter one a role. Hence, access rights can be provided to a single user, as well as to all users which have a certain role.
The concept of security identities might sound like additional overhead but it’s actually a very handy abstraction: it enables you to implement new types of subjects just by creating a new security identity. There might, for example, be a GroupSecurityIdentity
which allows administrators to grant access to a whole group of users (groups and roles aren’t the same thing!).
Adding ACEs
Now, I want to show you how simple it is to enable a single user to view a Thread
:
container->get('security.acl.provider'); $acl = $aclProvider->findAcl(ObjectIdentity::fromDomainObject($thread)); $securityId = UserSecurityIdentity::fromAccount($user); $acl->insertObjectAce($securityId, MaskBuilder::MASK_VIEW); $aclProvider->updateAcl($acl);
The call MutableAclProvider::updateAcl
is very important: prior to the call, the updated ACL is not written into the ACL’s backend (which is a database by default).
If a user should be allowed to perform multiple operations on an object, you will have to create the bitmask using the MaskBuilder
:
add(MaskBuilder::MASK_VIEW); $maskBuilder->add(MaskBuilder::MASK_EDIT); $mask = $maskBuiler->get(); ... $acl->insertObjectAce($securityId, $mask);
Updating ACEs
To alter an ACE after it has been inserted into an ACL, you first have to retrieve it from the ACL, then modify its bitmask, and finally update the ACL:
findAcl(ObjectIdentity::fromDomainObject($thread)); $securityId = UserSecurityIdentity::fromAccount($user); foreach($acl->getObjectAces() as $ace) { if ($ace->getSecurityIdentity()->equals($securityId)) { $maskBuilder = new MaskBuilder($ace->getMask()); $maskBuilder->remove(MaskBuilder::MASK_EDIT); $ace->setMask($maskBuilder->get()); break; } } $provider->updateAcl($acl);
Let me show you one ACL from the above mentioned example ACM modelled on Symfony2 ACLs. The following object diagram visualizes the object graph resulting from implementing the access rights for Thread X
as ACL.
Now you know how to grant subjects the rights to access objects using ACLs. Next, we should discuss how to test whether a user is allowed to perform the operation he requested.
Checking Access
Testing whether a user may perform an action he requested is trivial. Wherever you need to do this, you will need access to the current SecurityContext
. If you are working with the Dependency Injection Container, you simply have to make it inject the service @security.context
into your class.
With the SecurityContext
at hand, you just ask it, whether the current user is allowed to perform an action like this:
isGranted('VIEW', $thread)) { throw new AccessDeniedException(); }
Further Reading
Symfony2 provides several advanced concepts that are worth studying. I urge you to do so and desist from implementing your own security mechanisms.
- Symfony Cookbook: How to use Advanced ACL Concepts
- Symfony2 Findings: Symfony2 and ACLs
- Matthias Noback: Introduction to the Security Component Part III
- Johannes Schmitt: Security and AOP in Symfony2 – Slides from Symfony Live 2012 Berlin
References:
- Mark S. Miller, Ka-Ping Yee, Jonathan Shapiro: Capability Myths Demolished
- Ross J. Anderson: Security Engineering: A Guide to Building Dependable Distributed Systems
- Symfony Cookbook: How to use Access Control Lists (ACLs)
Schreibe einen Kommentar