rahpt / ci4-module-tenancy
Multi-tenancy support for CodeIgniter 4 modules
Requires
- php: ^8.1
- codeigniter4/framework: ^4.5
- rahpt/ci4-module: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-26 23:26:08 UTC
README
Isolamento de dados corporativo, estratégia desacoplada de resolução de tenants (TenantResolverInterface), proteção contra manipulação de tenant_id (anti-mass-assignment e IDOR), validação Zero-Trust de associação de membros (TenantMembershipInterface), execução global auditada e scoping transparente de banco via TenantModel.
🏛️ Fluxo de Segurança e Resolução
HTTP Request
│
▼
[TenantFilter] ───► [TenantResolverInterface] (Subdomain, Header, Session)
│ │ (Verifica trustedBaseDomains)
│ ▼
│ [TenantContext] (Ativa ID do Tenant)
│ │
│ ▼
├────────────► [TenantMembershipInterface] (Usuário pertence a este Tenant?)
│ │ (Zero-Trust Check)
▼ ▼
[Shield Auth & Policies] ──► [Controller & Service]
│
▼
[TenantModel]
├── SELECT: WHERE tenant_id = :current:
├── INSERT: tenant_id forçado do TenantContext
├── UPDATE: tentativa de alterar tenant_id bloqueada
└── DELETE: restrito ao tenant ativo
⚠️ Regras Centrais de Segurança
1. Tenant não é autorização. O fato de uma requisição possuir um
tenant_idválido não concede permissão de acesso a recursos. Autenticação (Shield) e autorização granular (Policies/Permissões) são obrigatórias em todas as etapas.2. Header nunca é autorização. Cabeçalhos como
X-Tenant-IDapenas solicitam contexto operacional. O acesso exige usuário autenticado, vínculo ativo de membro no tenant (TenantMembershipInterface) e permissões adequadas.3.
tenant_idé imutável. O identificador do tenant jamais pode ser aceito a partir de inputs fornecidos pelo usuário em formulários ou payloads JSON.
📋 Índice
- Características
- Instalação
- Configuração
- Estratégias de Resolução (TenantResolverInterface)
- TenantModel: Isolamento e Proteção de Dados
- TenantContext e Execução com Escopo
- Zero-Trust Membership (TenantMembershipInterface)
- Detecção e Filtro Nativo (TenantFilter)
- Helpers
- Histórico de Versões
- Licença
✨ Características
Isolamento de Dados & Anti-Tampering
- ✅ TenantModel com Scoping Automático - Injeta transparentemente cláusulas
WHERE tenant_id = :idem operações de SELECT, UPDATE e DELETE. - ✅ Proteção Anti-Mass Assignment no INSERT - Qualquer
tenant_idenviado pelo usuário no payload é ignorado e substituído pelo ID seguro mantido noTenantContext. - ✅ Bloqueio de Mutação no UPDATE - Tentativas de alterar a coluna
tenant_idem registros existentes são sumariamente bloqueadas e registradas como alerta crítico de segurança. - ✅ Fail-Safe contra Vazamento de Dados - Se nenhum tenant estiver no contexto ativo e a consulta exigir escopo, o
TenantModelinterrompe a operação comRuntimeException.
Resolução Desacoplada & Domínios Confiáveis
- ✅ Contrato
TenantResolverInterface- Estratégias modulares de extração de identificadores desacopladas da validação de regras de negócio. - ✅ Resolvers Nativos -
SubdomainTenantResolver,HeaderTenantResolvereSessionTenantResolver. - ✅ Domínios Confiáveis (
trustedBaseDomains) - Validação de sufixo no Host header para mitigar DNS Rebinding e Host Header Injection em resoluções por subdomínio. - ✅ Validação Estrita de Identificador - Rejeição de caracteres especiais, tentativas de SQLi ou Path Traversal em slugs de tenants.
Auditoria & Execução Scoped
- ✅ Scoped Execution (
TenantContext::run) - Troca temporária de contexto com restauração garantida do tenant original através de blocofinally. - ✅ Execução Global Segura (
TenantContext::runGlobal) - Bypass auditado para operações da matriz/plataforma exigindo justificativa obrigatória e verificação explícita de capability. - ✅ Rastreamento de Origem (
TenantContext::source) - Identifica a procedência da resolução (subdomain,header,session,cli,system).
🚀 Instalação
composer require rahpt/ci4-module-tenancy
⚙️ Configuração
Copie ou personalize o arquivo app/Config/Tenancy.php:
<?php namespace Config; use Rahpt\Ci4ModuleTenancy\Config\Tenancy as BaseTenancy; class Tenancy extends BaseTenancy { /** * Modo de detecção: 'subdomain', 'header' ou 'session' */ public string $detectionMode = 'subdomain'; /** * Índice do segmento de subdomínio (ex: 0 para empresa.meuapp.com) */ public string $detectionKey = '0'; /** * Cabeçalho HTTP para APIs e integrações (apenas contexto, nunca autorização!) */ public string $headerName = 'X-Tenant-ID'; /** * Domínios base autorizados para resolução de subdomínio (Anti Host Injection) */ public array $trustedBaseDomains = [ 'meuapp.com', 'staging.meuapp.com', 'localhost', ]; /** * Resolver customizado opcional implementando TenantResolverInterface */ public ?string $resolverClass = null; /** * Se true, bloqueia requisições sem tenant com HTTP 403 Forbidden */ public bool $requireTenant = true; /** * Zero-Trust: Valida se o usuário autenticado pertence ao tenant detectado */ public bool $validateMembership = true; /** * Classe responsável por verificar a associação do usuário com a empresa */ public ?string $membershipHandler = \App\Services\TenancyMembershipService::class; /** * Validação estrita de formato de slug (apenas alfanuméricos, hífens e underscores) */ public bool $strictTenantValidation = true; }
🔌 Estratégias de Resolução (TenantResolverInterface)
O pacote separa a resolução do identificador do tenant da sua autorização.
O Contrato
namespace Rahpt\Ci4ModuleTenancy\Contracts; use CodeIgniter\HTTP\RequestInterface; interface TenantResolverInterface { public function resolve(RequestInterface $request): ?string; }
Resolvers Embutidos
SubdomainTenantResolver: Extrai o slug a partir do subdomínio do Host, validando obrigatoriamente se pertence atrustedBaseDomains.HeaderTenantResolver: Extrai o valor do cabeçalho HTTP configurado (padrãoX-Tenant-ID).SessionTenantResolver: Extrai o tenant armazenado na sessão segura do usuário logado.
🗄️ TenantModel: Isolamento e Proteção de Dados
Estenda TenantModel nos Models de tabelas compartilhadas que contenham dados de clientes:
<?php namespace App\Modules\Faturas\Models; use Rahpt\Ci4ModuleTenancy\Models\TenantModel; class FaturaModel extends TenantModel { protected $table = 'faturas'; protected $primaryKey = 'id'; protected $allowedFields = ['cliente_nome', 'valor', 'status']; // tenant_id não precisa ser permitido em inputs externos! protected string $tenantColumn = 'tenant_id'; protected bool $requireTenant = true; }
Comportamento em Execução
$model = new FaturaModel(); // 1. SELECT automático: injeta WHERE faturas.tenant_id = 'org_atual' $faturas = $model->findAll(); // 2. INSERT protegido: mesmo que o payload contenha 'tenant_id' => 'hacker', // o modelo substitui o valor por TenantContext::id() ativo $model->insert([ 'cliente_nome' => 'Cliente ABC', 'valor' => 2500.00 ]); // 3. UPDATE protegido: tentativas de alterar 'tenant_id' são bloqueadas $model->update($id, [ 'tenant_id' => 'outra_empresa', // Ignorado e logado como incidente 'status' => 'liquidado' ]); // 4. Bypass auditado de escopo para relatórios globais da plataforma $todas = $model->withoutTenantScope('Consolidação financeira matriz', 'platform.reports.global') ->findAll();
🧭 TenantContext e Execução com Escopo
use Rahpt\Ci4ModuleTenancy\TenantContext; // 1. Obter identificador ativo ou disparar exceção se ausente $tenantId = TenantContext::require(); // 2. Executar rotina em escopo temporário de outro tenant com restauração garantida TenantContext::run('empresa_beta', function() { $model = new FaturaModel(); // Consultas aqui operam estritamente sob 'empresa_beta' return $model->findAll(); }, 'background_worker'); // 3. Execução global privilegiada (requer capability explícita) TenantContext::runGlobal(function() { // Escopo global ativo (TenantContext::isGlobal() === true) // Modelos não aplicarão filtro de tenant neste bloco }, 'Manutenção de índices da plataforma', 'platform.tenants.global'); // 4. Execução de rotinas do sistema (CLI / Cron) TenantContext::runSystem(function() { // Execução sob identidade do sistema }, 'Sincronização agendada de faturas', 'cron_sync');
🤝 Zero-Trust Membership (TenantMembershipInterface)
Para validar se o usuário do CodeIgniter Shield tem permissão para operar no tenant selecionado:
<?php namespace App\Services; use Rahpt\Ci4ModuleTenancy\Contracts\TenantMembershipInterface; class TenancyMembershipService implements TenantMembershipInterface { public function userBelongsToTenant(object|string|int $user, string $tenantId): bool { $userId = is_object($user) ? $user->id : $user; return (bool) db_connect()->table('tenant_members') ->where('user_id', $userId) ->where('tenant_id', $tenantId) ->where('status', 'active') ->countAllResults(); } public function getUserTenantRole(object|string|int $user, string $tenantId): ?string { $userId = is_object($user) ? $user->id : $user; $record = db_connect()->table('tenant_members') ->where('user_id', $userId) ->where('tenant_id', $tenantId) ->get()->getRow(); return $record?->role; } }
🛡️ Detecção e Filtro Nativo (TenantFilter)
Em app/Config/Filters.php:
public array $globals = [ 'before' => [ 'tenant', // Executa resolução e verificação Zero-Trust em todas as requisições ], ];
🛠️ Helpers
// Retorna o identificador do tenant atual ou null $tenantId = tenant(); // Verifica se há um tenant ativo no contexto if (has_tenant()) { echo "Tenant ativo: " . tenant(); }
🕒 Histórico de Versões
[1.3.0] - 2026-09-26
- Novo: Contrato
TenantResolverInterfacedesacoplando resolução da autorização. - Novo: Resolvers dedicados:
SubdomainTenantResolver,HeaderTenantResolvereSessionTenantResolver. - Segurança: Validação de domínios base autorizados (
trustedBaseDomains) para mitigação de Host Header Injection. - Segurança: Blindagem no
TenantModelcontra mutação detenant_idem UPDATE e stripping detenant_idexterno em INSERT. - Novo: Exigência de capability explícita para
TenantContext::runGlobal()com registro de auditoria. - Testes: Suíte abrangente de testes unitários para
TenantContextTest.
[1.2.0] - 2026-09-26
- Novo:
TenantModelnativo com scoping automático e fail-safe de isolamento. - Novo: Suporte a execução em escopo temporário via
TenantContext::run(). - Novo: Contrato
TenantMembershipInterfacepara verificação de associação Zero-Trust.
[1.0.1] - 2026-02-15
- Versão inicial com detecção de tenant.
📄 Licença
Distribuído sob a licença MIT. Veja LICENSE para mais detalhes.
Desenvolvido por Rahpt
Mantido pela equipe Rahpt / CodeIgniter 4 Modular Platform.