Securing Our User Endpoint - Part 2
In this video we will continue on with our testing our UserController
implementation, restricting down the GET
method from returning any requested User data, and instead, only allowing back the currently logged in User data to be returned.
As mentioned in the previous video, we are using the concept of a Repository to hold all our User objects. From the perspective of the UserController
and UserHandler
, how these repositories are implemented is really - or to put it another way, where these User objects are coming from - is really not of any real interest. That job is delegated.
Think of this as like a chain:
UserController
hands off to UserHandler
, which hands off to something implementing UserRepositoryInterface
.
UserHandler
is a standard Symfony service.
We will use Symfony's dependency injection to inject something that implements UserRepositoryInterface
. We will worry about whether this is a 'restricted' implementation a little later. As long as whatever we inject is implementing the expected interface, then we know that certain methods will be available (find
in this case).
Whichever repository we end up using (restricted or not), we will ultimately hand off ultimate responsibility of finding the requested entity to the Doctrine entity repository.
For reference, here is the UserRepositoryInterface
we will be using:
<?php
namespace AppBundle\Repository;
use AppBundle\Model\UserInterface;
/**
* Interface UserRepositoryInterface
* @package AppBundle\Repository
*/
interface UserRepositoryInterface
{
/**
* @param UserInterface $user
* @param array $arguments
*/
public function save(UserInterface $user, array $arguments = ['flush'=>true]);
/**
* @param UserInterface $user
* @param array $arguments
*/
public function delete(UserInterface $user, array $arguments = ['flush'=>true]);
/**
* @param $id
* @return mixed|null
*/
public function find($id);
}
The Three Repositories
The following is what I consider the most confusing part of this whole setup.
As mentioned above, the Controller and Handler both do as little as possible. They delegate the responsibility for finding objects to some class that implements UserRepositoryInterface
.
In this video we create two classes that implement UserRepositoryInterface
. These are:
AppBundle\Repository\Doctrine\UserRepository
AppBundle\Repository\Restricted\UserRepository
Why do we have two?
Well, the namespace should make it clear, but let's investigate anyway.
See, the Doctrine\UserRepository
isn't a Doctrine entity repository in the way that you might find in the Symfony documentation.
It will have a Doctrine entity repository injected in to it. We will configure this just as we would with any other Symfony service.
The naming is confusing as Doctrine uses the term 'repository' in a different context to the one we are referring to here.
To help keep things a little more organised, I like to keep my folder structure like this:
AppBundle\Repository
contains Repositories in the context of domain driven design.
AppBundle\Entity\Repository
contains Doctrine style repositories.
Confusing? Just a bit!
But let's continue as this will hopefully start to make sense.
As mentioned, AppBundle\Repository\Doctrine\UserRepository
will have a Doctrine entity repository injected in to it:
<?php
// src/AppBundle/Repository/Doctrine/UserRepository.php
namespace AppBundle\Repository\Doctrine;
use AppBundle\Entity\Repository\UserEntityRepository;
use AppBundle\Repository\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface
{
/**
* @var UserEntityRepository
*/
private $entityRepository;
public function __construct(UserEntityRepository $entityRepository)
{
$this->entityRepository = $entityRepository;
}
/**
* @param $id
* @return mixed
*/
public function find($id)
{
return $this->entityRepository->find($id);
}
}
We are injecting the underlying Doctrine entity repository.
Symfony 3 has changed the way this would be defined:
# app/config/services.yml
services:
crv.doctrine_entity_repository.user:
class: AppBundle\Entity\Repository\UserRepository
factory: ["@doctrine", getRepository]
arguments:
- AppBundle\Entity\User
crv.repository.doctrine_user_repository:
class: AppBundle\Repository\Doctrine\UserRepository
arguments:
- "@crv.doctrine_entity_repository.user"
Hint: This is the correct syntax, the syntax I used in the video is from Symfony 2 and will be fixed in the next video :)
Worth pointing out here, that in Symfony 3, the services must be quoted: "@crv.doctrine_entity_repository.user"
Back On Topic
We've covered how the Handler delegates to the repository.
All we need is the repository to implement the correct interface.
This allows us to change the repository (restricted, or unrestricted) depending on our needs. +1 for composability.
The important point really though is that both of the domain driven design syle repositories (those implementing UserRepositoryInterface
) really will still delegate the actual responsibility of finding the entity to Doctrine.
We could have another means of storing entities - maybe in an array, or in redis, or a file, or anywhere - which the DDD style repositories don't need to know about.
Again, I am using the repository concept perhaps not in the purest sense of the DDD term. But it works for me.
The only difference between the Restricted implementation and the 'Doctrine' implementation is that the Restricted implementation will do an authorisation check (using a Symfony voter) to ensure that the currently logged in User actually has permissions to view the requested User resource.
To make this somewhat clearer, let's review the two possible flows.
Unrestricted Flow
UserController
UserHandler
Doctrine\UserRepository
Entity\Repository\UserEntityRepository
Restricted Flow
UserController
UserHandler
Restricted\UserRepository
Doctrine\UserRepository
Entity\Repository\UserEntityRepository
Each bullet point indicates a delegation step. Handing over responsibility for a more specific part of the work flow.
The aim is to reduce any duplication, and re-use as much code as possible.
The good news is, all of this is tested using PHPSpec, so we know it will work :)
Implementing A Restricted User Repository
Looking at the flows above, the only Restricted flow still makes use of the unrestricted flow.
The reasoning for this is that we need to find the object - the User
in this instance - before we can check if we have access to that resource.
Let's look at the code for Restricted\UserRepository
:
<?php
// src/AppBundle/Repository/Restricted/UserRepository.php
namespace AppBundle\Repository\Restricted;
use AppBundle\Repository\UserRepositoryInterface;
use AppBundle\Repository\Doctrine\UserRepository as DoctrineUserRepository;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class UserRepository implements UserRepositoryInterface
{
/**
* @var DoctrineUserRepository
*/
private $userRepository;
/**
* @var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* UserRepository constructor.
* @param DoctrineUserRepository $userRepository
* @param AuthorizationCheckerInterface $authorizationChecker
*/
public function __construct(
DoctrineUserRepository $userRepository,
AuthorizationCheckerInterface $authorizationChecker
)
{
$this->userRepository = $userRepository;
$this->authorizationChecker = $authorizationChecker;
}
/**
* @param $id
* @return mixed
*/
public function find($id)
{
$user = $this->userRepository->find($id);
$this->denyAccessUnlessGranted('view', $user);
return $user;
}
/**
* @param $attribute
* @param null $object
* @param string $message
*/
protected function denyAccessUnlessGranted($attribute, $object = null, $message = 'Access Denied')
{
if ( ! $this->authorizationChecker->isGranted($attribute, $object)) {
throw new AccessDeniedHttpException($message);
}
}
}
This service will also need some service configuration:
# app/config/services.yml
services:
crv.repository.restricted.user_repository:
class: AppBundle\Repository\Restricted\UserRepository
arguments:
- "@crv.repository.doctrine_user_repository"
- "@security.authorization_checker"
We haven't yet configured the security voter that will handle authorisation checking for User
objects - that will come in the next step.
But here you can see, as described in the two flows above, how the Restricted User Repository gets the Doctrine User Repository injected.
Then, when we want to find a User we simply delegate, or hand off the responsibility to the injected Doctrine User Repository - reusing the code === fewer potential bugs - and getting the requested User object:
$user = $this->userRepository->find($id);
Then we call a protected method which really will trigger a Symfony vote, and throw an AccessDeniedHttpException
if the User is trying to request another User's data.
Wrapping Up
In the next video we will implement the UserVoter
, and refactor our UserController
to start using these new repositories.