Security Replicator

Posted 01/31/2014 by Sean Connall

Problem

A common request of most, if not all, multi-site solutions is to have site level security. For a recent project we had just such a request. In addition to having multiple roles for each site, the client requested that each site have its own set of roles. 

For example if two sites (Site A and Site B) exist and a site is to have the following roles

  • Author
  • Approver
  • Admin

The client wanted the following roles to be created:

  • Site A Author
  • Site A Approver
  • Site A Admin
  • Site B Author
  • Site B Approver
  • Site B Admin

Additionally, the client did not want to be required to create each of these roles manually and assign security as part of the site creation process. They wanted a way to define security and then replicate that same security to other sites as they are created. This required some event handling to create and manage the site specific roles as well as some code to replicate security from one item to another.

The solution outlined below was built specifically for managing site nodes. After seeing it in action we see an opportunity for future development that will provide more generic features allowing security replication on a much broader level.

Solution

Our solution was based on having a single item somewhere in the content tree that could serve as a base security item. We opted to create a new folder in /sitecore/system/Modules called Security Replicator Folder. in this new folder we created a an item called Base Security Item. For now this is a Standard Template item with no fields.

Once we had an item we added some base roles the old fashioned way via Role Manager. These roles will act as the building blocks much like base templates are when creating content data templates.

Base Security Item

In order to make the site roles as maintenance free as possible we hooked the solution up using some event handlers (item:created, item:renamed, and item:deleted). We also provided a custom button in the Security chunk to apply any changes to a site as needed. The code for the events and button command has been covered extensively in lots of other posts so I will not dive into that here. Let's pick up the code from the Execute method in the button command.

We created a SecurityReplicator class with  a CreateSiteRoles method that takes the command item as an input. Our code determines if the item is a site node and then calls this method.

        
        public override void Execute([NotNull] CommandContext context)
        {
            if (context.Items.Length < 1) return;

            //get context item
            var commandItem = context.Items[0];
          
            if (Sites.IsSite(commandItem))
            {
                //manage base role inheritance
                SecurityReplicator.ManageBaseRoleInheritance(commandItem);

                //create new roles
                SecurityReplicator.CreateSiteRoles(commandItem);
            }
        }

This version of the solution uses config file settings to initialize the Security Replicator. Future versions will move these settings into Sitecore as items to make the module more generic.

 A custom node set was created to define base roles, target names, and any inheritance.

 
<!-- custom nodeset to define base roles for access right replication when creating a new site-->
<baseroles>
   <baserole baseRoleName="sitecore\Base Author" targetRoleName="sitecore\{0} Site Author" memberOfRoleName="sitecore\Base" />
   <baserole baseRoleName="sitecore\Base Approver" targetRoleName="sitecore\{0} Site Approver" memberOfRoleName="sitecore\{0} Site Author" />
   <baserole baseRoleName="sitecore\Base Publisher" targetRoleName="sitecore\{0} Site Publisher" memberOfRoleName="sitecore\{0} Site Approver" />
   <baserole baseRoleName="sitecore\Base Content Admin" targetRoleName="sitecore\{0} Site Content Admin" memberOfRolename="sitecore\{0} Site Publisher" />
</baseroles>
Each baseRole node has three properties:
  1. baseRoleName = the Sitecore role used as the model to replicate security for the newly created role defined by the targetRoleName property
  2. targetRoleName = new Sitecore role that is created dynamically. The site name will replace the {0} token.
    EXAMPLE: A site called US would create US Site Author, US Site Approver, US Site publisher, and US Site Content Admin roles
  3. memberOfRoleName = can be a predefined Sitecore role or one of the dynamic roles defined earlier in the node set.
    EXAMPLE: the US Site Approver is a member of the US Site Author role based on the configuration above

 
        public static void CreateSiteRoles(Item siteItem)
        {
            //Gets the baseRoles nodeset that has the base role, target role name and role to be included
            var roles = Factory.GetConfigNodes("baseRoles/baseRole");

            foreach (XmlNode role in roles)
            {
                var baseRoleNameNode = role.Attributes["baseRoleName"];
                var targetRoleNameNode = role.Attributes["targetRoleName"];
                var memberOfRoleNameNode = role.Attributes["memberOfRoleName"];

                if (baseRoleNameNode != null && targetRoleNameNode != null)
                {
                    //set role names
                    var baseRoleName = baseRoleNameNode.Value;

                    //replace {0} with item.Name which corresponds to the name of the site node in content editor
                    var targetRoleName = string.Format(targetRoleNameNode.Value, siteItem.Name);

                    if (!Sitecore.Security.Accounts.Role.Exists(targetRoleName))
                    {
                        //create target role
                        Roles.CreateRole(targetRoleName);
                    }

                    //add new role to parent role from config if set
                    if (memberOfRoleNameNode != null)
                    {
                        //replace {0} with item.Name which corresponds to the name of the site node in content editor
                        var memberOfRoleName = string.Format(memberOfRoleNameNode.Value, siteItem.Name);

                        //assign base role to new role
                        SecurityReplicator.AssignRolesInRoles(memberOfRoleName, targetRoleName);
                    }

                    //get roles
                    Role baseRole = Sitecore.Security.Accounts.Role.FromName(baseRoleName);
                    Role targetRole = Sitecore.Security.Accounts.Role.FromName(targetRoleName);

                    // Get the ID from the config file and retrieve the base security item
                    Item baseSecurityItem = Sitecore.Context.ContentDatabase.GetItem(new ID(Sitecore.Configuration.Settings.GetSetting("SecurityReplicator.SecurityReplicatorBaseItemId")));

                    //set access rights from security item
                    SetRoleAccessFromBaseRole(siteItem, baseSecurityItem, baseRole, targetRole);

                    // Get the ID from the config file and retrieve the base workflow item
                    Item baseWorkflowItem = Sitecore.Context.ContentDatabase.GetItem(new ID(Sitecore.Configuration.Settings.GetSetting("SecurityReplicator.WorkflowBaseItemId")));

                    //set workflow access
                    SetWorkflowAccessFromBaseRole(baseWorkflowItem, baseRole, targetRole);
                }
            }
        }

The first part of this method gets the node set and creates the new roles. After the roles are actually created Role items are retrieves the base role, newly created target role, and the base security item. All of these elements, along with the site item, are passed into the SetRoleAccessFromBaseRole method.

   
        public static void SetRoleAccessFromBaseRole(Item siteItem, Item baseSecurityItem, Role baseRole, Role targetRole)
        {
            Assert.ArgumentNotNull(siteItem, "siteItem");
            Assert.ArgumentNotNull(baseSecurityItem, "baseSecurityItem");
            Assert.ArgumentNotNull(baseRole, "baseRole");
            Assert.ArgumentNotNull(targetRole, "targetRole");

            using (new Sitecore.SecurityModel.SecurityEnabler())
            {
                //Get current access rules for the base security item
                AccessRuleCollection baseAccessRules = baseSecurityItem.Security.GetAccessRules();

                // Get the current accessrules for the siteItem
                AccessRuleCollection targetAccessRules = siteItem.Security.GetAccessRules();

                //remove access rules for target role from target set
                targetAccessRules.RemoveAll(a => a.Account == targetRole);

                foreach (AccessRule baseAccessRule in baseAccessRules)
                {
                    //get access permission
                    AccessPermission baseAccessPermission = new AccessPermission();

                    //determine access permission
                    //NOTE: attempted to use the get access permission method below. Always returns not set
                    //AccessPermission baseAccessPermission = baseAccessRules.Helper.GetAccessPermission(baseRole, baseAccessRule.AccessRight, baseAccessRule.PropagationType);

                    //determine AccessPermission
                    switch (baseAccessRule.SecurityPermission)
                    {
                        case SecurityPermission.AllowAccess:
                        case SecurityPermission.AllowInheritance:
                            baseAccessPermission = AccessPermission.Allow;
                            break;

                        case SecurityPermission.DenyAccess:
                        case SecurityPermission.DenyInheritance:
                            baseAccessPermission = AccessPermission.Deny;
                            break;
                    }

                    //assign rule to target role if permission is set and access rule account matches the base role
                    if (baseAccessPermission != AccessPermission.NotSet && baseAccessRule.Account == baseRole)
                    {
                        //add base access rules for target role to access rules for target item
                        targetAccessRules.Helper.AddAccessPermission(targetRole, baseAccessRule.AccessRight, baseAccessRule.PropagationType, baseAccessPermission);
                    }
                }

                // commit changes
                siteItem.Security.SetAccessRules(targetAccessRules);
            }
        }

The premise here is that the security for the baseRole is defined on the baseSecurityItem. By getting the current baseAccessPermission for that role we can use it to replicate the same access permission for the new role on the new site item by adding each AccessPermission to the targetAccessRules set. The final step is to apply the targetAccessRules to the site item and voila, replicated security settings.

This is a high level look at this module. Future posts will be added as the module develops. Some thoughts on moving forward are:

  • template driven base security items for replicating security to specific sections of the content tree
  • data templates to replace config file for updating in Sitecore
  • content tree driven inheritance of dynamic roles

Security Replicator


Let us know what you think

Share:

Archive

Syndication