Skip to content

Commit 98c8778

Browse files
schmittjohfabpot
authored andcommitted
added some ACL documentation
1 parent 1e5606e commit 98c8778

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

guides/security/acl.rst

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
.. index::
2+
single: Security; Access Control Lists (ACLs)
3+
4+
Access Control Lists (ACLs)
5+
===========================
6+
7+
In complex applications, you will often face the problem that access decisions
8+
cannot only be based on the person (``Token``) who is requesting access, but
9+
also involve a domain object that access is being requested for. This is where
10+
the ACL system comes in.
11+
12+
Imagine you are designing a blog system where your users can comment on your
13+
posts. Now, you want a user to be able to edit his own comments, but not those
14+
of other users; besides, you yourself want to be able to edit all comments.
15+
In this scenario, ``Comment`` would be our domain object that you want to
16+
restrict access to. You could take several approaches to accomplish this using
17+
Symfony2, two basic approaches are (non-exhaustive):
18+
19+
- *Enforce security in your business methods*: Basically, that means keeping
20+
a reference inside each ``Comment`` to all users who have access, and then
21+
compare these users to the provided ``Token``.
22+
- *Enforce security with roles*: In this approach, you would add a role for
23+
each ``Comment`` object, i.e. ``ROLE_COMMENT_1``, ``ROLE_COMMENT_2``, etc.
24+
25+
Both approaches are perfectly valid. However, they couple your authorization
26+
logic to your business code which makes it less reusable elsewhere, and also
27+
increases the difficulty of unit testing. Besides, you could run into
28+
performance issues if many users would have access to a single domain object.
29+
30+
Fortunately, there is a better way, which we will talk about now.
31+
32+
Key Concepts
33+
------------
34+
35+
Symfony2's object instance security capabilities are based on the concept of
36+
an Access Control List. Every domain object **instance** has its own ACL
37+
instance. The ACL instance holds a detailed list of Access Control Entries
38+
(ACEs) which are used to make access decisions. Symfony2's ACL system
39+
focuses on two main objectives:
40+
41+
- providing a way to efficiently retrieve a large amount of ACLs/ACEs for
42+
your domain objects, and to modify them
43+
- providing a way to easily make decisions of whether a person is allowed
44+
to perform an action on a domain object or not
45+
46+
As indicated by the first point, one of the main capabilities of Symfony2's
47+
ACL system is a high-performance way of retrieving ACLs/ACEs. This is
48+
of utmost importance since each ACL might have several ACEs, and inherit
49+
from another ACL in a tree-like fashion. Therefore, we specifically do not
50+
leverage any ORM, but the default implementation interacts with your
51+
connection directly.
52+
53+
The default implementation uses five database tables as listed below. The
54+
tables are ordered from least rows to most rows in a typical application:
55+
56+
- *acl_security_identities*: This table records all security identities
57+
(SID) which hold ACEs. The default implementation ships with two
58+
security identities: ``RoleSecurityIdentity``, and ``UserSecurityIdentity``
59+
- *acl_classes*: This table maps class names to a unique id which can be
60+
referenced from other tables.
61+
- *acl_object_identities*: Each row in this table represents a single
62+
domain object instance.
63+
- *acl_object_identity_ancestors*: This table allows us to determine
64+
all the ancestors of an ACL in the blink of an eye, or faster :)
65+
- *acl_entries*: This table contains all ACEs. This is typically the
66+
table with the most rows. It can contain tens of millions without
67+
significantly impacting performance.
68+
69+
Bootstrapping
70+
-------------
71+
Now, before we finally can get into action, we need to do some bootstrapping.
72+
First, we need to configure the connection the ACL system is supposed to use:
73+
74+
.. configuration_block::
75+
76+
.. code_block:: yaml
77+
78+
# app/config/security.yml
79+
security.acl:
80+
connection: default
81+
82+
.. code-block:: xml
83+
84+
<!-- app/config/security.xml -->
85+
<acl>
86+
<connection>default</connection>
87+
</acl>
88+
89+
.. code-block:: php
90+
91+
// app/config/security.php
92+
$container->loadFromExtension('security', 'acl', array(
93+
'connection' => 'default',
94+
));
95+
96+
97+
After the connection is configured. We have to import the database structure.
98+
Fortunately, we have a task for this. Simply run the following command:
99+
100+
``php app/console init:acl``
101+
102+
103+
Getting Started
104+
---------------
105+
Coming back to our small example from the beginning, let's implement ACL for it.
106+
107+
1. Creating an ACL, and adding an ACE
108+
109+
.. code-block:: php
110+
111+
// BlogController.php
112+
113+
public function addCommentAction(Post $post)
114+
{
115+
$comment = new Comment();
116+
117+
// setup $form, and bind data
118+
// ...
119+
120+
if ($form->isValid()) {
121+
$entityManager = $this->container->get('doctrine.orm.default_entity_manager');
122+
$entityManager->persist($comment);
123+
$entityManager->flush();
124+
125+
// creating the ACL
126+
$aclProvider = $this->container->get('security.acl.provider');
127+
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
128+
$acl = $aclProvider->createAcl($objectIdentity);
129+
130+
// retrieving the security identity of the currently logged-in user
131+
$securityContext = $this->container->get('security.context');
132+
$user = $securityContext->getToken()->getUser();
133+
$securityIdentity = new UserSecurityIdentity($user);
134+
135+
// grant owner access
136+
$acl->insertObjectAce(0, MaskBuilder::MASK_OWNER, $securityIdentity, true);
137+
$aclProvider->updateAcl($acl);
138+
}
139+
}
140+
141+
There are a couple of important implementation decisions in this code snippet.
142+
143+
First, you may have noticed that ``->createAcl()`` does not accept domain objects
144+
directly, but only implementations of the ``ObjectIdentityInterface``. This
145+
additional step of indirection allows you to work with ACLs even when you have
146+
no actual domain object instance at hand.
147+
148+
The other interesting part is the ``->insertObjectAce()`` call. The first
149+
argument indicates the position at which the ACE is inserted (0-based). If
150+
there is already an ACE at this position it will be shifted, not replaced. The
151+
second argument is a bitmask representing the permissions that you want to
152+
grant. You do not have to worry about the bitmasking, we have a builder which
153+
abstracts most of that away for you. But in short, this allows us to save many
154+
different permissions in one database row. The third argument represents
155+
the entity that you grant access two, and finally the forth argument tells the
156+
system whether the entry is granting, or denying access.
157+
158+
159+
2. Checking Access
160+
161+
.. code-block:: php
162+
163+
// BlogController.php
164+
public function editCommentAction($commentId)
165+
{
166+
$objectIdentity = new ObjectIdentity($commentId, 'Bundle\BlogBundle\Entity\Comment');
167+
$securityContext = $this->container->get('security.context');
168+
169+
// check for edit access
170+
if (false === $securityContext->vote('EDIT', $objectIdentity))
171+
{
172+
throw new HttpForbiddenException();
173+
}
174+
175+
// do your editing here
176+
}

guides/security/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Security
88
users
99
authentication
1010
authorization
11+
acl

0 commit comments

Comments
 (0)