JonBlog
Thoughts on website ideas, PHP and other tech topics, plus going car-free
Handling complex permissions with sfGuard
Categories: Symfony

I work on a large web application using symfony 1.0, and we add new subsystems frequently, each having its own set of authorisation/security requirements. When we started writing the application, it was clear that we needed a unified permissions system that would be both simple to use, and highly flexible. I’ve talked about our solution previously on the symfony forum, but thought I would expand on it here by explaining how it works, and discussing some of the code.

Firstly, it is worth bearing in mind that in a web-based system, authorisation to see something can be defined using three basic levels, which I will call “page”, “sub-page” and “data”. The page level is the simplest; it determines whether the presently logged in user (or the current anonymous user) has permission to access the current page. In symfony, this is done via sfGuard (for the Propel ORM) or sfDoctrineGuard (for the Doctrine ORM). If the user does not meet the requirements defined in the relevant security.yml, the requested module/action is not executed (and either an error or the logon box is displayed, according to settings.yml configuration).

At the sub-page level, an item is displayed or not displayed according to whether the user has a particular credential, and at the data level, database rows are filtered according to further properties belonging to the user. I am sometimes asked what symfony does to achieve these second two, and the simple answer is – sadly! – nothing. This is because this level of authorisation is complex, and has to be handled at the application level; there is not much a framework can do to introspect what should be visible to each user. However, with a good framework to hand – such as symfony – we can handle both scenarios fairly easily.

OK, let’s take the sub-page level first, since it is easy. In one of my current projects, users get either read or write access to a system, depending which sfGuardGroup they belong to. Both user types have access to a Parts View screen, but only the write access users may edit Parts. Accordingly, the hyperlink to the edit screen is only available if the user has the sfGuard permission ‘field_returns_write’:

<td>
  <?php if ($sf_user->hasCredential('field_returns_write')): ?>
    <?php echo link_to(
      $part->getPartNumber(),
      '@nfFieldReturnsEdit?part_number=' . $part->getPartNumber()
    ) ?>
  <?php else: ?>
    <?php echo $part->getPartNumber() ?>
  <?php endif ?>
</td>

Here we have just modified a small screen element according to user credentials; it is, of course, quite possible to render whole blocks on screen differently according to what credentials a user holds. However, it is worth bearing in mind that if a screen radically differs according to user type, it is probably worth having two different templates, or even forwarding to a different action.

Now that’s covered, let’s now move onto data-level authorisation. The purpose of this is to hold values, or properties, on a user or user-group level (in my use case, user groups are usually the names of client organisations). This permits us to:

  • Offer and limit functionality according to user type
  • Filter list views according to some kind of ownership

The entities look like this, joining onto the standard sf_guard_user table:

We have user_main, which is our sfGuard profile table, as it has a 1:1 relationship with sf_guard_user. User-level properties are stored in user_main_properties; each user also belongs to a group, in user_org, and the properties at this level are stored in user_org_properties; and each organisation has a category, stored in user_cat.

In the usual way, a custom user class is set up in factories.yml, extending sfGuardSecurityUser. Note that the database code uses Propel, but Doctrine could be used just as easily.

class myUser extends sfGuardSecurityUser
{
    /**
     * Called when a user signs in successfully
     */
    public function signIn($user, $remember = false, $con = null)
    {
        // Do standard signin
        parent::signIn($user, $remember, $con);

        // then init the properties
        $this->initUserProperties();
        $this->initOrgProperties();

        // Set up user type name credential
        $this->setUserTypeCredential();
    }

    /**
     * Called when a user signs out - clears credentials
     */
    public function signOut()
    {
        // Clears user credentials
        $this->getAttributeHolder()->clear();

        // Pass call along to parent
        parent::signOut();
    }

    public function initUserProperties()
    {
        $c = new Criteria();
        $c->add(
            UserMainPropertyPeer::ID_USER_MAIN,
            $this->getProfile()->getIdUserMain()
        );
        $props = UserMainPropertyPeer::doSelect($c);

        return $this->initProperties($props, 'user_properties', 'user');
    }

    public function initOrgProperties()
    {
        $c = new Criteria();
        $c->add(
            UserOrgPropertyPeer::ID_USER_ORG,
            $this->getProfile()->getIdUserOrg()
        );
        $props = UserOrgPropertyPeer::doSelect($c);

        return $this->initProperties($props, 'org_properties', 'organisational');
    }

    /**
     * Shared private function for setting organisational and user properties
     *
     * @param $rs An array of Propel objects that must support getName and getValue (ie there must be `name` and `value` in the underlying table)
     * @param $strAttrib is the name of the attribute to set
     * @param $strType the name of the type, usually "organisational" or "user"
     */
    private function initProperties($rs, $strAttrib, $strType)
    {
        $text = array();
        $props = array();

        foreach ($rs as $prop)
        {
            $name = $prop->getName();
            $value = $prop->getValue();
            $props[$name] = $value;
            $text[] = "${name}=${value}";
        }

        sfContext::getInstance()->getLogger()->info(
            'There are ' . count($props) . " $strType properties: " . implode(', ', $text)
        );

        return $this->setAttribute($strAttrib, $props);
    }

    /**
     * Reads the user type from the organisation table, and sets it as a symfony user credential
     */
    private function setUserTypeCredential()
    {
        // Null all the intermediate derefencing values first
        $org = $cat = $type = null;

        // Dereference the shortname if it exists (it may not)
        $profile = $this->getProfile();
        if ($profile) $org = $profile->getUserOrg();
        if ($org) $cat = $org->getUserCat();
        if ($cat) $type = $cat->getShortName();

        // If we were successful, set up the credentials
        if ($type)
        {
            $this->addCredential("user_type_$type");
        }
    }

    // Methods to read user properties, org properties, org type etc. go here...
}

This means that when the user logs on, they are given:

  • a credential based on their user category, so a user belonging to a group which has a type of “client” will get a credential of “user_type_client”. This can be used in security.yml for page level access, or for sub-page level access as described earlier.
  • all the user properties belonging to them, plus all the properties assigned to the user’s organisation. This permits data-level filtering; for example an organisational property of “depot_code”, containing the value “MYDEP” can be read from the user session, and then incorporated into a database WHERE clause.

A complete user class, which also contains various getter methods to access property and category information, can be downloaded here.

class myUser extends sfGuardSecurityUser
{
/**
* Called when a user signs in successfully
*
* @author JHi
*/
public function signIn($user, $remember = false, $con = null)
{
// Do standard signin
parent::signIn($user, $remember, $con);// then init the properties
$this->initUserProperties();
$this->initOrgProperties();// Set up user type name credential
$this->setUserTypeCredential();
}/**
* Called when a user signs out – clears credentials
*
* @author JHi
*/
public function signOut()
{
// Clears user credentials
$this->getAttributeHolder()->clear();// Pass call along to parent
parent::signOut();
}

5 Comments to “Handling complex permissions with sfGuard”

  1. ReynierPM says:

    Hi Jon, sorry for write directly to you but I’m running into the same problem here. I read the article and it’s amazing but my problem differs a bit. See I’m developing a application using sfDoctrineGuardPlugin and Symfony 1.4.20 then I’ve three users and three users groups and some permissions as follow:

    user_name user_group permissions
    u1 Group1 can_admin_full, can_admin
    u2 Group2 can_admin
    u3 Group3 no_admin

    So **u1** should be able to add users to the application but only can see Group2 and Group3 under Groups Options, **u2** should be able to add users to the application to but only can see Group3 under Groups Options, so u1 and u2 shouldn’t add users belonging to Group1, how I can get this using sfDoctrineGuard? It’s possible?

    Is much to ask some help from you? I’m not a Symfony guru and neither an expert

    • Jon says:

      Hi Reynier, thanks for the comment. I am sure it is possible. It sounds like “u3” is just page-level authorisation, which sfGuardPlugin does out of the box – just ensure that your admin screen requires u1 or u2 as tokens, and this will happen automatically.

      For the second scenario – restricting what users can be seen according to group, you will need to do that at the application level. Where you have a database query, you’ll need to modify it according to group membership – use $this->getUser()->getProfile() to access the sfGuardUser object, and related classes I think can be accessed from there (if you use an autocompleting IDE it is made much easier).

      You’ll also need to actively prevent the saving of values in forms, so that users who do not have full admin permissions cannot create/modify involving groups they do not have access to.

      So, give this a go? If you are relying on the built-in generated admin, it may be better to write your own. I am sure the generated version is flexible enough to do what you want, but personally I found understanding it harder than just writing from scratch!

      If you get stuck, ask a Stack Overflow question, with the code you are stuck on, link it here, and I will take a look.

  2. Dorjsuren says:

    Hi Jon, I found your blog from forum.symfony-project.org 🙂 I need a your support about the working with Symfony 1.4, Oracle Stored Procedure.
    http://oldforum.symfony-project.org/index.php/t/24386/
    there have your solution. But cannot download attachment files. Is it possible to share again them?

    Thanks.

Leave a Reply