Contact

FUxCon 2013


Accounts & Security

CakePHP

CakePHP provides a flexible Auth component that does most of the work required for managing user accounts. It is declared in the controller or the common base class AppController (excerpt from file app/Controller/AppController.php):

<?php
class AppController extends Controller {
  public $theme = 'Bootstrap';
  public $components = array(
    'DebugKit.Toolbar',
    'Session', 
    'Auth' => array(
      'loginRedirect' => array('controller' => 'projects', 'action' => 'index'),
      'logoutRedirect' => array('controller' => 'projects', 'action' => 'index'),
      'authorize' => array('Controller'),
      'element' => 'message-auth',
    )
  );

  public function beforeFilter() {
    $this->Auth->allow('index', 'view');
  }

  public function isAuthorized($user) {
    // Admin can access every action
    if (isset($user['role']) && $user['role'] === 'admin') {
      return TRUE;
    }

    // Default deny
    return FALSE;
  }
}

Controllers overwrite allowed access by overwriting the isAuthorized() method:

<?php
class ProjectsController
{
   public function isAuthorized($user, $action = NULL) {
    // All registered users can add projects
    if ($this->action === 'add') {
      return true;
    }

    if (!$action) {
      $action = $this->action;
    }

    // The owner of a project can edit and delete it
    if (in_array($action, array('edit', 'delete'))) {
      $projectId = $this->request->params['pass'][0];
      if ($this->Project->isOwnedBy($projectId, $user['id'])) {
        return TRUE;
      }
    }
    return parent::isAuthorized($user);
  }

Apart from this simple, form-based authentication with controller-based authorization, other variants are possible as documented in the Cake Book.

Django

Our implementation uses Django’s built-in authentication app and middleware for managing user accounts. There is a page in the documentation on how Django stores passwords in the database. For object-level permissions, we write a custom authentication backend and register it as well:

virtualenv .env
source .env/bin/activate
pip install django-object-permissions 

The complete declaration for these components in projects/settings.py look like this:

MIDDLEWARE_CLASSES = (
    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'object_permissions',
)

AUTHENTICATION_BACKENDS = (
    'projects.models.PermBackend',
)

We mount the views for user management by including the routes in projects/urls.py:

urlpatterns += patterns('',
  url(r'^user/', include(django.contrib.auth.urls)),
)

The authentication backend in file projects/models.py implements a new method has_perm() and looks like this:

class Project(models.Model):
  class Meta:
    permissions = (
      ('can_edit', "Can edit project"),
    )

class PermBackend(ModelBackend):
  def has_perm(self, user, perm, obj=None):
    "Owner and admin can edit"

    if perm != 'can_edit' or obj is None:
      return super(PermBackend, self).has_perm(user, perm, obj)
   
    if user.is_superuser:
      return True
   
    return obj.user.id == user.id

This backend enhances the method has_perm() on user objects. We can therefore now check for object-level permissions in our edit view (file projects/views.py):

class ProjectEditView(UpdateView):

  def dispatch(self, request, *args, **kwargs):
    project = self.get_object()

    if not request.user.has_perm('can_edit', project):
      messages.error(request,
        'You are not allowed to edit project #' + str(project.id)
      )
      return HttpResponseRedirect(reverse('project', args=(project.id,)))
   
    return super(ProjectEditView, self).dispatch(request, *args, **kwargs)

Drupal

Drupal has user permissions built in. The specific permission “edit own content” can be configured through the backend:

Drupal Views admin

Symfony

In Symfony, to implement permissions, there are basically two approaches. Access control lists (ACLs) and voters. Access control lists store additional information about individual Access Control Entities (ACEs) and associated information in the database.

In situations where permissions can be derived from existing relationships between domain objects, this overhead can be avoided by implementing a voter.

Our implementation contains such a voter. With the voter, two things are possible:

  • Hide an edit link from the project detail page
  • Prevent unauthorized users from accessing the project edit page.

In case of the edit link, this is the responsible code in our template src/FUxCon2013/ProjectBundle/Resources/views/Project/show.html.twig:

<p class="actions">
   {% if is_granted("MAY_EDIT", project) %}
     <a class="btn" id="edit-project" href="{{ path('project_edit', { 'id': project.id }) }}">edit this project</a>
   {% endif %}
 </p>

This is the check in our method ProjectController.editAction():

<?php
if (!$this->get('security.context')->isGranted('MAY_EDIT', $project)) {
    $this->flash('You are not allowed to edit this project');
    return $this->show($project);
}

In both cases, a method is_granted() rsp. isGranted() is called with a role and the project object. Under the hood, a voter in file FUxCon2013/ProjectsBundle/Security/OwnerVoter.php does the checking:

<?php
function vote(TokenInterface $token, $object, array $attributes)
{
    if (!in_array('MAY_EDIT', $attributes)) {
        return self::ACCESS_ABSTAIN;
    }
    if (!($object instanceof Project)) {
        return self::ACCESS_ABSTAIN;
    }

    $user = $token->getUser();
    $securityContext = $this->container->get('security.context');

    return $securityContext->isGranted('IS_AUTHENTICATED_FULLY')
        && $user->getId() == $object->getUser()->getId()
        ? self::ACCESS_GRANTED
        : self::ACCESS_DENIED;
}

To make this work, some configuration is required. In file app/config/config.yml, we register the voter as a service:

services:
    fuxcon2013.security.owner_voter:
        class:      FUxCon2013\ProjectsBundle\Security\OwnerVoter
        public:     false
        arguments: [ @service_container ]
        tags:
            - { name: security.voter }

In app/config/security.yml, we have to set the strategy of the access decision manager and tell it to allow access if all voters abstain:

security:
    access_control:
        - { path: ^/project/\d+/edit, role: MAY_EDIT }

    access_decision_manager:
        # strategy can be: affirmative, unanimous or consensus
        strategy: unanimous
        allow_if_all_abstain: true


comments powered by Disqus