Browse Source

Merge remote-tracking branch 'beeldspraak/acl-refactor' into acl

Thomas Rabaix 13 years ago
parent
commit
b61fa86ecb

+ 17 - 8
Admin/Admin.php

@@ -510,6 +510,7 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
         $this->prePersist($object);
         $this->getModelManager()->create($object);
         $this->postPersist($object);
+        $this->createObjectOwner($object);
     }
 
     public function delete($object)
@@ -2077,20 +2078,18 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
     }
 
     /**
-     * Return the list of security name available for the current admin
+     * Return the roles and permissions per role for the admin ACL
      * This should be used by experimented users
      *
-     * @return array
+     * @return array [role] => array([permission], [permission])
      */
     public function getSecurityInformation()
     {
         return array(
-            'EDIT'      => array('EDIT'),
-            'LIST'      => array('LIST'),
-            'CREATE'    => array('CREATE'),
-            'VIEW'      => array('VIEW'),
-            'DELETE'    => array('DELETE'),
-            'OPERATOR'  => array('OPERATOR')
+            'GUEST'    => array('VIEW', 'LIST'),
+            'STAFF'    => array('EDIT', 'LIST', 'CREATE'),
+            'EDITOR'   => array('OPERATOR'),
+            'ADMIN'    => array('MASTER'),
         );
     }
 
@@ -2126,6 +2125,16 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
         }
     }
 
+    /**
+     * Create security object owner
+     *
+     * @param $object
+     */
+    public function createObjectOwner($object)
+    {
+        $this->getSecurityHandler()->createObjectOwner($this, $object);
+    }
+
     /**
      * @param \Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface $securityHandler
      * @return void

+ 2 - 1
Admin/AdminInterface.php

@@ -375,7 +375,6 @@ interface AdminInterface
      */
     function getListFieldDescription($name);
 
-
     function configure();
 
     function update($object);
@@ -397,4 +396,6 @@ interface AdminInterface
     function postRemove($object);
 
     function showIn($context);
+
+    function createObjectOwner($object);
 }

+ 8 - 6
Command/SetupAclCommand.php

@@ -11,6 +11,7 @@
 
 namespace Sonata\AdminBundle\Command;
 
+
 use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -65,7 +66,8 @@ class SetupAclCommand extends ContainerAwareCommand
                 $acl = $aclProvider->createAcl($objectIdentity);
             }
 
-
+            // create admin ACL, fe.
+            // Comment admin ACL
             $this->configureACL($output, $acl, $builder, $securityHandler->buildSecurityInformation($admin));
 
             $aclProvider->updateAcl($acl);
@@ -74,14 +76,14 @@ class SetupAclCommand extends ContainerAwareCommand
 
     public function configureACL(OutputInterface $output, AclInterface $acl, MaskBuilder $builder, array $aclInformations = array())
     {
-        foreach ($aclInformations as $name => $masks) {
-            foreach ($masks as $mask) {
-                $builder->add($mask);
+        foreach ($aclInformations as $role => $permissions) {
+            foreach ($permissions as $permission) {
+                $builder->add($permission);
             }
 
-            $acl->insertClassAce(new RoleSecurityIdentity($name), $builder->get());
+            $acl->insertClassAce(new RoleSecurityIdentity($role), $builder->get());
 
-            $output->writeln(sprintf('   - add role: %s, ACL: %s', $name, json_encode($masks)));
+            $output->writeln(sprintf('   - add role: %s, permissions: %s', $role, json_encode($permissions)));
 
             $builder->reset();
         }

+ 1 - 1
Controller/CRUDController.php

@@ -429,7 +429,7 @@ class CRUDController extends Controller
      */
     public function showAction($id = null)
     {
-        if (false === $this->admin->isGranted('SHOW')) {
+        if (false === $this->admin->isGranted('VIEW')) {
             throw new AccessDeniedException();
         }
 

+ 1 - 0
Resources/config/core.xml

@@ -35,6 +35,7 @@
 
         <service id="sonata.admin.security.handler.acl" class="Sonata\AdminBundle\Security\Handler\AclSecurityHandler">
             <argument type="service" id="security.context" on-invalid="null" />
+            <argument type="service" id="security.acl.provider" on-invalid="null" />
             <argument type="collection">
                 <argument>ROLE_SUPER_ADMIN</argument>
             </argument>

+ 191 - 12
Resources/doc/reference/security.rst

@@ -39,9 +39,12 @@ Configuration
         - the login form for authentification
         - the access control : resources with related required roles, the important part is the admin configuration
         - the ``acl`` option enable the ACL.
+        - the ``AdminPermissionMap`` defines the permissions of the Admin class
 
 .. code-block:: yaml
 
+    # app/config/security.yml
+
     parameters:
         # ... other parameters
         security.acl.permission.map.class: Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap
@@ -92,8 +95,8 @@ Configuration
 
 
         role_hierarchy:
-            ROLE_ADMIN:       ROLE_USER
-            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_SONATA_ADMIN, ROLE_ALLOWED_TO_SWITCH]
+            ROLE_ADMIN:       [ROLE_USER, ROLE_SONATA_ADMIN]
+            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
 
         acl:
             connection: default
@@ -102,7 +105,7 @@ Configuration
 
 - Create a new user :
 
-.. code-block::
+.. code-block:: sh
 
     # php app/console fos:user:create
     Please choose a username:root
@@ -113,32 +116,208 @@ Configuration
 
 - Promote an user as super admin :
 
-.. code-block::
+.. code-block:: sh
 
     # php app/console fos:user:promote root
     User "root" has been promoted as a super administrator.
 
 If you have Admin classes, you can install the related CRUD ACL rules :
 
-.. code-block::
+.. code-block:: sh
 
     # php app/console sonata:admin:setup-acl
     Starting ACL AdminBundle configuration
     > install ACL for sonata.media.admin.media
-       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_EDIT, ACL: ["EDIT"]
-       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_LIST, ACL: ["LIST"]
-       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_CREATE, ACL: ["CREATE"]
-       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_DELETE, ACL: ["DELETE"]
-       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_OPERATOR, ACL: ["OPERATOR"]
+       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_GUEST, permissions: ["VIEW","LIST"]
+       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_STAFF, permissions: ["EDIT","LIST","CREATE"]
+       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_EDITOR, permissions: ["OPERATOR"]
+       - add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_ADMIN, permissions: ["MASTER"]
     ... skipped ...
 
 If you try to access to the admin class you should see the login form, just logon with the ``root`` user.
 
-An Admin is displayed in the dashboard (and menu) when the user has the role ``LIST``. To change this override the ``showIn`` 
+An Admin is displayed in the dashboard (and menu) when the user has the role ``LIST``. To change this override the ``showIn``
 method in the Admin class.
 
+Roles and Access control lists
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A user can have several roles when working with an application. Each Admin class has several roles, and each role specifies the permissions
+of the user for the Admin class. Or more specific, what the user can do with the domain object(s) the Admin class is created for.
+
+By default each Admin class contains the following roles, override the property ``$securityInformation`` to change this:
+
+ - ROLE_SONATA_..._GUEST: a guest that is allowed to view an object and a list of objects;
+ - ROLE_SONATA_..._STAFF: probably the biggest part of the users, a staff user has the same permissions as guests and is additionally
+   allowed to EDIT and CREATE new objects;
+ - ROLE_SONATA_..._EDITOR: an editor is granted all access and, compared to the staff users, is allowed to DELETE;
+ - ROLE_SONATA_..._ADMIN: an administrative user is granted all access and on top of that, the user is allowed to grant other users access.
+
+Owner:
+ - when an object is created, the currently logged in user is set as owner for that object and is granted all access for that object;
+ - this means the user owning the object is always allowed to DELETE the object, even when it only has the staff role.
+
+Vocabulary used for Access Control Lists:
+    - Role: a user role;
+    - ACL: a list of access rules, the Admin uses 2 types:
+        - Class-Scope: created from the Security information of the Admin class and shares the Access Control Entries for all objects
+          with the class where the Admin is created for;
+        - Object-Scope: created for each new object and specifies the owner;
+    - Sid: Security identity, an ACL role for the Class-Scope ACL and the user for the Object-Scope ACL;
+    - Oid: Object identity, identifies the ACL, for the Admin ACL this is the admin code, for the object ACL this is the object id;
+    - ACE: a role (or sid) and its permissions;
+    - Permission: this tells what the user is allowed to do with the Object identity;
+    - Bitmask: a permission can have several bitmasks, each bitmask represents a permission. When permission VIEW is requested and
+      it contains the VIEW and EDIT bitmask and the user only has the EDIT permission, then the permission VIEW is granted.
+    - PermissionMap: configures the bitmasks for each permission, to change the default mapping create a voter for the domain class of the Admin.
+      There can be many voters that may have different permission maps. However, prevent that multiple voters are not allowed to vote on the same
+      class with overlapping bitmasks.
+
+See the cookbook article "Advanced ACL concepts" for the meaning of the different permissions:
+http://symfony.com/doc/current/cookbook/security/acl_advanced.html#pre-authorization-decisions.
+
+How is access granted?
+~~~~~~~~~~~~~~~~~~~~~~
+In the application the security context is asked if access is granted for a role or a permission (admin.isGranted):
+
+ - Token: a token identifies a user between requests;
+ - Voter: sort of judge that returns if access is granted of denied, if the voter should not vote for a case, it returns abstrain;
+ - AccessDecisionManager: decides if access is granted or denied according a specific strategy. It grants access if at least one (affirmative
+   strategy), all (unanimous strategy) or more then half (consensus strategy) of the counted votes granted access;
+ - RoleVoter: votes for all attributes stating with "ROLE_" and grants access if the user has this role;
+ - RoleHierarchieVoter: when the role ROLE_SONATA_ADMIN is voted for, it also votes "granted" if the user has the role ROLE_SUPER_ADMIN;
+ - AclVoter: grants access for the permissions of the Admin class if the user has the permission, the user has a permission that is
+   included in the bitmasks of the permission requested to vote for or the user owns the object.
+
+Create a custom voter or a custom permission map
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In some occasions you need to create a custom voter or a custom permission map because for example you want to restrict access using extra rules:
+
+ - create a custom voter class that extends the AclVoter
+
+.. code-block:: php
+
+   namespace Acme\DemoBundle\Security\Authorization\Voter;
+
+   use FOS\UserBundle\Model\UserInterface;
+   use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+   use Symfony\Component\Security\Acl\Voter\AclVoter;
+
+   class UserAclVoter extends AclVoter
+   {
+       /**
+        * {@InheritDoc}
+        */
+       public function supportsClass($class)
+       {
+           // support the Class-Scope ACL for votes with the custom permission map
+           // return $class === 'Sonata\UserBundle\Admin\Entity\UserAdmin' || $class === is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
+           // if you use php >=5.3.7 you can check the inheritance with is_a($class, 'Sonata\UserBundle\Admin\Entity\UserAdmin');
+           // support the Object-Scope ACL
+           return is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
+       }
+
+       public function supportsAttribute($attribute)
+       {
+           return $attribute === 'EDIT' || $attribute === 'DELETE';
+       }
+
+       public function vote(TokenInterface $token, $object, array $attributes)
+       {
+           if (!$this->supportsClass(get_class($object))) {
+               return self::ACCESS_ABSTAIN;
+           }
+
+           foreach ($attributes as $attribute) {
+               if ($this->supportsAttribute($attribute) && $object instanceof UserInterface) {
+                   if ($object->isSuperAdmin() && !$token->getUser()->isSuperAdmin()) {
+                       // deny a non super admin user to edit a super admin user
+                       return self::ACCESS_DENIED;
+                   }
+               }
+           }
+
+           // use the parent vote with the custom permission map:
+           // return parent::vote($token, $object, $attributes);
+           // otherwise leave the permission voting to the AclVoter that is using the default permission map
+           return self::ACCESS_ABSTAIN;
+       }
+   }
+
+ - optionally create a custom permission map, copy to start the Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap.php to your bundle
+
+ - declare the voter and permission map as a service
+
+.. code-block:: xml
+
+    <!-- src/Acme/DemoBundle/Resources/config/services.xml -->
+
+    <parameters>
+        <parameter key="security.acl.user_voter.class">Acme\DemoBundle\Security\Authorization\Voter\UserAclVoter</parameter>
+        <!-- <parameter key="security.acl.user_permission.map.class">Acme\DemoBundle\Security\Acl\Permission\UserAdminPermissionMap</parameter> -->
+    </parameters>
+
+    <services>
+        <!-- <service id="security.acl.user_permission.map" class="%security.acl.permission.map.class%" public="false"></service> -->
+
+        <service id="security.acl.voter.user_permissions" class="%security.acl.user_voter.class%" public="false">
+            <tag name="monolog.logger" channel="security" />
+            <argument type="service" id="security.acl.provider" />
+            <argument type="service" id="security.acl.object_identity_retrieval_strategy" />
+            <argument type="service" id="security.acl.security_identity_retrieval_strategy" />
+            <argument type="service" id="security.acl.permission.map" />
+            <argument type="service" id="logger" on-invalid="null" />
+            <tag name="security.voter" priority="255" />
+        </service>
+    </services>
+
+ - change the access decission strategy to ``unanimous``
+
+.. code-block:: yaml
+
+    # app/config/security.yml
+    security:
+        access_decision_manager:
+            # Strategy can be: affirmative, unanimous or consensus
+            strategy: unanimous
+
+ - to make this work the permission needs to be checked using the Object-Scope
+
+  - modify the template (or code) where applicable:
+
+.. code-block:: html
+
+    {% if admin.isGranted('EDIT', user_object) %} {# ... #} {% endif %}
+
+  - because the permission is checked at Object-Scope each object must have an object ACL with a Class-Scope ACL inherited, otherwise the AclVoter
+    will deny EDIT access for a non super admin user trying to edit another non super admin user. This is automatically done when the object is
+    created using the Admin. If objects are also created outside the Admin, have a look at the ``createObjectOwner`` method in the
+    AclSecurityHandler.
+
 Usage
 ~~~~~
 
 Everytime you create a new ``Admin`` class, you should create start the command ``php app/console sonata:admin:setup-acl``
-so the ACL database will be updated with the latest masks and roles informations.
+so the ACL database will be updated with the latest roles and permissions.
+
+In the templates, or in your code, you can use the Admin method ``isGranted``:
+
+ - check for an admin that the user is allowed to EDIT:
+
+.. code-block:: html
+
+    {# use the admin security method  #}
+    {% if admin.isGranted('EDIT') %} {# ... #} {% endif %}
+
+    {# or use the default is_granted symfony helper, the following will give the same result #}
+    {% if is_granted('ROLE_SUPER_ADMIN') or is_granted('EDIT', admin) %} {# ... #} {% endif %}
+
+ - check for an admin that the user is allowed to DELETE, the object is added to also check if the object owner is allowed to DELETE:
+
+.. code-block:: html
+
+    {# use the admin security method  #}
+    {% if admin.isGranted('DELETE', object) %} {# ... #} {% endif %}
+
+    {# or use the default is_granted symfony helper, the following will give the same result #}
+    {% if is_granted('ROLE_SUPER_ADMIN') or is_granted('DELETE', object) %} {# ... #} {% endif %}

+ 1 - 1
Resources/views/CRUD/base_edit.html.twig

@@ -88,7 +88,7 @@ file that was distributed with this source code.
                         <input type="submit" class="btn primary" name="btn_update_and_edit" value="{% trans from 'SonataAdminBundle' %}btn_update_and_edit_again{% endtrans %}"/>
                         <input type="submit" class="btn" name="btn_update_and_list" value="{% trans from 'SonataAdminBundle' %}btn_update_and_return_to_list{% endtrans %}"/>
 
-                        {% if admin.hasroute('delete') and admin.isGranted('DELETE') %}
+                        {% if admin.hasroute('delete') and admin.isGranted('DELETE', object) %}
                             {% trans from 'SonataAdminBundle' %}delete_or{% endtrans %}
                             <a class="btn danger" href="{{ admin.generateObjectUrl('delete', object) }}">{% trans from 'SonataAdminBundle' %}link_delete{% endtrans %}</a>
                         {% endif %}

+ 2 - 2
Resources/views/CRUD/base_list_field.html.twig

@@ -10,11 +10,11 @@ file that was distributed with this source code.
 #}
 
 <td class="sonata-ba-list-field sonata-ba-list-field-{{ field_description.type }}" objectId="{{ admin.id(object) }}">
-    {% if field_description.options.identifier is defined and admin.isGranted(['EDIT', 'SHOW']) %}
+    {% if field_description.options.identifier is defined and admin.isGranted('VIEW') %}
 
         {% if admin.hasroute('edit') and admin.isGranted('EDIT') %}
             <a href="{{ admin.generateObjectUrl('edit', object) }}">
-        {% elseif admin.hasroute('show') %}
+        {% elseif admin.hasroute('show') and admin.show|length > 0 %}
             <a href="{{ admin.generateObjectUrl('show', object) }}">
         {% endif %}
 

+ 37 - 8
Security/Acl/Permission/AdminPermissionMap.php

@@ -22,48 +22,77 @@ use Symfony\Component\Security\Acl\Permission\PermissionMapInterface;
  */
 class AdminPermissionMap implements PermissionMapInterface
 {
-    const PERMISSION_SHOW        = 'SHOW';
+    const PERMISSION_VIEW        = 'VIEW';
     const PERMISSION_EDIT        = 'EDIT';
     const PERMISSION_CREATE      = 'CREATE';
     const PERMISSION_DELETE      = 'DELETE';
     const PERMISSION_UNDELETE    = 'UNDELETE';
+    const PERMISSION_LIST        = 'LIST';
     const PERMISSION_OPERATOR    = 'OPERATOR';
     const PERMISSION_MASTER      = 'MASTER';
     const PERMISSION_OWNER       = 'OWNER';
 
-    const PERMISSION_LIST        = 'LIST';
-
+    /**
+     * Map each permission to the permissions it should grant access for
+     * fe. grant access for the view permission if the user has the edit permission
+     *
+     * @var array
+     */
     private $map = array(
-        self::PERMISSION_LIST => array(
-            MaskBuilder::MASK_LIST
-        ),
 
-        self::PERMISSION_SHOW => array(
+        self::PERMISSION_VIEW => array(
             MaskBuilder::MASK_VIEW,
+            MaskBuilder::MASK_LIST,
+            MaskBuilder::MASK_EDIT,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
         self::PERMISSION_EDIT => array(
             MaskBuilder::MASK_EDIT,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
         self::PERMISSION_CREATE => array(
             MaskBuilder::MASK_CREATE,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
         self::PERMISSION_DELETE => array(
             MaskBuilder::MASK_DELETE,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
         self::PERMISSION_UNDELETE => array(
             MaskBuilder::MASK_UNDELETE,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
+        ),
+
+        self::PERMISSION_LIST => array(
+            MaskBuilder::MASK_LIST,
+            MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
-        self::PERMISSION_OPERATOR => array(
+       self::PERMISSION_OPERATOR => array(
             MaskBuilder::MASK_OPERATOR,
+            MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER
         ),
 
         self::PERMISSION_MASTER => array(
             MaskBuilder::MASK_MASTER,
+            MaskBuilder::MASK_OWNER,
         ),
 
         self::PERMISSION_OWNER => array(

+ 4 - 0
Security/Acl/Permission/MaskBuilder.php

@@ -13,6 +13,10 @@ namespace Sonata\AdminBundle\Security\Acl\Permission;
 
 use Symfony\Component\Security\Acl\Permission\MaskBuilder as BaseMaskBuilder;
 
+/**
+ * {@inheritDoc}
+ * - LIST: the SID is allowed to view a list of the domain objects / fields
+ */
 class MaskBuilder extends BaseMaskBuilder
 {
     const MASK_LIST         = 4096;       // 1 << 12

+ 65 - 17
Security/Handler/AclSecurityHandler.php

@@ -13,21 +13,27 @@ namespace Sonata\AdminBundle\Security\Handler;
 
 use Symfony\Component\Security\Core\SecurityContextInterface;
 use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
+use Symfony\Component\Security\Acl\Model\AclProviderInterface;
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
+use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
+use Symfony\Component\Security\Acl\Permission\MaskBuilder;
 use Sonata\AdminBundle\Admin\AdminInterface;
 
 class AclSecurityHandler implements SecurityHandlerInterface
 {
     protected $securityContext;
-
+    protected $aclProvider;
     protected $superAdminRoles;
 
     /**
      * @param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
      * @param array $superAdminRoles
      */
-    public function __construct(SecurityContextInterface $securityContext, array $superAdminRoles)
+    public function __construct(SecurityContextInterface $securityContext, AclProviderInterface $aclProvider, array $superAdminRoles)
     {
         $this->securityContext = $securityContext;
+        $this->aclProvider = $aclProvider;
         $this->superAdminRoles = $superAdminRoles;
     }
 
@@ -40,19 +46,8 @@ class AclSecurityHandler implements SecurityHandlerInterface
             $attributes = array($attributes);
         }
 
-        if ($object instanceof AdminInterface) {
-            foreach ($attributes as $pos => $attribute) {
-                $attributes[$pos] = sprintf('ROLE_%s_%s',
-                    str_replace('.', '_', strtoupper($admin->getCode())),
-                    $attribute
-                );
-            }
-        }
-
-        $attributes = array_merge($attributes, $this->superAdminRoles);
-
         try {
-            return $this->securityContext->isGranted($attributes, $object);
+            return $this->securityContext->isGranted($this->superAdminRoles) || $this->securityContext->isGranted($attributes, $object);
         } catch (AuthenticationCredentialsNotFoundException $e) {
             return false;
         } catch (\Exception $e) {
@@ -60,18 +55,71 @@ class AclSecurityHandler implements SecurityHandlerInterface
         }
     }
 
+    public function getBaseRole(AdminInterface $admin)
+    {
+        return 'ROLE_'.str_replace('.', '_', strtoupper($admin->getCode())).'_%s';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function buildSecurityInformation(AdminInterface $admin)
     {
-        $baseRole = 'ROLE_'.str_replace('.', '_', strtoupper($admin->getCode())).'_%s';
+        $baseRole = $this->getBaseRole($admin);
 
         $results = array();
-        foreach ($admin->getSecurityInformation() as $name => $permissions) {
-            $results[sprintf($baseRole, $name)] = $permissions;
+        foreach ($admin->getSecurityInformation() as $role => $permissions) {
+            $results[sprintf($baseRole, $role)] = $permissions;
         }
 
         return $results;
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function createObjectOwner(AdminInterface $admin, $object)
+    {
+        $acl = $this->getNewObjectOwnerAcl($admin, $object);
+        $this->updateAcl($acl);
+    }
+
+    /**
+     * Get a new ACL with an object ACE where the currently logged in user is set as owner
+     *
+     * @param AdminInterface $admin
+     * @param object $object
+     * @return Symfony\Component\Security\Acl\Model\AclInterface
+     */
+    public function getNewObjectOwnerAcl(AdminInterface $admin, $object)
+    {
+        // creating the ACL, fe.
+        // Comment 1 ACL
+        $objectIdentity = ObjectIdentity::fromDomainObject($object);
+        $acl = $this->aclProvider->createAcl($objectIdentity);
+
+        // inherit class ACE's from the admin ACL, fe.
+        // Comment admin ACL
+        //  - Comment 1 ACL
+        $parentOid = ObjectIdentity::fromDomainObject($admin);
+        $parentAcl = $this->aclProvider->findAcl($parentOid);
+        $acl->setParentAcl($parentAcl);
+
+        // retrieving the security identity of the currently logged-in user
+        $user = $this->securityContext->getToken()->getUser();
+        $securityIdentity = UserSecurityIdentity::fromAccount($user);
+
+        // grant owner access
+        $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
+
+        return $acl;
+    }
+
+    /**
+     * Update the ACL
+     */
+    public function updateAcl(AclInterface $acl)
+    {
+        $this->aclProvider->updateAcl($acl);
+    }
 }

+ 7 - 0
Security/Handler/NoopSecurityHandler.php

@@ -30,4 +30,11 @@ class NoopSecurityHandler implements SecurityHandlerInterface
     {
         return array();
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function createObjectOwner(AdminInterface $admin, $object)
+    {
+    }
 }

+ 10 - 0
Security/Handler/SecurityHandlerInterface.php

@@ -29,4 +29,14 @@ interface SecurityHandlerInterface
      * @return void
      */
     function buildSecurityInformation(AdminInterface $admin);
+
+    /**
+     * Make the current user owner of the object
+     *
+     * @abstract
+     * @param \Sonata\AdminBundle\Admin\AdminInterface $admin
+     * @param object $object
+     * @return void
+     */
+    function createObjectOwner(AdminInterface $admin, $object);
 }

+ 5 - 0
Tests/Admin/BaseAdminModelManagerTest.php

@@ -14,6 +14,7 @@ namespace Sonata\AdminBundle\Tests\Admin;
 use Sonata\AdminBundle\Admin\Admin;
 use Sonata\AdminBundle\Route\RouteCollection;
 use Sonata\AdminBundle\Model\ModelManagerInterface;
+use Sonata\AdminBundle\Security\Handler\NoopSecurityHandler;
 
 class BaseAdminModelManager_Admin extends Admin
 {
@@ -24,6 +25,9 @@ class BaseAdminModelManagerTest extends \PHPUnit_Framework_TestCase
 {
     public function testHook()
     {
+        $securityHandler = $this->getMock('Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface');
+        $securityHandler->expects($this->once())->method('createObjectOwner');
+
         $modelManager = $this->getMock('Sonata\AdminBundle\Model\ModelManagerInterface');
         $modelManager->expects($this->once())->method('create');
         $modelManager->expects($this->once())->method('update');
@@ -31,6 +35,7 @@ class BaseAdminModelManagerTest extends \PHPUnit_Framework_TestCase
 
         $admin = new BaseAdminModelManager_Admin('code', 'class', 'controller');
         $admin->setModelManager($modelManager);
+        $admin->setSecurityHandler($securityHandler);
 
         $t = new \stdClass();
 

+ 13 - 11
Tests/Security/Handler/AclSecurityHandlerTest.php

@@ -27,7 +27,9 @@ class AclSecurityHandlerTest extends \PHPUnit_Framework_TestCase
             ->method('isGranted')
             ->will($this->returnValue(true));
 
-        $handler = new AclSecurityHandler($securityContext, array());
+        $aclProvider = $this->getMock('Symfony\Component\Security\Acl\Model\AclProviderInterface');
+
+        $handler = new AclSecurityHandler($securityContext, $aclProvider, array());
 
         $this->assertTrue($handler->isGranted($admin, array('TOTO')));
         $this->assertTrue($handler->isGranted($admin, 'TOTO'));
@@ -37,7 +39,7 @@ class AclSecurityHandlerTest extends \PHPUnit_Framework_TestCase
             ->method('isGranted')
             ->will($this->returnValue(false));
 
-        $handler = new AclSecurityHandler($securityContext, array());
+        $handler = new AclSecurityHandler($securityContext, $aclProvider, array());
 
         $this->assertFalse($handler->isGranted($admin, array('TOTO')));
         $this->assertFalse($handler->isGranted($admin, 'TOTO'));
@@ -59,7 +61,9 @@ class AclSecurityHandlerTest extends \PHPUnit_Framework_TestCase
             ->method('getSecurityInformation')
             ->will($this->returnValue($informations));
 
-        $handler = new AclSecurityHandler($securityContext, array());
+        $aclProvider = $this->getMock('Symfony\Component\Security\Acl\Model\AclProviderInterface');
+
+        $handler = new AclSecurityHandler($securityContext, $aclProvider, array());
 
         $results = $handler->buildSecurityInformation($admin);
 
@@ -69,16 +73,15 @@ class AclSecurityHandlerTest extends \PHPUnit_Framework_TestCase
     public function testWithAuthenticationCredentialsNotFoundException()
     {
         $admin = $this->getMock('Sonata\AdminBundle\Admin\AdminInterface');
-        $admin->expects($this->once())
-            ->method('getCode')
-            ->will($this->returnValue('test'));
 
         $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface');
         $securityContext->expects($this->any())
             ->method('isGranted')
             ->will($this->throwException(new AuthenticationCredentialsNotFoundException('FAIL')));
 
-        $handler = new AclSecurityHandler($securityContext, array());
+        $aclProvider = $this->getMock('Symfony\Component\Security\Acl\Model\AclProviderInterface');
+
+        $handler = new AclSecurityHandler($securityContext, $aclProvider, array());
 
         $this->assertFalse($handler->isGranted($admin, 'raise exception', $admin));
     }
@@ -89,16 +92,15 @@ class AclSecurityHandlerTest extends \PHPUnit_Framework_TestCase
     public function testWithNonAuthenticationCredentialsNotFoundException()
     {
         $admin = $this->getMock('Sonata\AdminBundle\Admin\AdminInterface');
-        $admin->expects($this->once())
-            ->method('getCode')
-            ->will($this->returnValue('test'));
 
         $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface');
         $securityContext->expects($this->any())
             ->method('isGranted')
             ->will($this->throwException(new \RunTimeException('FAIL')));
 
-        $handler = new AclSecurityHandler($securityContext, array());
+        $aclProvider = $this->getMock('Symfony\Component\Security\Acl\Model\AclProviderInterface');
+
+        $handler = new AclSecurityHandler($securityContext, $aclProvider, array());
 
         $this->assertFalse($handler->isGranted($admin, 'raise exception', $admin));
     }