Autorisations de propriété dynamiques faciles pour le CMS Optimizely

J’ai récemment travaillé avec un client qui souhaitait restreindre l’accès à des propriétés spécifiques des types de contenu. Nous avons configuré certains attributs du descripteur d’éditeur et appliqué un décorateur aux propriétés des classes de modèle, ce qui a bien fonctionné. Ensuite, ils ont voulu des changements.
Le problème est que pour déterminer qui a accès, il faut examiner le code. Toute mise à jour nécessite des modifications de code, ce qui devient un gros problème lorsque les propriétés sont déjà en production. Le client souhaitait un moyen d’effectuer ces modifications d’autorisations de propriété rapidement et sans avoir besoin de déployer des modifications de code, ce qui nécessitait une solution différente.
Une recherche rapide a trouvé quelques solutions possibles. Un, de Mattias Olsen (https://world.optimizely.com/forum/developer-forum/CMS/Thread-Container/2019/3/access-rights-for-properties/)était très prometteur, mais ses rôles et propriétés virtuels sont codés en dur dans la classe Editor Descriptor.
Une autre solution suggérait d’utiliser la console Paramètres d’administration pour remplacer les paramètres du CMS. Je ne voulais pas remplacer le code en modifiant les types de contenu dans le CMS. Cela rend très frustrant pour les développeurs de résoudre les problèmes liés aux modifications de propriétés dans le code.
Au lieu de cela, j’ai cherché un moyen de gérer cela dans le CMS. Mon objectif était de disposer d’une interface de configuration dans laquelle les administrateurs pouvaient définir le type de contenu, définir le nom de la propriété et attribuer des rôles virtuels à la propriété. Cela permettrait aux administrateurs de sécuriser la propriété pour les utilisateurs correspondant au rôle virtuel attribué. Pour tout le monde, ce serait comme si la propriété n’existait pas.
Quoi et qui
La première étape consiste à identifier qui doit avoir accès à quelles propriétés. Cette interface de configuration devant être sécurisée, je l’ai intégrée aux paramètres du site en créant une nouvelle classe SettingsContentType héritée de SettingsBase. J’ai ajouté deux listes de propriétés : une pour les pages et une pour les blocs.
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; } } }
J’ai adopté une approche simple, sachant que j’avais besoin des mêmes propriétés pour les deux listes. J’ai créé une nouvelle classe PropertyPermissionModel avec deux propriétés de chaîne :
- Nom de la propriété
- Nom du rôle virtuel
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; } } }
J’ai ensuite créé deux modèles supplémentaires, tous deux héritant du PropertyPermissionModel, et j’ai ajouté une propriété pour identifier l’ID du type de page ou l’ID du type de bloc. Étant donné que les identifiants ne sont pas faciles à identifier, j’ai configuré des usines de sélection pour obtenir une liste des types de contenu pertinents à afficher sous forme de champs déroulants.
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; } } }
De plus, j’ai créé des classes PropertyList à l’aide de PropertyDefinitionTypePlugin afin que les types de propriétés puissent être utilisés dans la classe Settings.
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> { } }
Comment, quand et où
Maintenant, j’ai une classe de configuration sécurisée avec des listes répétables qui définissent un type de contenu, la propriété à sécuriser et le rôle virtuel qui l’a sécurisé. Mais quand est-il utilisé ?
Il doit s’exécuter dès que l’élément de contenu est chargé dans EditUI, donc un MetadataExtender était la voie à suivre. J’ai créé une nouvelle classe implémentant l’interface IMetadataExtender puisque MetadataExtender fournit le type de contenu de l’objet actuel à comparer à la liste dans SiteSettings.
Lorsqu’un type de contenu correspond à une entrée, chaque propriété est comparée à la liste à sécuriser. Si la propriété existe sur le type de contenu et que l’éditeur est affecté au rôle virtuel correspondant, affichez-la. Si l’utilisateur n’est pas affecté au rôle virtuel, définissez les valeurs ShowForEdit et IsRequired de la propriété sur false. Cela masque la propriété dans EditUI et permet de la publier.
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>(); ; } } }
La dernière pièce consiste à câbler tout cela à l’aide d’un InitializationModule pour enregistrer la nouvelle classe d’extension de métadonnées dans les gestionnaires de métadonnées qui sont utilisés lorsque tout ce qui hérite du modèle ContentData est chargé.
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) { } } }
Configuration des paramètres
Exemple avec les autorisations de propriété
Sans autorisations de propriété
Le résultat final est un système facile à gérer qui permet aux administrateurs de sécuriser des propriétés spécifiques sur presque tous les types de contenu en attribuant des rôles virtuels aux propriétés et des utilisateurs aux rôles virtuels. Mieux encore, aucune modification de code n’est requise. Nous en sommes au stade de la validation de principe et il existe plusieurs endroits où les fonctionnalités peuvent être étendues, mais cela fonctionne. J’espère que tu trouves cela utile.
Un dernier mot de prudence
Étant donné que ce code s’exécutera pour chaque type de contenu au moment de l’édition dans EditUI, il peut introduire une légère surcharge de performances qui pourrait ralentir l’EditUI pour les auteurs de contenu.
Source link