Customizing Data Isolation
Warning
This advanced feature may compromise strict data isolation between tenants. Use it only with full awareness, after carefully assessing potential risks to data security and privacy.
Default Behavior
In shared database mode, data isolation is enforced using a tenant identifier column in each table containing tenant-specific data. By default, this column is named TenantId.
- Read operations: The tenant identifier is automatically added to the
WHEREclause of SQL queries to filter data for the current tenant. - Write operations: The tenant identifier is automatically included in
INSERTandUPDATEstatements to ensure data is written for the current tenant.
Customizing Read Behavior in Shared Database Mode
You can customize how data is filtered for the current tenant by modifying the condition used for data isolation. To enable this for a specific data table:
- In Neos Studio, check the
Allow data isolation customizationswitch in the table configuration. - Implement a custom data isolation provider by creating a class that implements the
ICustomizingTenantDataIsolationinterface.
Note
Custom data isolation is applied only to tables that have the Allow data isolation customization option enabled. Otherwise, the default data isolation behavior is used.
Example: Custom Data Isolation Provider
The following example demonstrates a custom data isolation provider that filters data based on a list of authorized tenants, retrieved from the additional properties of the current tenant info. Access to data for multiple tenants is allowed only for internal service invocations (e.g., calls from another cluster using the IRemoteServiceInvoker interface).
Note
This example uses a Neos application named ProductManagement.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using GroupeIsa.Neos.Shared.Infrastructure;
using GroupeIsa.Neos.Shared.Logging;
using GroupeIsa.Neos.Shared.MultiTenant;
using Microsoft.AspNetCore.Http;
namespace ProductManagement.Core.Application
{
public class CustomizingTenantDataIsolation : ICustomizingTenantDataIsolation
{
private readonly INeosTenantInfoAccessor _neosTenantInfoAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly INeosLogger<CustomizingTenantDataIsolation> _logger;
public CustomizingTenantDataIsolation(
INeosTenantInfoAccessor neosTenantInfoAccessor,
IHttpContextAccessor httpContextAccessor,
INeosLogger<CustomizingTenantDataIsolation> logger)
{
_neosTenantInfoAccessor = neosTenantInfoAccessor;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public Expression<Func<string, bool>>? NewQueryFilter
{
get
{
if (!IsLocalCall())
{
// If the call is not local, do not apply custom tenant data isolation.
_logger.LogDebug("CustomizingTenantDataIsolation: Not a local call, no tenant data isolation applied.");
return null;
}
// Retrieve the authorized tenants from the additional properties of NeosTenantInfo.
string[]? authorizedTenants = null;
if (_neosTenantInfoAccessor.NeosTenantInfo != null)
{
authorizedTenants = _neosTenantInfoAccessor.NeosTenantInfo.AdditionalProperties.GetStrings("AuthorizedTenants");
}
if (authorizedTenants == null)
{
// If no tenants are specified, the filter is not applied, the data isolation is not customized.
return null;
}
_logger.LogDebug("CustomizingTenantDataIsolation: Authorized tenants found: {AuthorizedTenants}", string.Join(", ", authorizedTenants));
// EF expression to filter data based on the authorized tenants.
return (t) => authorizedTenants.Contains(t);
}
}
private bool IsLocalCall()
{
IPAddress? remoteIp = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
return remoteIp != null && remoteIp.IsPrivate();
}
}
}
Note
The example above applies custom data isolation only if the call is local (from another pod in the same Kubernetes cluster). This prevents access to data from external networks.
Registering the Custom Data Isolation Provider
Register your custom data isolation provider in the dependency injection container using the AddCustomizingTenantDataIsolation extension method.
Startup.cs file of the business assembly:
using System.Diagnostics.CodeAnalysis;
using GroupeIsa.Neos.Shared.MultiTenant;
using Microsoft.Extensions.DependencyInjection;
namespace ProductManagement.Core.Application
{
/// <summary>
/// Represents the assembly startup.
/// </summary>
/// <remarks>
/// This class is automatically instantiated when the assembly is loaded.
/// </remarks>
[ExcludeFromCodeCoverage]
public static class Startup
{
/// <summary>
/// Configures services for dependency injection.
/// </summary>
/// <param name="services">The services collection.</param>
/// <remarks>
/// This method is automatically called when the assembly is loaded.
/// </remarks>
public static void ConfigureServices(IServiceCollection services)
{
services.AddCustomizingTenantDataIsolation<CustomizingTenantDataIsolation>();
}
}
}
Returning the tenantId property
You may want to include the tenant identifier in the GET API responses. To do so, you must explicitly include the TenantId property in your entity.
The type of the TenantId property must be string in the entity even if the actual type in the database is integer.
For this reason, you must use a converter to convert between the string type in the entity and the integer type in the database.
Add the following in your cluster configuration file (e.g., technicaldemos.yml):
PersistenceConverters:
- Name: StringToIntegerConverter
Type: GroupeIsa.Neos.Persistence.EntityFramework.Converters.StringToIntegerConverter
EntityPropertyDataType: String
DataColumnDataType: Integer
Then select the converter StringToIntegerConverter in the TenantId property of your entity in Neos Studio.
How It Works
The expression returned by the NewQueryFilter property replaces the Finbuckle Multitenant Entity Framework global filter.
Note
If a global filter is already defined for the entity, it will be preserved and combined with the custom multi-tenant filter using an AND operator.