diff --git a/src/Module/Core/Infrastructure/Doctrine/Migrations/Version20260414150034.php b/src/Module/Core/Infrastructure/Doctrine/Migrations/Version20260414150034.php new file mode 100644 index 0000000..9129390 --- /dev/null +++ b/src/Module/Core/Infrastructure/Doctrine/Migrations/Version20260414150034.php @@ -0,0 +1,223 @@ + ADD is_admin + * -> seed des roles systeme -> migration des donnees -> DROP roles. + * + * Dependance avec Task 6 (fixtures) : + * Les fixtures applicatives reposent sur l'existence des roles systeme + * 'admin' et 'user' seedes ici par SQL brut. Cette migration est donc + * auto-suffisante et n'a pas besoin que les fixtures soient executees. + */ +final class Version20260414150034 extends AbstractMigration +{ + public function getDescription(): string + { + return 'RBAC : tables permission/role + jointures + is_admin + migration des donnees depuis user.roles'; + } + + public function up(Schema $schema): void + { + // 1) Creation des tables RBAC (permission, role, jointures). + $this->addSql(<<<'SQL' + CREATE TABLE permission ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + code VARCHAR(255) NOT NULL, + label VARCHAR(255) NOT NULL, + module VARCHAR(100) NOT NULL, + orphan BOOLEAN DEFAULT false NOT NULL, + PRIMARY KEY (id) + ) + SQL); + $this->addSql('CREATE INDEX idx_permission_module ON permission (module)'); + $this->addSql('CREATE INDEX idx_permission_orphan ON permission (orphan)'); + $this->addSql('CREATE UNIQUE INDEX uniq_permission_code ON permission (code)'); + $this->addSql(<<<'SQL' + CREATE TABLE "role" ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + code VARCHAR(100) NOT NULL, + label VARCHAR(255) NOT NULL, + description TEXT DEFAULT NULL, + is_system BOOLEAN DEFAULT false NOT NULL, + PRIMARY KEY (id) + ) + SQL); + $this->addSql('CREATE INDEX idx_role_is_system ON "role" (is_system)'); + $this->addSql('CREATE UNIQUE INDEX uniq_role_code ON "role" (code)'); + $this->addSql(<<<'SQL' + CREATE TABLE role_permission ( + role_id INT NOT NULL, + permission_id INT NOT NULL, + PRIMARY KEY (role_id, permission_id) + ) + SQL); + $this->addSql('CREATE INDEX IDX_6F7DF886D60322AC ON role_permission (role_id)'); + $this->addSql('CREATE INDEX IDX_6F7DF886FED90CCA ON role_permission (permission_id)'); + $this->addSql(<<<'SQL' + CREATE TABLE user_role ( + user_id INT NOT NULL, + role_id INT NOT NULL, + PRIMARY KEY (user_id, role_id) + ) + SQL); + $this->addSql('CREATE INDEX IDX_2DE8C6A3A76ED395 ON user_role (user_id)'); + $this->addSql('CREATE INDEX IDX_2DE8C6A3D60322AC ON user_role (role_id)'); + $this->addSql(<<<'SQL' + CREATE TABLE user_permission ( + user_id INT NOT NULL, + permission_id INT NOT NULL, + PRIMARY KEY (user_id, permission_id) + ) + SQL); + $this->addSql('CREATE INDEX IDX_472E5446A76ED395 ON user_permission (user_id)'); + $this->addSql('CREATE INDEX IDX_472E5446FED90CCA ON user_permission (permission_id)'); + $this->addSql(<<<'SQL' + ALTER TABLE + role_permission + ADD + CONSTRAINT FK_6F7DF886D60322AC FOREIGN KEY (role_id) REFERENCES "role" (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + role_permission + ADD + CONSTRAINT FK_6F7DF886FED90CCA FOREIGN KEY (permission_id) REFERENCES permission (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + user_role + ADD + CONSTRAINT FK_2DE8C6A3A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + user_role + ADD + CONSTRAINT FK_2DE8C6A3D60322AC FOREIGN KEY (role_id) REFERENCES "role" (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + user_permission + ADD + CONSTRAINT FK_472E5446A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE + user_permission + ADD + CONSTRAINT FK_472E5446FED90CCA FOREIGN KEY (permission_id) REFERENCES permission (id) ON DELETE CASCADE + SQL); + + // 2) Ajout de la colonne is_admin sur "user" (avant la data-migration + // qui en a besoin pour marquer les super-admins). + $this->addSql('ALTER TABLE "user" ADD is_admin BOOLEAN DEFAULT false NOT NULL'); + + // 3) Seed des roles systeme avant toute migration de donnees utilisateurs. + // Les codes sont centralises dans App\Module\Core\Domain\Security\SystemRoles + // mais dupliques ici volontairement pour que la migration reste auto-suffisante. + $this->addSql("INSERT INTO \"role\" (code, label, description, is_system) VALUES ('admin', 'Administrateur', 'Role administrateur - bypass complet via is_admin', TRUE)"); + $this->addSql("INSERT INTO \"role\" (code, label, description, is_system) VALUES ('user', 'Utilisateur', 'Role de base sans permission specifique', TRUE)"); + + // 4) Bascule is_admin a TRUE pour tout user dont le JSON roles contient ROLE_ADMIN. + $this->addSql(<<<'SQL' + UPDATE "user" u + SET is_admin = TRUE + WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(COALESCE(u.roles::jsonb, '[]'::jsonb)) AS role_code + WHERE role_code = 'ROLE_ADMIN' + ) + SQL); + + // 5) Rattachement des admins au role systeme 'admin'. + $this->addSql(<<<'SQL' + INSERT INTO user_role (user_id, role_id) + SELECT u.id, r.id + FROM "user" u + CROSS JOIN "role" r + WHERE r.code = 'admin' + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(COALESCE(u.roles::jsonb, '[]'::jsonb)) AS role_code + WHERE role_code = 'ROLE_ADMIN' + ) + ON CONFLICT DO NOTHING + SQL); + + // 6) Rattachement des autres users (y compris roles vide ou NULL) au role 'user'. + $this->addSql(<<<'SQL' + INSERT INTO user_role (user_id, role_id) + SELECT u.id, r.id + FROM "user" u + CROSS JOIN "role" r + WHERE r.code = 'user' + AND NOT EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(COALESCE(u.roles::jsonb, '[]'::jsonb)) AS role_code + WHERE role_code = 'ROLE_ADMIN' + ) + ON CONFLICT DO NOTHING + SQL); + + // 7) Drop de la colonne "user".roles (DOIT etre la derniere instruction, + // apres la migration des donnees qui la lit). + $this->addSql('ALTER TABLE "user" DROP roles'); + } + + public function down(Schema $schema): void + { + // 1) Recreation de la colonne roles (avec un defaut pour permettre le + // backfill). Le NOT NULL est conserve comme dans le schema d'origine. + $this->addSql('ALTER TABLE "user" ADD roles JSON NOT NULL DEFAULT \'[]\''); + + // 2) Rehydratation du JSON roles depuis is_admin avant de perdre la colonne. + $this->addSql(<<<'SQL' + UPDATE "user" + SET roles = CASE + WHEN is_admin THEN '["ROLE_ADMIN"]'::json + ELSE '["ROLE_USER"]'::json + END + SQL); + + // 3) Drop des FK puis des tables de jointure (enfants d'abord). + $this->addSql('ALTER TABLE role_permission DROP CONSTRAINT FK_6F7DF886D60322AC'); + $this->addSql('ALTER TABLE role_permission DROP CONSTRAINT FK_6F7DF886FED90CCA'); + $this->addSql('ALTER TABLE user_role DROP CONSTRAINT FK_2DE8C6A3A76ED395'); + $this->addSql('ALTER TABLE user_role DROP CONSTRAINT FK_2DE8C6A3D60322AC'); + $this->addSql('ALTER TABLE user_permission DROP CONSTRAINT FK_472E5446A76ED395'); + $this->addSql('ALTER TABLE user_permission DROP CONSTRAINT FK_472E5446FED90CCA'); + $this->addSql('DROP TABLE user_permission'); + $this->addSql('DROP TABLE user_role'); + $this->addSql('DROP TABLE role_permission'); + + // 4) Drop des tables parentes permission et role. + $this->addSql('DROP TABLE permission'); + $this->addSql('DROP TABLE "role"'); + + // 5) Drop de is_admin, la colonne roles ayant ete rehydratee en amont. + $this->addSql('ALTER TABLE "user" DROP is_admin'); + } +}