Executando verificação de segurança...
1

Rotas API com parametros em um multi-tenant

Olá,

Estou desenvolvendo um sistema multi-tenant + api onde tenho a seguinte rota pra listar todos usuários de um determinado grupo em um tenant:

https://<DOMAIN>/api/<SESSION_UUID>/groups/<GROUP_ID>/users

Todas minhas tabelas possuem a coluna tenant_id.
Obviamente tenho middleware pra verificar questão de autorização e participação no tenant.

Minha dúvida é:
Em um sistema relativamente grande onde será no estilo multi-tenant, vocês buscariam pelo id do grupo mesmo?
Caso seja uma "má prática" buscar pelo id, como vocês fariam?

Agradeço!

Carregando publicação patrocinada...
3

O que eu faço com o Laravel:

1 - Minha rota está protegida pelo middleware de autenticação.

2 - Verifico direto no usuário autenticado qual é o tenant dele, para que não seja possível uma modificação maliciosa.

3 - Busco todos os usuários, filtrando pelo tenant, e envio a resposta;

Fica assim


// Rota index
Route::get('usuarios', [UsuarioController::class, 'index'])->middleware(['auth']);

// Método
public function index(){
    // usuarios do tenant
    $usuarios = auth()->user()->tenant->usuarios()->get();
    
    // response
    return response()->json($usuarios);
    
}
1
3

Sim, utilizo o id do model, mas faço validações no método para garantir a segurança.

// Rota de usuários - (index, show, create, store, edit, update e destroy)
Route::resource('usuarios', UsuarioController::class)->middleware(['auth']);

// método edit
public function edit(Usuario $usuario){
    // usuário não pertence ao mesmo tenant -> retorno erro e passo uma mensagem diferente de forbidden pra não chamar atenção
    abort_if($usuario->tenant_id !== auth()->user()->tenant_id, 403, 'Página não encontrada');
    
    // carrega a view com os dados do usuário
    return view('view-edit', compact('usuario'));
}

// método update
public function update(Request $request, Usuario $usuario){
    // $request -> tenho acesso a tudo do request
    // $usuario -> tenho acesso ao model do usuario que foi passado por parâmetro
    // posso validar diversas coisas aqui sobre esse usuário para garantir a segurança
    
    // exemplo: acesso negado se o usuário acessado pertence a um tenant diferente
    abort_if($usuario->tenant_id !== auth()->user()->tenant_id, 403);
}

Normalmente no lugar do Request eu crio uma classe Form Request separada para cuidar só do request e o código ficar mais limpo e organizado, mas coloquei dessa forma para facilitar o entendimento.

A rota gerada fica assim: https://dominio.com.br/usuarios/id

Dentro do método update você pode realizar validações como o exemplo fornecido, aí vai da sua regra de negócio, o quê deseja fazer.

O exemplo acima poderia se tratar de um usuário administrador que tem acesso ao gerenciamento de usuários do sistema, mas ele tenta acessar um id na url que não pertence ao tenant dele. Dessa forma o sistema irá abortar a requisição e gerar um acesso negado.

1
0

O problema dessa abordagem é que você expõe o ID de outros tenants, então caso o middleware falhe, o usuário pode conseguir acessar algo indevido. Além disso alguem consegue extrair a quantidade de registros por exemplo só pelo numeral do ID.

1

Na verdade não, acredito que houve um equívoco da sua parte, olhe o código postado abaixo em outra resposta minha e entenderá. Inclusive dá pra fazer esse filtro de tenant por global scope de forma automática.

A validação do tenant não se dá no middleware e sim no método que está sendo acessado. O middleware só me garante que quem acessa aquela rota está logado.

Quando usuário autenticado acessar o registro de um usuário (exemplo: /usuarios/3), o método recebe essa requisição, e eu valido se o usuário 3 pertence ao mesmo tenant de quem está solicitando as informações. Se sim, continuo com minha regra de negócio. Caso contrário, aborto a requisição e vira um acesso negado.

Sendo assim, não há problemas de segurança. Um usuário do tenant 2 não consegue acessar usuários do tenants 5.

Em relação à quantidade de usuários pelo id, não é uma informação sensível para o meu caso, ainda mais que será retornado uma página de erro por não ser do mesmo tenant que o usuário autenticado. O usuário comum que pode fuçar nem sabe diferenciar se é acesso negado ou página não encontrada.

2

Sempre que trabalhei com multi-tenant eu tinha uma tabela para mapear o domínio da aplicação:

iddomain
1example.com
2example.org

o id dessa tabela era o tenant_id de todas as outras.

sendo assim a cada tabela que pesquisava no banco de dados eu filtrava por esse id.

1

Agradeço pela resposta, Pilati!
Atualmente tudo fica em um dominio único (independente do tenant), apenas os dados precisam ser retornados com base no tenant do usuário logado.

1

Eu vejo duas opções para fazer:

Pelo ID do grupo, por tenant

Ao invés de ter uma primary key simples, eu faria a mesma pelas colunas tenant_id e id, sendo o id incremental para cada tenant.

Dessa forma você consegue ter um id que faz sentido sequencialmente. (vamos supor que o usuário vá usar esse id do grupo com frequencia, seria chato ele yer que lidar com um id 97643 ao invés de 8)

Com UUID

Se o usuário não precisar decorar o id, o problema é resolvido usando um UUID como chave primária.

A vantagem é que você não precisa lidar com uma primary key complexa, além de tornar mais difícil (virtualmente impossível) vazar informações entre os tenants caso o middleware falhe por exemplo.

2
1

Acredito que não basta ser UUID para garantir a segurança, se o usuário conseguir de qualquer forma descobrir o uuid, conseguirá acessar as informações.

Acaba que é necessária uma validação a mais para garantir que seja do mesmo tentant, então tanto faz se é id ou uuid, o trabalho será o mesmo.

Já o uuid possui algumas desvantagens:

  • Não é autoincrement, você terá que criar uma forma de gerar a string.
  • Os relacionamentos se tornam mais complicados de serem feitos, justamente por ser string.
  • Fica gigante na URL caso trabalhe com rotas aninhadas

Aí vai da sua necessidade e verificar qual o caminho que mais vale a pena.