Securing Our User Endpoint - Part 2

This video is available to view for members only.

Click here to Join!

Already a member?

Login


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.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Project Introduction 17:13
2 Setting Up Our Development Environment 05:08
3 Installing Symfony 3, Behat, and more 13:53
4 User Feature - Part 1 17:47
5 User Feature - Part 2 07:51
6 Talking English To Your Computer 11:05
7 Teaching Your Database To Forget 07:42
8 Creating User Data From Behat Background - Part 1 14:44
9 Creating User Data From Behat Background - Part 2 11:33
10 Creating A Custom RestApiContext 17:44
11 Our First Passing Behat User Scenario 12:01
12 Our Next Passing Step 13:10
13 Securing Our User Endpoint - Part 1 17:17
14 Securing Our User Endpoint - Part 2 24:27
15 Securing Our User Endpoint - Part 3 24:47
16 Log In To A Symfony API With JWTs (LexikJWTAuthenticationBundle) 11:02
17 Implementing PATCH for Users 18:17
18 Improving our API User Experience 13:59
19 GET a Collection of Accounts 12:15
20 POSTing in New Accounts 14:34
21 PUT and PATCH for Accounts 12:14
22 How To DELETE Existing Accounts 05:11
23 File Feature Overview 11:40
24 File - Using Existing Resources as Boilerplate 15:17
25 File POST 14:53
26 Fixing A Bug In POST Guided By Behat 12:50
27 Wrapping Up With File DELETE 07:47