Controlling Access with Zend Framework's Controller Plugins

A common problem with developing web applications is controlling user access to certain sections or single pages. Zend Framework's Controller Plugin system provides a clean and efficient way to manage these checks, without muddling up your Controllers.

Controller plug-ins are an extension of Zend_Controller_Plugin_Abstract. They allow you to modify the request (or perform any other action) during any point in the Zend Framework request life-cycle. For more information about controller plug-ins themselves, visit the Zend Framework manual.

Lets start out with the controller plugin itself. The first thing we need to know is what point(s) of the request process lifecycle we should be operating on. In our case, we need to know what controller and action the end user is requesting, so all routing must have occurred. I chose to use preDispatch() because it ensures that an optimal amount of plugins have had their opportunity to make changes to the request, and gives us the highest chance of checking access to the final destination. We can also catch any forwards from our action controller by performing the check here.

<?php
/**
 * A front controller plugin to check the current user against an access control list.
 *
 * @author A.J. Brown
 * @category Examples
 * @package Application_Controller
 * @subpackage Plugin
 * @version $Id$
 *
 */
class Application_Controller_Plugin_CheckHasAccess
    extends Zend_Controller_Plugin_Abstract
{

    public function preDispatch( Zend_Controller_Request_Abstract $request )
    {
        $isLoggedIn = Zend_Auth::getInstance()->hasIdentity();

        // Save some cycles if we're already logged in
        if( $isLoggedIn ) {
             return;
        }

 	$config     = $this->_getConfig();
        $action     = $request->getParam( 'action' );
        $controller = $request->getParam( 'controller' );

        // Make sure we don't end up in a loop
        if( $controller == $config->loginController
            && $action == $config->loginAction )
        {
            return;
        }

        $secure = $this->_checkIsSecure(
			$request->getParam( 'action' )
			, $request->getParam( 'controller' )
		);

        if( $secure ) {
            $request->setParam( 'ref', $request->getPathInfo() );
            $request->setControllerName( $config->loginController );
            $request->setActionName( $config->loginController );
            $request->setDispatched( false );
        }
    }

    /**
     * Load the configuration.
     *
     * @return Zend_Config_Ini
     */
    protected function _getConfig()
    {
        static $config = null;
        if( null === $config ) {
            $config = new Zend_Config_Ini(
                APPLICATION_PATH . '/configs/access.ini' , 'global' );
        }
        return $config;
    }

    protected function _checkIsSecure( $action, $controller, $module = 'default' )
    {
        $config = $this->_getConfig();

        // If no match is found, what should be the default?
        $public = ( isset( $config->defaultAccess ) && $config->defaultAccess == 'public' );

        // Check the action level, then controller
        if( isset( $config->controllers->$controller->actions->$action->access ) ) {
            $public = ( $config->controllers->$controller->actions->$action->access == 'public' );
        } elseif( isset( $config->controllers->$controller->access ) ) {
            $public = ( $config->controllers->$controller->access == 'public' );
        }

        return !$public;
    }
}

As you can see, the plug-in makes use of an ini configuration file to determine if a given controller and action is public or not. If a setting for the action doesn't exist, we fall back to the controller's setting. If the controller doesn't have a setting, we use whatever the default is.

Lets take a look at the configuration file.

[global]

defaultAccess = "private"
loginController = "index"
loginAction = "login"

controllers.index.acccess = "public"
controllers.index.actions.profile.access = "private"

controllers.account.access = "private"
controllers.account.actions.confirm = "public"

In this configuration, all routes will be private unless they're specifically made private. All of our actions within the "index" controller will be public, except for the "profile" action. For the "account" controller, only the "confirm" action will be public.

The last step is making sure our plugin is registered with the front controller. If we forget this part, our plug-in will never have a chance to intercept the request. Registering can be done anywhere you want and at any point during the request process. In fact, your plug-ins can even register other plug-ins. The easiest and most common way is by adding an entry in our application.ini file for the FrontController resource.

resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.params.displayExceptions = 0
resources.frontController.plugins.CheckHasAcess = "Application_Controller_Plugin_CheckHasAccess"

Conclusion

The nice part about designing an access control system using Zend Framework's controller plug-in system is that our code is separated from our controller code. Using this system, a developer doesn't necessarily have to concern himself with modifying the system. Any new controllers that are added will automatically use the system without any additional code, and permissions can be changed quickly simply by modifying a configuration file.

Happy Coding!


PHP 5.3 and Cake 1.2.0

I'm writing this quick note for anyone else that might come across this problem. I recently decided to upgrade one of my CentOS servers running a cleint's CakePHP application from a PHP 5.2.X version up to PHP 5.3.1. The package was from the remi repository, with no modifications. Suddenly, Cake stopped using my controllers' actions to generate output, and was displaying output from the default controller.

After pulling my head out, I found the issue in the dispatcher. Around line 359 or so (I hacked up my dispatcher with additional comments while debugging) in cake/Dispatcher.php, you will find the following:

$output = call_user_func_array(array(&$controller, $params['action']), empty($params['pass'])? null: $params['pass']);

The problem is, call_user_func_array requires the 2nd parameter to be an array. As you can see, cake is passing 'null' if we have no additional parameters to pass to the controller action. To fix, just change 'null' to 'array()':

$output = call_user_func_array(array(&$controller, $params['action']), empty($params['pass'])? array(): $params['pass']);

Now everything should be working again.

Note that this particular application is using a very early version of Cake 1.2. This problem might be fixed in later versions.


What does "meeting the dealine" mean to you?

One of the most important aspects of maintaining good relationships is doing what you say you're going to do, when you say you're going to do it. As a software developer, your time to shine in this context is delivering the goods before your customer has to wonder where they are.

It's equally important to know what done means to the customer, and how it may conflict with your definition of done. If I'm hiring you to line a parking lot for my store, and the deadline is the day of the grand opening, done is when the paint is dry, not when the last stroke is sprayed (and still wet!). Since I don't paint asphalt for a living, you surely can't expect me to know how long it takes the paint to dry.

A Real World Example

I once worked for an international online retailer who's idea of "meeting a deadline" was a little different than mine. We were scheduled to do a major release of the website consisting of a brand new design, and some additional features. It had a large supporting marketing campaign advertising it as a new generation of the store with super-awesome-o features. A hard launch date was set in the marketing material and community hype, so launching on that date was critical. The developers (through no fault of their own) ended up needing to work 20+ hour shifts in the days approaching the launch in order to make sure all of the promised features made the cut. After gallons of coffee and mountain dew, the launch was finally completed at 11:50pm local time on the day of the deadline.

In the coming days, the launch was touted as a total success, and the words "we met the deadline" were strewn about the meeting halls by the software development managers. I felt that expressing my opinion about the so called meeting of the deadline would create a lot of flak and would be taken as a slam to my fellow developers, so I decided to keep it to myself.

Quite frankly, this was not a satisfactory meeting of the deadline in my opinion. The expectation as a customer of the site was that they would log-on on the launch date and see a brand new website with pretty little features. Instead, we kept them waiting and wondering. Our blog site for the store was lit up with comments by loyal customers, wondering where the site is. Many had given up on it being launched that day, and only those 7 hours behind GMT (the time zone any international company should be paying attention to) or greater actually saw the website on the launch date. Yet, we were proud of meeting this tough deadline because "hey, we technically finish it on the date we promised!".

The real issue in the online retailer case is the approach to the problem. The delivery date and the marketing campaign date should not have been the same. The deadline for software development should have been at least 1 day prior to the launch, giving the developers a deadline that could be approached in the manner it was, but still allowing the site to actually be launched and available to the customer when they expected it to be. The developers could push their efforts right up to the wire (as we did) without any negative impact.

Conclusion

You've probably heard the saying "If you're 5 minutes early, you're on time. If you're on time, you're late!" or some similar version of it. This saying definitely holds a lot of merit when it comes to delivering your products. Surprise your customers and clients by delivering "5 minutes early" instead of making them come looking. Don't use the vagueness of your own deadline as an excuse to say you delivered on time. Save that for the cable company, and their "sometime between 9am and 5pm" appointments.


Zend_Service_PayPal Proposal Back on Track

I just wanted to drop a quick note letting everyone know that after nearly 2 years of dormancy, Zend_Service_PayPal is back in the works. Shahar Evron has been too busy with his work over at Zend Technologies to continue work on the proposal, so I've volunteered my time to get it back on track.

If you're interested in seeing this component in Zend Framework soon, please follow progress at the Zend Framework Contributors Wiki, This blog, and on the project page at GitHub. Any feedback, suggestions, and patches you can provide will be nothing but useful. Send me an email, comment on the wiki, or This proposal is still in it's early stages, but I hope to be knocking out a lot of the work in the next couple of weeks.