• Publisert
  • 3 min

Handling page access denied scenarios in EPiServer CMS 7 MVC

Access denied error handling in EPiServer CMS 7 using MVC and a base page controller class instead of the old timer PageBase class in web forms.

We would like to be able to log incoming page requests that are denied before telling the user about it in CMS 7 when using MVC. This has been a feature in many of our CMS 6 projects for a long time and it has proved to be useful, but it does not work the same way when using MVC. The MVC solution is pretty simple, and it is implemented in a base page controller class.

Back in the day

In CMS 6 this kind of logging is done by overriding the GetPage method from the EPiServer.PageBase class and catching an AccessDeniedException like this (where the private field "log" is an instance of log4net’s ILog):

public override PageData GetPage(PageReference pageLink, ILanguageSelector selector)
{
    try
    {
        return base.GetPage(pageLink, selector);
    }
    catch (AccessDeniedException ex)
    {
        log.Error("AccessDenied", ex);
        throw new AccessDeniedException(pageLink);
    }
}

This piece of code placed on a custom base page class that all our page template files inherit from nicely covers the scenario in CMS 6. If a user lacks permissions to access a given page, we log it along with the page ID and re-throw the AccessDeniedException. Done.

In MVC we cannot use this method since the views does not inherit from PageBase in any way. In fact the views are not responsible for anything along the lines of getting page data at all. They are recipients of data.

The new era

So, how to achieve this in CMS7 when using MVC? We have no use for the inheritance from PageBase class in those projects, so I turned to our controller base class for a solution instead.

All our page controller classes inherit from a common abstract base class which in turn inherits from EPiServer’s generic PageController<T> class. The OnAuthorization (from System.Web.Mvc.Controller class) method seems to be the right place to start. From there I implemented an access check using the current page being requested. If access is to be denied I’ll log it before letting a DefaultAccessDeniedHandler class decide what to do next.

I am using a DefaultAccessDeniedHandler since this class will handle the type of access denied to serve up in a standard way;

  • HTTP status 302 and 200 (redirect to EPiServer login) for forms authentication
  • Straight up HTTP status 401 when using windows authentication.

The access check itself is very easy, and is basically just a call to the extension method .QueryAccess() on IContent and comparing the result to AccessLevel.Read.
So, our (simplified) controller base class looks like this:

using System.Web.Mvc;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;
using EPiServer.Web.Mvc;using log4net;

public abstract class AwesomeControllerBase<T> : PageController<T>
    where T : PageData
{
    protected T CurrentPage
    {
        get
        {
            //returns the current page as T using a PageRouteHelper
            //As mentioned by Fredrik Vig in the comments:
            return (T)PageContext.Page;
        }
    }

    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        CheckAccess();
        base.OnAuthorization(filterContext);
    }

    private void CheckAccess()
    {
        if (CurrentPage.QueryAccess().HasFlag(AccessLevel.Read))
            return;
        ServeAccessDenied();
    }

    private void ServeAccessDenied()
    {
        log.Error(
            "AccessDenied",
            new AccessDeniedException(CurrentPage.ContentLink));

        AccessDeniedDelegate accessDenied
            = DefaultAccessDeniedHandler.CreateAccessDeniedDelegate();
        accessDenied(CurrentPage);
    }
}

Again, the field named "log" is an instance of log4net’s ILog.

Summary

A few more lines of code compared to the CMS 6 version, but on the upside there is no try/catch going on. In addition it feels right to have this logic on a controller level rather than in a view’s base class. It is also nice to know that we quite easily could implement our own custom "401 unauthorized" behavior should the need arise. Maybe we want a YSOD explaining things in development environment and so on.