Personalize a segurança do portal do cliente InforCRM

A segurança do portal do cliente pode ser personalizada implementando e registrando um IEntitySecurityService. Praticamente, isso significa estender CustomerPortalSecurityService e substituir 3 métodos:

  • BuildRestriction – adicionar uma restrição a uma consulta baseada em critérios
  • BuildFilterNode – adicionar uma restrição para uma consulta baseada em HQL
  • CanAccessEntity – verifica se o usuário tem acesso a uma entidade (isso é usado ao visitar uma página específica, portanto, só é importante se você conceder acesso à entidade por meio de uma página)
  • IsTypeExempttambém pode ser substituído para excluir um tipo da segurança por completo. O tipo aprovado é o tipo real, não o tipo de interface, então lembre-se de fazer seu teste como:

    if (typeof(ISomeCustomType).IsAssignableFrom(type))
    {
    return true;
    }
  • IsTypeBlocked– muitas vezes você terá que substituir esse porque os tipos personalizados são bloqueados por padrão. No entanto, os tipos que são “isentos” serão automaticamente “desbloqueados”.

Os métodos não podem retornar null, então apenas chame o método da classe base como um fallback.

Este é um exemplo onde desejo possibilitar o acesso a Dashboard Pages e Widgets no portal, portanto preciso customizar a segurança do plugin. Preciso ter certeza de incluir a restrição original e, em seguida, ampliá-la um pouco para permitir o acesso a esses tipos de plug-in.

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;
}

Esta é uma regra muito simples, mas ainda há muito código envolvido. Portanto, os testes de unidade são MUITO importantes com a regra de segurança do portal do cliente porque podem ser bastante frágeis. Esta é minha classe de teste e certifique-se de testar com critérios e consultas baseadas em HQL:

[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 RfiCustomerPortalSecurityService());
}

[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);
}
}
}

Para plug-ins, também precisamos permitir que os usuários recuperem os dados do PluginBlob porque eles são retornados lentamente usando uma consulta separada, então podemos apenas adicionar uma exceção para ele em 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);
}

E teste correspondente:

[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);
}
}
}

Mais um truque que você pode usar: esse rapaz permitirá que você escreva uma consulta que contorne as regras de segurança:

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

Você pode usá-lo assim:

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

Não é muito útil, geralmente, porque geralmente há muitos lugares onde você não pode controlar a consulta que está sendo executada. Mas às vezes isso é tudo de que precisamos.

Última coisa. Isso não funciona com grupos. Eles ainda estão usando SQL bruto para suas consultas. Se você precisar personalizar a segurança do grupo agora, a única opção é descompilar o GroupBuilder.dll, fazer as alterações no GroupInfo.InjectCustomerPortalSecuritymétodo e reconstruir. Entrei em contato com a equipe de desenvolvimento do InforCRM e eles estão trabalhando para melhorar a extensibilidade lá.

Boa codificação.