Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

This is an API that can be consumed with basic auth requests and there won't be login or logout features. Users are allowed to access URIs that they are permitted to otherwise "401 Unauthorised" error gets produced. The users, credentials, roles and URIs are all defined in "security.yml". For more info, click here.


End-points, users and access right



Security.yml


To assign multiple roles to a user, use roles: [ROLE_STUDENT, ROLE_LECTURER, ROLE_ADMIN].


# app/config/security.yml
security:
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12

# http://symfony.com/doc/current/book/security.html#hierarchical-roles
role_hierarchy:
ROLE_STUDENT: ROLE_USER
ROLE_LECTURER: ROLE_STUDENT
ROLE_ADMIN: [ROLE_LECTURER, ROLE_ALLOWED_TO_SWITCH]

# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
in_memory:
memory:
users:
student:
password: $2a$10$GaEjFjAlt4R5Sc3.rGUAXu087b/hN/mQqe0oDLGAzBRIxinYuCVq2 # student
roles: ROLE_STUDENT
lecturer:
password: $2a$10$GEOb1t0g5QusbzaJwtRfqOuVJhQAzH5jcJt1m487UO.DsnUmd6ul2 # lecturer
roles: ROLE_LECTURER
admin:
password: $2a$10$GogNFjqLFM8vlopZTdl3te2EoVsgX9EGDOilodUige/syc5vKFFwO # admin
roles: ROLE_ADMIN

# the main part of the security, where you can set up firewalls
# for specific sections of your app
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# the default controller has to be accessible for everybody
unsecured:
pattern: ^/$
security: false
# secures part of the application
api_secured:
pattern: ^/api/
stateless: true
http_basic:
realm: "Inanzzz Webservice API"
provider: in_memory

# with these settings you can restrict or allow access for different parts
# of your application based on roles, ip, host or methods
# http://symfony.com/doc/current/cookbook/security/access_control.html
access_control:
- { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/, role: ROLE_STUDENT }

Configuration


Install "jms/serializer-bundle" : "1.1.0" with composer and add new JMS\SerializerBundle\JMSSerializerBundle(), to AppKernel file.


Routing.yml


# app/config/routing.yml
application_api:
resource: "@ApplicationApiBundle/Controller"
prefix: /
type: annotation

Services.yml


# app/config/services.yml
services:
doctrine_common_inflector:
class: Doctrine\Common\Inflector\Inflector

Controllers.yml


# src/Application/ApiBundle/Resources/config/controllers.yml
services:
application_api.controller.abstract:
class: Application\ApiBundle\Controller\AbstractController
abstract: true
arguments:
- @serializer
- @validator
- @doctrine_common_inflector

application_api.controller.default:
class: Application\ApiBundle\Controller\DefaultController
parent: application_api.controller.abstract

application_api.controller.api:
class: Application\ApiBundle\Controller\ApiController
parent: application_api.controller.abstract

AbstractController


namespace Application\ApiBundle\Controller;

use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class AbstractController
{
private $validContentTypes = ['json' => 'application/json', 'xml' => 'application/xml'];

protected $serializer;
protected $validator;
protected $inflector;

public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
$this->serializer = $serializer;
$this->validator = $validator;
$this->inflector = $inflector;
}

/**
* @param string $contentType
*
* @return string|Response
*/
protected function validateContentType($contentType)
{
if (!in_array($contentType, $this->validContentTypes)) {
return $this->createFailureResponse(
['content_type' => sprintf('Invalid content type [%s].', $contentType)],
'json',
Response::HTTP_UNSUPPORTED_MEDIA_TYPE
);
}

return array_search($contentType, $this->validContentTypes);
}

/**
* @param string $payload
* @param string $model
* @param string $format
*
* @return object|Response
*/
protected function validatePayload($payload, $model, $format)
{
$payload = $this->serializer->deserialize($payload, $model, $format);

$errors = $this->validator->validate($payload);
if (count($errors)) {
return $this->createFailureResponse($errors, $format);
}

return $payload;
}

/**
* @param array|object $content
* @param string $format
*
* @return Response
*/
protected function createSuccessResponse($content, $format = 'json')
{
return $this->getResponse($content, $format, Response::HTTP_OK);
}

/**
* @param array|ConstraintViolationListInterface $content
* @param string $format
*
* @return Response
*/
protected function createFailureResponse($content, $format = 'json')
{
$errorList = null;

if ($content instanceof ConstraintViolationList) {
foreach ($content as $error) {
$error = $this->getErrorFromValidation($error);
$errorList[$error['key']] = $error['value'];
}
} else {
$errorList = $content;
}

return $this->getResponse(['errors' => $errorList], $format, Response::HTTP_BAD_REQUEST);
}

/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
private function getResponse($content, $format, $status)
{
$context = new SerializationContext();
$context->setSerializeNull(false);

$response = $this->serializer->serialize($content, $format, $context);

return new Response($response, $status, ['Content-Type' => $this->validContentTypes[$format]]);
}

/**
* @param ConstraintViolationInterface $error
*
* @return array
*/
private function getErrorFromValidation($error)
{
$properties = $this->inflector->tableize($error->getPropertyPath());

return ['key' => $properties, 'value' => $error->getMessage()];
}
}

DefaultController


namespace Application\ApiBundle\Controller;

use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @Route("", service="application_api.controller.default")
*/
class DefaultController extends AbstractController
{
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
parent::__construct($serializer, $validator, $inflector);
}

/**
* @Method({"GET"})
* @Route("")
*
* @return Response
*/
public function indexAction()
{
return $this->createSuccessResponse('Welcome to Webservice API!');
}
}

ApiController


As you can see below, unlike other two methods we didn't have to set @Security for studentAction method because, access control settings in security.yml dictates that logged in users (ROLE_STUDENT, ROLE_LECTURER, ROLE_ADMIN) can access to all /api/... end-points by default. To override this behaviour, we use @Security annotations.


namespace Application\ApiBundle\Controller;

use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @Route("api", service="application_api.controller.api")
*/
class ApiController extends AbstractController
{
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
parent::__construct($serializer, $validator, $inflector);
}

/**
* @Method({"GET"})
* @Route("/student")
*
* @return Response
*/
public function studentAction()
{
return $this->createSuccessResponse('Hello ROLE_STUDENT user');
}

/**
* @Method({"GET"})
* @Route("/lecturer")
* @Security("has_role('ROLE_LECTURER')")
*
* @return Response
*/
public function lecturerAction()
{
return $this->createSuccessResponse('Hello ROLE_LECTURER user');
}

/**
* @Method({"GET"})
* @Route("/admin")
* @Security("has_role('ROLE_ADMIN')")
*
* @return Response
*/
public function adminAction()
{
return $this->createSuccessResponse('Hello ROLE_ADMIN user');
}
}

Tests


All the user roles can access to / so I won't demonstrate it here. Also, I won't show every single end-point tests here but you can trust what I say below.


IS_AUTHENTICATED_ANONYMOUSLY


Forbidden access to all /api/... end-points.



ROLE_STUDENT


Can access to /api/student but not /api/lecturer and /api/admin end-points.




ROLE_LECTURER


Can access to /api/student and /api/lecturer but not /api/admin end-point.




ROLE_ADMIN


Can access to all /api/... end-points.