Un SaaS con un inquilino por cliente

30 minAvanzado

Cada cliente vive en su propio inquilino del mismo clúster, con sus usuarios y sus bases, sin ver a los demás.

Un SaaS con muchos clientes pequeños no necesita un clúster por cliente, porque en TeselaDB cada cliente puede ser un inquilino del mismo clúster. Cada usuario pertenece a un inquilino. La autenticación fija el inquilino de la conexión a partir de su usuario y la autorización impide entrar en las bases de otro.

Lo que vas a construir

Dos inquilinos, acme y globex, cada uno con un usuario para su aplicación y su propia base tienda. Los dos usan el mismo nombre de base y cada aplicación solo ve las bases de su inquilino.

Requisitos

  • Docker y mongosh, para arrancar TeselaDB con TLS y usuarios en tu máquina.

Prepara el entorno

Shell
docker run -d --name teseladb -p 27017:27017 viuzi/teseladb:latest \
  start --dbpath /data --bind_ip 0.0.0.0
until docker logs teseladb 2>&1 | grep -q '^ready'; do sleep 1; done
ROOT_PASSWORD=$(docker exec teseladb cat /data/root-credentials.txt | sed -n 's/^password=//p')
docker cp teseladb:/data/pki/ca.pem ./ca.pem
export TESELADB_URI="mongodb://root:$ROOT_PASSWORD@127.0.0.1:27017/?tls=true&tlsCAFile=./ca.pem&authSource=admin"
mongosh "$TESELADB_URI" --quiet --eval 'db.runCommand({ ping: 1 }).ok'
mongosh "$TESELADB_URI"

El contenedor arranca un nodo en modo seguro y su primer arranque crea la autoridad de certificación y el usuario root con una contraseña al azar, que es un usuario del inquilino 0. Las órdenes siguientes esperan a la línea ready, sacan esa contraseña y la CA y dejan en TESELADB_URI una cadena con TLS como root. Cuando mongosh responde 1, la última línea abre la sesión de root desde la que crearás los usuarios de cada inquilino.

1. Crea los inquilinos

Shell
docker exec teseladb teseladb admin --dbpath /data tenant create acme
docker exec teseladb teseladb admin --dbpath /data tenant create globex

Dentro del contenedor, teseladb admin toma sus credenciales de la PKI de /data y se conecta al puerto de administración del nodo, el 27200, sin que tengas que publicarlo en tu máquina. En un clúster, la misma orden lleva --node con la dirección del puerto de administración de cualquiera de sus nodos. En los dos casos responde con el número de cada inquilino nuevo.

2. Crea un usuario en cada inquilino

Desde una conexión del inquilino 0, el argumento $tenant hace que el comando trabaje en otro inquilino. El nombre de usuario es único en todo el clúster, así que cada inquilino lleva el suyo:

JavaScript
for (const tenant of ["acme", "globex"]) {
  db.getSiblingDB("admin").runCommand({
    createUser: `${tenant}-api`,
    pwd: passwordPrompt(),
    roles: [{ role: "readWrite", db: "tienda" }],
    $tenant: tenant,
  });
}

3. Conecta cada aplicación con su usuario

Cada cliente conecta su aplicación con la cadena de su usuario. El inquilino lo fija la autenticación, y como los usuarios viven en la base admin, la cadena lleva authSource=admin. Sustituye <contraseña> por la que escribiste en el paso 2 y abre una sesión nueva en otra terminal:

Shell
export TESELADB_URI="mongodb://acme-api:<contraseña>@127.0.0.1:27017/tienda?tls=true&tlsCAFile=./ca.pem&authSource=admin"
mongosh "$TESELADB_URI"

Probarlo

Conectado como acme-api con la cadena del paso 3, escribe un pedido en la colección pedidos de tienda y después lista las bases del clúster para ver a cuáles de ellas llega la aplicación del cliente acme:

JavaScript
db.pedidos.insertOne({ cliente: "Ana", total: 30 });
db.adminCommand({ listDatabases: 1, nameOnly: true });

listDatabases muestra solo tienda, la de acme. Conectado como globex-api, la colección pedidos de su base tienda está vacía, y sobre el clúster solo tiene listDatabases, bypassDefaultMaxTimeMS y useUUID.

Limpiar

Desde la sesión de root, borra la base y el usuario de cada inquilino con el mismo argumento $tenant del paso 2. tenant drop falla mientras al inquilino le quedan bases o usuarios, así que los inquilinos se borran después. Al final borra también el contenedor y la CA con las dos últimas órdenes; el nodo guarda los datos dentro, sin volumen, y con él se va todo lo que escribiste:

JavaScript
for (const tenant of ["acme", "globex"]) {
  db.getSiblingDB("tienda").runCommand({ dropDatabase: 1, $tenant: tenant });
  db.getSiblingDB("admin").runCommand({ dropUser: `${tenant}-api`, $tenant: tenant });
}
Shell
docker exec teseladb teseladb admin --dbpath /data tenant drop acme
docker exec teseladb teseladb admin --dbpath /data tenant drop globex
docker rm -f teseladb
rm ca.pem

Siguientes pasos