The first time you develop any kind of private web application, it eventually occurs to you someone else might find it and start mucking with your data. So you slap a password on it and call it good. Then you show it to your boss, and he loves it and wants everyone to start using it, so you slap on user accounts, each with their own passwords.
So far, your application supports authentication. Authentication says who can access your application.
A few months down the road you boss’s boss pulls you into her office and wants to know why Fred was able to change some sales numbers. After this meeting you rush out to block Fred from certain parts of the application. You’ve just moved from authentication (who can use the system) to access control (which parts of the system can specific users access). This is where things can quickly get messy and bloated with a formalized system
if($user == 'Fred' || $user == 'Judy' || $user = 'ThatAdminWhoWontDateMe' || ...)
Magento’s Admin Panel includes an authentication system (usernames and passwords), and also includes a robust system for creating Access Control Lists (ACL), which allows a store owner to create fine grained roles for each and every user in their system. This article will cover the Admin Panel’s implementation of ACL and show you how to leverage it in your own custom applications/modules.
Before we do that, let’s take a look at the general concept of Access Control Lists.
Bearded Access Control
If you’ve spent anytime installing PHP web applications on Unix/Linux/BSD based systems, you’re already familiar with one access control system; Unix file permissions. When you say
chmod 755 file.php
chmod +x file
you’re telling your computer that certain users on your system can either read, write, or execute a particular file, (if you’re interested in the specifics, this tutorial has you covered).
The system is cryptic, but by using simple numbers to represent file permissions, the underlying system code can run very quickly. This was hugely important in the early days of computers, when processing speed was extremely limited. Even today it makes sense since the system has to run a permissions check any time any file is accessed. For example, if a PHP page includes 10 different PHP files, your computer is running 10 ACL checks behind he scenes.
Modern Access Control
As computers have gotten faster, and more specialized systems that come under less sustained use than a file system have been developed, modern access control has moved towards using logical names organized into a tree like structure.
For example, let’s say we were modeling access control for who could enter what areas of a bank. First, we might develop a number of resources (the areas of the bank)
- /bank/lobby
- /bank/teller_station
- /bank/desk
- /bank/vault
Then, we’d develop a number of roles that describe groups of Bank employees
- Bank Teller
- Banker
- Bank Guard
Then, we’d assign some of our resources to specific roles
Role Bank Teller could access
- /bank/lobby
- /bank/teller_station
Role Banker could access
- /bank/lobby
- /bank/desk
Role Bank Guard could access
- /bank/lobby
- /bank/desk
- /bank/vault
Finally, we’d assign a role to each bank employee.
- Bob Smith is a Bank Guard
- Jenny Henderson is a Banker
- John Jones is a Teller
- etc...
Magento Access Controller
This brings us to Magento’s ACL implementation. If you browse to
System -> Permissions -> Roles
in the Admin Panel, you can see a list of all the Roles defined in your system. A default installation comes with one role, Administrators. You can add new roles by clicking on the “Add New Role” button. If you click on “Add New Role” and browser to the “Role Resources” tab, you can see a tree-list of all the available resources in your system.
Behind the scenes, these roles are modeled using a simple, non-EAV model class.
Mage_Admin_Model_Roles
Mage::getModel('admin/roles')
There’s no direct access resources via the Core model api. Instead, the Roles model contains a getResourcesTree
method which will allow you to programmatically access a list of resources with code something like this
$resources = Mage::getModel('admin/roles')->getResourcesTree();
$nodes = $resources->xpath('//*[@aclpath]');
echo '<dl>';
foreach($nodes as $node)
{
echo '<dt>' . (string)$node->title . '</dt>';
echo '<dd>' . $node->getAttribute('aclpath') . '</dd>';
}
echo '</dl>';
This reads the ACL resource for the Admin Panel from the Magento config. You’ll see this again when we add our own resources.
A Role is essentially a collection of resources. You define a role, and then assign any number of resources to it. In addition to the node based resources above, there’s a special resource named all. A role with the all resource is a super user who will be able to access any existing and any future resource in the system.
You might create a role named Customer Administrator, and then give it access to the following resources
admin/customer/group
admin/customer/manage
This would give any user account with the “Customer Administrator” role access to resources that would allow them to manage customers.
Users accounts can be found at
System -> Permissions -> Users
When you create a user you assign the accout a password in the User Info tab, which is used for authentication. If you click on the User Role tab you can select a role for your user. The default user you created during the installation of Magento is automatically assigned to the Administrator role. The Administrator role has access to the special resource all, giving you super-user access to the system.
Applying Magento ACL
So, that’s all good in an abstract theory kind of way, but you’re probably wondering how Magento actually enforces the ACL hierarchy we just described. “Access to a Resource” seems like a pretty abstract concept.
Let’s take a look at the Mage_Adminhtml_Controller_Action
class. This is the controller that all Admin Panel action controller inherit from. Within the preDispatch
method (called before dispatching any controller), you’ll see the following snippet
if ($this->getRequest()->isDispatched()
&& $this->getRequest()->getActionName() !== 'denied'
&& !$this->_isAllowed()) {
$this->_forward('denied');
$this->setFlag('', self::FLAG_NO_DISPATCH, true);
return $this;
}
Notice the call to the _isAllowed
method? Let’s take a look at that.
protected function _isAllowed()
{
return true;
}
The _isAllowed
method is where you, as a Admin Panel programmer, should place your ACL checks. As you can see, by default, this method returns true. That means if you don’t define your own _isAllowed
method your Admin Panel features will be open to any user with an Admin Panel account, and people using your code will have no way to restrict access to your features. This may not be a problem, but it’s definitely something you should be aware of.
We can hear the gears in your mind clicking away, already trying to figure out the most efficient way to fetch a list of resources for a logged in user to determine if they have access to a specific resource or not. Fortunately, the Magento system engineers have already offered us a simple way to do this. Take a look at the _isAllowed
method in the Mage_Adminhtml_System_ConfigController
class
protected function _isAllowed()
{
return Mage::getSingleton('admin/session')
->isAllowed('system/config');
}
Here we’re getting a reference to an admin/session
object (Mage_Admin_Model_Session
), and calling its isAllowed
method. This method takes a single parameter, which is a string representation of the resource you want to check for. Although the full resource name is admin/system/config
, you need only pass in the system/config
portion of the node/path.
That’s the bare minimum you should do when creating your own Admin Panel action controllers. However, you can also make additional calls for more finely grained access control. For example, in the same config controller there’s this
protected function _isSectionAllowed($section)
{
try {
$session = Mage::getSingleton('admin/session');
$resourceLookup = "admin/system/config/{$section}";
$resourceId = $session->getData('acl')->get($resourceLookup)->getResourceId();
if (!$session->isAllowed($resourceId)) {
throw new Exception('');
}
return true;
}
catch (Exception $e) {
$this->_forward('denied');
return false;
}
}
This checks to see if a user has access to a particular System Config section. If you’ve been following along with the other tutorials, you’ll remember the need to setup a resource if you were adding a config section from the Custom Magento System Configuration article. The above code is what’s checking for that resource.
The controller layer is where the primary access control check is done, but in the grand tradition of configuration based systems, Magento isn’t very opinionated on where checks should go. For example, the core team has additional ACL checks in some Block classes to ensure they’re not viewed by people without access.
Sales/Block/Adminhtml/Billing/Agreement/View.php
...
protected function _isAllowed($action)
{
return Mage::getSingleton('admin/session')->isAllowed($action);
}
...
The ACL is also used to determine which Menu navigation items should be displayed to a logged in user.
File: app/code/core/Mage/Adminhtml/Block/Page/Menu.php
...
protected function _checkAcl($resource)
{
try {
$res = Mage::getSingleton('admin/session')->isAllowed($resource);
} catch (Exception $e) {
return false;
}
return $res;
}
We’ll cover this in greater detail after we’ve setup our own resource. The point here is don’t be shy about adding in additional resource checks.
API Detour
Before we move to on defining our own ACL resources, we’re going to take a little detour into Magento Web Services. If you’ve ever setup Magento to use the SOAP or XMLRPC APIs, or maybe even gotten adventurous and defined your own methods for those APIs, you know that there’s an entirely separate ACL system for those Web Services.
Although Web Services have a separate ACL system, its implementation is almost identical to the Admin Panel ACL system. It has Users, Resources, and Roles, which you can view via the Admin Panel at
System -> Web Services -> Users
System -> Web Services -> Roles
You may also programmatically view the resource list in a similar fashion. Notice we’re looking at api/roles
this time instead of admin/roles
$resources = Mage::getModel('api/roles')
$nodes = $resources->xpath('//*[@aclpath]');
echo '<dl>';
foreach($nodes as $node)
{
echo '<dt>' . (string)$node->title . '</dt>';
echo '<dd>' . $node->getAttribute('aclpath') . '</dd>';
}
echo '</dl>';
The API system and ACL’s role in it is an article unto itself. We’re mentioning it here only to draw a distinction between the two systems.
Configuring a Resource
So, overtly exhaustive background out of the way, we’re going to learn where Magento stores its ACL resources, as well as how we can add our own. By adding our own ACL resources, we allow the end users of our modules “enterprise level” control over who may access their functionality, including automatic integration with the Admin Panel’s navigation system.
Unsurprisingly, Magento stores its ACL resources in the global config. In older version of Magento, this means a module’s config.xml
file. However, newer versions have broken the ACL resources out into adminhtml.xml
files.
Within the <adminhtml>
node there’s a node named <acl/>
which contains all the resources. There are other nodes in the config named <acl/>
, so be sure you’re looking at the right one
<config>
<adminhtml>
<acl>
<resources>
<all>...</all>
<admin>
...admin definitions go here...
</admin>
</resources>
</acl>
</adminhtml>
</config>
Remember, if you’re putting these rules into the adminhtml.xml
file you don’t need the surrounding <adminhtml>
tag, see
app/code/core/Mage/Adminhtml/etc/adminhtml.xml
for examples. Magento appears to merge all the adminhtml.xml
files, and insert them as an <adminhtml/>
node into the global config.
So, within the <resources/>
node, the top level <admin>
node is where we’ll be placing our resources. That’s because all resources take the form
admin/path/to/resource
The <all>...</all>
node is for the super user role we discussed earlier.
The syntax here gets a little confusing. We’re using an XML file to describe a node syntax, along with other meta-data for that node syntax. If that seems overly recursive to you, you haven’t spent much time with programmers! Take a deep breath, and remember that despite the verboseness, there is a logic to the configuration syntax.
Let’s take a look at the definition of the factory default resource admin/system/store
. First, there’s the full definition of the “admin” portion of the resource
<config>
<adminhtml>
<acl>
<resources>
<!-- ... -->
<admin translate="title" module="adminhtml">
<title>Magento Admin</title>
</admin>
</resources>
</acl>
<adminhtml>
</config>
The <title/>
node contains the value that will show up in the Admin Panel’s node tree display.
So, that gets us the first part of of our resource admin/
. Next, we want to add a sub-node named system
, for a full resource path named admin/system
. To do this, we add a set of sub-nodes to the <admin/>
node, all contained with a node named <children/>
(remember, deep breaths!)
<resources>
<all>
<title>Allow everything</title>
</all>
<admin translate="title" module="adminhtml">
<title>Magento Admin</title>
<children>
<system translate="title">
<title>System</title>
<sort_order>90</sort_order>
</system>
</children>
</admin>
</resources>
Specifically, we’re adding the following
<children>
<system translate="title">
<title>System</title>
<sort_order>90</sort_order>
</system>
</children>
The node name is <system>
, because that’s the name of the resource path. Again, <title>
dictates what will show up in the Admin Panel when the node tree is displayed, and <sort_order>
is used to create the display order. These last two nodes have nothing to do with the ACL check itself, they’re meta-data for the resource.
The <children/>
syntax may seem overly verbose, but by adopting it we’re able to define both the path (admin/system
), as well as include meta-data about the resource (title, sort_order). Remember, we’re using XML (a node based system) to configure and describe another node based system.
So, with the above we have a resource named admin/system
. To get the final resources of admin/system/store
, we’d need to add another <children/>
node, this time within <system/>
.
<resources>
<all>
<title>Allow everything</title>
</all>
<admin translate="title" module="adminhtml">
<title>Magento Admin</title>
<children>
<system translate="title">
<title>System</title>
<sort_order>90</sort_order>
<children>
<store translate="title">
<title>Manage Stores</title>
</store>
</children>
</system>
</children>
</admin>
</resources>
Specifically, we’re adding
<children>
<store translate="title">
<title>Manage Stores</title>
</store>
</children>
The same meta-data rules apply to this, and all other <children/>
nodes.
Creating our Own
So, if you completed the previous tutorial, you know you can configure the Admin Panel with custom navigation items. The Magento Block that renders those navigation items is at
class Mage_Adminhtml_Block_Page_Menu
app/code/core/Mage/Adminhtml/Block/Page/Menu.php
The class includes an ACL check
$aclResource = 'admin/' . ($child->resource ? (string)$child->resource : $path . $childName);
if (!$this->_checkAcl($aclResource)) {
continue;
}
This constructs a resource name by appending your menu’s node structure to the string 'admin/'
, and then checks if the logged in user has access to that resource. If not, it skips rendering the menu. For example, for the menus defined in the previous tutorial, the resources of
admin/tutorial_menu
admin/tutorial_menu/first_page
admin/system/another_menu_from_us
will be checked before rendering the menus
tutorial_menu
tutorial_menu/first_page
system/another_menu_from_us
So, if we defined resources in our module’s config.xml
(or adminhtml.xml
) file with the above names, our menus would only show up for users whose roles contains the above resources. Let’s give it a try.
If you haven’t yet, complete the previous tutorial, and then add the following resources to your config.xml
(or, if you’re using a newer version of Magento, feel free to add them to adminhtml.xml
).
<config>
<adminhtml>
<!-- ... -->
<acl>
<!-- ... -->
<resources>
<admin>
<children>
<tutorial_menu translate="title" module="adminhelloworld">
<title>Tutorial Menu</title>
<sort_order>9999</sort_order>
<children>
<first_page module="adminhelloworld">
<title>Our First Page</title>
</first_page>
</children>
</tutorial_menu>
<system>
<children>
<another_menu_from_us>
<title>Here Too!</title>
</another_menu_from_us>
</children>
</system>
</children>
</admin>
</resources>
<!-- ... -->
</acl>
<!-- ... -->
</adminhtml>
</config>
Sharp readers will notice that the syntax for ACL rules is almost exactly the same as menu rules, minus the <action/>
node. Let’s take a look at our rules. You’ll need to clear your Magento cache, as well as log out of the Admin Panel and either clear out your browser session, or clear your Magento sessions.
The last step is necessary because Magento caches the ACL resources you have have access to in your session data. This helps improve performance (an ACL list doesn’t need to be fetched from the database on each request), but can cause a little bit of confusion whenever you’re adding new ACL roles. You can fully clear out your sessions by deleting all the files in var/session
, and deleting all the data in the core_session
tables.
Once this is done, log into your Admin Panel with your main admin account, or another account with access to the “all” resource, and browse to
System -> Permissions -> Roles -> Add New Role
then click on the Role Resource tab
The resource tree now contains your resources, which will allow you to automatically hide the navigation menu from users without the resource, as well as programmatically validate logged in users against that resource.
Wrapup
In this article we covered Magento Access Control Lists, and how they provide fine grained, enterprise level permission control for the Magento Admin Panel. We examined how Magento code uses ACL to protect its Admin Panel resources, as well as how you may create custom ACL resources for your own modules. By taking the time to create ACL resources for your custom modules, you allow you users a greater control and flexibility over the deployment of their system.