Kris Wallsmith

Symfony Guru at opensky.com.
Discussing web development, Symfony and fatherhood.

Jan 16

Symfony2 Security Voters

I answered this question on StackOverflow today that is probably worth repeating here. The poster was asking how to implement subscription-based authorization logic in Symfony2. I imagine he models his problem something like this:

class Subscription
{
    const SECURED_AREA_FOO = 'FOO';
    const SECURED_AREA_BAR = 'BAR';

    // ...

    /** @ManyToOne(targetEntity="User", inversedBy="subscriptions") */
    public $user;

    /** @Column */
    public $securedArea;

    /** @Column(type="date") */
    public $start;

    /** @Column(type="date") */
    public $end;

    public function isActive()
    {
        $now = new DateTime();

        return $now >= $this->start && $now < $this->end;
    }

    // ...
}

class User implements UserInterface
{
    // ...

    public function getRoles()
    {
        $roles = $this->roles;

        foreach ($this->subscriptions as $subscription) {
            if ($subscription->isActive()) {
                $roles[] = 'ROLE_SUBSCRIPTION_'.$subscription->securedArea;
            }
        }

        return $roles;
    }

    // ...
}

He may then configure access control in his security.yml:

access_control:
    - { path: "^/sections/foo", roles: [ ROLE_SUBSCRIPTION_FOO ] }
    - { path: "^/sections/bar", roles: [ ROLE_SUBSCRIPTION_BAR ] }

This solution is nice enough, but we can do better. As the poster pointed out when someone suggested this solution, it doesn’t make sense to hydrate all of a user’s subscription objects (some of which may have expired years ago) just to fetch a string value. On top of that, fetching all of these objects every request is a waste of resources because the system will only be checking for the existence of one of them in any given request.

This authorization logic can be implemented much more lightly by encapsulating it in a custom security voter.

Voters

When questions about authorization are asked in Symfony2, the answer is arrived at by a process of voting. For example, when someone requests an URL that matches a configured access_control rule, a vote is held to decide whether to allow or deny access to that resource.

This voting process is similar in some ways to a US appeals court. There are a certain numbers of judges presiding over any given proceeding and in the end a decision is rendered. Occasionally judges recuse themselves from a case for this or that reason.

The authorization portion of the Symfony2 security component also includes a panel of judges called voters. These voters each have a say whenever questions of authorization come up in your application. Not every voter will have an opinion on every decision; some will abstain.

There are two basic types of votes: whether a user is granted a certain security attribute (i.e. the ROLE_USER attribute) and whether a user is granted a certain security attribute for a certain object (i.e. the EDIT attribute on blog post X). Each voter can be tuned to only chime in when certain votes are held. For example, you could write a voter that only participates when considering users with Gmail addresses.

In this particular case, we want to create a voter that only chimes in when questions regarding access to areas secured by subscriptions are raised. We can signify this by creating a new set of attributes that start with the string SUBSCRIPTION_.

access_control:
    - { path: "^/sections/foo", roles: [ SUBSCRIPTION_FOO ] }

This configuration proposes that only users with access to the security attribute SUBSCRIPTION_FOO should be granted access to any URL that starts with /sections/foo. Since there are no voters configured to evaluate this particular set of attributes, access with always be denied. But that is solved easy enough by creating a custom security voter.

This voter will look something like this:

class SubscriptionVoter implements VoterInterface
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function supportsAttribute($attribute)
    {
        return 0 === strpos($attribute, 'SUBSCRIPTION_');
    }

    public function supportsClass($class)
    {
        return true;
    }

    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $user = $token->getUser();

        foreach ($attributes as $attribute) {
            if ($this->supportsAttribute($attribute)) {
                $securedArea = substr($attribute, strlen('SUBSCRIPTION_'));

                // use the entity manager to query for active
                // subscriptions that connect the current user to the
                // requested secured area

                return $success ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
            }
        }

        return VoterInterface::ACCESS_ABSTAIN;
    }
}

And be configured in the service container something like this:

services:
    subscription_voter:
        class: SubscriptionVoter
        public: false
        arguments:
            - @doctrine.orm.entity_manager
        tags:
            - { name: security.voter }

And that’s all there is to it. You have encapsulated your custom authorization logic in one clean class and added it to the Symfony2 security layer.

Other Applications

This is an example of one specific application of security voters, but there are many more. If you are struggling with how to implement some special access control logic that doesn’t fit nicely into either security roles or the security component’s ACL, consider creating a custom voter.


Jan 6

Twig Node Visitors (Part 2)

This is the second in a series of articles on Twig node visitors. Please read part one first.

Node visitors can be used for any number of things. The Twig_NodeVisitorInterface interface itself is just three methods:

interface Twig_NodeVisitorInterface
{
    /**
     * @return Twig_NodeInterface The modified node
     */
    function enterNode(Twig_NodeInterface $node, Twig_Environment $env);

    /**
     * @return Twig_NodeInterface The modified node
     */
    function leaveNode(Twig_NodeInterface $node, Twig_Environment $env);

    /**
     * @return integer The priority level
     */
    function getPriority();
}

The interface is simple and powerful. It provides a mechanism for manipulating nodes before a template is compiled down to a PHP class. It puts no constraints on what it can be used for. In the Twig core there are node visitors for escaping and optimization, both of which bear no semblance to what we are going to do here.

Our use case at OpenSky has to do with querying blocks of CMS content from the database eagerly, based on what blocks have been included in the template using the following function:

{{ cms_block('header') }}

We can accomplish this eager loading by using a node visitor to statically analyze each template and stashing the CMS blocks it calls for. The first method of the interface, enterNode(), can be used to look at every node in each template to see if it represents a call to this function.

public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
{
    if ($cmsBlock = $this->getCmsBlockKey($node)) {
        $this->cmsBlocks[] = $cmsBlock;
    }

    return $node;
}

// ...

private function getCmsBlockKey(Twig_NodeInterface $node)
{
    if ($node instanceof Twig_Node_Expression_Function
        && 'cms_block' == $node->getAttribute('name')) {
        return $node->getNode('arguments')->getNode(0)->getAttribute('value');
    }
}

This code looks at each node to see if it represents a call to the cms_block function and pushes the first argument (the name of the CMS block) to an internal array for use later.

After running this and debugging what was being stacked onto that array we found a few issues. First, the visitor was not smart enough to crawl included or imported templates and look for CMS blocks there. Second, the node visitor was not being reset for each template so by the end of cache warmup all CMS blocks called across the entire application were stacked on that internal array.

Solving the first issue meant adding a way to recursively crawl the graph of each template’s children — a child being a call to either {% include %} or {% import %} in our case. We did this by adding another internal stack:

public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
{
    if ($cmsBlock = $this->getCmsBlockKey($node)) {
        $this->cmsBlocks[] = $cmsBlock;
    } elseif ($templateName = $this->getIncludedTemplateName($node)) {
        $this->includes[] = $templateName;
    }

    return $node;
}

// ...

private function getIncludedTemplateName(Twig_NodeInterface $node)
{
    if ($node instanceof Twig_Node_Include || $node instanceof Twig_Node_Import) {
        return $node->getNode('expr')->getAttribute('value');
    }
}

We solved the second issue by listening for the root node, an instance of Twig_Node_Module, and clearing the internal stacks when we leave that node:

public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
{
    if ($node instanceof Twig_Node_Module) {
        // todo: make these stacks available at runtime

        // reset
        $this->cmsBlocks = array();
        $this->includes  = array();
    }

    return $node;
}

We’re in pretty good shape at this point. We have built a node visitor that collects the information necessary for eagerly querying our CMS for the blocks that each template calls for. Now we just need to make this information available at runtime, when the eager query needs to be executed.

We’ll do this in the next post by wrapping the Twig_Node_Module in our own module node that compiles down the a PHP class with the necessary public methods. Stay tuned!


Nov 30

Getting Twiggy With It: Node Visitors

I am going to write about node visitors: one of the more obscure but powerful concepts in Twig. To help make sense of it I will be using a simple, real world example.

At OpenSky we recently added a basic CMS to our site that allows us to make edits to text without going through the hassle of editing a template and redeploying the entire codebase. We added a module to our admin that manages these CMS “blocks” as documents in MongoDB. Each document represents a block and includes a unique, descriptive key and the text content. In our templates we render these blocks using a simple Twig function. For example:

{{ cms_block('welcome') }}

When rendering a template, Twig would come across this function and issue a query to MongoDB for the welcome document in the collection of CMS blocks. Easy enough, right?

Not quite. We would like to speckle these blocks all over pages across the site: a paragraph here, a header there, an image over there, meta tags, Facebook tags… We could be looking at adding a dozen or more queries to a page for our little CMS, which is unacceptable.

Twig’s own Flux Capacitor

Imagine being able to look into the future to see what CMS blocks a template was going to use and issue a single query to prefetch them all from the database. That is exactly the sort of thing you can do by taking advantage of the static compilation phase, when Twig converts your templates into optimized PHP classes.

A Quick Primer on Twig Internals

Let’s take a step back and review some of the guts of Twig. I promise I’ll return to Back to the Future references later.

Compilation of a template into a PHP class is a four step process:

  1. Load
  2. Tokenize
  3. Parse
  4. Compile

The first step involves an implementation of Twig_LoaderInterface, of which only one method is pertinent to us: getSource(). This method accepts a template name and returns the raw content of that template. In the case of the default Twig_Loader_Filesystem implementation, this boils down to a simple call to file_get_contents().

The second step, tokenizing the loaded source, is handled by an implementation of Twig_LexerInterface which consists of one method, tokenize(). If you are having a hard time sleeping at night you can read up on lexical analysis on Wikipedia. For the purpose of this article, you only need to understand that the lexer converts what you’ve written in your template using Twig’s grammar into a stream of simple PHP objects called tokens.

In the third step the stream of tokens created by the lexer is parsed into a multi-dimensional tree of nodes. This work is done by the parser in cooperation with a collection of token parsers. This is the extension point you would hook into if you wanted to create a new {% foo %} tag, a topic outside the scope of this article.

In the final step the node tree created by the parser is compiled into runtime PHP code. Each node in the tree implements Twig_NodeInterface, which includes a compile() method that allows it to write arbitrary code to the resulting template class.

Great Scott!

That description of the Twig engine was criminally brief, but it should give you enough knowledge to understand where node visitors come in. After the parser creates the node tree but before the tree is compiled into PHP code, the parser recursively iterates over the tree and filters each node through its registered node visitors. Each visitor has a chance to inspect every single node in the tree, make changes, replace it with another, or even remove it altogether. It’s using this tool that we are able to inspect each template and magically anticipate what CMS blocks each template will need, before that template is rendered.

Coming Soon…

Dive into code with a working example of a Twig node visitor.


Oct 17

Cancer

I’ve been sitting on this for awhile and think I should share now.

My wife of 7 years, partner of 10 and mother of my three young children was diagnosed with stage 4 breast cancer a few months ago. It’s been an extremely difficult fact to come to terms with. The diagnosis is serious, but we are very hopeful. She has just finished her third round of chemotherapy and the scans say all the marshmallows are shrinking. That news and the outpouring of support from our family, friends and colleagues adds up to a lot to be grateful for.

If you pray, please pray for the complete eradication of cancer from Franya’s body. If you don’t pray, please send some happy thoughts our way.

Anyway… this is why I’ve been relatively quiet lately, will only be at ZendCon for less than 24 hours and won’t be coming to Cologne for Symfony Day at all. I’m not leaving anything, I just have less time and energy to spread around.

Blessings and protection,
Kris


Jan 28

My first passport…

…has expired! I have stamps for these countries:

  • The Bahamas
  • France
  • The Netherlands
  • The British Virgin Islands
  • Japan

What will the next 10 years will bring?


Dec 3
Old Glory

Old Glory


Oct 29

Oct 27

Look Behind the “Feature Veil”

I’ve been thinking about a decision Apple made awhile ago to allow free iOS apps to offer in-app purchases. My recollection of their argument against doing this is that users would be frustrated by downloading a free app only to have to purchase something in-app to get it to work.

Around the same time Apple decided to allow free apps to offer in-app purchases, the App Store also began promoting each application’s “Top In-App Purchases.” This feature is pretty silly (who cares?), but it’s positioned prominently at the top of each app’s page.

It seems clear that Apple’s intention here is to denote which apps ask you to buy more stuff. This is a valid objective, but they’ve accomplished it in a strange way. The feature, when taken at face value, appears to do something unrelated.

This particular “feature veil” is pretty easy to see through, but I’m guessing the technique has been used before, by Apple and others, in less obvious instances. Can you think of any?


Oct 26
Who can find the jet lagged American?

Who can find the jet lagged American?


Oct 24

Keep a trim autoloader

The symfony 1.4 autoloader works by scanning your PHP class files and remembering where each class and interface is defined, so it can magically load it when needed. This class-to-filename mapping is stored as an array in your application’s cache directory.

You can (and should) look at this array by opening this cache file in your text editor from time to time: cache/%SF_APP%/%SF_ENVIRONMENT%/config/config_autoload.yml.php. Inside you’ll see a big array with // comment headers.

<?php
// auto-generated by sfAutoloadConfigHandler
// date: 2010/10/24 05:29:41
return array(

  // sfDoctrineGuardPlugin_lib
  'pluginsfguardgroupformfilter' => '/path/to/plugins/sfDoctrineGuardPlugin/lib/filter/doctrine/PluginsfGuardGroupFormFilter.class.php',
  'pluginsfguardpermissionformfilter' => '/path/to/plugins/sfDoctrineGuardPlugin/lib/filter/doctrine/PluginsfGuardPermissionFormFilter.class.php',
  'pluginsfguarduserformfilter' => '/path/to/plugins/sfDoctrineGuardPlugin/lib/filter/doctrine/PluginsfGuardUserFormFilter.class.php',

  // ...

A few things to look for in this file:

  1. You should not see any reference to libraries that have their own autoloader. This includes the symfony core classes, Swiftmailer, Zend Framework, etc. If you see references to classes from one of these libraries, your autoloader is not configured correctly.
  2. You should not see any non-runtime classes.

You’ll probably need to add some configuration to accomplish that second point. For example, your tasks and Doctrine migration classes will never be involved in a runtime request, so they should not be included in the autoloader’s cache.

Configure the autoloader

Fortunately, excluding these files is easy enough using autoload.yml.

Add this file to your project…

# config/autoload.yml
autoload:
  # extend the "project" configuration block defined in the symfony core
  project:
    exclude: [vendor, symfony, model, migration, task]

Clear your cache…

$ php symfony cache:clear

Visit your site in the browser to prime the cache, and re-inspect the cached config_autoload.yml.php file. Your task and Doctrine migration classes should no longer be represented there.

Add this to your installer

The new installer mechanism in symfony 1.4 is the perfect place to add this sort of thing since you’re going to want to do it on every project. For an example of how this is done, check out commit dede92e on my GitHub symfony-installer project.

Read more

If you want to learn more about configuring the symfony 1.4 autoloader, please read through The symfony Reference Guide, specifically the section on autoload.yml.


Page 1 of 6