Integrate Rules Engine in Sitecore Security

The Problem:

We want to have a security role where this role has access to certain page templates in all sitecore languages except for English. Sitecore currently have the option to set rules on a certain language to deny or give access to it but not combining language with template security. 

The proposed solution:

  1. Add a new Rules field on each item.
  2. When sitecore authorization provider runs the security check to check if the user has access or not, it will add the rules specified in the rules field that was added in step 1.

Steps to accomplish this solution:

1. Add a new Rule field to the templates
  1. Create a new base template called Rules Base with a single field of type “Rules” field called Rule and mark the field to be shared across languages.
      
  2. As a source for this field set it to point to the custom rules that we will be implemented in this field.
  3. All the pages that needed this special rule need to inherit from the Rules Base Template.

2. Create Rule items in sitecore
  1. As mentioned in point 2, You will need to start creating your rules in sitecore.
  2. Go to /sitecore/system/Settings/Rules and insert a new Rules Context Folder. In my case I named it Templates Access.
  3. Create a new Element Folder in /sitecore/system/Settings/Rules/Definitions/Elements called Template Access, so that we can add a new custom rule.
  4. Add a new Action to your element folder and set the name for it to be Deny Access to Language.
     
  5. In the Action text put this value.
    Deny Access to Role [RoleName,UserRoles,,RoleName] in [Language,Tree,root=/sitecore/system/Languages,Language] with Propagation [Propagation,,,Propagation].
    If you want to know more about the rules syntax you can read  https://sdn.sitecore.net/upload/sitecore6/61/rules_engine_cookbook_sc61_...
  6. The Type field will be pointing to the custom code we are going to implement for this Action.
  7. Now go ahead and create a base class in your solution. This base class will inherit from Sitecore.Rules.RuleContext. The class will have an AccessRuleCollection attribute to manage the security permissions. This attribute will be passed to the rule context when it is initialized.
    public class AccessRuleContext: Sitecore.Rules.RuleContext
    {
            public AccessRuleCollection RuleCollection { get; set; }
    
            public AccessRuleContext(AccessRuleCollection ruleCollection):base()
            {
                RuleCollection = ruleCollection;
            }
     }
    
     
  8. The implementation of the actual action will be:
    public class DenyAccessToLanguages<T> : global::Sitecore.Rules.Actions.RuleAction<T> where T : AccessRuleContext
        {
            public override void Apply(T ruleContext)
            {
                //get current user
                var user = Sitecore.Context.User;
                // check if user is in the role specified
                if (user != null && user.IsInRole(RoleName))
                {
                    //get current language for the item
                    var contextLanguage = ruleContext.Item.Language;
                    if (contextLanguage != null)
                    {
                        //get language item from sitecore
                        var languageItem = ruleContext.Item.Database.GetItem(Language);
                        if (languageItem==null)
                        {
                            return;
                        }
                        //check propgation type in the rule. the three values that user should enter are "Any" OR "Entity"  OR "Descendants"
                        PropagationType propType;
                        switch (Propagation)
                        {
                            case "Any":
                                propType = PropagationType.Any;
                                break;
                            case "Entity":
                                propType = PropagationType.Entity;
                                break;
                            case "Descendants":
                                propType = PropagationType.Descendants;
                                break;
                                default:
                                    propType = PropagationType.Any;
                                    break;
                        }
                        
    
                        //get iso code from the language item
                        string languageName = languageItem["Iso"];
                        // check if the item is in current language then deny Access to the item else allow access
                        ruleContext.RuleCollection.Helper.AddAccessPermission(user, AccessRight.ItemWrite,
                            propType,
                            contextLanguage.GetIsoCode(ruleContext.Item.Database) == languageName ? AccessPermission.Deny : AccessPermission.Allow);
                    }
                }
            }
            // Values coming from the rules parameters
            public string RoleName { get; set; }
            public string Language { get; set; }
            public string Propagation { get; set; }
        }
    
       
  9. Go back to sitecore and make sure you have included the rule in the Context Folder. Go to /sitecore/system/Settings/Rules/Templates Access/Tags/Default. And add your Templates Access and System Tags to the Tags field.
3. Setting rules on Items
  1. Now when you go to any item you should be able to see your new Rule field.
  2. Click on Edit Rule -> set condition to always true.
  3. In the action choose the new custom rule. Set the parameters to the right values. 

4. Make Sitecore Consume the rules field when checking item access
  1. To change the default authorization provider in sitecore to the new implementation of the authorization provide, add a config file in your project to override the default provider.
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
      <sitecore>
        <authorization>
          <!-- Set the custom AuthorizationProvider as the default provider -->
          <patch:attribute name="defaultProvider">customProvider</patch:attribute>
          <providers>
            <clear />
            <add name="customProvider" type="AMSM.Library.CustomSitecore.Pipelines.Security.LanguageAccessAuthorizationProvider, AMSM.Library" connectionStringName="core" embedAclInItems="true" />
          </providers>
        </authorization>
      </sitecore>
    </configuration>
    
      
  2. The custom provider implementation will be the following:
    public class LanguageAccessAuthorizationProvider : SqlServerAuthorizationProvider
    {
        public LanguageAccessAuthorizationProvider() : base()
        {
            //user your own authorization helper to override the function
            _itemHelper = new LanguageAccessAuthorizationHelper();
        }
    
        private ItemAuthorizationHelper _itemHelper;
    
        //override the default itemHelper
        protected override ItemAuthorizationHelper ItemHelper
        {
            get { return _itemHelper; }
            set { _itemHelper = value; }
        }
    }
    
    public class LanguageAccessAuthorizationHelper : ItemAuthorizationHelper
    {
        //override the GetAccessRules function to consume your field
        public override AccessRuleCollection GetAccessRules(Item item)
        {
            //Deserialize the Security field on the item (Default behavior)
            AccessRuleCollection results = new AccessRuleSerializer().Deserialize(item[FieldIDs.Security]);
            //check if the item is derived from our base template
            if (item.IsDerived(IRules_BaseConstants.TemplateId))
            {
                RulesField field = null;
                using (new SecurityDisabler())
                {
                    //get Rules field
                    field = item.Fields[IRules_BaseConstants.RuleFieldName];
                }
                //check if field is null or doesn't have a value then return
                if (field == null || field.Value.IsNullOrEmpty())
                {
                    return results;
                }
                //Parse the rules in the rule field
                RuleList<AccessRuleContext> rules =
                    RuleFactory.ParseRules<AccessRuleContext>(item.Database, field.Value);
                //check if there are no rules then do nothing
                if (rules != null && rules.Count > 0)
                {
                    //create a new rule context and pass the AccessRuleCollection as well as the item to the rule
                    var ruleContext = new AccessRuleContext(results)
                    {
                        Item = item
                    };
    
                    //run the rule
                    rules.Run(ruleContext);
                }
    
                return results;
            }
    
            return results;
        }
    }