Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-tenant messaging: filter out events from unknown tenants #275

Merged
merged 6 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackagesVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsPackagesVersion)" />
<PackageReference Include="MediatR.Contracts" Version="$(MediatRContractsPackageVersion)" />
</ItemGroup>

</Project>
84 changes: 61 additions & 23 deletions src/Messaging/NBB.Messaging.MultiTenancy/TenantMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
using NBB.MultiTenancy.Identification.Services;
using NBB.MultiTenancy.Abstractions;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using MediatR;
using System.Linq;

namespace NBB.Messaging.MultiTenancy
{
Expand All @@ -21,45 +24,80 @@ namespace NBB.Messaging.MultiTenancy
/// obtained from the current identification strategy and builds the tenant context.
/// </summary>
/// <seealso cref="IPipelineMiddleware{MessagingEnvelope}" />
public class TenantMiddleware : IPipelineMiddleware<MessagingContext>
{
private readonly ITenantContextAccessor _tenantContextAccessor;
private readonly ITenantIdentificationService _tenantIdentificationService;
private readonly IOptions<TenancyHostingOptions> _tenancyOptions;
private readonly ITenantRepository _tenantRepository;

public TenantMiddleware(ITenantContextAccessor tenantContextAccessor, ITenantIdentificationService tenantIdentificationService, IOptions<TenancyHostingOptions> tenancyOptions, ITenantRepository tenantRepository)
{
_tenantContextAccessor = tenantContextAccessor;
_tenantIdentificationService = tenantIdentificationService;
_tenancyOptions = tenancyOptions;
_tenantRepository = tenantRepository;
}

public class TenantMiddleware(
ITenantContextAccessor tenantContextAccessor,
ITenantIdentificationService tenantIdentificationService,
IOptions<TenancyHostingOptions> tenancyOptions,
ITenantRepository tenantRepository,
ILogger<TenantMiddleware> logger
) : IPipelineMiddleware<MessagingContext>
{
public async Task Invoke(MessagingContext context, CancellationToken cancellationToken, Func<Task> next)
{
if (_tenantContextAccessor.TenantContext != null)
if (tenantContextAccessor.TenantContext != null)
{
throw new ApplicationException("Tenant context is already set");
}

if (_tenancyOptions.Value.TenancyType == TenancyType.MonoTenant)
if (tenancyOptions.Value.TenancyType == TenancyType.MonoTenant)
{
_tenantContextAccessor.TenantContext = new TenantContext(Tenant.Default);
tenantContextAccessor.TenantContext = new TenantContext(Tenant.Default);
await next();
return;
}

var tenantId = await _tenantIdentificationService.GetTenantIdAsync();
var tenant = await _tenantRepository.Get(tenantId, cancellationToken)
?? throw new ApplicationException($"Tenant {tenantId} not found");
Tenant tenant;

if (context.MessagingEnvelope.Payload is INotification)
{
tenant = await TryLoadTenant(context.TopicName, cancellationToken);
if (tenant == null)
{
return;
}
}
else
{
tenant = await LoadTenant(cancellationToken);
}

_tenantContextAccessor.TenantContext = new TenantContext(tenant);

Activity.Current?.SetTag(TracingTags.TenantId, tenantId);
tenantContextAccessor.TenantContext = new TenantContext(tenant);

Activity.Current?.SetTag(TracingTags.TenantId, tenant.TenantId);

await next();
}

private async Task<Tenant> LoadTenant(CancellationToken cancellationToken)
{
var tenantId = await tenantIdentificationService.GetTenantIdAsync();
var tenant = await tenantRepository.Get(tenantId, cancellationToken)
?? throw new ApplicationException($"Tenant {tenantId} not found");

return tenant;
}


private async Task<Tenant> TryLoadTenant(string topic, CancellationToken cancellationToken)
{
var tenantId = await tenantIdentificationService.TryGetTenantIdAsync();
if (!tenantId.HasValue)
{
logger.LogDebug("Tenant could not be identified. Message {Topic} will be ignored.", topic);
return null;
}

var tenant = await tenantRepository.TryGet(tenantId.Value, cancellationToken);
if (tenant == null)
{
logger.LogDebug("Tenant {Tenant} not found or not enabled. Message {Topic} will be ignored.", tenantId.Value, topic);
return null;
}

return tenant;
}

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ public CachedTenantRepositoryDecorator(ITenantRepository tenantRepository, IDist
_cache = cache;
}

public async Task<Tenant> Get(Guid id, CancellationToken token)
public async Task<Tenant> TryGet(Guid id, CancellationToken token)
{
var cacheKey = CacheTenantByIdKey(id);
var cachedTenant = await GetTenantFromCache(cacheKey, token);
if (cachedTenant != null)
{
return cachedTenant;
}
var dbTenant = await _tenantRepository.Get(id, token);

var dbTenant = await _tenantRepository.TryGet(id, token);
if (dbTenant == null)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,14 @@ public ConfigurationTenantRepository(IConfiguration configuration, IOptions<Tena
};
}

public Task<Tenant> Get(Guid id, CancellationToken token = default)
public Task<Tenant> TryGet(Guid id, CancellationToken token = default)
{
if (!tenantMap.TryGetValue(id, out var result))
if (!tenantMap.TryGetValue(id, out var result) || !result.Enabled)
{
throw new TenantNotFoundException(id);
return Task.FromResult(default(Tenant));
}


return Task.FromResult(result.Enabled ? result : throw new Exception($"Tenant {result.Code} is disabled "));
return Task.FromResult(result);
}

public Task<Tenant> GetByHost(string host, CancellationToken token = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ namespace NBB.MultiTenancy.Abstractions.Repositories
{
public interface ITenantRepository
{
Task<Tenant> Get(Guid id, CancellationToken token = default);
Task<Tenant> TryGet(Guid id, CancellationToken token = default);
Task<Tenant> GetByHost(string host, CancellationToken token = default);
Task<List<Tenant>> GetAll(CancellationToken token = default);

async Task<Tenant> Get(Guid id, CancellationToken token = default)
=> await TryGet(id, token) ?? throw new TenantNotFoundException(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,14 @@ public async Task get_tenant_should_throw_for_disabled_tenant()

var options = new OptionsWrapper<TenancyHostingOptions>(tenancyHostingOptions);

var repo = new ConfigurationTenantRepository(configuration, options);
ITenantRepository repo = new ConfigurationTenantRepository(configuration, options);

//Act
Func<Task> action = async() =>
await repo.Get(Guid.Parse(tenantId));

//Assert
await action.Should().ThrowAsync<Exception>().WithMessage("*disabled*");
await action.Should().ThrowAsync<TenantNotFoundException>();
}

[Fact]
Expand Down Expand Up @@ -232,7 +232,7 @@ public async Task get_should_bind_tenant_code_from_section_name()
var repo = new ConfigurationTenantRepository(configuration, options);

//arrange
var actual = await repo.Get(System.Guid.Parse("ef8d5362-9969-4e02-8794-0d1af56816f6"));
var actual = await repo.TryGet(Guid.Parse("ef8d5362-9969-4e02-8794-0d1af56816f6"));

// Assert
actual.Should().NotBeNull();
Expand Down
Loading