Mark Everard

The only consistency is change itself

Archive for the ‘ASP.NET’ Category

Role and user based property security for EPiServer

without comments

One of the requirements I’ve often seen from some of our bigger (more enterprise) EPiServer clients – is the ability to add more granular levels of security to the page editing process. By default EPiServer allows the following security to be set up for page editing:

  • Specify edit / admin rights for each page – this allows you to define parts of the page tree that editors have permissions over. These permissions allow you to use the to specify one of the following access levels for each page (Read / Create / Change/ Delete / Publish / Administer) which dictates an editor’s ability to create / view and publish a page.
  • Specify access levels required to view each edit mode tab – this allows you to group particular page properties onto particular tabs and show or hide those based on the access levels defined above.

However – what is missing here is a more granular approach for individual properties on each page. For example allowing users in a ‘MetaEditor’ role to update page meta data without being able to edit anything else.

EPiServer exposes an EditPanel.LoadedPage event which allows you to modify the loaded PageData object and modify it accordingly. One of the code properties available on an EPiServer property is DisplayEditUI which dictates whether the property is shown on the edit panel. Using the EditPanel.LoadedPage event to set property visibility in this way isn’t new.  However now we have strongly typed PageType classes (thanks to PageTypeBuilder) we can revisit this and treat it as a true application cross cutting concern.

What I wanted to be able to achieve was a true attribute based security system, that worked with the standard ASP.Net Membership model allowing developers to set which roles and users would be able to view and modify each property by setting an code based attribute in the TypedPageData class.

The solution relies on the following pieces:

  • An EPiServer Initialization module that hooks up a method to the EditPanel.LoadedPage event.
  • An Authorize attribute that you can place on PageTypeBuilder TypedPageData classes and properties.
  • A locator which will scan your current page type for each property and modify the DisplayEditUI property based on the attributes values.

The following rules apply:

  • A property level [Authorize] attribute will override any setting from a class level [Authorize] attribute
  • [Authorize] attributes can set either usernames or roles which will be honoured – see the example below.
  • The [Authorize] attribute allows you to specify whether you wish to apply the security rules to the built in-EPiServer properties as well as any you have defined through code.

Here is an example usage:

using System;
using FortuneCookie.PropertySecurity;
using PageTypeBuilder;
using EPiServer.Templates.AlloyTech.PageTypes.Tabs;

namespace EPiServer.Templates.AlloyTech.PageTypes.AlloyTech
{
   [PageType("74f6ef3e-407b-4132-8108-7fa831910197",
   Name = "[AlloyTech] Standard page",
   Filename = "/Templates/AlloyTech/Pages/Page.aspx",
   DefaultChildSortOrder = EPiServer.Filters.FilterSortOrder.None,
   Description = "The standard page is the most commonly used page on the web site.",
   DefaultVisibleInMenu = true,
   AvailablePageTypes = new Type[] {  })]
   [Authorize(Principals = "StandardPageEditorRole,mark.everard", ApplyToDefaultProperties = false)]
   public class StandardpagePageType : TypedPageData
   {
      [PageTypeProperty(EditCaption = "Main body",
      HelpText = "The main body will be shown in the main content area of the page",
      Tab = typeof(InformationTab),
      Type = typeof(EPiServer.SpecializedProperties.PropertyXhtmlString))]
      [Authorize(Principals = "MainBodyEditorRole")]
      public virtual string MainBody { get; set; }

      [PageTypeProperty(EditCaption = "Secondary body",
      HelpText = "The contents of this property will be shown in the right column of the page, you can use both text and images for layout.",
      Tab = typeof(InformationTab),
      Type = typeof(EPiServer.SpecializedProperties.PropertyXhtmlString))]
      public virtual string SecondaryBody { get; set; }
   }
}

In this case – any editor will be able to see the default EPiServer page properties (PageName, PageCategories etc). Editors in the StandardPageEditorRoles and me (user with username mark.everard) will be able to view / edit the SecondaryBody property, and only editors in the MainBodyEditorRole will be able to modify the MainBody property.

At present,  I’m not too happy with the blanket approach to the default properties that the current version has. I’d be interested to hear any better ideas for dealing with this – or better yet send me a pull request to the project on GitHub.

A Nuget package (FortuneCookie.PropertySecurity) built against .Net 4.0 / EPiServer 6R2 and PTB 2.0 has been uploaded for review to the EPiServer Nuget feed (and so should be available soon). The source code is available on GitHub – https://github.com/markeverard/FortuneCookie.PropertySecurity

Written by Mark

February 13th, 2012 at 10:00 am

Posted in ASP.NET,C#,EPiServer

Creating a custom ModelBinder allowing validation of injected composite models

with one comment

Model Binding – is the ‘auto-magic’ step performed by the ASP.NET MVC framework to convert user submitted data (either http post values, querystring values or url route values) into a strongly typed model, used in your controller actions.

Out of the box, the MVC framework also allows you to set validation attributes on your models which are inspected at the model binding stage, meaning that your controller actions can inspect the ModelState.IsValid property to assess whether the user submitted data meet expectations. This attribute based approach to validation provides a clean way to handle validation (a cross-cutting concern) without introducing additional code into your controller action.

One of the features needed when putting together the Fortune Cookie Personalization Engine for EPiServer was to perform validation on a user submitted criteria value. As a reminder, in the context of the Personalization Engine, a criteria is an editor submitted string which is persisted and used by a ContentProvider to allow for a more granular method of content retrieval. For further background and explanation, check out one of my earlier posts – Personalization Engine – ContentProvider Criteria Models

In the full Personalization Engine domain model, criteria properties belong to IContentModel objects which are used to specify the user interface displayed to an editor to allow them to enter the criteria. Below is an example of a TextBoxCriteriaModel which renders as a textbox in the Admin interface.

criteriamodel-validation-interface

The string value from this criteria input is posted to the controller action.  However as the type of IContentModel depends on the value of the ContentProvider dropdown, validation attributes cannot be set directly on the model passed to/from this view as different concrete IContentModel types need to be able to specify different validation rules.

To provide validation of the composite IContentModel using validation attributes, we have to hook in to one of the extension points of the ASP.NET MVC framework and create a custom model binder.

Validating composite models

Our custom ModelBinder needs to perform the following tasks:

  • Bind the incoming data against an AdminViewModel (the model passed from the user interface shown above).
  • Obtain an instance of the specified ICriteriaModel and update the ICriteriaModel’s criteria property with the value posted by the form.
  • Validate the composite (and now populated) ICriteriaModel
  • Update the ModelState with the validation results from the composite model, along with the original parent model validation results

As the custom ModelBinder needs to perform all of the existing validation and binding that the DefaultModelBinder would, I’ve chosen to inherit from it and add the additional composite model validation logic into the overriden BindModel method. An instantiated IContentModel is obtained from the AdminViewModel, and the ModelValidator framework class allows us to validate the composite IContentModel, before updating the bindingContext.ModelState with an validation errors.


public class CriteriaValidationModelBinder : DefaultModelBinder
{
   const string ValidationPropertyName = "Criteria";

   public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
      if (bindingContext.Model != null)
         return bindingContext.Model;

      var model = base.BindModel(controllerContext, bindingContext);

      var adminViewModel = model as AdminViewModel;
      if (adminViewModel == null)
         return model;

      var criteriaValue = bindingContext.ValueProvider.GetValue(ValidationPropertyName);
      adminViewModel.CriteriaModel.Criteria = criteriaValue != null ? criteriaValue.AttemptedValue : string.Empty;

      ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => adminViewModel.CriteriaModel, typeof(ICriteriaModel));
      ModelValidator compositeValidator = ModelValidator.GetModelValidator(modelMetadata, controllerContext);

      foreach (ModelValidationResult result in compositeValidator.Validate(null))
         bindingContext.ModelState.AddModelError(ValidationPropertyName, result.Message);

      return model;
   }
}

Our custom ModelBinder can be hooked into our application in Global.asax, or in an EPiServer IInitializableModule using the following method.


private void AddCriteriaValidationModelBinder()
{
    ModelBinders.Binders.Add(typeof(AdminViewModel), new CriteriaValidationModelBinder());
}

And that’s it…. ModelBinders are an important piece of the MVC framework, and in the majority of scenarios you can rely on the DefaultModeBinder to handle all of your requirements. However creating your own ModelBinder for more advanced requirements is pretty straightforward, depending on your requirements for your binder :)

Written by Mark

July 18th, 2011 at 10:00 am

Posted in ASP.NET,C#,EPiServer,MVC

Serving videos to iOS devices from EPiServer VPP folders

without comments

We recently launched an EPiServer site that made moderate, but high-profile usage of video and was also designed to be iOS device friendly – meaning no Flash and Html5 used to display video. Late on in development / integration we came across an issue where the mp4 / m4v videos file that were being served from an EPiServer VPP folder did not play when viewed on an iOS device. A little investigation found that the video files would play correctly when served natively from a static / non-content managed location.

(Note: If you’re serving a large amount of media content, then you’d be better off looking at a full media hosting solution rather than just serving from the EPiServer VPP).

Http-Headers

Obviously the files were identical, so the only difference was in how IIS was serving them. Below are the Http responses (captured using FireBug) for the identical .m4v video files, one served from a VPP, and one from a native path.

The significant difference is the Accept-Ranges : bytes header. Cool – so we can just add in this header to our response and we’re sorted right? WRONG I’m afraid!

HTTP/1.1 Accept-Ranges header field

The Accept-Ranges response-header field allows the server to indicate its acceptance of range requests for a resource. This means that the server is open to partial downloads of files, and that clients (web-browsers) can download a limited ‘chunk’ of a file by requesting a specific byte-range, for-example bytes 5001-9999.

Going back to our original issue of why the files didn’t play on an iOS device, a quick look on the Apple developer notes (via StackOverflow) confirms that the web server needs to be configured to correctly handle byte-range requests. This explains why our file doesn’t work on an iOS device when served from an EPiServer VPP.

EPiServer.Web.StaticFileHandler

Our site was running using IIS7.5 in integrated pipeline mode, meaning that pretty much the whole Request / Response pipeline is handled by ASP.NET modules. Looking in the EPiServer site web.config you can see that the location path elements for VPP paths (Global /PageFiles etc) contain the HttpHandlers that are responsible for serving VPP filess. This is a bespoke EPiServer file handler implementation (EPiServer.Web.StaticFileHandler), which unlike the default IIS7 StaticFileHandler does not support Range-Requests :(

The Solution

Now you didn’t think that I’d explain all of this, without heroically coming up with a solution did you? Well I am providing a solution, but its hardly heroic as most of the hard work was done many years ago by Scott Mitchell – see Range-Specific Requests in ASP.NET. In his post Scott explains and provides an example of how to roll a HttpHandler with support for range-specific HTTP requests.

I’ve inherited from the RangeRequestHandlerBase class that Scott posted in the above article, and overidden a few methods adding in some EPiServer specifics, such as mapping the request path to the physical VPP file path and also using the EPiServer.Web.MimeMapping class to handle mapping from an extension to a mime type.


    public class RangeRequestFileHandler : RangeRequestHandlerBase
    {
        ///
        /// Returns a FileInfo object from the mapped VirtualPathProviderFile
        ///
        /// <param name="context" />
        ///
        public override FileInfo GetRequestedFileInfo(HttpContext context)
        {
            UnifiedFile file = GetFileInfoFromVirtualPathProvider(context);

            if (file == null)
                return null;

            PreventRewriteOnOutgoingStream();
            return new FileInfo(file.LocalPath);
        }

        private static UnifiedFile GetFileInfoFromVirtualPathProvider(HttpContext context)
        {
            return GenericHostingEnvironment.VirtualPathProvider.GetFile(context.Request.FilePath) as UnifiedFile;
        }

        ///
        /// Returns the MIME type from the mapped VirtualPathProviderFile
        ///
        /// <param name="context" />
        ///
        public override string GetRequestedFileMimeType(HttpContext context)
        {
            UnifiedFile file = GetFileInfoFromVirtualPathProvider(context);
            return MimeMapping.GetMimeMapping(file.Name);
        }

        ///
        /// Prevents episerver rewrite on outgoing stream.
        ///
        private void PreventRewriteOnOutgoingStream()
        {
            if (UrlRewriteProvider.Module != null)
                UrlRewriteProvider.Module.FURLRewriteResponse = false;
        }

This handler can be used to serve files from your VPP folders by adding the following configuration (IIS7 only folks – which if you’re not using then you really should be pushing for an upgrade!). This handler listed below is set up only to serve files of type .m4v – and remember that it does not provide any of the native EPiServer functionality for setting the staticFile cache expiration time.


<location path="Global">
     <system.webServer>
         <handlers>
              <add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader"/>
              <add name="videofiles" path="*.m4v" verb="*" type="FortuneCookie.RangeRequestFileHandler.RangeRequestFileHandler, FortuneCookie.RangeRequestFileHandler"/>
              <add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer"/>
         </handlers>
      </system.webServer>
      <staticFile expirationTime="-1.0:0:0"/>
 </location>

The full source code can be downloaded from the Code section at EPiServer World and a NuGet package is available that will auto-magically add in the code and configure your Global and PageFile VPP’s to serve .mp4, .m4v and .mov video files using the RangeRequestFileHandler.

Written by Mark

July 5th, 2011 at 10:00 am