[RUBY] Multi-tenant avec Rails utilisant la stratégie de sécurité au niveau des lignes de PostgreSQL

Je crée un SaaS appelé Clipkit. C'est un système multi-tenant avec une méthode multi-schémas, mais comme le nombre de locataires approche les 1 000 et que cela devient difficile, nous vérifions d'autres méthodes.

LD;TR

J'ai essayé une implémentation multi-locataire en utilisant la politique de sécurité au niveau des lignes de PostgreSQL.

J'ai pu l'implémenter sans aucun problème et cela semblait bien fonctionner. Cependant, à la fin, j'ai oublié de l'embaucher cette fois. La méthode multi-locataire de RDB présente des avantages et des inconvénients et est difficile.

Personnellement, j'ai pensé qu'il valait mieux commencer par la méthode multi-schémas au lieu de RLS.

introduction

Dans les services Web de type SaaS, il existe une méthode dans laquelle des applications indépendantes pour chaque client coexistent dans un système. C'est ce qu'on appelle le multi-tenant.

Méthode multi-tenant RDB

Tout d'abord, quand vous y pensez normalement, vous pouvez penser à une conception qui mélange les données de plusieurs locataires dans une table. Cependant, s'il y a un bogue dans le programme, cela peut causer un très gros problème de sécurité tel que les données des autres locataires sont visibles.

Il faut donc réfléchir à une méthode pour séparer les données pour chaque locataire afin qu'il n'y ait pas de franchissement.

Il existe à peu près trois façons de réaliser des multi-locataires avec RDB.

Multi-instance (silo)

Utilisez une instance de base de données indépendante (machine virtuelle, etc.) pour chaque client. Indépendance élevée, mais faible coût et avantages de maintenabilité.

Schéma multi-instance unique (pont)

Préparez un schéma pour chaque client dans une seule instance de base de données. Étant donné que chaque client a une table indépendante, la gestion de la définition de table est compliquée.

Schéma unique (pool)

Mélangez les données de tous les locataires dans une table dans un même schéma. C'est le plus économe en ressources, mais s'il y a un bogue dans le programme, il y a un grand risque que les données des autres locataires soient mélangées.

La méthode multi-schémas peut être facilement réalisée

Rails a un joyau appelé Appartement. Avec cela, la migration simultanée vers tous les locataires sera effectuée sans autorisation.

Inconvénients de la méthode multi-schémas

En séparant les schémas pour chaque client, lors de la modification de la structure de la table, il est nécessaire d'effectuer la même migration pour tous les schémas. Même si le processus de migration prend 2 à 3 secondes, il deviendra plus difficile lorsque le nombre de locataires dépassera plusieurs milliers. Il existe également un coût de gestion élevé pour garantir que la migration est complète pour tous les locataires.

Par conséquent, si le contrôle d'accès peut être assuré, je pense que la méthode du schéma unique est idéale.

Méthode de schéma unique utilisant la stratégie de sécurité au niveau de la ligne

Aperçu

PostgeeSQL 9.5 et versions ultérieures a une fonctionnalité appelée RLS (Row Level Security Policy). Il s'agit d'une fonctionnalité qui autorise l'accès uniquement aux lignes avec des conditions pré-spécifiées en fonction du rôle de l'utilisateur et des paramètres d'exécution.

Méthode de réglage

Plus précisément, définissez comme suit.

Exemple) Je souhaite rendre invisible la colonne tenant_id de la table users, sauf pour les enregistrements avec une valeur spécifique.

Réglez RLS. (Il s'agit d'un paramètre qui contrôle en fonction des paramètres d'exécution)

ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_isolation_policy ON users FOR ALL USING (tenant_id = current_setting('tenant.id')::BIGINT);

Après cela, si vous définissez les paramètres d'exécution comme suit, vous ne pourrez plus accéder à rien d'autre que l'enregistrement avec tenant_id = 999.

SET tenant.id = 999;

Implémentation dans les rails (brouillon)

Créez une table des clients (modèle Tenant) qui gère les clients. (* Comme il s'agit d'un exemple d'explication, la définition de la table est omise.)

Implémentez pour que les locataires puissent être commutés avec la méthode Tenant # switch. Il est également utile de pouvoir récupérer le locataire actuel avec Tenant.current.

class Tenant < ApplicationRecord
  def switch
    ActiveRecord::Base.connection.execute("SET tenant.id = #{id}")
  end
  def self.current
    find(ActiveRecord::Base.connection.execute('SHOW tenant.id').getvalue(0, 0))
  end
end

ʻThe before_action d'ApplicationController` amène le locataire à basculer en fonction du domaine de la demande.

class ApplicationController < ActionController::API
  before_action :switch_tenant

  def switch_tenant
    Tenant.find_by(domain: request.host).switch
  end
end

Désormais, vous ne pouvez toucher que les données de votre locataire.

Cependant, lors de l'ajout de données, vous devez entrer vous-même tenant_id. Comme cela est gênant, nous allons l'implémenter dans la classe de base (ʻApplicationRecord`) de Model afin qu'il soit entré automatiquement.

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  after_initialize :set_tenant_id

  def set_tenant_id
    if new_record?
      if has_attribute?(:tenant_id)
        self.tenant_id = Tenant.current.id
      end
    end
  end
end

Vous pouvez désormais accéder aux données de manière transparente sans connaître le locataire.

point important

RLS ne fonctionne que pour les utilisateurs généraux

La restriction RLS n'est pas valide pour les utilisateurs qui ont CREATE TABLE et SUPER USER. Ainsi, la migration est exécutée par SUPER USER et l'application est lancée par un utilisateur général. Etc.

Donnons aux utilisateurs généraux les privilèges nécessaires comme suit.

GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA public TO PUBLIC;
GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO PUBLIC;

Notez la contrainte UNIQUE

La contrainte UNIQUE doit être un index composite avec tenant_id. (Comme il n'est pas visible pour l'application, il n'est pas nécessaire de créer une condition composée pour la validation)

Précautions lors de l'insertion

SELECT ne voit que les enregistrements contraints de manière transparente, mais vous devez définir vous-même tenant_id lorsque vous effectuez INSERT. (Dans le plan d'implémentation ci-dessus, il a été automatiquement entré en utilisant la classe de base ʻApplicationRecord` de Model)

Notes de migration

La CREATE POLICY requise lors de l'ajout d'une table devra être effectuée par migration, mais dans ce cas, elle ne sera pas reflétée dans schema.rb, donc db: reset / db: setup ne peut pas être utilisé. (Db: migrate: reset est ok)

Démérite

Cette fois, je ne l'ai pas réellement fait fonctionner avec cette implémentation, mais je soulèverai les inconvénients possibles.

La table grandit

RDB devient difficile à gérer lorsque le nombre d'enregistrements devient énorme. Même si l'index est défini, il ne pénètre pas dans la mémoire et devient soudainement lourd.

Par conséquent, nous envisagerons d'utiliser la fonction de partitionnement (partitionnement de table). Il existe une méthode appelée partition de liste qui divise en fonction de la valeur de la colonne, vous l'utiliserez donc probablement.

Divisez la table pour chaque tenant_id. J'ai d'abord proposé la stratégie, mais en général, il semble qu'il ne soit pas prévu de créer une table enfant qui dépasse 100 en partitionnement, et il y a des rapports indiquant que des problèmes de performances se produisent (essayez-le en fait) Je ne). Cette approche ne semble pas très réaliste.

Il semble que la stratégie sera de le diviser manuellement à mesure que les données augmentent. C'est ennuyant.

Problème de suppression de locataires

Cela semble difficile car je dois effacer les enregistrements de toutes les tables. Dans le cas de la méthode multi-schémas, c'était facile car vous ne supprimiez que le schéma.

Difficile de migrer des données depuis d'autres environnements

Bien qu'il s'agisse d'un service de type SaaS, il est également fourni sur site. Dans un tel cas, il semble difficile lorsqu'il devient nécessaire de migrer des données de sur site vers SaaS. C'est parce que l'ID de chaque table change. Dans le cas de la méthode multi-schémas, tout ce que vous avez à faire est de vider et de restaurer.

RLS a décidé de renoncer

Après tout, la table gonflée semble être douloureuse. Je ne pouvais pas me débarrasser de mon inquiétude.

La difficulté de migrer avec la méthode multi-schémas est un problème uniquement lors du déploiement, et j'estime que c'est bien mieux que de se soucier des performances au quotidien.

Il n'y a pratiquement aucun problème avec la migration si le nombre de locataires est d'environ plusieurs centaines, donc en fonction du nombre de locataires, la méthode multi-schémas peut être utilisée au stade du démarrage. J'ai pensé.

Apartment dispose également d'une fonction qui vous permet de changer le serveur de base de données en fonction du locataire, ce qui est plus sécurisé en termes de performances.

Autres solutions

Citus https://www.citusdata.com/

Une extension PostgreSQL qui fait du bien à plusieurs locataires.

Puisqu'il s'agit d'OSS, il peut être installé sur EC2, mais il ne peut pas être utilisé sur RDS ...

Il semble qu'il existait un service appelé Citus Cloud qui fournit des services gérés sur AWS à partir de 2016.

Cependant, en 2019, Microsoft a acquis Citus. Citus Cloud est fermé. Il semble qu'il puisse être utilisé dans Azure à la place. Ah "~

AWS est un must, donc c'est difficile ...

Le développement des appartements stagne

D'accord, je vais continuer avec l'appartement! J'ai essayé de l'utiliser dans un nouveau projet (Rails 6), mais cela n'a pas fonctionné.

Je pense que c'est un joyau assez important, mais à ce stade (juillet 2020), il n'était pas encore compatible avec Rails 6.

Il y avait une version de Fork qui était activement maintenue, il semble donc normal de l'utiliser pour le moment.

Recommended Posts

Multi-tenant avec Rails utilisant la stratégie de sécurité au niveau des lignes de PostgreSQL
Japaneseize en utilisant i18n avec Rails
Remarques sur l'utilisation de FCM avec Ruby on Rails