Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions guides/security/acl.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
.. index::
single: Security; Access Control Lists (ACLs)

Access Control Lists (ACLs)
===========================

In complex applications, you will often face the problem that access decisions
cannot only be based on the person (``Token``) who is requesting access, but
also involve a domain object that access is being requested for. This is where
the ACL system comes in.

Imagine you are designing a blog system where your users can comment on your
posts. Now, you want a user to be able to edit his own comments, but not those
of other users; besides, you yourself want to be able to edit all comments.
In this scenario, ``Comment`` would be our domain object that you want to
restrict access to. You could take several approaches to accomplish this using
Symfony2, two basic approaches are (non-exhaustive):

- *Enforce security in your business methods*: Basically, that means keeping
a reference inside each ``Comment`` to all users who have access, and then
compare these users to the provided ``Token``.
- *Enforce security with roles*: In this approach, you would add a role for
each ``Comment`` object, i.e. ``ROLE_COMMENT_1``, ``ROLE_COMMENT_2``, etc.

Both approaches are perfectly valid. However, they couple your authorization
logic to your business code which makes it less reusable elsewhere, and also
increases the difficulty of unit testing. Besides, you could run into
performance issues if many users would have access to a single domain object.

Fortunately, there is a better way, which we will talk about now.

Bootstrapping
-------------
Now, before we finally can get into action, we need to do some bootstrapping.
First, we need to configure the connection the ACL system is supposed to use:

.. configuration_block ::

.. code_block:: yaml

# app/config/security.yml
security.acl:
connection: default

.. code-block:: xml

<!-- app/config/security.xml -->
<acl>
<connection>default</connection>
</acl>

.. code-block:: php

// app/config/security.php
$container->loadFromExtension('security', 'acl', array(
'connection' => 'default',
));


After the connection is configured. We have to import the database structure.
Fortunately, we have a task for this. Simply run the following command:

``php app/console init:acl``


Getting Started
---------------
Coming back to our small example from the beginning, let's implement ACL for it.

Creating an ACL, and adding an ACE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: php

// BlogController.php
public function addCommentAction(Post $post)
{
$comment = new Comment();

// setup $form, and bind data
// ...

if ($form->isValid()) {
$entityManager = $this->container->get('doctrine.orm.default_entity_manager');
$entityManager->persist($comment);
$entityManager->flush();

// creating the ACL
$aclProvider = $this->container->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);

// retrieving the security identity of the currently logged-in user
$securityContext = $this->container->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = new UserSecurityIdentity($user);

// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}

There are a couple of important implementation decisions in this code snippet. For now,
I only want to highlight two:

First, you may have noticed that ``->createAcl()`` does not accept domain objects
directly, but only implementations of the ``ObjectIdentityInterface``. This
additional step of indirection allows you to work with ACLs even when you have
no actual domain object instance at hand. This will be extremely helpful if you
want to check permissions for a large number of objects without actually hydrating
these objects.

The other interesting part is the ``->insertObjectAce()`` call. In our example,
we are granting the user who is currently logged in owner access to the Comment.
The ``MaskBuilder::MASK_OWNER`` is a pre-defined integer bitmask; don't worry
the mask builder will abstract away most of the technical details, but using
this technique we can store many different permissions in one database row
which gives us a considerable boost in performance.

.. tip::

The order in which ACEs are checked is significant. As a general rule, you
should place more specific entries at the beginning.

Checking Access
~~~~~~~~~~~~~~~

.. code-block:: php

// BlogController.php
public function editCommentAction(Comment $comment)
{
$securityContext = $this->container->get('security.context');

// check for edit access
if (false === $securityContext->vote('EDIT', $comment))
{
throw new AccessDeniedException();
}

// do your editing here
}

In this example, we check whether the user has the ``EDIT`` permission. Internally,
Symfony2 maps the permission to several integer bitmasks, and checks whether the
user has any of them.

.. note::

You can define up to 32 base permissions (depending on your OS PHP might vary
between 30 to 32). In addition, you can also define cumulative permissions.

Cumulative Permissions
----------------------
In our first example above, we only granted the user the ``OWNER`` base
permission. While this effectively also allows the user to perform any operation
such as view, edit, etc. on the domain object, there are cases where we want to
grant these permissions explicitly.

The ``MaskBuilder`` can be used for creating bit masks easily by combining
several base permissions:

.. code-block:: php

$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete')
;
$mask = $builder->get(); // int(15)

This integer bitmask can then be used to grant a user the base permissions you
added above:

.. code-block:: php

$acl->insertObjectAce(new UserSecurityIdentity('johannes'), $mask);

The user is now allowed to view, edit, delete, and un-delete objects.
174 changes: 174 additions & 0 deletions guides/security/acl_advanced.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
.. index::
single: Security; Advanced ACL concepts

Advanced ACL Concepts
=====================

The aim of this chapter is to give a more in-depth view of the ACL system, and
also explain some of the design decisions behind it.

Design Concepts
---------------
Symfony2's object instance security capabilities are based on the concept of
an Access Control List. Every domain object **instance** has its own ACL.
The ACL instance holds a detailed list of Access Control Entries (ACEs) which
are used to make access decisions. Symfony2's ACL system focuses on two main
objectives:

- providing a way to efficiently retrieve a large amount of ACLs/ACEs for
your domain objects, and to modify them
- providing a way to easily make decisions of whether a person is allowed
to perform an action on a domain object or not

As indicated by the first point, one of the main capabilities of Symfony2's
ACL system is a high-performance way of retrieving ACLs/ACEs. This is
extremely important since each ACL might have several ACEs, and inherit
from another ACL in a tree-like fashion. Therefore, we specifically do not
leverage any ORM, but the default implementation interacts with your
connection directly using Doctrine's DBAL.

Object Identities
~~~~~~~~~~~~~~~~~
The ACL system is completely decoupled from your domain objects. They don't even
have to be stored in the same database, or on the same server. In order to
achieve this decoupling, in the ACL system your objects are represented through
object identity objects. Everytime, you want to retrieve the ACL for a domain
object, the ACL system will first create an object identity from your domain
object, and then pass this object identity to the ACL provider for further
processing.


Security Identities
~~~~~~~~~~~~~~~~~~~
This is analog to the object identity, but represents a user, or a role in your
application. Each role, or user has its own security identity.


Database Table Structure
------------------------
The default implementation uses five database tables as listed below. The
tables are ordered from least rows to most rows in a typical application:

- *acl_security_identities*: This table records all security identities
(SID) which hold ACEs. The default implementation ships with two
security identities: ``RoleSecurityIdentity``, and ``UserSecurityIdentity``
- *acl_classes*: This table maps class names to a unique id which can be
referenced from other tables.
- *acl_object_identities*: Each row in this table represents a single
domain object instance.
- *acl_object_identity_ancestors*: This table allows us to determine
all the ancestors of an ACL in a very efficient way.
- *acl_entries*: This table contains all ACEs. This is typically the
table with the most rows. It can contain tens of millions without
significantly impacting performance.


Scope of Access Control Entries
-------------------------------
Access control entries can have different scopes in which they apply. In Symfony2,
we have basically two different scopes:

- Class-Scope: These entries apply to all objects with the same class.
- Object-Scope: This was the scope we solely used in the previous chapter, and
it only applies to one specific object.

Sometimes, you will find the need to apply an ACE only to a specific field of
the object. Let's say you want the ID only to be viewable by an administrator,
but not by your customer service. To solve this common problem, we have added
two more sub-scopes:

- Class-Field-Scope: These entries apply to all objects with the same class, but
only to a specific field of the objects.
- Object-Field-Scope: These entries apply to a specific object, and only to a
specific field of that object.


Pre-Authorization Decisions
---------------------------
For pre-authorization decisions, that is decisions before any method, or secure
action is invoked, we rely on the proven AccessDecisionManager service that is
also used for reaching authorization decisions based on roles. Just like roles,
the ACL system adds several new attributes which may be used to check for
different permissions.

Built-in Permission Map
~~~~~~~~~~~~~~~~~~~~~~~
+------------------+----------------------------+-----------------------------+
| Attribute | Intended Meaning | Integer Bitmasks |
+==================+============================+=============================+
| VIEW | Whether someone is allowed | VIEW, EDIT, OPERATOR, |
| | to view the domain object. | MASTER, or OWNER |
+------------------+----------------------------+-----------------------------+
| EDIT | Whether someone is allowed | EDIT, OPERATOR, MASTER, |
| | to make changes to the | or OWNER |
| | domain object. | |
+------------------+----------------------------+-----------------------------+
| DELETE | Whether someone is allowed | DELETE, OPERATOR, MASTER, |
| | to delete the domain | or OWNER |
| | object. | |
+------------------+----------------------------+-----------------------------+
| UNDELETE | Whether someone is allowed | UNDELETE, OPERATOR, MASTER, |
| | to restore a previously | or OWNER |
| | deleted domain object. | |
+------------------+----------------------------+-----------------------------+
| OPERATOR | Whether someone is allowed | OPERATOR, MASTER, or OWNER |
| | to perform all of the above| |
| | actions. | |
+------------------+----------------------------+-----------------------------+
| MASTER | Whether someone is allowed | MASTER, or OWNER |
| | to perform all of the above| |
| | actions, and in addition is| |
| | allowed to grant | |
| | any of the above | |
| | permissions to others. | |
+------------------+----------------------------+-----------------------------+
| OWNER | Whether someone owns the | OWNER |
| | domain object. An owner can| |
| | perform any of the above | |
| | actions. | |
+------------------+----------------------------+-----------------------------+

Permission Attributes vs. Permission Bitmasks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Attributes are used by the AccessDecisionManager, just like roles are attributes
used by the AccessDecisionManager. Often, these attributes represent in fact an
aggregate of integer bitmasks. Integer bitmasks on the other hand, are used by
the ACL system internally to efficiently store your users' permissions in the
database, and perform access checks using extremely fast bitmask operations.

Extensibility
~~~~~~~~~~~~~
The above permission map is by no means static, and theoretically could be
completely replaced at will. However, it should cover most problems you encounter,
and for interoperability with other bundles, we encourage you to stick to the
meaning we have envisaged for them.


Post Authorization Decisions
----------------------------
Post authorization decisions are made after a secure method has been invoked, and
typically involve the domain object which is returned by such a method. After
invocation providers also allow to modify, or filter the domain object before it
is returned.

Due to current limitations of the PHP language, there are no post-authorization
capabilities build into the core Security component. However, there is an
experimental SecurityExtraBundle_ which adds these capabilities. See its
documentation for further information on how this is accomplished.

.. _SecurityExtraBundle: https://github.com/schmittjoh/SecurityExtraBundle


Process for Reaching Authorization Decisions
--------------------------------------------
The ACL class provides two methods for determining whether a security identity
has the required bitmasks, ``isGranted`` and ``isFieldGranted``. When the ACL
receives an authorization request through one of these methods, it delegates
this request to an implementation of PermissionGrantingStrategy. This allows you
to replace the way access decisions are reached without actually modifying the
ACL class itself.

The PermissionGrantingStrategy first checks all your object-scope ACEs if none
is applicable, the class-scope ACEs will be checked, if none is applicable, then
the process will be repeated with the ACEs of the parent ACL. If no parent ACL
exists, an exception will be thrown.
1 change: 1 addition & 0 deletions guides/security/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Security
users
authentication
authorization
acl