Advance customization of the 403 error page in Symfony2

Advance customization of the 403 error page in Symfony2

If you currently are testing the brand new Symfony2 framework, you might have tried to customize the 403 page. Actually, you have two documented ways to do so, first by overriding the default exception template, second by listening to the onCoreException event and filtering the right exception.

Unfortunately, neither of these two methods satisfied me. The simple customization of the template was not enough for my needs and the addition of an ExceptionListener impose a “maybe” to much complex filtering in order to retrieve just the 403 exception.

Note: Actually, my needs are similar to those I exposed in this blog post on symfony 1.4 : Symfony 1.4 vs. sfGuardUserPlugin : understand credentials and permissions.

What I want to do is to get the unsatisfied requirements and display them to the user.

First secure an action
In order to secure the access to my action with specific requirements, I use the @Secure annotation on my controller’s method. The annotation is provided by the JMSSecurityExtraBundle included in the symfony2 standard edition:

[php]
// /src/MyVendor/MyBundle/Controller/MyController.php

/**
* …
* @extra:Secure(roles="ROLE_MEMBER")
*/
public function newAction()
{
// …
}
[/php]

Here in order to access my action, the user will need to have the role named ROLE_MEMBER. If not, the security layer will throw an Exception resulting in a 403 response.

Change the unauthorized action
You can change the action that will display the 403 page in the security configuration:

[yml]
# /app/config/security.yml
security:
access_denied_url: /unauthorized
[/yml]

Now when dealing with an unauthorized Exception, the security layer will call the action associated to this URL. You need to specify the action that matches this URL in your routing configuration:

[yml]
# /src/MyVendor/MyBundle/Resources/config/routing.yml
unauthorized:
pattern: /unauthorized
defaults: { _controller: FrontendBundle:Default:unauthorized }
[/yml]

Now, the action responsible for rendering a 403 page is the following:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
// …

/**
* @extra:Template()
*/
public function unauthorizedAction()
{
return array();
}
}
[/php]

And the associated template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

403 error: Unauthorized

[/html]

Display the unmatched requirements
Now, the last things I want to do are to retrieve the required roles of the original request and display them to the user, so that he knows why his request did not succeed.
The tricky now is to obtain the original request, deduce the controller, load its annotations and thus deduce his requirements. Here is what I did:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

public function unauthorizedAction()
{
// get the previous requests
$requests = $this->container->getCurrentScopedStack(‘request’);

// get the previous controller
$controllerResolver = new SymfonyComponentHttpKernelControllerControllerResolver();
list($controller, $method) = $controllerResolver->getController($requests[‘request’][‘request’]);
// isolate the method
$method = new ReflectionMethod($controller, $method);
// load the metas
$reader = new JMSSecurityExtraBundleMappingDriverAnnotationReader();
$converter = new JMSSecurityExtraBundleMappingDriverAnnotationConverter();
$annotations = $reader->getMethodAnnotations($method);
$metadata = $converter->convertMethodAnnotations($method, $annotations)->getAsArray();
// isolate the required roles
$requiredRoles = $metadata[‘roles’];

return array(‘requiredRoles’ => $requiredRoles);
}
[/php]

The first thing I do is to obtain from the container the request stack that contains the original request. From the request I use the ControllerResolver in order to retrieve the initial controller and then isolate the method. With this method, I can use the AnnotationReader and AnnotationConverter from the JMSSecurityExtraBundle and get back the required roles.
Despite the fact that the method is pretty straightforward, it is not as elegant as I wanted it to be (in the whole process, the controller resolving and annotation extraction will be done twice). But it does the job.

The final thing to do is the show the required roles to the user in the template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

Required roles:

{% for role in requiredRoles %}
{{ role }}

{% endfor %}

[/html]

Force validation of RIA entities

Force validation of RIA entities

As I am currently developing a Silverlight Application using RIA Services (and Prism), I ran into some little tricks that can really help the developer’s life.

One of the common needs is to force the validation of all fields of an exposed entity in one time, and not just one field just after it has been updated.
This is very important especially when you want to insure our object is strictly validated before submitting changes.

The trick is to create a ValidationContext and manually validate the whole object. The thing to be careful of is not to forget to update the ValidationErrors list of the entity.

[csharp]
public static void Validate(this Entity entity)
{
// prepare the result
var validationResults = new List<ValidationResult>();
// create a validation context
var validationContext = new ValidationContext(entity, null, null);
// validate
Validator.TryValidateObject(entity, validationContext, validationResults);
// reset the validation errors of the entity
entity.ValidationErrors.Clear();
foreach (var error in validationResults)
entity.ValidationErrors.Add(error);
}
[/csharp]

As you can see, I made this method as an extension to the Entity type. So that you can easily use it on every entity you have.