Archive for the ‘C#’ Category
Role and user based property security for EPiServer
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
Creating a custom ModelBinder allowing validation of injected composite models
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.
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
Personalization Engine – User Interface
One of the things I wanted to provide with the PersonalizationEngine framework was a series of simple User Interface elements , which would provide an easy way to demonstrate the Personalization Engine within front-end templates, and also provide developers with some simple examples of how to use the API.
Lee Crowe has stepped up to the mark and contributed these for me
– Thanks Lee
These User Interface elements are contained in a separate NuGet package: FortuneCookie.PersonalizationEngine.UI (dependent on FortuneCookie.PersonalizationEngine) ,which is available to download the EPiServer NuGet feed.
The packages contains:
1) A custom property, (using Lee’s multiple selection custom property) – which allow you to select a limited set of defined Personalization Engine content providers from which to return content. This allows you to have PersonalizationEngine rules defined on a page-by-page basis.
2) Two dynamic content controls (using the new DynamicContentPlugin attribute) – one which just displays PersonalizationEngine results from the full rule-set, and one which uses the custom property described above to allow a limited sub-set.
FortuneCookie.PersonalizationEngine 1.1
Additionally there is a new version of the core FortuneCookie.PersonalizationEngine, which contains some minor updates and enhancements
1) Added a new event that you can hook into to filter any results prior to them being returned from the PersonalizatioEngine. A good usage for this may be to filter by PageType definition in the case you don’t want your PersonalizationEngine results to contain a particular PageType
The example below will filter all Article pages from any content returned from a PagesWithPageNameContentProvider. The event is hooked on in Global.asax
protected void Application_Start(Object sender, EventArgs e)
{
PersonalizationEngine.OnContentProviderGetContent += PersonalizationEngine_OnContentProviderGetContent;
}
private void PersonalizationEngine_OnContentProviderGetContent(ContentProviderEventArgs e)
{
if (e.ContentProviderType == typeof(PagesWithPageNameContentProvider))
e.ContentProviderPages = e.ContentProviderPages.Where(p => p.PageTypeID != PageTypeResolver.Instance.GetPageTypeID(typeof(ArticlePageType)));
}
2) Changed the call to DataFactory.Instance.GetPages, to an iterated call to DataFactory.Instance.GetPage in the CachedContentProviderBase class. Although the latter method is less performant – there is an acknowledged bug in CMS6R2 which means that PageTypeBuilder does not hook into the GetPages method result meaning that returned results are unTyped.
Both of these packages are now available in the EPiServer Nuget Feed
