Using a Sitecore Item Provider for Component Security

You can use Data Providers for Security?

Recently, @techphoria414 asked on the Sitecore Stack Exchange: “What are some appropriate uses for Sitecore Data Providers?” My answer was:

When Security is just too complicated for Sitecore.

People offered a lot of suggestions with lots of ways to implement data providers using different strategies and philosophies. Still, I thought it could be helpful to elaborate a bit more on my own response.
 

A Complex Scenario

Once upon a time I worked on a portal solution in Sitecore. For this particular domain, the security was a big deal, and the subject matter was difficult (think healthcare). We wanted to have components placed on pages, and for any given page there may have been around 50 components.

Pretty typical so far, right? The kicker was that we only needed to show subsets of these components to users, and the rules for that subset were defined in another system which managed each user’s account (think a dedicated Profile Management System that provided federated accounts).

Imagine a carousel on a homepage. This carousel has 10 panes inside of it. If I’m a nurse at a hospital, I will see a subset of those carousel panes. However, if I’m in corporate accounting, I will see a different (maybe intersecting) subset. The product owner wanted this–for literally everything on the site (menu systems, pages, components, etc.). This was the driving factor behind our implementation.
 

The Security Rules

User Accounts

After users authenticated, there was an intersection of three authorization ideas attached to each account:

  1. What role(s) does the user have? I admit, this could have been achieved through Sitecore security. The problem, however, was complicated by the fact that the source system allowed for new roles to be created on the fly.
  2. What department is the user a part of? This one was subtle, as departments were not thought of as roles. A user can only have a single department in this case, yet they could view wildly different information (think pharmacy, nursing, supply chain, accounting, etc).
  3. What is the user’s organizational context? For this one, Sitecore security just couldn’t cut it. Imagine a hierarchy of companies, each with its own divisions (or any number of organizational layers). Each division has brick and mortar locations. If you’re an executive, you would have visibility to your entire corporation, but if you’re a hospital worker you may only have visibility to your facility. A user could be assigned to any number of nodes within this tree. This defined the facilities the user was a part of. There were tens of thousands of facilities.

The administration of user accounts was simple. We spent a long time building our Security System, and we treated it as a federated identity provider to Sitecore and every other system at our company. It could manage user accounts, roles, profiles, organizational contexts and other bits that the various portals required. Everything was exposed through a series of API’s, and that interaction was natural.
 

Content Authoring

For content authoring in Sitecore, this was a little trickier. There were a series of requirements that looked something like:

  1. For every content item under the site, content authors have the option to specify:

    1. Zero or more roles that should see this content.
    2. Zero or more departments that should see this content.
    3. Zero or more organizational contexts which should see this content.
  2. If no security rules are selected from the above, then this content is viewable by everyone.
  3. Organizational Context is dynamic and the source system is the CRM (which also feeds the security system).
  4. To determine if a user has access to the selected Organizational Context, perform this calculation (remember, the Organizational Context was hierarchical in nature, and referred to company structure):

    1. Take the organizational tree nodes selected for the content item and convert them to a list of all leaves beneath those nodes.
    2. Take the organizational nodes selected for the user and convert them to a list of all leaves beneath those nodes.
    3. If the two lists have any intersecting values, then the user passes this security check.

So, in reality, the rule was simplified to:


// rough pseudo code
public bool UserHasAccess(User user, List<Role> roles, List<Departments> depts, List<Nodes> context)
{
    return user.HasOne(roles) && user.HasOne(depts) && user.Organizations.Contains(context);
}

 

So how do we build this in Sitecore?

For this setup, we ended up creating a template called Security Base. On Security Base, we had 3 fields:

  1. Roles: a multilist which pulls from Role content items (think selections) located at /sitecore/content/my-site/site-settings/roles. Each Role item contains a role key that comes from the Security system.
  2. Department: a multilist which pulls from Department content items located at /sitecore/content/my-site/site-settings/departments. Each Department item contains a department key that comes from the Security system.
  3. Organizational Context: an iFrame field which loads a custom dialog from a code file located at /sitecore/my-site/organizational-context.aspx.

    • Keep in mind that the details behind that dialog are beyond the scope of this blog post. Selecting an organizational context to attach to a user was essentially done through a tree list of checkboxes. Imagine something like this– http://demos.telerik.com/kendo-ui/treeview/checkboxes–except with hundreds of nodes. We ended up storing paths to nodes as the raw value of this field.

The idea here is that I can now have any other template inherit from Security Base, and can then control a derived item’s visibility on the end website. I’ll achieve this by overriding the default Data Provider. Data Providers in Sitecore are generally used to expose data from external systems into the Sitecore CMS. A data provider could essentially talk to anything. With data providers, you can choose to expose new data in Sitecore or, in my case, enrich existing Sitecore content items.

To extend or override the built-in data provider, you need to make a configuration change. Mine looks like this:

<!-- throw this in your App_Config\Include directory and swap out the assembly reference as needed -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <itemManager defaultProvider="default">
      <providers>
        <add name="default" type="Sitecore.Data.Managers.ItemProvider, Sitecore.Kernel">
          <patch:attribute name="type">MyWebsite.Custom.Providers.SecurityItemProvider, MyWebsite.Custom</patch:attribute>
        </add>
      </providers>
    </itemManager>
  </sitecore>
</configuration>

 

After you do this, Sitecore will look to pull ALL of its items from SecurityItemProvider. Keep in mind that EVERY item that is accessed will run through this code, so it needs to be as snappy as you can possibly make it.


My item provider looked something like this:

public class SecurityItemProvider : Sitecore.Data.Managers.ItemProvider
{
    // templates have this template ID (in 7.2 at least)
    private const string STANDARD_TEMPLATE_ID = "{AB86861A-6030-46C5-B394-E8F99E8B87DB}";
 
    // this is your base security template that each item you want to secure would inherit from
    private const string SECURITY_TEMPLATE_ID = "guid of your custom security template";
 
    // the name of your site from the site config.  If we're not in the context of that site, we don't want to run
    private const string WEBSITE_NAME = "website_name";
 
    protected override Item ApplySecurity(Item item, SecurityCheck securityCheck)
    {
        // if this item's a template, just return standard security
        if (item.TemplateID == ID.Parse(STANDARD_TEMPLATE_ID))
        {
            return base.ApplySecurity(item, securityCheck);
        }
 
        // make sure we have a site context
        // detect if we're running in the target site
        // make sure the security disabler isn't enabled
        // make sure we're not in page editor
        // make sure this content item inherits from security base
        if (Context.Site != null
            && Context.Site.Name.ToLower() == WEBSITE_NAME
            && securityCheck != SecurityCheck.Disable
            && Context.PageMode.IsNormal
            && item.IsDerivedFrom(ID.Parse(SECURITY_TEMPLATE_ID)))
        {
            // perform your calculation here to apply whatever security you need.
            // in this case, I have a collection of extension methods to help me.
            // specific implementation will be dependent on your use case.
            if (item.HasOrgContextOver(Sitecore.Context.User)
                && Context.User.IsInDepartmentFor(item)
                && Context.User.HasRolesFor(item))
            {
                // if the user passes the above checks, apply default security
                return base.ApplySecurity(item, securityCheck);
            }
            else
            {
                // trick Sitecore into thinking that the item doesn't exist
                return null;
            }
        }
 
        return base.ApplySecurity(item, securityCheck);
    }
}

 
There are a few extension methods I used to determine if an item derives from a template, or if a template derives from another template. You can easily find these on the internet, or if you’re using SCORE or another framework there are similar methods available:

public static class ItemExtensions
{
    public static bool IsDerivedFrom([NotNull] this Item item, [NotNull] ID templateId)
    {
        return TemplateManager.GetTemplate(item).IsDerived(templateId);
    }
}
 
public static class TemplateExtensions
{
    public static bool IsDerived([NotNull] this Template template, [NotNull] ID templateId)
    {
        return template.ID == templateId || template.GetBaseTemplates().Any(baseTemplate => IsDerived(baseTemplate, templateId));
    }
}

 

And that’s it!

Keep in mind that I used 3 extension methods above to perform my actual checks. Here are some of them:

public static class ItemExtensions
{
    public static bool HasOrgContextOver([NotNull] this Item item, [NotNull] User user)
    {
        // check if this item has any organizational contexts
        // if it does, check if they intersect with any of the user's organizational contexts
        // if it doesn't, return true/false depending on requirements
 
        // unfortunately, this code is very domain specific, so I'm leaving implementation details out.
    }
}
 
public static class UserExtensions
{
    public static bool HasRolesFor(this User user, Item item)
    {
        // if this is a CMS user, we short circuit this check and always let them through
        if (user.Domain.Name == "sitecore")
        {
            return true;
        }
 
        // Roles comes from Security Base
        var field = item.Fields["Roles"];
 
        // not every item inherits from security base, but if it does it will have a Roles field
        if (field != null)
        {
            var roleItems = ((MultilistField)field).GetItems();
 
            if (!roleItems.Any())
            {
                return true;
            }
 
            var roleKeys = roleItems.Select(x => x.Fields["Role Key"].ToString().Trim().ToLower()).ToList();
 
            return user.Roles.Select(x => x.Name.ToLower().Trim()).Intersect(roleKeys).Any();
        }
 
        return true;
    }
}

 
Of course, when my users log into the system I run a set of code that looks something like this…

// UserInfo is a poco that contains data obtained from the identity provider at authentication time
public void Login(UserInfo userInfo)
{
    // be sure to keep the user's domain as extranet to enforce Sitecore security rules on content
    var virtualUser = AuthenticationManager.BuildVirtualUser("extranet\\" + user, true);
 
    virtualUser.Profile.FullName = userInfo.FirstName + " " + userInfo.LastName;
    virtualUser.Profile.Email = userInfo.Email;
    virtualUser.Profile.SetCustomProperty("OrganizationalContext", userInfo.OrganizationalContext);
    virtualUser.Profile.SetCustomProperty("Department", userInfo.Department);
    virtualUser.Profile.Save();
 
    foreach (var role in userInfo.Roles)
    {
        // in this case, RoleKey is an active directory group.  We're essentially attaching virtual roles to our accounts
        virtualUser.Roles.Add(Role.FromName("extranet\\" + role.RoleKey));
    }
 
    // create the actual Sitecore session for the user
    AuthenticationManager.Login(virtualUser);
}

 

I hope this helps someone out! Thanks for reading.

Dylan McCurry

I am a certified Sitecore developer with a passion for the web. I hopped into the .NET space 5 years ago to work on enterprise-class applications and never looked back. I love building things—everything from from Legos to software that solves real problems. I have a strong foundation of backend skills, with sweet spots like security, portal solutions and APIs. Early on, before I had the benefit of SCORE, I made a lot of mistakes with Sitecore but learned a lot in the course of the struggle. I would like to support other developers by contributing my perspective on doing things “the Sitecore way,” rather than fighting the framework. Did I mention I love video games? Learn more about Dylan McCurry.

2 comments on Using a Sitecore Item Provider for Component Security

Steve KarrmannJanuary 18, 2017 - Reply

I am currently working on a project with similar content exclusion requirements. Your approach of filtering items through a custom item provider seems like an excellent solution in our case. Great article, thank you!

Dylan McCurryFebruary 3, 2017 - Reply

That’s good to hear. We are also using this on a concept that we refer to as “Site Clusters” (single tree, multiple site definitions resolving to it), where we’re able to exclude pages for different sites. I’ll blog about that in the near future as well!

Add a Comment

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

Or request call back