The customer portal security can be customized by implementing and registering a IEntitySecurityService. Practically, this means extending CustomerPortalSecurityService and overriding 3 methods (and optionally a 4th one):

  • BuildRestriction – add a restriction to a criteria based query
  • BuildFilterNode – add a restriction for an HQL based query
  • CanAccessEntity – verifies whether the user has access to an entity (this is used when visiting a particular page so it’s only important if you are going to give access to the entity via a page)
  • IsTypeExempt can be overridden as well to exclude a type from the security altogether.

The methods can’t return null so just call the base class method as a fallback.

This is an example where I want to make it possible to access Dashboard Pages and Widgets in the portal, therefore I need to customize the plugin security. I need to make sure to include the original restriction and then widen it a little bit to allow access to those plugin types.

protected override ICriterion BuildRestriction(ICriteria criteria, System.Type type)
{
    if (type.FullName == "Sage.SalesLogix.Plugins.Plugin")
    {
        // allow pulling dashboard widgets and pages
        return Restrictions.Or(Restrictions.In("Type", new[] { PluginType.DashboardWidget, PluginType.DashboardPage }),
            Restrictions.Eq("DataCode", "CUSTOMERPORTAL").IgnoreCase()
            );
    }
    return base.BuildRestriction(criteria, type);
}

protected override IASTNode BuildFilterNode(IASTNode ast, IASTNode rangeNode, EntitySecurityServiceBase.ParameterNodeBuilder paramBuilder, System.Type type)
{
    if (type.FullName == "Sage.SalesLogix.Plugins.Plugin")
    {
        return BuildPluginFilterNode();
    }
    return base.BuildFilterNode(ast, rangeNode, paramBuilder, type);
}

private static IASTNode BuildPluginFilterNode()
{
    return CreateNode(HqlParser.OR,
        CreateNode(HqlParser.IN,
            CreateNode(HqlParser.IDENT, "Type"),
            CreateNode(HqlParser.IN_LIST,
                CreateNode(HqlParser.NUM_INT, ((int)PluginType.DashboardPage).ToString()),
                CreateNode(HqlParser.NUM_INT, ((int)PluginType.DashboardWidget).ToString()))),
        CreateNode(HqlParser.EQ,
            CreateNode(HqlParser.IDENT, "DataCode"),
            CreateNode(HqlParser.QUOTED_String, "'CUSTOMERPORTAL'")));
}

// not really useful here, but for completeness...
protected override bool CanAccessEntity(object entity, System.Type type)
{
    var plugin = entity as Plugin;
    if (plugin != null)
    {
        return string.Equals(plugin.DataCode, "customerportal", StringComparison.OrdinalIgnoreCase) ||
               plugin.Type == PluginType.DashboardPage ||
               plugin.Type == PluginType.DashboardPage;
    }

This is a very simple rule but there is still a lot of code involved. Therefore unit tests are VERY important with customer portal security rule because they can be quite brittle. This is my test class and be sure to test both with criteria and HQL based queries:

[TestFixture]
public class TestCustomerPortalSecurity
{
    [SetUp]
    public void Setup()
    {
        // we want to make sure the queries in these tests are under customer portal security...
        ApplicationContext.Current.Services.Add(typeof(IEntitySecurityService),
            new CustomizedCustomerPortalSecurityService());
    }

    [TearDown]
    public void TearDown()
    {
        // very important otherwise all the other tests will use the customer portal security
        ApplicationContext.Current.Services.Remove<IEntitySecurityService>();
    }

    [Test]
    public void HqlShouldNotAllowRetrievalOfGroupsWithoutDataCodeCustomerPortal()
    {
        using (var sess = new SessionScopeWrapper())
        {
            var result = sess.CreateQuery("from Plugin where Type=8 and DataCode <> 'CustomerPortal'").List();
            Assert.AreEqual(0, result.Count);
        }
    }

    [Test]
    public void CriterionShouldNotAllowRetrievalOfGroupsWithoutDataCode()
    {
        using (var sess = new SessionScopeWrapper())
        {
            var res = sess.QueryOver<Plugin>()
                .Where(p => p.Type == PluginType.ACOGroup && p.DataCode != "CustomerPortal")
                .List();
            Assert.AreEqual(0, res.Count);
        }
    }

    [Test]
    public void CriterionShouldAllowRetrievalOfDashboardWidgets()
    {
        using (var sess = new SessionScopeWrapper())
        {
            var res = sess.QueryOver<Plugin>()
                .Where(p => p.Type == PluginType.DashboardWidget && p.DataCode != "CustomerPortal")
                .List();
            Assert.Greater(res.Count, 0);
        }
    }

    [Test]
    public void HqlShouldAllowRetrievalOfDashboardWidgets()
    {
        using (var sess = new SessionScopeWrapper())
        {
            var result = sess.CreateQuery("from Plugin where Type=35 and DataCode <> 'CustomerPortal'").List();
            Assert.Greater(result.Count, 0);
        }
    }        
}

For plugins we also need to let the users retrieve the PluginBlob data because that is returned lazily using a separate query, so we can just add an exception for it in IsTypeExempt:

protected override bool IsTypeExempt(Type type)
{
    if (type == typeof (PluginBlob))
    {
        // let them read PluginBlob, if they are able to read the plugin that should be alright
        return true;
    }
    return base.IsTypeExempt(type);
}

And corresponding test:

[Test]
public void ShouldBeAbleToReadBlobWhenAbleToReadPlugin()
{
    using (var sess = new SessionScopeWrapper())
    {
        var result = sess.CreateQuery("from Plugin where Type=35 and DataCode <> 'CustomerPortal'").List<Plugin>();
        foreach (var plugin in result)
        {
            Assert.IsNotNull(plugin.Blob.Data);
        }
    }
}

One more trick you can use: this little guy will let you write a query that bypasses the security rules:

/// <summary>
/// Allow bypassing built-in security for queries run within the scope
/// </summary>
/// <returns></returns>
public static IDisposable BypassSecurityScope()
{
    var securityService =
        (CustomizedCustomerPortalSecurityService)ApplicationContext.Current.Services.Get<IEntitySecurityService>();
    return securityService.BypassSecurity();
}

You can use it like this:

using (CustomizedCustomerPortalSecurityService.BypassSecurityScope())
{
    var plugins = PluginManager.GetPluginList(PluginType.DashboardPage, true, false);
}

It’s not super useful, generally, because there are often a lot of places where you can’t control the query that’s being run. But sometimes that’s all that’s needed.

Last thing is a bit of bad news. This does not work with groups. They are still using raw SQL for their queries. If you need to customize group security right now the only option is to decompile GroupBuilder.dll, make the changes in theGroupInfo.InjectCustomerPortalSecurity method, and rebuild. I have been in touch with the InforCRM dev team and they are working on improving the extensibility there.

Happy coding.