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.
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 has user permissions built in. The specific permission “edit own content” can be configured through the backend:
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:
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