Multi-Tenancy¶
Arquitetura multi-tenant do TepConfina para isolamento de dados entre organizacoes.
Visao Geral¶
O TepConfina utiliza multi-tenancy por coluna discriminadora (TenantId), garantindo que cada organizacao/fazenda acesse apenas seus proprios dados.
graph TB
subgraph "Banco de Dados Unico"
subgraph "Tabela Lotes"
A["Lote 1 - TenantId: AAA"]
B["Lote 2 - TenantId: AAA"]
C["Lote 3 - TenantId: BBB"]
D["Lote 4 - TenantId: BBB"]
end
end
E[Fazenda A - TenantId: AAA] -->|Ve apenas| A
E -->|Ve apenas| B
F[Fazenda B - TenantId: BBB] -->|Ve apenas| C
F -->|Ve apenas| D Estrategia¶
| Aspecto | Implementacao |
|---|---|
| Tipo | Coluna discriminadora (shared database) |
| Coluna | TenantId (Guid) em todas as entidades |
| Filtragem | EF Core Global Query Filter |
| Contexto | Extraido do JWT via ICurrentUserProvider |
| Isolamento | Garantido no nivel do banco de dados |
Implementacao¶
Coluna TenantId¶
Todas as entidades que herdam de BaseEntity possuem a coluna TenantId:
public abstract class BaseEntity
{
public Guid Id { get; set; }
public Guid TenantId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
}
Global Query Filter¶
O EF Core aplica automaticamente um filtro em todas as consultas:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var currentTenantId = _currentUserProvider.TenantId;
modelBuilder.Entity<Lote>()
.HasQueryFilter(e => !e.IsDeleted && e.TenantId == currentTenantId);
modelBuilder.Entity<Animal>()
.HasQueryFilter(e => !e.IsDeleted && e.TenantId == currentTenantId);
// Aplicado a todas as entidades...
}
Duplo filtro
O global query filter combina duas condicoes: IsDeleted == false (soft delete) e TenantId == currentTenantId (isolamento de tenant).
ICurrentUserProvider¶
Interface que extrai informacoes do usuario autenticado a partir do JWT:
public interface ICurrentUserProvider
{
Guid UserId { get; }
Guid TenantId { get; }
string Role { get; }
string Email { get; }
}
public class CurrentUserProvider : ICurrentUserProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public Guid TenantId =>
Guid.Parse(_httpContextAccessor.HttpContext?.User
.FindFirst("tenantId")?.Value ?? Guid.Empty.ToString());
// Demais propriedades...
}
Atribuicao Automatica¶
O TenantId e atribuido automaticamente ao salvar novos registros:
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.TenantId = _currentUserProvider.TenantId;
entry.Entity.CreatedAt = DateTime.UtcNow;
}
}
return await base.SaveChangesAsync(ct);
}
Seguranca¶
Garantias de isolamento
O global query filter e aplicado em todas as consultas, incluindo LINQ, raw SQL via FromSqlRaw (quando usando DbSet) e navegacoes de entidades.
O que esta protegido¶
| Operacao | Protecao |
|---|---|
| Consultas (SELECT) | Filtro automatico por TenantId |
| Insercoes (INSERT) | TenantId atribuido automaticamente |
| Atualizacoes (UPDATE) | Filtro impede acesso a dados de outro tenant |
| Exclusoes (DELETE) | Filtro impede exclusao de dados de outro tenant |
Cenarios de ataque prevenidos¶
- Acesso horizontal: Usuario do Tenant A nao consegue acessar dados do Tenant B, mesmo manipulando IDs nas requisicoes
- Enumeracao: Listagens retornam apenas dados do tenant do usuario autenticado
- Manipulacao de TenantId: O TenantId e extraido do JWT (servidor), nao de parametros da requisicao (cliente)
Excecoes ao Filtro¶
Algumas entidades nao possuem filtro de tenant:
| Entidade | Motivo |
|---|---|
| PrecoMercado | Dados publicos de cotacoes compartilhados |
| AuditLog | Administradores podem consultar cross-tenant |
IgnoreQueryFilters
Em casos especificos (administracao), e possivel ignorar o filtro com .IgnoreQueryFilters(). Use com extrema cautela e apenas em contextos administrativos autorizados.