Bug decouvert a l'execution de 'make db-reset' sur base vide : Doctrine Migrations 3.x avec plusieurs 'migrations_paths' execute les migrations dans l'ordre (namespace, version) et non (version, namespace). Le Version20260414150034 sous 'App\Module\Core\...' passait donc avant Version20260407095546 sous 'DoctrineMigrations', provoquant un "relation user does not exist". Deplacement du fichier vers 'migrations/' (namespace DoctrineMigrations). Le chemin modulaire reste configure pour les futurs modules, mais la migration RBAC d'initialisation vit a la racine pour que 'make db-reset' fonctionne en one-shot. Smoke test end-to-end valide : - db-reset + fixtures : admin (is_admin=t, role admin), alice/bob (is_admin=f, role user) - app:sync-permissions : 4 permissions Core ajoutees, idempotent au 2e run - User::getRoles() : ['ROLE_USER', 'ROLE_ADMIN'] pour admin, ['ROLE_USER'] pour alice/bob - User::getEffectivePermissions() : union triee des permissions via roles Ticket #343 - 7/7 : smoke test end-to-end OK. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
9.9 KiB
PHP
224 lines
9.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
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');
|
|
}
|
|
}
|