Security Field Fallback

Posted 10/31/2013 by vconcepcion

One of our clients requested a feature to allow content authors to secure individual modules or components within a page, that is, hide them from unauthenticated visitors. Each of these modules was setup to use a data source item, which made this an ideal scenario for using Sitecore's out-of-the-box item level security.

The easiest way to implement this, of course, is to simply train content authors to apply security permissions to module items as needed via the Security Editor tools. However, unless the data source items happen to be organized into folders which mirror the different levels of security, it can quickly become challenging to manage such granular security permissions across a site. Furthermore, many content authors may not have a strong enough understanding of Sitecore security (roles, inheritance, etc) to feel comfortable assigning these security permissions.

To address these issues, we decided to add a drop-down field to our module base template which would allow authors to select the desired level of security in familiar business terms (e.g., "Public"/"Private").

Security drop-down

The drop-down values would be tied to global "Module Access Level" items which encapsulate the corresponding security permissions (I knew the Security field type would come in handy some day).

Access level items

Access level permissions

In order to dynamically link the selected access level to the security settings, we setup field fallback from the module's __Security field to the Module Access Level item's Permissions field. Thankfully, Alex Shyba's Language Fallback code provided a good starting point for this. I encourage you to check out Alex's code and/or the Field Fallback module on Marketplace if you haven't already.

Below are the key parts of our custom StandardValuesProvider which implements security field fallback.

        public override string GetStandardValue(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            var item = field.Item;
            Assert.ArgumentNotNull(item, "item");

            // Enable fallback on configured databases only
            if (!DatabaseCheck(item))
            {
                return base.GetStandardValue(field);
            }

            // Enable fallback for __Security field
            if (field.ID != Sitecore.FieldIDs.Security)
            {
                return base.GetStandardValue(field);
            }

            // Only applies to items with "Module Access Level" field
            var accessLevelField = item.Fields[ModuleAccessLevel];
            if (accessLevelField == null)
            {
                return base.GetStandardValue(field);
            }

            // Return cached value if it exists
            string returnValue;
            if (TryCacheFirst(item, out returnValue)) return returnValue;

            // Use Standard Value if fallback value is null (i.e., fallback item not found)
            var fallbackValue = GetFallbackValue(accessLevelField, item);
            return fallbackValue ?? base.GetStandardValue(field);
        }

        protected virtual string GetFallbackValue(Field accessLevelField, Item item)
        {
            var fallbackValue = GetFallbackValuesFromCache(item);
            if (fallbackValue == null)
            {
                // Get value and add to cache
                ID accessLevelItemId;
                if (ID.TryParse(accessLevelField.Value, out accessLevelItemId))
                {
                    var accessLevelItem = item.Database.GetItem(accessLevelItemId);
                    if (accessLevelItem != null && accessLevelItem.Fields[Permissions] != null)
                    {
                        // Return the value of the Permissions field on the referenced Access Level item
                        fallbackValue = accessLevelItem.Fields[Permissions].Value;
                        AddFallbackValueToCache(item, fallbackValue);
                    }
                }
            }
            return fallbackValue;
        }

        private void ClearFallbackCaches(ItemUri itemUri, Database database)
        {
            var cache = _securityFallbackCaches[database.Name];
            if (cache != null)
            {
                string key = itemUri.ToString();
                if (cache.InnerCache.ContainsKey(key))
                {
                    // evict from fallback cache and clear access result cache
                    cache.RemoveKeysContaining(key);
                    var accessResultCache = CacheManager.GetAccessResultCache();
                    if (accessResultCache != null)
                    {
                        accessResultCache.Clear();
                    }
                }
            }
        }

Lastly, we added the following to our BaseSublayout.cs class, which is inherited by all our module sublayouts, to ensure that a module is hidden when the DataSource cannot be read by the current user. Without this, the sublayout may output HTML even when the DataSourceItem is null.

        protected override void OnLoad(EventArgs e)
        {
            // Hide if data source is provided but item not found or not accessible (due to access level)
            if (!string.IsNullOrEmpty(DataSource) && DataSourceItem == null)
            {
                this.Visible = false;
            }
            base.OnLoad(e);
        }

A couple things to note regarding caching:

  • We used the module item URI as the security fallback cache key. I believe the original language fallback code uses a combination of the item URI and field ID, but in this case there's only one __Security field on an item.
  • When clearing the fallback cache, we had to also clear the AccessResultCache in order to see the changes take effect. Unfortunately, I couldn't find an easy way to evict only certain entries from the AccessResultCache given the fallback cache key...but perhaps someone can prove me wrong :)

And there you have Yet Another Field Fallback. #yaff

Share:

Archive

Syndication