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.

在复杂的应用程序里,你经常面临这样一个问题:确定权限不仅有赖于要求访问的用户(令牌),还包括域对象。这就涉及到ACL系统了。

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):

假设你正在设计一个博客系统,该系统允许用户评论你的博文。你想用户可以编辑自己的评论,但不能编辑其他人的;还有,你自己可以编辑所有对你博文的评论。在这个场景里,评论就是我们要限制访问的域对象。利用Symfony2,你可以采取不同的方式去完成这个要求,两个基本的方法(非详细方法)有:

  • 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.
  • 用角色确保安全:在这个方式里,你要为每条评论对象添加一个角色,如ROLE_COMMENT_1,ROLE_COMMENT_2等等。

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:

首先,我们需要配置ACL系统使用的连接。

  1. # app/config/security.yml 
  2. security: 
  3.     acl: 
  4.         connection: default 

The ACL system requires at least one Doctrine DBAL connection to be configured. However, that does not mean that you have to use Doctrine for mapping your domain objects. You can use whatever mapper you like for your objects, be it Doctrine ORM, Mongo ODM, Propel, or raw SQL, the choice is yours.

ACL系统要求至少配置一个Doctrine DBAL连接。尽管如此,这并不意味你必须使用Doctrine去映射你的域对象。你可以使用任何你喜欢的DBAL去映射你的对象,可以是Doctrine ORM、Mongo ODM、Propel或原始的SQL语句,一切由你自己选择。

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

连接配置后,我们需要导入数据库结构。幸运地是,我们只需简单运行下列命令即可完成:

  1. php app/console init:acl

Getting Started

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

我们由一个简单的例子开始,为这个例子实现ACL。

Creating an ACL, and adding an ACE(创建一个访问控制,并添加一个访问控制项)

  1. // BlogController.php 
  2. public function addCommentAction(Post $post
  3.     $comment = new Comment(); 
  4.  
  5.     // setup $form, and bind data 
  6.     // ... 
  7.  
  8.     if ($form->isValid()) { 
  9.         $entityManager = $this->get('doctrine.orm.default_entity_manager'); 
  10.         $entityManager->persist($comment); 
  11.         $entityManager->flush(); 
  12.  
  13.         // 创建ACL 
  14.         $aclProvider = $this->get('security.acl.provider'); 
  15.         $objectIdentity = ObjectIdentity::fromDomainObject($comment); 
  16.         $acl = $aclProvider->createAcl($objectIdentity); 
  17.  
  18.         // 检索当前登录用户的安全标识
  19.         $securityContext = $this->get('security.context'); 
  20.         $user = $securityContext->getToken()->getUser(); 
  21.         $securityIdentity = UserSecurityIdentity::fromAccount($user); 
  22.  
  23.         // 授予所有者权限
  24.         $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER); 
  25.         $aclProvider->updateAcl($acl); 
  26.     } 

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.

首先,你可能需要注意->createAcl()没有直接使用域对象,而是直接用ObjectIdentityInterface实现。这个多余的步骤允许你在没有实际的域对象实例的时候使用ACLs。这将使你在需要检查大量没有与实际对象结合的权限时非常有用。

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.

另外一个比较有趣的部分是->insertObjectAce()函数。在我们的例子里,我们授予当前登录用户对评论具有所有者权限。MaskBuilder::MASK_OWNER是一个预定义的整型掩码(bitmask);不必担心mask builder会抽象大部分的技术细节,使用这个技术可以将不同的权限存储在一条数据库记录中,这将大大地提升性能。

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

ACE的顺序检查是有意义的。作为一条普通的规则,你需要在开始的地方放置更多的指定条目。

Checking Access(检查权限)

  1. // BlogController.php  
  2. public function editCommentAction(Comment $comment)  
  3. {  
  4.     $securityContext = $this->get('security.context');  
  5.   
  6.     // check for edit access  
  7.     if (false === $securityContext->isGranted('EDIT'$comment))  
  8.     {  
  9.         throw new AccessDeniedException();  
  10.     }  
  11.   
  12.     // retrieve actual comment object, and do your editing here  
  13.     // ...  

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.

在这个例子里,我们检查用户是否有EDIT权限。在Symfony2内部会将权限映射成几个整型的掩码,然后再检查用户是否具有这些权限。

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.

你可以定义最多32个基本权限(这取决于你的操作系统,使用PHP最多可以定义30-32个)。另外,你还能定义迭加权限。

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.

在上面第一个例子里,我们仅仅授予了用户OWNER基本权限。当这个生效后还需要允许用户实现例如对对象的view,edit等等其他任何操作,这就需要我们明确地授予这些权限。

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

MaskBuilder可以轻易地将几个基本权限合在一起生成掩码。

  1. $builder = new MaskBuilder(); 
  2. $builder 
  3.     ->add('view') 
  4.     ->add('edit') 
  5.     ->add('delete') 
  6.     ->add('undelete') 
  7. $mask = $builder->get(); // int(15) 

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

该整型掩码可以被用来授给你在上面添加的用户这些基本访问权限。

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

The user is now allowed to view, edit, delete, and un-delete objects.

该用户现在被允许view,edit,delete和un-delete对象。