Zend Framework: Zend_Acl Component Proposal
| Proposed Component Name | Zend_Acl |
|---|---|
| Developer Notes | http://framework.zend.com/wiki/display/ZFDEV/Zend_Acl |
| Proposers | Simon Mundy |
| Revision | 0.2 - Finalised proposal (wiki revision: 10) |
Table of Contents
1. Overview
Zend_Acl provides lightweight and flexible access control list (ACL) functionality. In general, an application may utilize such functionality to control access to certain protected objects by other requesting objects.
In this proposal:
- an Access Control Object (ACO) is an object to which access is controlled.
- an Access Request Object (ARO) is an object that may request access to an ACO.
For example, if a person requests access to a car, then the person would be the requesting ARO, and the car would be the ACO, since its access is under control.

Through the specification and use of an access control list (ACL), an application may control how requesting objects (AROs) are granted access to protected objects (ACOs).
2. References
3. Component Requirements, Constraints, and Acceptance Criteria
- No database backend required - complete PHP implementation
- Permits explicit allow-all, allow-specific, neutral, deny-specific, deny-all for all groups
- Authorization queries must be computed with efficiency and speed
- Performance penalties, where reasonably unavoidable, should be isolated to ACL data structure modifications
- Instances must be serializable (e.g., for caching ACL data structures)
4. Dependencies on Other Framework Components
- Zend_Exception
5. Theory of Operation
The goal of Zend_Acl was to create a simple, flexible means of creating ACLs, free from the dependencies of Databases, template engines or complex administration tools. The starting point was phpGACL, as its 'allow,deny' and inheritance scheme is straightforward and allows huge flexibility for implementation.
Zend_Acl builds on the tree-based model of phpGACL, providing a means to add multiple ACOs that may represent 'areas' or 'domains' of control within the ACL. Objects under access control may be organized from general (at the tree root) to specific (toward tree leaves). This approach allows for creating very fine-grained access controls, where desirable, with minimal effort.
Each ACO may also be assigned its own set of 'actions' or 'permissions' (e.g., add, edit, delete) that can be used to determine the level of access each requesting ARO would have. Actions may be assigned to ACOs simply by calling allow() or deny() on the ACO in question.
AROs may be organized in different ways, according to the particular needs of an application. To accommodate these various organizations, the ARO registry of Zend_Acl is represented by a directed acyclic graph (DAG) data structure. This approach supports having multiple roles or groups, and AROs may also inherit permissions from multiple AROs.
To illustrate by example, an application may need access roles for administrators, members, and the general public. Each of these roles would have a corresponding ARO in the access control list. The general public would have the least access privileges, members would have additional permissions (e.g., to edit their profiles), and administrators would have exclusive permissions to perform administrative operations (e.g., blocking abuse). Suppose we also have an ARO for each login user identity. Each of these "user" AROs may inherit permissions that have been assigned to one or more of the "role" AROs previously mentioned (e.g., administrators). In this way we can add users to, and remove users from, such roles without having to otherwise modify the access controls.
6. Milestones / Tasks
7. Class Index
- Zend_Acl
- Zend_Acl_Aro
- Zend_Acl_Aro_Registry
- Zend_Acl_Permission
8. Use Cases
At its most simplistic level, Zend_Acl can provide the bare necessities of an authorisation scheme. To illustrate, we'll assume the following 'real-world' scenario:-
ACL - Football match
ACOs - The ground
- Stadium
- Public seating
- Reserved seating
- Corporate seating
- Coaches box
- Dressing rooms
- Pitch
AROs - The groups
- Public (or '_default' group) - unreserved seats or reserved seats
- Guests - members of the public who are invited to a corporate box
- Coaching staff
- Players
- Club officials
Basic setup
We create an ACL instance and add our AROs, adding inheritence where necessary.
Now we start adding the expected rules typical to a game of football
The above example demonstrates how a moderately complex ACL can be created with a minimum of effort. However, if you wanted to refine these generic rules so that the AROs can perform only certain actions, then you can start getting selective
Now it's time to use your ACL during the execution of your script. A simple 'valid' method provides a boolean answer to each of your queries
Zend_Acl will assume that non-specific AROs or contexts will apply to any member, so you can provide null values to these arguments as you add permissions. However, if you add specific 'allow' or 'deny' actions to an ACO, you are effectively providing only limited access to that ACO. You can, however, add multiple permissions to the same ACO to different AROs to ensure your permissions are as fine-grained as you require. Zend_Acl will also check for conflicts when adding permissions to remove the likelihood of 'deadlocks'.
9. Class Skeletons
Indeed, the _getContext method will accept either a string (which is transformed into an array) or an existing array.
While I look at this again with fresh eyes, though, I realise that a 'setAllow' or 'setDeny' doesn't allow you to incrementally add values - I will update the proposal shortly to allow addAllow and addDeny. It will also automatically resolve conflicts whilst doing so (e.g. if 'edit' existed in 'deny' and then I added 'edit' to 'allow' it will remove the former to prevent a neutral result).
OK, I've updated the class to allow incremental permissions via addAllow, addDeny, removeAllow and removeDeny. You can also pass an array or a string to any of the values (context, container and/or group) to allow more flexible and concise population of permissions.
Here's an example of a live site I have running:-
(You would obviously optimise this in your own app, but the redundant 'addDeny' methods are added for the purpose of demonstration).
With these 9 lines I have a pretty robust approach for permission checking across the whole site. In my app, I have a sitemap class that uses the above rules to automatically show/hide sections for staff/members/admins. This also works in conjunction with a Zend_Controller_Action_Plugin that performs automatic ACL checks and auto re-routes to a nopermissionAction if a user has insufficient privileges for that controller.
1) Role/Group/User should act same as permission object. Any permission object can inherit permissions from another permission object. Doesn't care if the permission object is authentificationable or not. It seems that's the way you do it.
2) At initialization time, permissions should be made static as much as possible. I mean they should inherit their permissions and be converted into simple set of area-action. And these static permission should be cached. Whather permission object or its ancestor changes the cache is deleted. It can be done via cache's tags or via dependencies and chain reaction. Doing so make frontend checks/validations much faster - just in_array. On the other hand changes to permissions (which aren't so often in comparsion to checks) are little more complex due to requirement of deleting caches.
3) There should be much more transparent solution for checks.
- Posibility to set default permission object.
- Posibility to auto-magically check area and action using debug_backtrace().
My dreaming use case is like this:
4) Posibility to check against set of permission objects and so set, add and remove default set of permission objects.
Hi Marek
You're right on point 1 - authentication is something best tested for outside of this component.
Point 2 touches on caches and inheritance but they're not really issues with the current implementation. The permissions checking works its way from the outside->in rather than vice-versa. It means that you get a definite answer much quicker if explicit permissions are set to either allow or deny.
For example, say I have a container of $acl->house->upstairs->bedroom->cupboard set to 'deny' for the group 'children' and $acl->house set to 'allow' for 'parents'. When $acl->house->upstairs->bedroom->cupboard->valid('rummage', 'children') is performed, the validation starts from the outermost path (i.e. cupboard). Since an explicit deny is found, the validation returns false immediately which is nice and quick. If $acl->house->upstairs->bedroom->cupboard->valid('rummage', 'parents') is called, it tries 'cupboard' first and finds no explicit or implied permissions for 'parents', so it checks the next valid permission back up the tree. Now, because no explicit permissions have been set for 'upstairs' or 'bedroom' either, the next explicit permissions object to check is back at 'house', where it finds 'allow' for 'parents' and that returns a true. So as you can see, the performance is quite good as there are no calls to redundant/non-existent objects along the path and any dynamic changes to permissions on the object are quite straightforward to achieve.
Point 3 I may need some further explanation. How/why will debug_backtrace be required? And why do we need to check for specific objects within the ACL - is there any benefit over simply identifying it by a string? I can see where you're heading with 'multivalid' (the existing valid method would still work with this kind of call) but did you imagine that those checks would be AND instead of OR - i.e. Return true for $acl->house->valid() if context is in 'rummage,store,add' and group is 'parent' OR context is in 'hide' and group is in 'children'? I could add this as a convenience but it still may be simpler just to add this to your own logic rather than beefing up the component's methods.
And Point 4 - what are we checking for when comparing permissions objects? Can you provide an example of the kinds of properties to check against or the contents of $objectNameOrId to illustrate the point?
Many thanks - look forward to your feedback
If there are any plans for DB permissions, how it will be done?
My idea is to have column permissionId at each table i wanna be able to check. Then at the inicialization time, permission object will get 3 sets of permissionId's for updates, deletes and selects. Then when it queries against DB then additional where is added. It should be done via extended Zend_Db e.g. Zend_Db_Perms.
The permissionId supplies set of accessibilities like which permission objects have which rights (update, delete, select).
This is most optimal for performance and there isn't any deal. But it shows problem of permissions from another perspective. Because It's not the permission object who tell which records are for him even It's not DB record who tells which users can hadle him. There is 1:1 between records and permissionId due performance decision. And relationship M:N between permissionId and permission object with set of actions (delete, update, select) which is negotiated during initialization into 3 permissions sets described above. Simply accesses for record are grouped into permissionId.
Solution should be like this: These three sets can be unified under areas (in acl naming conventions paths) db-delete, db-update and db-select with actions (in acl naming conventions contexts) as sets of permissionsId (which reflect to the column in DB).
But the main deal is while editing db record. You need to convert (by event) given permissions into new/existing permissionId and add this permissionId into permission objects.
Maybe It should be done via completely another rountine or extended one. But if not, think about it here. There is too much common for them - e.g. inheritance and workarounds for managing them.
I didn't see the class itself needing to be completely driven by a DB driver - all it really needs to do is load data from a source (such as a Zend_Config container or via a Zend_Db object) and parse that to make the root Zend_Acl object.
But then again if there's any specific benefits that can be gained by moving the permissions checking into SQL then I'd be happy to look at that. Performance at the moment is quite good and the ACL can be serialised/cached, so I don't see any advantages yet that can't be achieved by loading permissions once from the database.
I've received feedback regarding storing and loading the ACL to/from files or databases. I also see a need to mix and match ACO and ARO trees both, without having to merge everything into a single "blob" before storing or loading. If everyone agrees that there is a need and value in the ability to save and load individual ACO/ARO trees, then do we need detailed use cases, or would it be easy to support storing, loading, and using multiple ACO/ARO trees? For example, the ability to getSerializedACO(), getSerializedARO(), setACO($serializedARO) and setACO($serializedACO) appear to solve the requests I've heard for persistence, excluding the mixing and matching idea (seems less important to me).
Zend CommentsSummaryAccording to the proposal overview, Zend_Acl would enable assigning permissions to both 'containers' and 'context' to one or more groups. We should be well served by further explanation of the terminology and an analysis of the problem. Now let's revise the proposal - this version is noted as being a "rough draft" - with respect to the issues outlined below and the attached code, in order to arrive at a version which is expected to be approved for the framework incubator. We are closing in on a good authorization component design, and with the continued help of the community, we should succeed in providing a solution that fits well for most use cases. These comments are intended to be neither normative nor authoritative. It is encouraged, rather, that we continue to question and debate and revise various issues toward reasonable conclusions. Notes
|
I think it's a great idea to add an assert method of some kind, that works like valid(), but throws an exception instead of returning false.
This way, you could assert permissions, say, inside of a Controller_Action, and dispatch any security exceptions to a default access denied view.
This is one of the component I am waiting to use desperately. Is there a working example or demo so that I can test it on my website.
Thank you.
Maru
We currently have code (and docs) in the incubator for this component:
http://framework.zend.com/svn/framework/trunk/incubator/library/Zend
Acl.php and the Acl/ directory contain the code. Currently I am working on a revision that includes some changes to the code and documentation as well as a first crack at the unit tests. Please feel free to try it out presently, though be warned there are no guarantees to stability at this time, and today I expect to make at least an interstitial commit.
Darby,
Thank you for the quick response. I am looking for an example to learn how to use this component. Simon Mundy had on Aug 02, 2006 posted a code above he used on a site. I wonder if he can share how he implemented it on the site. It may be to early to see a tutorial on this.
Thanks again.
Maru
I'll let Simon speak for himself, but I suspect there will be plenty of "real-world" integrated examples once the new MVC components have been added and documented, as well as once we have a few more revisions to the Zend_Acl code and docs.
Thanks for your continued patience as these parts evolve!
ZF Home Page
Code Browser
Wiki Dashboard
For setAllow/setDeny, I think an array would be more appropriate for the first parameter rather than a delimited string. Perhaps the type could be mixed where a string would indicate a single permission and an array would indicate multiple permissions. Beyond that, I think the API is structured well and very much hope to see it adopted into the core.