diff --git a/guides/security/acl.rst b/guides/security/acl.rst new file mode 100644 index 00000000000..22d6e3ae557 --- /dev/null +++ b/guides/security/acl.rst @@ -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 + + + + default + + + .. 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. diff --git a/guides/security/acl_advanced.rst b/guides/security/acl_advanced.rst new file mode 100644 index 00000000000..f52b6e33c22 --- /dev/null +++ b/guides/security/acl_advanced.rst @@ -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. \ No newline at end of file diff --git a/guides/security/index.rst b/guides/security/index.rst index ea7b5d1d847..eb2df000259 100644 --- a/guides/security/index.rst +++ b/guides/security/index.rst @@ -8,3 +8,4 @@ Security users authentication authorization + acl