Tips and tricks for implementing a custom Sitecore SqlAuthorizationProvider

In this post I would like to share an example of implementing a custom SqlAuthorizationProvider for a Sitecore solution. First things first, you have to really consider using it for a large solution, it’s a customization for accessing items for users and roles and it’s a core functionality of Sitecore XM – this provider is called every time (if no cache hit), when anyone tries to get access to an item – with Experience Editor, Content Editor, Sitecore Item API, etc.).

First, let’s have a look at the default providers. You can see it by simply going to the /sitecore/admin/showconfig.aspx and go to /sitecore/authorization/providers configuration node. The following providers are the default ones:

  • sql – default provider, reading the __Security field on items and decide permissions based on it
  • disabled – simply bypassing authorization, it could be useful on CD servers where all published content is available for all visitors
  • custom – it’s added by the Sitecore.Buckets.config to handle bucket authorization and this is set by default on a vanilla Sitecore installation

As the BucketAuthorizationProvider used by default, I suggest to inherit from it in your custom provider. Here is an example of the CustomAuthorizationProvider.

namespace Feature.Providers.ItemAccessibility
{
public class CustomAuthorizationProvider : BucketAuthorizationProvider
{
[Obsolete]
public CustomAuthorizationProvider() : base()
{
ItemHelper = (ItemAuthorizationHelper)ServiceLocator.ServiceProvider.GetService(typeof(ItemAuthorizationHelper));
}
public CustomAuthorizationProvider(SqlServerDataApi api) : base(api)
{
ItemHelper = (ItemAuthorizationHelper)ServiceLocator.ServiceProvider.GetService(typeof(ItemAuthorizationHelper));
}
protected sealed override ItemAuthorizationHelper ItemHelper { get; set; }
}
}

You can see, this does not really contain any logic, because the actual item level permission handling is in the ItemAuthorizationHelper, which is injected in the provider. To use our own, we have to put our own helper in the DI container to able to resolve in the provider. In this example I overwrite the original one, but you can also keep both.

namespace Feature.DI
{
public class RegisterContainer : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<ItemAuthorizationHelper, CustomItemAuthorizationHelper>();
}
}
}

Well, here is an example implementation of the ItemAuthorizationHelper. This is a really simple one, it just allows access to all items, by bypassing Sitecore authorization. I created some kind of a queue of functions to be called after each other, to able separate features easily – if you follow Helix principles, you could extend this list outside of the class just like a “pipeline” and inject different functions in to this queue. Although, in this implementation the order matters, because the previousAccessResult is always the output of the previous called function.

namespace Feature.Providers.ItemAccessibility
{
public class CustomItemAuthorizationHelper : ItemAuthorizationHelper
{
private static readonly List<Func<Item, Account, AccessResult, AccessResult>> GetItemAccessChecks = new List<Func<Item, Account, AccessResult, AccessResult>>
{
GetProductItemAccess,
GetProductFamilyItemAccess,
GetProductCategoryItemAccess
};
public CustomItemAuthorizationHelper(
BaseAccessRightManager accessRightManager,
BaseRolesInRolesManager rolesInRolesManager,
BaseItemManager itemManager) : base(accessRightManager, rolesInRolesManager, itemManager) {}
/// <summary>
/// It's only called if the AccessResult is not found yet in the AccessResultCache
/// </summary>
protected override AccessResult GetItemAccess(Item item, Account account, AccessRight accessRight, PropagationType propagationType)
{
var originalAccessResult = base.GetItemAccess(item, account, accessRight, propagationType);
var accessResult = originalAccessResult;
foreach (var check in GetItemAccessChecks)
{
accessResult = check.Invoke(item, account, accessResult);
}
return accessResult;
}
private static AccessResult GetProductItemAccess(Item item, Account account, AccessResult previousAccessResult)
{
// Insert custom logic here
return new AccessResult(AccessPermission.Allow, new AccessExplanation($"Use meaningful a explanation."));
}
private static AccessResult GetProductFamilyItemAccess(Item item, Account account, AccessResult previousAccessResult)
{
// Insert custom logic here
return new AccessResult(AccessPermission.Allow, new AccessExplanation($"Use meaningful a explanation."));;
}
private static AccessResult GetProductCategoryItemAccess(Item item, Account account, AccessResult previousAccessResult)
{
// Insert custom logic here
return new AccessResult(AccessPermission.Allow, new AccessExplanation($"Use meaningful a explanation."));
}
}
}

Caching ⚡ – AccessResultCache in the middle layer

AccessResultCache is a storage for user-item permissions, so the provider does not need to run Sitecore Item API queries every time a user tries to access to an item. You probably already heard of it in middle of hardening a Sitecore solution. If you go to /sitecore/admin/cache.aspx, you can see how much this cache is in use. The SqlAuthorizationProvider by default is adding and reading cache values and this layer is sitting in front of the ItemAuthorizationHelper. So, whatever result we return in the GetItemAccess(...) or any other overwritten method it will be added to this cache and the key is generated from the given parameters (Item, Account, etc.).

Because of the reasons above, it’s important to handle the caching in case of you are not using the default __Security field.

By default, Sitecore is doing a partial cache clear whenever the __Security field is changed but it does not do when other custom or standard fields are changed!

It’s important to handle cache clearing in this case, unfortunately, partial cache update is not something trivial, it really depends on your business logic, but you can implement a simple save handler to clear the AccessResultCache, like the following.

namespace Feature.EventHandlers
{
public class ClearAccessResultCache
{
private static readonly Dictionary<ID, ID> TemplatesToCheck = new Dictionary<ID, ID>()
{
{ Constants.Template1.TemplateId, Constants.Template1.Fields.Role },
{ Constants.Template2.TemplateId, Constants.Template2.Fields.Role }
};
public void OnItemSaved(object sender, EventArgs args)
{
var savedItem = Event.ExtractParameter(args, 0) as Item;
var changes = Event.ExtractParameter(args, 1) as ItemChanges;
if (savedItem == null || changes == null || !TemplatesToCheck.ContainsKey(savedItem.TemplateID))
return;
var fieldIdToCheck = TemplatesToCheck[savedItem.TemplateID];
if (!changes.IsFieldModified(fieldIdToCheck))
return;
// This is not optimal, but will do it for now
CacheManager.ClearAccessResultCache();
}
}
}

This cache clearing solution only works in cases where the dependent fields does not change too frequently because it’s purging the full cache. To do a single cache entry clear, you can use the CacheManager.GetAccessResultCache().Remove(...).

Last, but not least, the configuration to use the new provider and the save event handlers.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/&quot; xmlns:set="http://www.sitecore.net/xmlconfig/set/&quot; xmlns:role="http://www.sitecore.net/xmlconfig/role/&quot; xmlns:localenv="http://www.sitecore.net/xmlconfig/localenv/"&gt;
<sitecore role:require="ContentManagement or Standalone">
<events>
<event name="item:saved">
<handler type="Feature.EventHandlers.ClearAccessResultCache, Feature" method="OnItemSaved" role:require="ContentManagement or Standalone"/>
</event>
</events>
<authorization>
<providers>
<add name="custom" type="Sitecore.Buckets.Security.BucketAuthorizationProvider, Sitecore.Buckets" connectionStringName="security" embedAclInItems="true">
<patch:attribute name="type">Feature.Providers.ItemAccessibility.CustomAuthorizationProvider, Feature</patch:attribute>
</add>
</providers>
</authorization>
</sitecore>
</configuration>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s