Skip to main content

Optimizely

Easy Dynamic Property Permissions for the Optimizely CMS

Cybersecurity Concept Laptop

I recently worked with a client who wanted to restrict access to specific properties of the content types. We set up some Editor Descriptor attributes, and applied a decorator to the properties in the model classes, which worked well. Then they wanted changes.

The problem is that figuring out who has access requires looking at the code. Any updates require code changes, which becomes a big deal when the properties are already in Production. The client wanted a way to make these property permission changes quickly and without needing to deploy code changes, which required a different solution.

A quick search found a couple possible solutions. One, by Mattias Olsen (https://world.optimizely.com/forum/developer-forum/CMS/Thread-Container/2019/3/access-rights-for-properties/),  was very promising, but it has the virtual roles and properties hardcoded into the Editor Descriptor class.

Another solution suggested using the Admin Settings console to override the settings in the CMS. I didn’t want to override the code by making changes to the content types in the CMS. That makes it very frustrating for developers troubleshooting property changes in code.

Instead of these, I set out to figure out a way to manage this in the CMS. My goal was to have a configuration interface where administrators could set the content type, set the property name, and assign virtual roles to the property. This would allow the administrators to secure the property to those users matching the assigned virtual role. For everyone else, it would be like the property didn’t exist.

 

What and Who

The first step is to identify who should have access to what properties. Since this configuration interface needed to be secured, I made it part of the Site Settings by creating a new SettingsContentType class inherited from SettingsBase. I added two property lists: one for Pages and one for Blocks.

PropertyPermissionsSettings.cs

using Foundation.Infrastructure.PropertyPermissions.PropertyList;
using EPiServer.Cms.Shell.UI.ObjectEditing.EditorDescriptors;
using Foundation.Infrastructure.Cms.Settings;

namespace Foundation.Infrastructure.PropertyPermissions
{
    [SettingsContentType(
        DisplayName = "Property Permissions",
        GUID = "147a352e-e9cd-4e99-b146-63f1e1fe8e52",
        Description = "Content type based property permission settings",
        AvailableInEditMode = true,
        SettingsName = "Property Permissions")]
    [AvailableContentTypes(Availability = Availability.None)]
    public class PropertyPermissionsSettings : SettingsBase
    {
        [Display(
            Name = "Page types",
            Order = 10,
            Description = "Access permissions will be applied to all selected page types")]
        [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<PropertyPermissionsPageTypeModel>))]
        public virtual IList<PropertyPermissionsPageTypeModel> PageTypes { get; set; }

        [Display(
            Name = "Block types",
            Order = 20,
            Description = "Access permissions will be applied to all selected block types")]
        [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<PropertyPermissionsBlockTypeModel>))]
        public virtual IList<PropertyPermissionsBlockTypeModel> BlockTypes { get; set; }
    }
}

 

I took a simple approach, knowing I needed the same properties for both lists. I created a new PropertyPermissionModel class with two string properties:

  • PropertyName
  • VirtualRoleName

PropertyPermissionModel.cs

namespace Foundation.Infrastructure.PropertyPermissions.Models
{
    public class PropertyPermissionModel
    {
        [Display(
            Name = "Property Name",
            GroupName = SystemTabNames.Content,
            Description = "Property to apply access permissions to",
            Order = 20)]
        public virtual string PropertyName { get; set; }

        [Display(
            Name = "Virtual Role Name",
            GroupName = SystemTabNames.Content,
            Description = "Virtual role to grant access permissions to",
            Order = 30)]
        public virtual string VirtualRoleName { get; set; }
    }
}

 

I then created two additional models, both inheriting from the PropertyPermissionModel, and I added a property to identify the page type ID or the block type ID. Since the IDs are not easily identified, I set up some selection factories to get a list of the relevant content types to display as drop-down fields.

PageTypeSelectionFactory.cs

namespace Foundation.Infrastructure.SelectionFactories
{
    public class PageTypeSelectionFactory : ISelectionFactory
    {
        private readonly IContentTypeRepository _contentTypeRepository;

        public PageTypeSelectionFactory(IContentTypeRepository contentTypeRepository)
        {
            _contentTypeRepository = contentTypeRepository;
        }

        public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
        {
           var pageTypes = _contentTypeRepository.List().OfType<PageType>().Where(x => x.IsAvailable).OrderBy(x => x.Name)
                .Select(p => new SelectItem { Text = p.DisplayName ?? p.Name, Value = p.ID.ToString()});

            return pageTypes;
        }
    }
}

 

BlockTypeSelectionFactory.cs

namespace Foundation.Infrastructure.SelectionFactories
{
    public class BlockTypeSelectionFactory : ISelectionFactory
    {
        private readonly IContentTypeRepository _contentTypeRepository;
        
        public BlockTypeSelectionFactory(IContentTypeRepository contentTypeRepository)
        {
            _contentTypeRepository = contentTypeRepository;
        }

        public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
        {
            var blockTypes = _contentTypeRepository.List().OfType<BlockType>().Where(x => x.IsAvailable).OrderBy(x => x.Name)
                .Select(b => new SelectItem { Text = b.DisplayName ?? b.Name, Value = b.ID.ToString() });

            return blockTypes;
        }
    }
}

 

In addition, I created PropertyList classes using the PropertyDefinitionTypePlugin so the property types could be used in the Settings class.

PropertyPermissionsPageTypeModel.cs

using Foundation.Infrastructure.PropertyPermissions.Models;
using Foundation.Infrastructure.SelectionFactories;

namespace Foundation.Infrastructure.PropertyPermissions.PropertyList
{
    public class PropertyPermissionsPageTypeModel : PropertyPermissionModel
    {
        [Display(
            Name = "Page Types",
            GroupName = SystemTabNames.Content,
            Order = 10)]
        [SelectOne(SelectionFactoryType = typeof(PageTypeSelectionFactory))]
        public virtual string PageTypeId { get; set; }
    }
}

PropertyPermissionsPageTypePropertyList.cs

using EPiServer.PlugIn;

namespace Foundation.Infrastructure.PropertyPermissions.PropertyList
{
    [PropertyDefinitionTypePlugIn]
    public class PropertyPermissionsPageTypePropertyList : PropertyList<PropertyPermissionsPageTypeModel>
    {
    }
}

 

PropertyPermissionsBlockTypeModel.cs

using Foundation.Infrastructure.PropertyPermissions.Models;
using Foundation.Infrastructure.SelectionFactories;

namespace Foundation.Infrastructure.PropertyPermissions.PropertyList
{
    public class PropertyPermissionsBlockTypeModel : PropertyPermissionModel
    {
        [Display(
            Name = "Block Types",
            GroupName = SystemTabNames.Content,
            Order = 10)]
        [SelectOne(SelectionFactoryType = typeof(BlockTypeSelectionFactory))]
        public virtual string BlockTypeId { get; set; }
    }
}

PropertyPermissionsBlockTypePropertyList.cs

using EPiServer.PlugIn;

namespace Foundation.Infrastructure.PropertyPermissions.PropertyList
{
    [PropertyDefinitionTypePlugIn]
    public class PropertyPermissionsBlockTypePropertyList : PropertyList<PropertyPermissionsBlockTypeModel>
    {
    }
}

 

How, When, and Where

Now, I have a secured configuration class with repeatable lists that define a content type, the property to be secured, and the virtual role that secured it. But when is it used?

It needs to run as soon as the content item is loaded into the EditUI, so a MetadataExtender was the way to go. I created a new class implementing the IMetadataExtender interface since the MetadataExtender provides the content type for the current object to compare against the list in the SiteSettings.

When a content type matches an entry, each property is checked against the list to be secured. If the property exists on the content type and the editor is assigned to the matching virtual role, show it. If the user isn’t assigned to the virtual role, set the ShowForEdit and IsRequired values on the property to false. This hides the property in the EditUI and allows it to be published.

 

PropertyPermissionsMetadataExtender.cs

using Foundation.Infrastructure.PropertyPermissions.Models;
using EPiServer.Security;
using Foundation.Infrastructure.Cms.Settings;

namespace Foundation.Infrastructure.PropertyPermissions
{
    public class PropertyPermissionsMetadataExtender : IMetadataExtender
    {
        private readonly Injected<ISettingsService> _settingsService;
        private readonly Injected<IPrincipalAccessor> _principalAccessor;

        public void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            var propertyList = MatchContentType(metadata);

            if (propertyList != null && propertyList.Any())
            {
                foreach (var property in propertyList)
                {
                    var matchedProperty = metadata.Properties.FirstOrDefault(x => x.PropertyName == property.PropertyName);
                    if (matchedProperty != null)
                    {
                        if (_principalAccessor.Service.Principal.IsInRole(property.VirtualRoleName))
                        {
                            matchedProperty.ShowForEdit = true;
                        }
                        else
                        {
                            matchedProperty.ShowForEdit = false;
                            matchedProperty.IsRequired = false;
                        }
                    }
                }
            }
        }

        private IEnumerable<PropertyPermissionModel> MatchContentType(ExtendedMetadata metadata)
        {
            var ppSiteSettings = _settingsService.Service.GetSiteSettings<PropertyPermissionsSettings>();

            if (ppSiteSettings == null) return Enumerable.Empty<PropertyPermissionModel>();

            var currentContent = (IContent)metadata.Model;

            switch (currentContent)
            {
                case PageData:
                    {
                        if (ppSiteSettings.PageTypes == null) return null;

                       return ppSiteSettings.PageTypes.Where(x => x.PageTypeId == currentContent.ContentTypeID.ToString())
                            .Select(p => new PropertyPermissionModel { PropertyName = p.PropertyName, VirtualRoleName = p.VirtualRoleName });
                    }
                case BlockData:
                    {
                        if (ppSiteSettings.BlockTypes == null) return null;

                        return ppSiteSettings.BlockTypes.Where(x => x.BlockTypeId == currentContent.ContentTypeID.ToString())
                            .Select(p => new PropertyPermissionModel { PropertyName = p.PropertyName, VirtualRoleName = p.VirtualRoleName });
                    }
            }

            return Enumerable.Empty<PropertyPermissionModel>(); ;
        }
    }
}

 

The final piece is wiring all this up using an InitializationModule to register the new metadata extender class into the metadata handlers that are used when anything inheriting from the ContentData model is loaded.

PropertyPermissionsInitializationModule.cs

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;

namespace Foundation.Infrastructure.PropertyPermissions
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Cms.Shell.InitializableModule))]
    public class PropertyPermissionsInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            if (context.HostType == HostType.WebApplication)
            {
                var registry = context.Locate.Advanced.GetInstance<MetadataHandlerRegistry>();
                registry.RegisterMetadataHandler(typeof(ContentData), new PropertyPermissionsMetadataExtender(), null, EditorDescriptorBehavior.PlaceLast);
            }
        }

        public void Uninitialize(InitializationEngine context)
        {

        }
    }
}

 

Settings Configuration
Screen shot displaying the Dynamic Property Permissions Settings configuration screen


Example with Property Permissions

Screen shot showing an example of a content page with all of the properties displayed

Without Property Permissions

Example screen shot showing a content page with out the secured properties

 

The end result is an easy-to-manage system that allows administrators to secure specific properties on almost any content type by assigning virtual roles to properties and users to the virtual roles. Best of all, no code changes are required. This is in a proof-of-concept stage, and there are several places where functionality can be expanded, but it works. I hope you find it useful.

A Final Word of Caution

Since this code will run for every content type at edit time in the EditUI, it can introduce a decent bit of a performance overhead that could slow down the EditUI for the content authors.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Joe Mayberry

Joe Mayberry is a Senior Technical Consultant specializing in the Optimizely CMS platform, with over 15 years of experience working with content management systems.

More from this Author

Follow Us