Nederlands
  • saas
  • multi-tenancy
  • postgres
  • row-level security
  • rls
  • trigger functie
  • multi-tenant architectuur
  • single-tenant architectuur

Multi-tenancy implementatie met PostgreSQL: leer via een eenvoudig praktijkvoorbeeld

Leer hoe je een multi-tenant architectuur implementeert met PostgreSQL Row-Level Security (RLS) en databaserollen door middel van een praktijkvoorbeeld voor veilige gegevensisolatie tussen tenants.

Yijun
Yijun
Developer

In enkele van onze eerdere artikelen hebben we ons verdiept in het concept van multi-tenancy en de toepassing ervan in producten en praktijksituaties in het bedrijfsleven.

In dit artikel verkennen we hoe je een multi-tenant architectuur voor je applicatie implementeert met PostgreSQL vanuit een technisch perspectief.

Wat is een single-tenant architectuur?

Single-tenant architectuur verwijst naar een softwarearchitectuur waarbij elke klant zijn eigen toegewijde instantie van de applicatie en database heeft.

In deze architectuur zijn de gegevens en bronnen van elke tenant volledig geïsoleerd van andere tenants.

Single-tenancy

Wat is een multi-tenant architectuur?

Multi-tenant architectuur is een softwarearchitectuur waarbij meerdere klanten (tenants) dezelfde applicatie-instantie en infrastructuur delen, terwijl ze gegevensisolatie behouden. In deze architectuur bedient een enkele instantie van de software meerdere tenants, waarbij de gegevens van elke tenant apart worden gehouden van anderen door middel van verschillende isolatiemechanismen.

Multi-tenancy

Single-tenant architectuur vs multi-tenant architectuur

Single-tenant architectuur en multi-tenant architectuur verschillen op het gebied van gegevensisolatie, hulpbronnengebruik, schaalbaarheid, beheer en onderhoud, en beveiliging.

In een single-tenant architectuur heeft elke klant een onafhankelijke gegevensruimte, wat leidt tot een lager gebruik van hulpbronnen maar relatief eenvoudiger voor maatwerk. Typisch is single-tenant software afgestemd op specifieke klantbehoeften, zoals voorraadbeheersystemen voor een bepaalde stoffenleverancier of een persoonlijke blogwebapp. Gemeenschappelijk kenmerk van deze systemen is dat elke klant een aparte instantie van de applicatiedienst heeft, wat maatwerk vergemakkelijkt om aan specifieke eisen te voldoen.

In een multi-tenant architectuur delen meerdere tenants dezelfde onderliggende middelen, wat resulteert in een hoger gebruik van hulpbronnen. Het is echter cruciaal om gegevensisolatie en beveiliging te waarborgen.

Multi-tenant architectuur is vaak de voorkeurssoftwarearchitectuur wanneer serviceproviders gestandaardiseerde diensten aanbieden aan verschillende klanten. Deze diensten hebben doorgaans een laag niveau van maatwerk en alle klanten delen dezelfde applicatie-instantie. Wanneer een applicatie een update vereist, is het bijwerken van één applicatie-instantie gelijk aan het bijwerken van de applicatie voor alle klanten. CRM (Customer Relationship Management) is bijvoorbeeld een gestandaardiseerde vereiste. Deze systemen maken doorgaans gebruik van een multi-tenant architectuur om dezelfde dienst aan alle tenants te bieden.

Tenant gegevensisolatiestrategieën in multi-tenant architectuur

In een multi-tenant architectuur delen alle tenants dezelfde onderliggende middelen, waardoor de isolatie van middelen tussen tenants cruciaal is. Deze isolatie hoeft niet noodzakelijk fysiek te zijn; het vereist eenvoudigweg dat middelen tussen tenants niet zichtbaar zijn voor elkaar.

In het ontwerp van de architectuur kunnen verschillende gradaties van middelenisolatie tussen tenants worden bereikt:

Geïsoleerd naar gedeeld

In het algemeen geldt: hoe meer middelen er gedeeld worden tussen tenants, hoe lager de kosten voor systeemiteratie en onderhoud. Omgekeerd, hoe minder gedeelde middelen, hoe hoger de kosten.

Starten met multi-tenant implementatie met een praktijkvoorbeeld

In dit artikel gebruiken we een CRM-systeem als voorbeeld om een eenvoudige maar praktische multi-tenant architectuur te introduceren.

We beseffen dat alle tenants dezelfde standaarddiensten gebruiken, dus hebben we besloten om alle tenants dezelfde basisbronnen te laten delen, en we zullen gegevensisolatie tussen verschillende tenants op databasetniveau implementeren met behulp van PostgreSQL's Row-Level Security.

Daarnaast zullen we voor elke tenant een aparte gegevensverbinding creëren om het beheer van tenantrechten te vergemakkelijken.

Vervolgens zullen we introduceren hoe we deze multi-tenant architectuur kunnen implementeren.

Hoe implementeer je multi-tenant architectuur met PostgreSQL

Voeg tenant-identificatie toe voor alle bronnen

In een CRM-systeem hebben we veel middelen die in verschillende tabellen worden opgeslagen. Klantinformatie wordt bijvoorbeeld opgeslagen in de customers-tabel.

Voordat we multi-tenancy implementeren, worden deze middelen niet geassocieerd met een tenant:

Om de tenants te onderscheiden die verschillende middelen bezitten, introduceren we een tenants-tabel om tenantinformatie op te slaan (waar db_user en db_user_password worden gebruikt om de database-verbindinginformatie voor elke tenant op te slaan, hieronder nader toegelicht). We voegen ook een tenant_id-veld toe aan elke bron om te identificeren aan welke tenant het toebehoort:

Nu is elke bron gekoppeld aan een tenant_id, waardoor we theoretisch een where-clausule kunnen toevoegen aan alle queries om de toegang tot resources voor elke tenant te beperken:

Op het eerste gezicht lijkt dit eenvoudig en haalbaar. Het heeft echter de volgende problemen:

  • Bijna elke query zal deze where-clausule bevatten, wat de code rommelig maakt en moeilijker te onderhouden, vooral bij het schrijven van complexe join-statements.
  • Nieuwkomers in de codebase kunnen gemakkelijk vergeten deze where-clausule toe te voegen.
  • Gegevens tussen verschillende tenants zijn niet echt geïsoleerd, omdat elke tenant nog steeds rechten heeft om gegevens te benaderen die tot andere tenants behoren.

Daarom zullen we deze aanpak niet gebruiken. In plaats daarvan zullen we PostgreSQL' Row Level Security gebruiken om deze zorgen aan te pakken. Voordat we verder gaan, zullen we echter een dedicated database-account creëren voor elke tenant om toegang te krijgen tot deze gedeelde database.

Stel DB-rollen in voor tenants

Het is een goede gewoonte om een databasero om toe te wijzen aan elke gebruiker die toegang kan krijgen tot de database. Dit biedt betere controle over de toegang van elke gebruiker tot de database, vergemakkelijkt de isolatie van operaties tussen verschillende gebruikers en verbetert de stabiliteit en beveiliging van het systeem.

Omdat alle tenants dezelfde databasebedieningsrechten hebben, kunnen we een basisrol creëren om deze rechten te beheren:

Vervolgens, om elke tenantrol te onderscheiden, wordt bij het aanmaken van een tenant een rol afgeleid van de basisrol aan elke tenant toegewezen:

Vervolgens zal de databaseverbindinginformatie voor elke tenant worden opgeslagen in de tenants-tabel:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Dit mechanisme biedt elke tenant zijn eigen databasero, en deze rollen delen de rechten die aan de crm_tenant-rol zijn toegekend.

Vervolgens kunnen we het toestemmingsbereik voor tenants definiëren met behulp van de crm_tenant-rol:

  • Tenants moeten CRUD-toegang hebben tot alle CRM-systeembrontabellen.
  • Tabellen die niet gerelateerd zijn aan CRM-systeembronnen moeten onzichtbaar zijn voor tenants (ervan uitgaande dat alleen de systems-tabel).
  • Tenants mogen de tenants-tabel niet wijzigen, en alleen de velden id en db_user mogen zichtbaar zijn voor hen om hun eigen tenant-id op te vragen bij het uitvoeren van databasebewerkingen.

Zodra de rollen voor tenants zijn ingesteld, wanneer een tenant toegang vraagt tot de service, kunnen we met de database communiceren met behulp van de databasero die die tenant vertegenwoordigt:

Beveilig tenantgegevens met PostgreSQL Row-Level Security

Tot nu toe hebben we overeenkomstige databasero voor tenants ingesteld, maar dit beperkt de gegevens toegang tussen tenants niet. Vervolgens zullen we de Row-Level Security-functie van PostgreSQL gebruiken om de toegang van elke tenant tot hun eigen gegevens te beperken.

In PostgreSQL kunnen tabellen row security policies hebben die bepalen welke rijen toegankelijk zijn voor queries of aangepast kunnen worden door datamanipulatiecommando's. Deze functie staat ook bekend als RLS (Row-Level Security).

Standaard hebben tabellen geen rij-beveiligingsbeleid. Om RLS te gebruiken, moet je het inschakelen voor de tabel en beveiligingsbeleid creëren die elke keer dat de tabel wordt benaderd, worden uitgevoerd.

Aan de hand van de customers-tabel in het CRM-systeem als voorbeeld, zullen we RLS inschakelen en een beveiligingsbeleid creëren om elke tenant alleen toegang te geven tot hun eigen klantgegevens:

In de verklaring voor het maken van het beveiligingsbeleid:

  • for all (optioneel) geeft aan dat dit toegangsbeleid zal worden gebruikt voor select, insert, update en delete bewerkingen op de tabel. Je kunt een toegangsbeleid specificeren voor specifieke operaties met for gevolgd door het commando-woord.
  • to crm_tenant geeft aan dat dit beleid van toepassing is op gebruikers met de databasero crm_tenant, dat wil zeggen alle tenants.
  • as restrictive specificeert de handhavingsmodus van het beleid, wat aangeeft dat toegang strikt beperkt moet worden. Standaard kan een tabel meerdere beleidsregels hebben, meerdere permissive beleidsregels worden gecombineerd met een OF-relatie. In dit scenario verklaren we dit beleid als restrictive omdat we willen dat deze beleidscontrole verplicht is voor gebruikers die behoren tot CRM-systeemtenants.
  • using expressie definieert de voorwaarden voor daadwerkelijke toegang, waarbij de huidige query-databasegebruikers worden beperkt om alleen gegevens te bekijken die behoren tot hun respectieve tenant. Deze beperking is van toepassing op rijen die door een commando zijn geselecteerd (select, update of delete).
  • with check expressie definieert de beperking die nodig is bij het wijzigen van gegevensrijen (insert of update), die ervoor zorgt dat tenants alleen records voor zichzelf kunnen toevoegen of updaten.

Het gebruik van RLS om de toegang van tenants tot onze resourcetabellen te beperken biedt verschillende voordelen:

  • Dit beleid voegt effectief where tenant_id = (select id from tenants where db_user = current_user) toe aan alle query-operaties (select, update of delete). Bijvoorbeeld, wanneer je select * from customers uitvoert, is dat gelijk aan select * from customers where tenant_id = (select id from tenants where db_user = current_user). Dit elimineert de noodzaak om expliciet where-voorwaarden toe te voegen in de applicatiecode, waardoor deze wordt vereenvoudigd en de kans op fouten wordt verminderd.
  • Het controleert centraal de toegangsrechten van gegevens tussen verschillende tenants op databaseniveau, waardoor het risico van kwetsbaarheden of inconsistenties in de applicatie wordt verminderd en de veiligheid van het systeem wordt verhoogd.

Er zijn echter enkele punten om rekening mee te houden:

  • RLS-beleid worden voor elke rij gegevens uitgevoerd. Als de queryvoorwaarden binnen het RLS-beleid te complex zijn, kan dit aanzienlijke invloed hebben op de systeemprestaties. Gelukkig is onze tenant gegevenstoets query simpel genoeg en zal de prestaties niet beïnvloeden. Als je van plan bent om later andere functionaliteiten te implementeren met RLS, kun je de Row-Level Security prestatie aanbevelingen van Supabase volgen om RLS-prestaties te optimaliseren.
  • RLS-beleid vullen tenant_id niet automatisch in tijdens insert-operaties. Ze beperken alleen tenants tot het invoeren van hun eigen gegevens. Dit betekent dat we bij het invoeren van gegevens nog steeds de tenant ID moeten opgeven, wat niet consistent is met het queryproces en voor verwarring kan zorgen tijdens de ontwikkeling, waardoor de kans op fouten toeneemt (dit zal in de volgende stappen worden aangepakt).

Naast de customers-tabel moeten we dezelfde operaties toepassen op alle CRM-systeembrontabellen (dit proces kan een beetje vervelend zijn, maar we kunnen een programma schrijven om het te configureren tijdens tabelinitialisatie), zodat gegevens van verschillende tenants geïsoleerd zijn.

Maak triggerfunctie voor gegevensinvoer

Zoals eerder vermeld, stelt RLS (Row-Level Security) ons in staat om queries uit te voeren zonder ons zorgen te maken over het bestaan van tenant_id, omdat de database dit automatisch afhandelt. Voor insert-operaties moeten we echter nog steeds handmatig de bijbehorende tenant_id specificeren.

Om dezelfde gebruiksgemak van RLS voor gegevensinvoer te bereiken, moet de database tenant_id automatisch kunnen verwerken tijdens gegevensinvoer.

Dit heeft een duidelijk voordeel: op het niveau van applicatieontwikkeling hoeven we niet meer na te denken over tot welke tenant de gegevens behoren, waardoor de kans op fouten wordt verkleind en onze mentale last bij het ontwikkelen van multi-tenant applicaties wordt verminderd.

Gelukkig biedt PostgreSQL krachtige triggerfunctionaliteit.

Triggers zijn speciale functies die aan tabellen zijn gekoppeld en automatisch specifieke acties uitvoeren (zoals insert, update of delete) wanneer deze worden uitgevoerd op de tabel. Deze acties kunnen worden geactiveerd op rij niveau (voor elke rij) of instructieniveau (voor de gehele instructie). Met triggers kunnen we aangepaste logica uitvoeren voor of na specifieke databasebewerkingen, wat ons gemakkelijk in staat stelt om ons doel te bereiken.

Laten we eerst een triggerfunctie set_tenant_id maken die wordt uitgevoerd vóór elke gegevensinvoer:

Vervolgens koppelen we deze triggerfunctie aan de customers-tabel voor invoerbewerkingen (vergelijkbaar met het inschakelen van RLS voor een tabel, deze triggerfunctie moet worden gekoppeld aan alle relevante tabellen):

Deze trigger zorgt ervoor dat ingevoerde gegevens het juiste tenant_id bevatten. Als de nieuwe gegevens al een tenant_id bevatten, doet de triggerfunctie niets. Anders vult het automatisch het tenant_id-veld op basis van de informatie van de huidige gebruiker in de tenants-tabel.

Op deze manier bereiken we automatische verwerking van tenant_id op databaseniveau tijdens gegevensinvoer door tenants.

Samenvatting

In dit artikel hebben we ons verdiept in de praktische toepassing van multi-tenant architectuur, met als voorbeeld een CRM-systeem om een praktische oplossing aan te tonen met behulp van PostgreSQL-database.

We bespreken database rolbeheer, toegangscontrole en de Row-Level Security-functie van PostgreSQL om gegevensisolatie tussen tenants te waarborgen. Bovendien maken we gebruik van triggerfuncties om de cognitieve belasting voor ontwikkelaars in het beheer van verschillende tenants te verminderen.

Dat is alles voor dit artikel. Als je je multi-tenant applicatie verder wilt verbeteren met gebruikers toegangsbeheer, kun je Een eenvoudige gids om te beginnen met Logto organisaties - voor het bouwen van een multi-tenant app raadplegen voor meer inzichten.