From a94c30642c4cc639bbc2db44f22b67c047f064fa Mon Sep 17 00:00:00 2001 From: Mehdi Zaidi Date: Mon, 22 Apr 2024 11:32:06 +0200 Subject: [PATCH] fixup --- README.md | 47 ------ doc.md | 139 ------------------ .../TenantUserDatabaseNotCreatedException.php | 7 - src/Factory/HistoryFactory.php | 26 ---- src/Form/MachineFormType.php | 27 ---- 5 files changed, 246 deletions(-) delete mode 100644 doc.md delete mode 100644 src/Exceptions/TenantUserDatabaseNotCreatedException.php delete mode 100644 src/Factory/HistoryFactory.php delete mode 100644 src/Form/MachineFormType.php diff --git a/README.md b/README.md index 0e026d9..e2d2728 100644 --- a/README.md +++ b/README.md @@ -1,48 +1 @@ # POC - Multi Tenant - -## Statut - -- La route `/register` permet de créer un utilisateur - - On peut le mettre "CLIENT_ADMIN" en cochant la checkbox - - A la création de l'utilisateur, lancer la commande `app:database:create {id}` permet de lui créer : - - Son user psql - - Sa db psql - - Ses views dans la db common - - Ses tables dans sa db - -- La route `/user` permet à l'utilisateur connecté de voir ses identifiants psql - -- La route `/api/login_check` permet de récupérer son token JWT -```bash -curl -k -X POST \ - -H "Content-Type: application/json" https://localhost/api/login_check \ - -d '{"username":"mehdi@les-tilleuls.coop","password":"123456"}' \ -``` - -- Un listener `src/Listener/MigrationCommandListener` permet de jouer la commande `app:database:foreigns` qui va relancer la création des views / tables pour chaque utilisateur quand une migration est réussie - -- Un subscriber `src/Subscriber/APIRequestSubscriber` devrait permettre de changer la connection doctrine vers la DB de l'utilisateur auteur de la requête - - !!! Non testé et je n'ai pas trouvé comment dire à API-P d'utiliser la connection "clients" et pas la connection "default" - - Actuellement APIP joue les requêtes depuis "default" donc même si ce subscriber fonctionne, le résultat se fera sur la db main - -## SQL -Concernant la partie SQL, après recherche il devrait être possible de faire le système suivant : -1. Supprimer les attributs "owner" de toutes les entités Symfony -2. Demander à PSQL d'automatiquement créer la colonne "owner_id" pour chaque table créé - -Non testé mais [ce code](https://gist.github.com/Checksum/5942ad6a38e75d71e0a9c0912ac83601) devrait le permettre - -3. Demander à PSQL d'automatiquement UPDATE l'entité persisté avec son bon owner_id - -Non testé mais le trigger ressemblerait à priori à quelque chose comme : - -(A créer pour chaque table dans les db clients) -```sql -CREATE TRIGGER update_machine_owner_id -AFTER INSERT ON client_machines -AS -BEGIN -SET owner_id = id_owner_en_dur -WHERE id IN (SELECT DISTINCT id FROM inserted) -END -``` diff --git a/doc.md b/doc.md deleted file mode 100644 index 97bc6e3..0000000 --- a/doc.md +++ /dev/null @@ -1,139 +0,0 @@ -# POC - Multi Tenant - -## Description - -The project's goal is to adapt the integration of doctrine in Symfony to make it work with multi tenant applications -In our case, we have a single postgresql server managing our main database and an "infinite-ish" amount of client databases - -Each client database share the same schema but must be isolated from each other for privacy and security reasons -If we have 50 clients, we want to play a doctrine migration and update each of these 50 databases easily - -## App design: - -### Entities -All entities are located in the default `src/Entity` folder. -The only requirement about entities is the need of an attribute "owner_id" storing his... owner. - -### Migrations -All migrations must be generated with the `bin/console doctrine:migrations:diff` command -and are located in the default `migrations` folder. - -Playing this command will trigger a listener checking the result of the diff and updating clients databases if needed. - -## Connection handling -Thanks to doctrine, we can manage multiple connections natively -We've defined 2 different connections with its associated entity manager : - -- Clients -This connection has a wrapper which allow to change the current user and database of the connection. -This feature is mandatory in order to execute sql statements in the clients databases. - - -- Main -This connection has the default behavior we can expect from it and is mostly (not to say only) used to play migrations. - -## Database explanation -In this example, we will have users and entities. -Entities are illustrated as a single table "client_entity" but we could have an infinity of entities tables it would be the same. - -![common-db](./docs/images/db-common.png) - -In this case, we want : - -- User "ua" is able to see and manage entities from users "ua, uc, ud" -- User "ub" is able to see and manage entities from users "ub, ue" -- User "uc" is able to see and manage entities from users "uc, ua, ud" -- User "ud" is able to see and manage entities from users "ud, ua, uc" -- User "ue" is able to see and manage entities form users "ue, ub" - -I will omit users "ud" and "ue" in my schema as it is the exact same as for "uc" - -First thing first, we will create views of these tables for each users -These views will be used to generate the tables in our client databases -By using views, we can add a restriction with a "WHERE" clause in our creation statement -to limit informations shown. - -Creating the view of entities for the database of "ua" would lead to: - -```sql -CREATE VIEW clients_entity_ua -AS SELECT * FROM client_entity -WHERE id = 'ua' OR owner_id = 'ua'); -``` - -*note: `WHERE id = 'ua'` is a trick to show himself in his `users` table* - -Executing this statement for each users and each tables would result in: - -![common-view](./docs/images/db-views.png) - -At this step, we could already use it in our project by doing : -1. Create a psql user for each users able to connect on the database common -2. Limit his DQL rights on his views -3. Use the "client" connection as described above to connect on common with his user - -In this scenario, all DQL statements could only be used on his views which is the result wanted. -But, in our case, this leaves us with two problems: - -1. `FROM` clause in our statement would need to change for each users as views are all -stored in the same database and contains the user's id in their name. -2. Making dump of some users data would be pain as it will require conditions on views - -In order to fix these problems, we will : - -1. Create a database for each users -2. Import views of users as foreign tables in this database -3. Rename foreign tables as the default tables name of common -4. Grant him rights on these foreign tables - -This way, we could leave default `FROM` clause as all tables would now have the same names (generated in the migration) -and dumping data could be done easily by dumping the user's database. - -We will end up with : - -![users-database](./docs/images/users-dbs.png) - -Now, we "simply" need to listen to the login event and change our "client" doctrine connection -to use the credentials of the logged user. - -Last problem but not least : **I can create entities for any users** - -A view works like this : -- A `WHERE` clause at the creation restrict what data are showed when doing a `SELECT *` -- If I can see the data, I can update or delete it -- If I can see data from a table, I can create a new data of this type - -If somewhere in our code we end up having a sql injection breach, user "uc" could do something like: -`INSERT INTO client_entity(owner_id) VALUES('ub');` -This statement would lead in creating a new entity owned by "ub" meanwhile my parent is "ua" - -User "ub" would now see : -![uc-inject-ub](./docs/images/uc-inject-ub.png) - -To prevent this behavior, we will create a sql `TRIGGER` when importing foreign tables. -```sql -CREATE OR REPLACE FUNCTION override_id() RETURNS trigger as $override_id$ - BEGIN - NEW.owner_id := 'ua'; - RETURN NEW; - END; -$override_id$ -LANGUAGE plpgsql; - -CREATE OR REPLACE TRIGGER override_id_trigger -BEFORE INSERT -ON "client_entity" -FOR EACH ROW -EXECUTE PROCEDURE override_id(); -``` - -Executing this on the database of our users linked to "ua" would override any `INSERT` statement to use his correct owner -id under any circumstances - -***note :*** -As you may have notice, if a user has a child ("ua" is parent of "uc"), both databases -end up being the exact same. - -In our implementation, we use the parent database when a child logged in to prevent storing useless data that are redundant - -I have voluntary added these extra data in this documentation to make it easier to understand. diff --git a/src/Exceptions/TenantUserDatabaseNotCreatedException.php b/src/Exceptions/TenantUserDatabaseNotCreatedException.php deleted file mode 100644 index 2b77aea..0000000 --- a/src/Exceptions/TenantUserDatabaseNotCreatedException.php +++ /dev/null @@ -1,7 +0,0 @@ -setOwner($user->getOwner() ?: $user); - $history->setComment($comment); - - return $history; - } - - public static function createHistoryFromParams(User $user, string $entityClass, string $type, string $subject): History - { - $date = new \DateTimeImmutable(); - $comment = sprintf('[%s] (%s %s) %s by %s', $date->format('Y-m-d H:i:s'), $entityClass, $type, $subject, $user); - - return HistoryFactory::createHistory($user, $comment); - } -} diff --git a/src/Form/MachineFormType.php b/src/Form/MachineFormType.php deleted file mode 100644 index 23c5388..0000000 --- a/src/Form/MachineFormType.php +++ /dev/null @@ -1,27 +0,0 @@ -add('name') - ->add('Create', SubmitType::class) - ; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => Machine::class, - ]); - } -}