feat(core) : RBAC Task 5 - migration Doctrine RBAC + data-migration JSON roles
- Nouvelles tables permission, role, role_permission, user_role, user_permission - Ajout user.is_admin (BOOLEAN, default false) - Seed des roles systeme admin et user via SQL brut (autonome, pas besoin de fixtures pour cette etape) - Migration des donnees : is_admin reflete ROLE_ADMIN du JSON roles, puis rattachement user_role selon admin/user - Drop user.roles en dernier (apres la migration de donnees) - down() recree la colonne roles et la rehydrate depuis is_admin Ticket #343 - 5/7 : persistance + migration donnees safe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Core\Infrastructure\Doctrine\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Migration RBAC - Task 5.
|
||||
*
|
||||
* Ce que fait cette migration :
|
||||
* - Cree les tables du modele RBAC relationnel : permission, role,
|
||||
* role_permission, user_role et user_permission.
|
||||
* - Ajoute la colonne "user".is_admin (booleen, defaut false) qui sert de
|
||||
* bypass complet pour les super-administrateurs.
|
||||
* - Migre les donnees existantes depuis la colonne JSON "user".roles vers
|
||||
* le nouveau modele relationnel : les users ayant ROLE_ADMIN passent a
|
||||
* is_admin = TRUE et sont rattaches au role systeme 'admin', les autres
|
||||
* sont rattaches au role systeme 'user'.
|
||||
* - Supprime enfin la colonne "user".roles devenue obsolete.
|
||||
*
|
||||
* Ordonnancement critique :
|
||||
* Les INSERT de data-migration DOIVENT s'executer AVANT le DROP COLUMN
|
||||
* "user".roles, sinon l'information d'appartenance a ROLE_ADMIN est perdue.
|
||||
* L'ordre respecte donc strictement : creation des tables -> 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user