From f60dcc0f34329dbe1e68853f9e6f28f1bd2189c3 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:56:07 +0200 Subject: [PATCH 01/32] Move v1.3.2 to `archives` --- asgard--1.3.1--1.3.2.sql => archives/asgard--1.3.1--1.3.2.sql | 0 asgard--1.3.2.sql => archives/asgard--1.3.2.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename asgard--1.3.1--1.3.2.sql => archives/asgard--1.3.1--1.3.2.sql (100%) rename asgard--1.3.2.sql => archives/asgard--1.3.2.sql (100%) diff --git a/asgard--1.3.1--1.3.2.sql b/archives/asgard--1.3.1--1.3.2.sql similarity index 100% rename from asgard--1.3.1--1.3.2.sql rename to archives/asgard--1.3.1--1.3.2.sql diff --git a/asgard--1.3.2.sql b/archives/asgard--1.3.2.sql similarity index 100% rename from asgard--1.3.2.sql rename to archives/asgard--1.3.2.sql From 275d4d9a8fe08242beec8be35724299c2f314e21 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:56:29 +0200 Subject: [PATCH 02/32] Update asgard.control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle version de référence. --- asgard.control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asgard.control b/asgard.control index c184475..4f0997d 100644 --- a/asgard.control +++ b/asgard.control @@ -1,5 +1,5 @@ # extension asgard -default_version = '1.3.2' +default_version = '1.4.0' comment = 'ASGARD. Gestion des droits pour PostgreSQL.' superuser = true encoding = 'UTF8' From 0253876ae6a96e9f8f5e9fbb5dad684a515bd421 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:56:47 +0200 Subject: [PATCH 03/32] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Petites améliorations de rédaction. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e89032..717bc44 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ ASGARD. Système de gestion des droits pour PostgreSQL. ## Résumé -ASGARD est une extension du SGBDRO PostgreSQL qui propose un cadre méthodologique et des outils pour faciliter la gestion des droits. +ASGARD est une extension du système de gestion de bases de données PostgreSQL. Elle propose un cadre méthodologique et des outils pour faciliter la gestion des droits sur les bases PostgreSQL. -Elle a été conçue et développée dans le cadre du groupe de travail PostGIS réunissant des services du pôle ministériel de la transition écologique, de la cohésion des territoires et de la mer, ainsi que du ministère de l'agriculture et de l'alimentation. +ASGARD a été conçue et développée dans le cadre du groupe de travail PostGIS réunissant des services du pôle ministériel de la transition écologique, de la cohésion des territoires et de la mer, ainsi que du ministère de l'agriculture et de l'alimentation. ## Compatibilité From ae25a56ddce2921a34c3d64336834fea198457a6 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:57:21 +0200 Subject: [PATCH 04/32] Create asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialisation à partir du script de la v1.3.2. --- asgard--1.4.0.sql | 6664 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 6664 insertions(+) create mode 100644 asgard--1.4.0.sql diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql new file mode 100644 index 0000000..7474107 --- /dev/null +++ b/asgard--1.4.0.sql @@ -0,0 +1,6664 @@ +\echo Use "CREATE EXTENSION asgard" to load this file. \quit +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- ASGARD - Système de gestion des droits pour PostgreSQL, version 1.4.0 +-- +-- Copyright République Française, 2020-2022. +-- Secrétariat général du Ministère de la transition écologique, du +-- Ministère de la cohésion des territoires et des relations avec les +-- collectivités territoriales et du Ministère de la Mer. +-- Service du numérique. +-- +-- contributeurs : Leslie Lemaire (SNUM/UNI/DRC) et Alain Ferraton +-- (SNUM/MSP/DS/GSG). +-- +-- mél : drc.uni.snum.sg@developpement-durable.gouv.fr +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Documentation : +-- https://snum.scenari-community.org/Asgard/Documentation/ +-- +-- GitHub : +-- https://github.com/MTES-MCT/asgard-postgresql +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Ce logiciel est un programme informatique complémentaire au système de +-- gestion de base de données PosgreSQL ("https://www.postgresql.org/"). Il +-- met à disposition un cadre méthodologique et des outils pour la gestion +-- des droits sur les serveurs PostgreSQL. +-- +-- Ce logiciel est régi par la licence CeCILL-B soumise au droit français +-- et respectant les principes de diffusion des logiciels libres. Vous +-- pouvez utiliser, modifier et/ou redistribuer ce programme sous les +-- conditions de la licence CeCILL-B telle que diffusée par le CEA, le +-- CNRS et l'INRIA sur le site "http://www.cecill.info". +-- Lien SPDX : "https://spdx.org/licenses/CECILL-B.html". +-- +-- En contrepartie de l'accessibilité au code source et des droits de copie, +-- de modification et de redistribution accordés par cette licence, il n'est +-- offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +-- seule une responsabilité restreinte pèse sur l'auteur du programme, le +-- titulaire des droits patrimoniaux et les concédants successifs. +-- +-- A cet égard l'attention de l'utilisateur est attirée sur les risques +-- associés au chargement, à l'utilisation, à la modification et/ou au +-- développement et à la reproduction du logiciel par l'utilisateur étant +-- donné sa spécificité de logiciel libre, qui peut le rendre complexe à +-- manipuler et qui le réserve donc à des développeurs et des professionnels +-- avertis possédant des connaissances informatiques approfondies. Les +-- utilisateurs sont donc invités à charger et tester l'adéquation du +-- logiciel à leurs besoins dans des conditions permettant d'assurer la +-- sécurité de leurs systèmes et ou de leurs données et, plus généralement, +-- à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +-- +-- Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +-- pris connaissance de la licence CeCILL-B, et que vous en avez accepté +-- les termes. +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Cette extension ne peut être installée que par un super-utilisateur +-- (création de déclencheurs sur évènement). +-- +-- Elle n'est pas compatible avec les versions 9.4 ou antérieures de +-- PostgreSQL. +-- +-- Schémas contenant les objets : z_asgard et z_asgard_admin. +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/* 1 - PREPARATION DES ROLES + 2 - PREPARATION DES OBJETS + 3 - CREATION DES EVENT TRIGGERS + 4 - FONCTIONS UTILITAIRES + 5 - TRIGGERS SUR GESTION_SCHEMA + 6 - GESTION DES PERMISSIONS SUR LAYER_STYLES */ + +-- MOT DE PASSE DE CONTRÔLE : 'x7-A;#rzo' + +--------------------------------------- +------ 1 - PREPARATION DES ROLES ------ +--------------------------------------- +/* 1.1 - CREATION DES NOUVEAUX ROLES + 1.2 - AJUSTEMENTS DIVERS SUR LES PRIVILEGES */ + + +------ 1.1 - CREATION DES NOUVEAUX ROLES ------ + +DO +$$ +DECLARE + b_createrole boolean ; + b_canlogin boolean ; +BEGIN + + -- Role: g_admin + + SELECT rolcreaterole, rolcanlogin + INTO b_createrole, b_canlogin + FROM pg_catalog.pg_roles + WHERE rolname = 'g_admin' ; + + IF NOT FOUND + THEN + + CREATE ROLE g_admin WITH + NOLOGIN + NOSUPERUSER + INHERIT + CREATEDB + CREATEROLE + NOREPLICATION + BYPASSRLS ; + + COMMENT ON ROLE g_admin IS 'Rôle d''administration du serveur.' ; + + ELSE + IF NOT b_createrole + THEN + + ALTER ROLE g_admin WITH CREATEROLE ; + + END IF ; + + IF b_canlogin + THEN + + RAISE WARNING 'Pour le bon fonctionnement d''ASGARD, le rôle g_admin ne doit en aucun cas être un rôle de connexion.' + USING HINT = 'Pour lui retirer l''attribut LOGIN, vous pouvez exécuter la requête suivante : ALTER ROLE g_admin NOLOGIN ;' ; + + END IF ; + END IF ; + + IF NOT has_database_privilege('g_admin', current_database(), 'CREATE WITH GRANT OPTION') + THEN + + EXECUTE 'GRANT CREATE ON DATABASE ' || quote_ident(current_database()) || ' TO g_admin WITH GRANT OPTION' ; + + END IF ; + + -- Role: g_admin_ext + + IF NOT 'g_admin_ext' IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + + CREATE ROLE g_admin_ext WITH + NOLOGIN + NOSUPERUSER + INHERIT + NOCREATEDB + NOCREATEROLE + NOREPLICATION + NOBYPASSRLS ; + + COMMENT ON ROLE g_admin_ext IS 'Rôle technique réservé à g_admin.' ; + + END IF ; + + IF NOT pg_has_role('g_admin', 'g_admin_ext', 'MEMBER') + THEN + + GRANT g_admin_ext TO g_admin ; + + END IF ; + + -- Role: g_consult + + IF NOT 'g_consult' IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + + CREATE ROLE g_consult WITH + NOLOGIN + NOSUPERUSER + INHERIT + NOCREATEDB + NOCREATEROLE + NOREPLICATION + NOBYPASSRLS ; + + COMMENT ON ROLE g_consult IS 'Rôle de consultation des données publiques (accès aux données en lecture seule).' ; + + END IF ; + + -- Role: "consult.defaut" + + IF NOT 'consult.defaut' IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + + CREATE ROLE "consult.defaut" WITH + LOGIN + PASSWORD 'consult.defaut' + NOSUPERUSER + INHERIT + NOCREATEDB + NOCREATEROLE + NOREPLICATION + NOBYPASSRLS ; + + COMMENT ON ROLE "consult.defaut" IS 'Rôle de connexion générique pour la consultation des données publiques. Membre de g_consult.' ; + + END IF ; + + IF NOT pg_has_role('consult.defaut', 'g_consult', 'MEMBER') + THEN + + GRANT g_consult TO "consult.defaut" ; + + END IF ; + + +------ 1.2 - AJUSTEMENTS DIVERS SUR LES PRIVILEGES ------ + + -- on retire à public la possibilité de créer des objets dans le schéma de même nom + + IF has_schema_privilege('public', 'public', 'CREATE') + THEN + + REVOKE CREATE ON SCHEMA public FROM public ; + + END IF ; + +END +$$ ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +---------------------------------------- +------ 2 - PREPARATION DES OBJETS ------ +---------------------------------------- +/* 2.1 - CREATION DES SCHEMAS + 2.2 - TABLE GESTION_SCHEMA + 2.3 - TABLE DE PARAMETRAGE + 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA + 2.5 - VUE POUR MENUBUILDER + 2.6 - VUE POUR ASGARDMENU + 2.7 - VUE POUR ASGARDMANAGER + 2.8 - VERSION LECTURE SEULE DE GESTION_SCHEMA_USR */ + + + +------ 2.1 - CREATION DES SCHEMAS ------ + + +-- Schema: z_asgard_admin + +CREATE SCHEMA z_asgard_admin + AUTHORIZATION g_admin ; + +COMMENT ON SCHEMA z_asgard_admin IS 'ASGARD. Administration - RESERVE ADL.' ; + +GRANT USAGE ON SCHEMA z_asgard_admin TO g_admin_ext ; + + +-- Schema: z_asgard + +CREATE SCHEMA z_asgard + AUTHORIZATION g_admin_ext ; + +COMMENT ON SCHEMA z_asgard IS 'ASGARD. Utilitaires pour la gestion des droits.' ; + +GRANT USAGE ON SCHEMA z_asgard TO g_consult ; + + +------ 2.2 - TABLE GESTION_SCHEMA ------ + +-- Table: z_asgard_admin.gestion_schema + +CREATE TABLE z_asgard_admin.gestion_schema +( + bloc character varying(1) COLLATE pg_catalog."default", + nomenclature boolean NOT NULL DEFAULT False, + niv1 character varying COLLATE pg_catalog."default", + niv1_abr character varying COLLATE pg_catalog."default", + niv2 character varying COLLATE pg_catalog."default", + niv2_abr character varying COLLATE pg_catalog."default", + nom_schema character varying COLLATE pg_catalog."default" NOT NULL, + oid_schema oid, + creation boolean NOT NULL DEFAULT False, + producteur character varying COLLATE pg_catalog."default" NOT NULL, + oid_producteur oid, + editeur character varying COLLATE pg_catalog."default", + oid_editeur oid, + lecteur character varying COLLATE pg_catalog."default", + oid_lecteur oid, + ctrl text[], + CONSTRAINT gestion_schema_pkey PRIMARY KEY (nom_schema), + CONSTRAINT gestion_schema_oid_schema_unique UNIQUE (oid_schema), + CONSTRAINT gestion_schema_bloc_check CHECK (bloc IS NULL OR bloc = 'd' OR nom_schema::text ~ (('^'::text || bloc::text) || '_'::text) AND bloc ~ '^[a-z]$'), + CONSTRAINT gestion_schema_oid_roles_check CHECK ((oid_lecteur IS NULL OR NOT oid_lecteur = oid_producteur) + AND (oid_editeur IS NULL OR NOT oid_editeur = oid_producteur) + AND (oid_lecteur IS NULL OR oid_editeur IS NULL OR NOT oid_lecteur = oid_editeur)), + CONSTRAINT gestion_schema_ctrl_check CHECK (ctrl IS NULL OR array_length(ctrl, 1) >= 2 AND ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'MANUEL', 'EXIT')) +) +WITH ( + OIDS = FALSE +) +TABLESPACE pg_default; + +ALTER TABLE z_asgard_admin.gestion_schema + OWNER to g_admin; + +GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE z_asgard_admin.gestion_schema TO g_admin_ext; + +COMMENT ON TABLE z_asgard_admin.gestion_schema IS 'ASGARD. Table d''attribution des fonctions de producteur, éditeur et lecteur sur les schémas.' ; + +COMMENT ON COLUMN z_asgard_admin.gestion_schema.bloc IS E'Le cas échéant, lettre identifiant le bloc normalisé auquel appartient le schéma, qui sera alors le préfixe du schéma : +c : schémas de consultation (mise à disposition de données publiques) +w : schémas de travail ou d''unité +s : géostandards +p : schémas thématiques ou dédiés à une application +r : référentiels +x : données confidentielles +e : données externes (opendata, etc.) +z : utilitaires.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.nomenclature IS 'Booléen. True si le schéma est répertorié dans la nomenclature COVADIS, False sinon.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.niv1 IS 'Nomenclature. Premier niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.niv1_abr IS 'Nomenclature. Premier niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.niv2 IS 'Nomenclature. Second niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.niv2_abr IS 'Nomenclature. Second niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.nom_schema IS 'Nom du schéma. Clé primaire.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.oid_schema IS 'Identifiant système du schéma.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.creation IS 'Booléen. True si le schéma existe dans le base de données, False sinon.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.producteur IS 'Rôle désigné comme producteur pour le schéma (modification des objets).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.oid_producteur IS 'Identifiant système du rôle producteur.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.editeur IS 'Rôle désigné comme éditeur pour le schéma (modification des données).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.oid_editeur IS 'Identifiant système du rôle éditeur.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.lecteur IS 'Rôle désigné comme lecteur pour le schéma (consultation des données).' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.oid_lecteur IS 'Identitifiant système du rôle lecteur.' ; +COMMENT ON COLUMN z_asgard_admin.gestion_schema.ctrl IS 'Champ de contrôle.' ; + +-- la table est marquée comme table de configuration de l'extension +SELECT pg_extension_config_dump('z_asgard_admin.gestion_schema'::regclass, '') ; + + +------ 2.3 - TABLE DE PARAMETRAGE ------ [supprimé version 1.1.1] + +-- Table: z_asgard_admin.asgard_parametre + + +------ 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA ------ + +-- View: z_asgard.gestion_schema_usr + +CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( + SELECT + gestion_schema.nom_schema, + gestion_schema.bloc, + gestion_schema.nomenclature, + gestion_schema.niv1, + gestion_schema.niv1_abr, + gestion_schema.niv2, + gestion_schema.niv2_abr, + gestion_schema.creation, + gestion_schema.producteur, + gestion_schema.editeur, + gestion_schema.lecteur + FROM z_asgard_admin.gestion_schema + WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR + CASE + WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL + THEN pg_has_role(quote_ident(gestion_schema.producteur::text)::name, 'USAGE'::text) + WHEN gestion_schema.creation + THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) + ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + END +) ; + +ALTER VIEW z_asgard.gestion_schema_usr + OWNER TO g_admin_ext; + +GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult ; + +COMMENT ON VIEW z_asgard.gestion_schema_usr IS 'ASGARD. Vue pour la gestion courante des schémas - création et administration des droits.' ; + +COMMENT ON COLUMN z_asgard.gestion_schema_usr.bloc IS E'Le cas échéant, lettre identifiant le bloc normalisé auquel appartient le schéma, qui sera alors le préfixe du schéma : +c : schémas de consultation (mise à disposition de données publiques) +w : schémas de travail ou d''unité +s : géostandards +p : schémas thématiques ou dédiés à une application +r : référentiels +x : données confidentielles +e : données externes (opendata, etc.) +z : utilitaires.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.nomenclature IS 'Booléen. True si le schéma est répertorié dans la nomenclature COVADIS, False sinon.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.niv1 IS 'Nomenclature. Premier niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.niv1_abr IS 'Nomenclature. Premier niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.niv2 IS 'Nomenclature. Second niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.niv2_abr IS 'Nomenclature. Second niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.nom_schema IS 'Nom du schéma.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.creation IS 'Booléen. True si le schéma existe dans le base de données, False sinon.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.producteur IS 'Rôle désigné comme producteur pour le schéma (modification des objets).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.editeur IS 'Rôle désigné comme éditeur pour le schéma (modification des données).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_usr.lecteur IS 'Rôle désigné comme lecteur pour le schéma (consultation des données).' ; + + +-- View: z_asgard.gestion_schema_etr + +CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( + SELECT + gestion_schema.bloc, + gestion_schema.nom_schema, + gestion_schema.oid_schema, + gestion_schema.creation, + gestion_schema.producteur, + gestion_schema.oid_producteur, + gestion_schema.editeur, + gestion_schema.oid_editeur, + gestion_schema.lecteur, + gestion_schema.oid_lecteur, + gestion_schema.ctrl + FROM z_asgard_admin.gestion_schema + WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR + CASE + WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL + THEN pg_has_role(quote_ident(gestion_schema.producteur::text)::name, 'USAGE'::text) + WHEN gestion_schema.creation + THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) + ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + END +) ; + +ALTER VIEW z_asgard.gestion_schema_etr + OWNER TO g_admin_ext; + +GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult ; + +COMMENT ON VIEW z_asgard.gestion_schema_etr IS 'ASGARD. Vue technique pour l''alimentation de la table z_asgard_admin.gestion_schema par les déclencheurs.' ; + +COMMENT ON COLUMN z_asgard.gestion_schema_etr.bloc IS E'Le cas échéant, lettre identifiant le bloc normalisé auquel appartient le schéma, qui sera alors le préfixe du schéma : +c : schémas de consultation (mise à disposition de données publiques) +w : schémas de travail ou d''unité +s : géostandards +p : schémas thématiques ou dédiés à une application +r : référentiels +x : données confidentielles +e : données externes (opendata, etc.) +z : utilitaires.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.nom_schema IS 'Nom du schéma.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.oid_schema IS 'Identifiant système du schéma.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.creation IS 'Booléen. True si le schéma existe dans le base de données, False sinon.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.producteur IS 'Rôle désigné comme producteur pour le schéma (modification des objets).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.oid_producteur IS 'Identifiant système du rôle producteur.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.editeur IS 'Rôle désigné comme éditeur pour le schéma (modification des données).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.oid_editeur IS 'Identifiant système du rôle éditeur.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.lecteur IS 'Rôle désigné comme lecteur pour le schéma (consultation des données).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.oid_lecteur IS 'Identitifiant système du rôle lecteur.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_etr.ctrl IS 'Champ de contrôle.' ; + + + +------ 2.5 - VUE POUR MENUBUILDER ------ [supprimé version 1.1.1] + +-- View: z_asgard.qgis_menubuilder_metadata + + +------ 2.6 - VUE POUR ASGARDMENU ------ + +-- View: z_asgard.asgardmenu_metadata + +CREATE OR REPLACE VIEW z_asgard.asgardmenu_metadata AS ( + SELECT + row_number() OVER(ORDER BY nom_schema) AS id, + gestion_schema.nom_schema, + gestion_schema.bloc, + gestion_schema.niv1, + gestion_schema.niv2, + CASE WHEN pg_has_role(gestion_schema.oid_producteur, 'USAGE') THEN 'producteur' + WHEN pg_has_role(gestion_schema.oid_editeur, 'USAGE') THEN 'editeur' + WHEN pg_has_role(gestion_schema.oid_lecteur, 'USAGE') THEN 'lecteur' + ELSE 'autre' END AS permission + FROM z_asgard_admin.gestion_schema + WHERE gestion_schema.creation +) ; + +ALTER VIEW z_asgard.asgardmenu_metadata + OWNER TO g_admin_ext ; + +GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult ; + +COMMENT ON VIEW z_asgard.asgardmenu_metadata IS 'ASGARD. Données utiles à l''extension QGIS AsgardMenu.' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.id IS 'Identifiant entier unique.' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.bloc IS E'Le cas échéant, lettre identifiant le bloc normalisé auquel appartient le schéma, qui sera alors le préfixe du schéma : +c : schémas de consultation (mise à disposition de données publiques) +w : schémas de travail ou d''unité +s : géostandards +p : schémas thématiques ou dédiés à une application +r : référentiels +x : données confidentielles +e : données externes (opendata, etc.) +z : utilitaires +d : [spécial, hors nomenclature] corbeille.' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.niv1 IS 'Nomenclature. Premier niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.niv2 IS 'Nomenclature. Second niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.nom_schema IS 'Nom du schéma.' ; +COMMENT ON COLUMN z_asgard.asgardmenu_metadata.permission IS 'Profil de droits de l''utilisateur pour le schéma de la relation : ''producteur'', ''editeur'', ''lecteur'' ou ''autre''.' ; + + +------ 2.7 - VUE POUR ASGARDMANAGER ------ + +-- View: z_asgard.asgardmanager_metadata + +CREATE OR REPLACE VIEW z_asgard.asgardmanager_metadata AS ( + SELECT + row_number() OVER(ORDER BY nom_schema) AS id, + gestion_schema.nom_schema, + gestion_schema.oid_producteur, + gestion_schema.oid_editeur, + gestion_schema.oid_lecteur + FROM z_asgard_admin.gestion_schema + WHERE gestion_schema.creation +) ; + +ALTER VIEW z_asgard.asgardmanager_metadata + OWNER TO g_admin_ext ; + +GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult ; + +COMMENT ON VIEW z_asgard.asgardmanager_metadata IS 'ASGARD. Données utiles à l''extension QGIS AsgardManager.' ; +COMMENT ON COLUMN z_asgard.asgardmanager_metadata.id IS 'Identifiant entier unique.' ; +COMMENT ON COLUMN z_asgard.asgardmanager_metadata.nom_schema IS 'Nom du schéma.' ; +COMMENT ON COLUMN z_asgard.asgardmanager_metadata.oid_producteur IS 'Identifiant système du rôle producteur.' ; +COMMENT ON COLUMN z_asgard.asgardmanager_metadata.oid_editeur IS 'Identifiant système du rôle éditeur.' ; +COMMENT ON COLUMN z_asgard.asgardmanager_metadata.oid_lecteur IS 'Identitifiant système du rôle lecteur.' ; + + +------ 2.8 - VERSION LECTURE SEULE DE GESTION_SCHEMA_USR ------ + +-- View: z_asgard.gestion_schema_read_only + +CREATE OR REPLACE VIEW z_asgard.gestion_schema_read_only AS ( + SELECT + row_number() OVER(ORDER BY nom_schema) AS id, + gestion_schema.nom_schema, + gestion_schema.bloc, + gestion_schema.nomenclature, + gestion_schema.niv1, + gestion_schema.niv1_abr, + gestion_schema.niv2, + gestion_schema.niv2_abr, + gestion_schema.creation, + gestion_schema.producteur, + gestion_schema.editeur, + gestion_schema.lecteur + FROM z_asgard_admin.gestion_schema +) ; + +ALTER VIEW z_asgard.gestion_schema_read_only + OWNER TO g_admin_ext; + +GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult ; + +COMMENT ON VIEW z_asgard.gestion_schema_read_only IS 'ASGARD. Vue de consultation des droits définis sur les schémas. Accessible à tous en lecture seule.' ; + +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.id IS 'Identifiant entier unique.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.bloc IS E'Le cas échéant, lettre identifiant le bloc normalisé auquel appartient le schéma, qui sera alors le préfixe du schéma : +c : schémas de consultation (mise à disposition de données publiques) +w : schémas de travail ou d''unité +s : géostandards +p : schémas thématiques ou dédiés à une application +r : référentiels +x : données confidentielles +e : données externes (opendata, etc.) +z : utilitaires.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.nomenclature IS 'Booléen. True si le schéma est répertorié dans la nomenclature COVADIS, False sinon.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.niv1 IS 'Nomenclature. Premier niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.niv1_abr IS 'Nomenclature. Premier niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.niv2 IS 'Nomenclature. Second niveau d''arborescence (forme littérale).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.niv2_abr IS 'Nomenclature. Second niveau d''arborescence (forme normalisée).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.nom_schema IS 'Nom du schéma.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.creation IS 'Booléen. True si le schéma existe dans le base de données, False sinon.' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.producteur IS 'Rôle désigné comme producteur pour le schéma (modification des objets).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.editeur IS 'Rôle désigné comme éditeur pour le schéma (modification des données).' ; +COMMENT ON COLUMN z_asgard.gestion_schema_read_only.lecteur IS 'Rôle désigné comme lecteur pour le schéma (consultation des données).' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +-------------------------------------------- +------ 3 - CREATION DES EVENT TRIGGERS ------ +-------------------------------------------- +/* 3.1 - EVENT TRIGGER SUR ALTER SCHEMA + 3.2 - EVENT TRIGGER SUR CREATE SCHEMA + 3.3 - EVENT TRIGGER SUR DROP SCHEMA + 3.4 - EVENT TRIGGER SUR CREATE OBJET + 3.5 - EVENT TRIGGER SUR ALTER OBJET */ + + +------ 3.1 - EVENT TRIGGER SUR ALTER SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_alter_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_schema() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par l'event trigger asgard_on_alter_schema qui + répercute dans la table z_asgard_admin.gestion_schema (via la vue + z_asgard.gestion_schema_etr) les modifications de noms + et propriétaires des schémas réalisées par des commandes + ALTER SCHEMA directes. +DECLENCHEMENT : ON DDL COMMAND END. +CONDITION : WHEN TAG IN ('ALTER SCHEMA') */ +DECLARE + obj record ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EAS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EAS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + + FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() + WHERE object_type = 'schema' + LOOP + + ------ RENAME ------ + UPDATE z_asgard.gestion_schema_etr + SET nom_schema = replace(obj.object_identity, '"', ''), + ctrl = ARRAY['RENAME', 'x7-A;#rzo'] + WHERE oid_schema = obj.objid + AND NOT quote_ident(nom_schema) = obj.object_identity ; + IF FOUND + THEN + RAISE NOTICE '... Le nom du schéma % a été mis à jour dans la table de gestion.', replace(obj.object_identity, '"', '') ; + END IF ; + + ------ OWNER TO ------ + UPDATE z_asgard.gestion_schema_etr + SET (producteur, oid_producteur, ctrl) = ( + SELECT + replace(nspowner::regrole::text, '"', ''), + nspowner, + ARRAY['OWNER', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + WHERE obj.objid = pg_namespace.oid + ) + WHERE oid_schema = obj.objid + AND NOT oid_producteur = ( + SELECT nspowner + FROM pg_catalog.pg_namespace + WHERE obj.objid = pg_namespace.oid + ) ; + IF FOUND + THEN + RAISE NOTICE '... Le producteur du schéma % a été mis à jour dans la table de gestion.', replace(obj.object_identity, '"', '') ; + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EAS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_alter_schema() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; + + +-- Event Trigger: asgard_on_alter_schema + +CREATE EVENT TRIGGER asgard_on_alter_schema ON DDL_COMMAND_END + WHEN TAG IN ('ALTER SCHEMA') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_schema() ; + +COMMENT ON EVENT TRIGGER asgard_on_alter_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; + + + +------ 3.2 - EVENT TRIGGER SUR CREATE SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_create_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_schema() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par l'event trigger asgard_on_create_schema qui + répercute dans la table z_asgard_admin.gestion_schema (via la vue + z_asgard.gestion_schema_etr) les créations de schémas + réalisées par des commandes CREATE SCHEMA directes. +DECLENCHEMENT : ON DDL COMMAND END. +CONDITION : WHEN TAG IN ('CREATE SCHEMA') */ +DECLARE + obj record ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'ECS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'INSERT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'ECS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + + FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() + WHERE object_type = 'schema' + LOOP + + ------ SCHEMA PRE-ENREGISTRE DANS GESTION_SCHEMA ------ + UPDATE z_asgard.gestion_schema_etr + SET (oid_schema, producteur, oid_producteur, creation, ctrl) = ( + SELECT + obj.objid, + replace(nspowner::regrole::text, '"', ''), + nspowner, + true, + ARRAY['CREATE', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + WHERE obj.objid = pg_namespace.oid + ) + WHERE quote_ident(nom_schema) = obj.object_identity + AND NOT creation ; -- creation vaut true si et seulement si la création a été initiée via la table + -- de gestion dans ce cas, il n'est pas nécessaire de réintervenir dessus + IF FOUND + THEN + RAISE NOTICE '... Le schéma % apparaît désormais comme "créé" dans la table de gestion.', replace(obj.object_identity, '"', '') ; + + ------ SCHEMA NON REPERTORIE DANS GESTION_SCHEMA ------ + ELSIF NOT obj.object_identity IN (SELECT quote_ident(nom_schema) FROM z_asgard.gestion_schema_etr) + THEN + INSERT INTO z_asgard.gestion_schema_etr (oid_schema, nom_schema, producteur, oid_producteur, creation, ctrl)( + SELECT + obj.objid, + replace(obj.object_identity, '"', ''), + replace(nspowner::regrole::text, '"', ''), + nspowner, + true, + ARRAY['CREATE', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + WHERE obj.objid = pg_namespace.oid + ) ; + RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', replace(obj.object_identity, '"', '') ; + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'ECS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_create_schema() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; + + +-- Event Trigger: asgard_on_create_schema + +CREATE EVENT TRIGGER asgard_on_create_schema ON DDL_COMMAND_END + WHEN TAG IN ('CREATE SCHEMA') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_schema() ; + +COMMENT ON EVENT TRIGGER asgard_on_create_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; + + + + +------ 3.3 - EVENT TRIGGER SUR DROP SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_drop_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_drop_schema() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par l'event trigger asgard_on_drop_schema qui + répercute dans la table z_asgard_admin.gestion_schema (via la vue + z_asgard.gestion_schema_etr) les suppressions de schémas + réalisées par des commandes DROP SCHEMA directes ou exécutées + dans le cadre de la désinstallation d'une extension. +DECLENCHEMENT : ON SQL DROP. +CONDITION : WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION') */ +DECLARE + obj record ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EDS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EDS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + WHERE object_type = 'schema' + LOOP + ------ ENREGISTREMENT DE LA SUPPRESSION ------ + UPDATE z_asgard.gestion_schema_etr + SET (creation, oid_schema, ctrl) = (False, NULL, ARRAY['DROP', 'x7-A;#rzo']) + WHERE quote_ident(nom_schema) = obj.object_identity ; + IF FOUND THEN + RAISE NOTICE '... La suppression du schéma % a été enregistrée dans la table de gestion (creation = False).', replace(obj.object_identity, '"', ''); + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EDS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$; + +ALTER FUNCTION z_asgard_admin.asgard_on_drop_schema() + OWNER TO g_admin; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_drop_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou exécutées dans le cadre de la désinstallation d''une extension.' ; + + +-- Event Trigger: asgard_on_drop_schema + +CREATE EVENT TRIGGER asgard_on_drop_schema ON SQL_DROP + WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_drop_schema() ; + +COMMENT ON EVENT TRIGGER asgard_on_drop_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou exécutées dans le cadre de la désinstallation d''une extension.' ; + + + +------ 3.4 - EVENT TRIGGER SUR CREATE OBJET ------ + +-- Function: z_asgard_admin.asgard_on_create_objet() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_objet() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par l'event trigger asgard_on_create_objet qui + veille à attribuer aux nouveaux objets créés les droits prévus + pour le schéma dans la table de gestion. +AVERTISSEMENT : Les commandes CREATE OPERATOR CLASS, CREATE OPERATOR FAMILY +et CREATE STATISTICS ne sont pas prises en charge pour l'heure. +DECLENCHEMENT : ON DDL COMMAND END. +CONDITION : WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', +'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', +'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', +'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', +'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE'). +À partir de PostgreSQL 11, 'CREATE PROCEDURE' déclenche également l'exécution +de la présente fonction. */ +DECLARE + obj record ; + roles record ; + src record ; + proprietaire text ; + xowner text ; + e_mssg text ; + e_hint text ; + e_detl text ; + l text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'ECO1. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'ECO2. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + + FOR obj IN SELECT DISTINCT classid, objid, object_type, schema_name, object_identity + FROM pg_event_trigger_ddl_commands() + WHERE schema_name IS NOT NULL + ORDER BY object_type DESC + LOOP + + -- récupération des rôles de la table de gestion pour le schéma de l'objet + -- on se base sur les OID et non les noms pour se prémunir contre les changements + -- de libellés ; des jointures sur pg_roles permettent de vérifier que les rôles + -- n'ont pas été supprimés entre temps + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj.schema_name ; + + -- on ne traite que les schémas qui sont gérés par ASGARD + -- ce qui implique un rôle producteur non nul + IF roles.producteur IS NOT NULL + THEN + -- récupération du nom du champ contenant le propriétaire + -- courant de l'objet + SELECT attname::text INTO xowner + FROM pg_catalog.pg_attribute + WHERE attrelid = obj.classid AND attname ~ 'owner' ; + -- pourrait ne rien renvoyer pour certains pseudo-objets + -- comme les "table constraint" + + IF FOUND + THEN + + -- récupération du propriétaire courant de l'objet + -- génère une erreur si la requête ne renvoie rien + EXECUTE format('SELECT %s::regrole::text FROM %s WHERE oid = %s', + xowner, obj.classid::regclass, obj.objid) + INTO STRICT proprietaire ; + + -- si le propriétaire courant n'est pas le producteur + IF NOT roles.producteur::text = proprietaire + THEN + + ------ PROPRIETAIRE DE L'OBJET (DROITS DU PRODUCTEUR) ------ + RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', obj.object_identity ; + l := format('ALTER %s %s OWNER TO %I', obj.object_type, + obj.object_identity, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + + ------ DROITS DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + -- sur les tables : + IF obj.object_type IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %s TO %I', + obj.object_identity, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + + -- sur les séquences : + ELSIF obj.object_type IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %s TO %I', + obj.object_identity, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ DROITS DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + -- sur les tables : + IF obj.object_type IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %s TO %I', + obj.object_identity, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + + -- sur les séquences : + ELSIF obj.object_type IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %s TO %I', + obj.object_identity, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ VERIFICATION DES DROITS SUR LES SOURCES DES VUES ------- + IF obj.object_type IN ('view', 'materialized view') + THEN + FOR src IN ( + SELECT + DISTINCT + nom_schema, + relnamespace, + relname, + liblg, + relowner, + oid_producteur, + oid_editeur, + oid_lecteur + FROM pg_catalog.pg_rewrite + LEFT JOIN pg_catalog.pg_depend + ON objid = pg_rewrite.oid + LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = refobjid + LEFT JOIN z_asgard.gestion_schema_etr + ON relnamespace::regnamespace::text = quote_ident(gestion_schema_etr.nom_schema) + LEFT JOIN unnest( + ARRAY['Table', 'Table partitionnée', 'Vue', 'Vue matérialisée', 'Table étrangère', 'Séquence'], + ARRAY['r', 'p', 'v', 'm', 'f', 'S'] + ) AS t (liblg, libcrt) + ON relkind = libcrt + WHERE ev_class = obj.objid + AND rulename = '_RETURN' + AND ev_type = '1' + AND ev_enabled = 'O' + AND is_instead + AND classid = 'pg_rewrite'::regclass::oid + AND refclassid = 'pg_class'::regclass::oid + AND deptype = 'n' + AND NOT refobjid = obj.objid + AND NOT has_table_privilege(roles.producteur, refobjid, 'SELECT') + ) + LOOP + IF src.oid_producteur IS NOT NULL + -- l'utilisateur courant a suffisamment de droits pour voir le schéma de la source + -- dans sa table de gestion + THEN + RAISE WARNING 'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', + format('%s %s', CASE WHEN obj.object_type = 'materialized view' + THEN 'matérialisée ' ELSE '' END, obj.object_identity) + USING DETAIL = format('%s source %I.%I, producteur %s, éditeur %s, lecteur %s.', + src.liblg, src.nom_schema, src.relname, src.oid_producteur::regrole, + coalesce(src.oid_editeur::regrole::text, 'non défini'), + coalesce(src.oid_lecteur::regrole::text, 'non défini') + ), + HINT = CASE WHEN src.oid_lecteur IS NULL + THEN format('Pour faire du producteur de la vue %s le lecteur du schéma source, vous pouvez lancer la commande suivante : UPDATE z_asgard.gestion_schema_usr SET lecteur = %L WHERE nom_schema = %L.', + CASE WHEN obj.object_type = 'materialized view' THEN 'matérialisée ' ELSE '' END, + roles.producteur, src.nom_schema) + ELSE format('Pour faire du producteur de la vue %s le lecteur du schéma source, vous pouvez lancer la commande suivante : GRANT %s TO %I.', + CASE WHEN obj.object_type = 'materialized view' THEN 'matérialisée ' ELSE '' END, + src.oid_lecteur::regrole, roles.producteur) + END ; + ELSE + RAISE WARNING'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', + format('%s %s', CASE WHEN obj.object_type = 'materialized view' + THEN 'matérialisée ' ELSE '' END, obj.object_identity) + USING DETAIL = format('%s source %s.%I, propriétaire %s.', src.liblg, + src.relnamespace::regnamespace, src.relname, src.relowner::regrole) ; + END IF ; + END LOOP ; + END IF ; + + END IF ; + END IF; + + END LOOP; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'ECO0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$; + +ALTER FUNCTION z_asgard_admin.asgard_on_create_objet() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_objet() IS 'ASGARD. Fonction appelée par l''event trigger qui applique les droits pré-définis sur les nouveaux objets.' ; + + +-- Event Trigger: asgard_on_create_objet +DO +$$ +BEGIN + IF current_setting('server_version_num')::int < 110000 + THEN + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet(); + ELSE + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE PROCEDURE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet(); + END IF ; +END +$$ ; + +COMMENT ON EVENT TRIGGER asgard_on_create_objet IS 'ASGARD. Event trigger qui applique les droits pré-définis sur les nouveaux objets.' ; + + + +------ 3.5 - EVENT TRIGGER SUR ALTER OBJET ------ + +-- Function: z_asgard_admin.asgard_on_alter_objet() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_objet() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par l'event trigger asgard_on_alter_objet, qui + assure que le propriétaire de l'objet reste le propriétaire du + schéma qui le contient après l'exécution d'une commande ALTER. + Elle vise en particulier les SET SCHEMA (lorsque le schéma + cible a un producteur différent de celui du schéma d'origine, elle + modifie le propriétaire de l'objet en conséquence) et les + OWNER TO (elle inhibe leur effet en rendant la propriété de + l'objet au producteur du schéma). + Elle n'agit pas sur les privilèges. +AVERTISSEMENT : Les commandes ALTER OPERATOR CLASS, ALTER OPERATOR FAMILY +et ALTER STATISTICS ne sont pas pris en charge pour l'heure. +DECLENCHEMENT : ON DDL COMMAND END. +CONDITION : WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', +'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', +'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', +'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', +'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE'). +À partir de PostgreSQL 11, 'ALTER PROCEDURE' et 'ALTER ROUTINE' déclenchent +également l'exécution de la présente fonction. */ +DECLARE + obj record ; + n_producteur regrole ; + a_producteur regrole ; + l text ; + e_mssg text ; + e_hint text ; + e_detl text ; + xowner text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EAO1. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EAO2. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + END IF ; + + FOR obj IN SELECT DISTINCT classid, objid, object_type, schema_name, object_identity + FROM pg_event_trigger_ddl_commands() + WHERE schema_name IS NOT NULL + ORDER BY object_type DESC + LOOP + + -- récupération du rôle identifié comme producteur pour le schéma de l'objet + -- (à l'issue de la commande) + -- on se base sur l'OID et non le nom pour se prémunir contre les changements + -- de libellés + SELECT oid_producteur::regrole INTO n_producteur + FROM z_asgard.gestion_schema_etr + WHERE nom_schema = obj.schema_name ; + + IF FOUND + THEN + -- récupération du nom du champ contenant le propriétaire + -- de l'objet + SELECT attname::text INTO xowner + FROM pg_catalog.pg_attribute + WHERE attrelid = obj.classid AND attname ~ 'owner' ; + -- ne renvoie rien pour certains pseudo-objets comme les + -- "table constraint" + + IF FOUND + THEN + -- récupération du propriétaire courant de l'objet + -- génère une erreur si la requête ne renvoie rien + EXECUTE format('SELECT %s::regrole::text FROM %s WHERE oid = %s', xowner, + obj.classid::regclass, obj.objid) + INTO STRICT a_producteur ; + + -- si les deux rôles sont différents + IF NOT n_producteur = a_producteur + THEN + ------ MODIFICATION DU PROPRIETAIRE ------ + -- l'objet est attribué au propriétaire désigné pour le schéma + -- (n_producteur) + RAISE NOTICE 'attribution de la propriété de % au rôle producteur du schéma :', obj.object_identity ; + l := format('ALTER %s %s OWNER TO %s', obj.object_type, + obj.object_identity, n_producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + END IF ; + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EAO0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$; + +ALTER FUNCTION z_asgard_admin.asgard_on_alter_objet() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_objet() IS 'ASGARD. Fonction appelée par l''event trigger qui assure que le producteur d''un schéma reste propriétaire de tous les objets qu''il contient.' ; + + +-- Event Trigger: asgard_on_alter_objet + +DO +$$ +BEGIN + IF current_setting('server_version_num')::int < 110000 + THEN + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet(); + ELSE + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER PROCEDURE', + 'ALTER ROUTINE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet(); + END IF ; +END +$$ ; + +COMMENT ON EVENT TRIGGER asgard_on_alter_objet IS 'ASGARD. Event trigger qui assure que le producteur d''un schéma reste propriétaire de tous les objets qu''il contient.' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +--------------------------------------- +------ 4 - FONCTIONS UTILITAIRES ------ +--------------------------------------- +/* 4.1 - LISTES DES DROITS SUR LES OBJETS D'UN SCHEMA + 4.2 - LISTE DES DROITS SUR UN OBJET + 4.3 - MODIFICATION DU PROPRIETAIRE D'UN SCHEMA ET SON CONTENU + 4.4 - TRANSFORMATION GRANT EN REVOKE + 4.5 - INITIALISATION DE GESTION_SCHEMA + 4.6 - DEREFERENCEMENT D'UN SCHEMA + 4.7 - NETTOYAGE DES RÔLES + 4.8 - REINITIALISATION DES PRIVILEGES SUR UN SCHEMA + 4.9 - REINITIALISATION DES PRIVILEGES SUR UN OBJET + 4.10 - DEPLACEMENT D'OBJET + 4.11 - OCTROI D'UN RÔLE À TOUS LES RÔLES DE CONNEXION + 4.12 - IMPORT DE LA NOMENCLATURE DANS GESTION_SCHEMA + 4.13 - REAFFECTATION DES PRIVILEGES D'UN RÔLE + 4.14 - REINITIALISATION DES PRIVILEGES SUR TOUS LES SCHEMAS + 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL + 4.16 - DIAGNOSTIC DES DROITS NON STANDARDS + 4.17 - EXTRACTION DE NOMS D'OBJETS A PARTIR D'IDENTIFIANTS + 4.18 - EXPLICITATION DES CODES DE PRIVILÈGES */ + +------ 4.1 - LISTES DES DROITS SUR LES OBJETS D'UN SCHEMA ------ + +-- Function: z_asgard.asgard_synthese_role(regnamespace, regrole) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_synthese_role(n_schema regnamespace, n_role regrole) + RETURNS TABLE(commande text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction renvoie une table contenant une + liste de commandes GRANT et REVOKE permettant de + recréer les droits de "n_role" sur les objets du + schéma "n_schema" (et le schéma lui-même). +ARGUMENTS : +- "n_schema" est un nom de schéma valide, casté en regnamespace ; +- "n_role" est un nom de rôle valide, casté en regrole. +SORTIE : Une table avec un unique champ nommé "commande". */ +BEGIN + ------ SCHEMAS ------ + -- privilèges attribués (hors propriétaire) : + RETURN QUERY + SELECT format('GRANT %s ON SCHEMA %s TO %%I', privilege, n_schema) + FROM pg_catalog.pg_namespace, + aclexplode(nspacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = n_schema + AND nspacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = nspowner ; + -- privilèges révoqués du propriétaire : + RETURN QUERY + SELECT format('REVOKE %s ON SCHEMA %s FROM %%I', expected_privilege, n_schema) + FROM pg_catalog.pg_namespace, + unnest(ARRAY['USAGE', 'CREATE']) AS expected_privilege + WHERE oid = n_schema + AND nspacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(nspacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = nspowner ; + ------ TABLES ------ + -- inclut les vues, vues matérialisées, tables étrangères et partitionnées + -- privilèges attribués (hors propriétaire) : + RETURN QUERY + SELECT format('GRANT %s ON TABLE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND relkind IN ('r', 'v', 'm', 'f', 'p') + AND relacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = relowner ; + -- privilèges révoqués du propriétaire : + RETURN QUERY + SELECT format('REVOKE %s ON TABLE %s FROM %%I', expected_privilege, oid::regclass) + FROM pg_catalog.pg_class, + unnest(ARRAY['SELECT', 'INSERT', 'UPDATE', 'DELETE', + 'TRUNCATE', 'REFERENCES', 'TRIGGER']) AS expected_privilege + WHERE relnamespace = n_schema + AND relkind IN ('r', 'v', 'm', 'f', 'p') + AND relacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = relowner ; + ------ SEQUENCES ------ + -- privilèges attribués (hors propriétaire) : + RETURN QUERY + SELECT format('GRANT %s ON SEQUENCE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND relkind = 'S' + AND relacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = relowner ; + -- privilèges révoqués du propriétaire : + RETURN QUERY + SELECT format('REVOKE %s ON SEQUENCE %s FROM %%I', expected_privilege, oid::regclass) + FROM pg_catalog.pg_class, + unnest(ARRAY['SELECT', 'USAGE', 'UPDATE']) AS expected_privilege + WHERE relnamespace = n_schema + AND relkind = 'S' + AND relacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = relowner ; + ------ COLONNES ------ + -- privilèges attribués : + RETURN QUERY + SELECT format('GRANT %s (%I) ON TABLE %s TO %%I', privilege, attname, attrelid::regclass) + FROM pg_catalog.pg_class JOIN pg_catalog.pg_attribute + ON pg_class.oid = pg_attribute.attrelid, + aclexplode(attacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND attacl IS NOT NULL + AND n_role = grantee ; + ------ ROUTINES ------ + -- ... sous la dénomination FUNCTION jusqu'à PG 10, puis en + -- tant que ROUTINE à partir de PG 11, afin que les commandes + -- fonctionnent également avec les procédures. + -- privilèges attribués (hors propriétaire) : + RETURN QUERY + SELECT format('GRANT %s ON %s %s TO %%I', privilege, + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + oid::regprocedure) + FROM pg_catalog.pg_proc, + aclexplode(proacl) AS acl (grantor, grantee, privilege, grantable) + WHERE pronamespace = n_schema + AND proacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = proowner ; + -- privilèges révoqués du propriétaire : + RETURN QUERY + SELECT format('REVOKE %s ON %s %s FROM %%I', expected_privilege, + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + oid::regprocedure) + FROM pg_catalog.pg_proc, + unnest(ARRAY['EXECUTE']) AS expected_privilege + WHERE pronamespace = n_schema + AND proacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(proacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = proowner ; + ------ TYPES ------ + -- inclut les domaines + -- privilèges attribués (hors propriétaire) : + RETURN QUERY + SELECT format('GRANT %s ON TYPE %s.%I TO %%I', privilege, n_schema, typname) + FROM pg_catalog.pg_type, + aclexplode(typacl) AS acl (grantor, grantee, privilege, grantable) + WHERE typnamespace = n_schema + AND typacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = typowner ; + -- privilèges révoqués du propriétaire : + RETURN QUERY + SELECT format('REVOKE %s ON TYPE %s.%I FROM %%I', expected_privilege, n_schema, typname) + FROM pg_catalog.pg_type, + unnest(ARRAY['USAGE']) AS expected_privilege + WHERE typnamespace = n_schema + AND typacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(typacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = typowner ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_synthese_role(regnamespace, regrole) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_synthese_role(regnamespace, regrole) IS 'ASGARD. Fonction qui liste les commandes permettant de reproduire les droits d''un rôle sur les objets d''un schéma.' ; + + +-- Function: z_asgard.asgard_synthese_public(regnamespace) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_synthese_public(n_schema regnamespace) + RETURNS TABLE(commande text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction renvoie une table contenant une + liste de commandes GRANT et REVOKE permettant de + recréer les droits de public sur les objets du + schéma "schema" (et le schéma lui-même). +REMARQUE : La fonction ne s'intéresse pas aux objets de type +type routine (fonctions, dont agrégats, et procédures) et type +(dont domaines), sur lesquels public reçoit des droits par défaut +qu'il n'est pas judicieux de reproduire sur un autre rôle, ni de +révoquer lors d'un changement de lecteur/éditeur. Si des privilèges +par défaut ont été révoqués pour public, la révocation restera valable +pour les futurs lecteur/éditeurs puisqu'il n'y a pas d'attribution +de privilèges supplémentaires pour les lecteurs/éditeurs sur +ces objets. +ARGUMENT : "schema" est un nom de schéma valide, casté en +regnamespace. +SORTIE : Une table avec un unique champ nommé "commande". */ +BEGIN + ------ SCHEMAS ------ + RETURN QUERY + SELECT format('GRANT %s ON SCHEMA %s TO %%I', privilege, n_schema) + FROM pg_catalog.pg_namespace, + aclexplode(nspacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = n_schema + AND nspacl IS NOT NULL + AND grantee = 0 ; + ------ TABLES ------ + -- inclut les vues, vues matérialisées, tables étrangères et partitions + RETURN QUERY + SELECT format('GRANT %s ON TABLE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND relkind IN ('r', 'v', 'm', 'f', 'p') + AND relacl IS NOT NULL + AND grantee = 0 ; + ------ SEQUENCES ------ + RETURN QUERY + SELECT format('GRANT %s ON SEQUENCE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND relkind = 'S' + AND relacl IS NOT NULL + AND grantee = 0 ; + ------ COLONNES ------ + RETURN QUERY + SELECT format('GRANT %s (%I) ON TABLE %s TO %%I', privilege, attname, attrelid::regclass) + FROM pg_catalog.pg_class JOIN pg_catalog.pg_attribute + ON pg_class.oid = pg_attribute.attrelid, + aclexplode(attacl) AS acl (grantor, grantee, privilege, grantable) + WHERE relnamespace = n_schema + AND attacl IS NOT NULL + AND grantee = 0 ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_synthese_public(regnamespace) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_synthese_public(regnamespace) IS 'ASGARD. Fonction qui liste les commandes permettant de reproduire les droits de public sur les objets d''un schéma.' ; + + +------ 4.2 - LISTE DES DROITS SUR UN OBJET ------ + +-- Function: z_asgard.asgard_synthese_role_obj(oid, text, regrole) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_synthese_role_obj(obj_oid oid, obj_type text, n_role regrole) + RETURNS TABLE(commande text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction renvoie une table contenant une + liste de commandes GRANT et REVOKE permettant de + recréer les droits de "n_role" sur un objet de type + table, table étrangère, partition de table, vue, + vue matérialisée, séquence, routine (fonctions, + dont agrégats, et procédures), type (dont domaines). +ARGUMENTS : +- "obj_oid" est l'identifiant interne de l'objet ; +- "obj_type" est le type de l'objet au format text ('table', +'view', 'materialized view', 'sequence', 'function', 'type', +'domain', 'foreign table', 'partitioned table', 'aggregate', +'procedure', 'routine') ; +- "n_role" est un nom de rôle valide, casté en regrole. +SORTIE : Une table avec un unique champ nommé "commande". */ +BEGIN + ------ TABLE, VUE, VUE MATERIALISEE ------ + IF obj_type IN ('table', 'view', 'materialized view', 'foreign table', 'partitioned table') + THEN + -- privilèges attribués (si n_role n'est pas le propriétaire de l'objet) : + RETURN QUERY + SELECT format('GRANT %s ON TABLE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = relowner ; + -- privilèges révoqués du propriétaire (si n_role est le propriétaire de l'objet) : + RETURN QUERY + SELECT format('REVOKE %s ON TABLE %s FROM %%I', expected_privilege, oid::regclass) + FROM pg_catalog.pg_class, + unnest(ARRAY['SELECT', 'INSERT', 'UPDATE', 'DELETE', + 'TRUNCATE', 'REFERENCES', 'TRIGGER']) AS expected_privilege + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = relowner ; + ------ COLONNES ------ + -- privilèges attribués : + RETURN QUERY + SELECT format('GRANT %s (%I) ON TABLE %s TO %%I', privilege, attname, attrelid::regclass) + FROM pg_catalog.pg_attribute, + aclexplode(attacl) AS acl (grantor, grantee, privilege, grantable) + WHERE pg_attribute.attrelid = obj_oid + AND attacl IS NOT NULL + AND n_role = grantee ; + ------ SEQUENCES ------ + ELSIF obj_type = 'sequence' + THEN + -- privilèges attribués (si n_role n'est pas le propriétaire de l'objet) : + RETURN QUERY + SELECT format('GRANT %s ON SEQUENCE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = relowner ; + -- privilèges révoqués du propriétaire (si n_role est le propriétaire de l'objet) : + RETURN QUERY + SELECT format('REVOKE %s ON SEQUENCE %s FROM %%I', expected_privilege, oid::regclass) + FROM pg_catalog.pg_class, + unnest(ARRAY['SELECT', 'USAGE', 'UPDATE']) AS expected_privilege + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = relowner ; + ------ FONCTIONS ------ + -- ... sous la dénomination FUNCTION jusqu'à PG 10, puis en + -- tant que ROUTINE à partir de PG 11, afin que les commandes + -- fonctionnent également avec les procédures. + ELSIF obj_type IN ('function', 'aggregate', 'procedure', 'routine') + THEN + -- privilèges attribués (si n_role n'est pas le propriétaire de l'objet) : + RETURN QUERY + SELECT format('GRANT %s ON %s %s TO %%I', privilege, + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + oid::regprocedure) + FROM pg_catalog.pg_proc, + aclexplode(proacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND proacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = proowner ; + -- privilèges révoqués du propriétaire (si n_role est le propriétaire de l'objet) : + RETURN QUERY + SELECT format('REVOKE %s ON %s %s FROM %%I', expected_privilege, + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + oid::regprocedure) + FROM pg_catalog.pg_proc, + unnest(ARRAY['EXECUTE']) AS expected_privilege + WHERE oid = obj_oid + AND proacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(proacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = proowner ; + ------ TYPES ------ + -- inclut les domaines + ELSIF obj_type IN ('type', 'domain') + THEN + -- privilèges attribués (si n_role n'est pas le propriétaire de l'objet) : + RETURN QUERY + SELECT format('GRANT %s ON TYPE %s.%I TO %%I', privilege, typnamespace::regnamespace, typname) + FROM pg_catalog.pg_type, + aclexplode(typacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND typacl IS NOT NULL + AND n_role = grantee + AND NOT n_role = typowner ; + -- privilèges révoqués du propriétaire (si n_role est le propriétaire de l'objet) : + RETURN QUERY + SELECT format('REVOKE %s ON TYPE %s.%I FROM %%I', expected_privilege, typnamespace::regnamespace, typname) + FROM pg_catalog.pg_type, + unnest(ARRAY['USAGE']) AS expected_privilege + WHERE oid = obj_oid + AND typacl IS NOT NULL + AND NOT expected_privilege IN ( + SELECT privilege + FROM aclexplode(typacl) AS acl (grantor, grantee, privilege, grantable) + WHERE n_role = grantee + ) + AND n_role = typowner ; + ELSE + RAISE EXCEPTION 'FSR0. Le type d''objet % n''est pas pris en charge', obj_type ; + END IF ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_synthese_role_obj(oid, text, regrole) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_synthese_role_obj(oid, text, regrole) IS 'ASGARD. Fonction qui liste les commandes permettant de reproduire les droits d''un rôle sur un objet.' ; + + +-- Function: z_asgard.asgard_synthese_public_obj(oid, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_synthese_public_obj(obj_oid oid, obj_type text) + RETURNS TABLE(commande text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction renvoie une table contenant une + liste de commandes GRANT et REVOKE permettant de + recréer les droits de public sur un objet de type + table, table étrangère, partition de table, vue, + vue matérialisée ou séquence. +REMARQUE : La fonction ne s'intéresse pas aux objets de type +fonction (dont agrégats) et type (dont domaines), sur lesquels +public reçoit des droits par défaut qu'il n'est pas judicieux +de reproduire sur un autre rôle, ni de révoquer lors d'un +changement de lecteur/éditeur. Si des privilèges par défaut ont +été révoqués pour public, la révocation restera valable pour les +futurs lecteur/éditeurs puisqu'il n'y a pas d'attribution +de privilèges supplémentaires pour les lecteurs/éditeurs sur +ces objets. +ARGUMENTS : +- "obj_oid" est l'identifiant interne de l'objet ; +- "obj_type" est le type de l'objet au format text ('table', +'view', 'materialized view', 'sequence', 'foreign table', +'partitioned table'). +SORTIE : Une table avec un unique champ nommé "commande". */ +BEGIN + ------ TABLE, VUE, VUE MATERIALISEE ------ + IF obj_type IN ('table', 'view', 'materialized view', 'foreign table', 'partitioned table') + THEN + RETURN QUERY + SELECT format('GRANT %s ON TABLE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND grantee = 0 ; + ------ COLONNES ------ + RETURN QUERY + SELECT format('GRANT %s (%I) ON TABLE %s TO %%I', privilege, attname, attrelid::regclass) + FROM pg_catalog.pg_attribute, + aclexplode(attacl) AS acl (grantor, grantee, privilege, grantable) + WHERE pg_attribute.attrelid = obj_oid + AND attacl IS NOT NULL + AND grantee = 0 ; + ------ SEQUENCES ------ + ELSIF obj_type = 'sequence' + THEN + RETURN QUERY + SELECT format('GRANT %s ON SEQUENCE %s TO %%I', privilege, oid::regclass) + FROM pg_catalog.pg_class, + aclexplode(relacl) AS acl (grantor, grantee, privilege, grantable) + WHERE oid = obj_oid + AND relacl IS NOT NULL + AND grantee = 0 ; + ELSE + RAISE EXCEPTION 'FSP0. Le type d''objet % n''est pas pris en charge', obj_type ; + END IF ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_synthese_public_obj(oid, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_synthese_public_obj(oid, text) IS 'ASGARD. Fonction qui liste les commandes permettant de reproduire les droits de public sur un objet.' ; + + + +------ 4.3 - MODIFICATION DU PROPRIETAIRE D'UN SCHEMA ET SON CONTENU ------ + +-- Function: z_asgard.asgard_admin_proprietaire(text, text, boolean) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_admin_proprietaire( + n_schema text, n_owner text, b_setschema boolean DEFAULT True + ) + RETURNS int + LANGUAGE plpgsql + AS $_$ +/* OBJET : Gestion des droits. Cette fonction permet d''attribuer + un schéma et tous les objets qu'il contient à un [nouveau] + propriétaire. +AVERTISSEMENT : Les objets de type operator class, operator family +et extended planner statistic ne sont pas pris en charge pour l'heure. +ARGUMENTS : +- "n_schema" est une chaîne de caractères correspondant au nom du + schéma à considérer ; +- "n_owner" est une chaîne de caractères correspondant au nom du + rôle (rôle de groupe ou rôle de connexion) qui doit être + propriétaire des objets ; +- "b_setschema" est un paramètre booléen optionnel (vrai par défaut) + qui indique si la fonction doit changer le propriétaire du schéma + ou seulement des objets qu'il contient. +RESULTAT : la fonction renvoie un entier correspondant au nombre +d''objets effectivement traités. Les commandes lancées sont notifiées +au fur et à mesure. */ +DECLARE + item record ; + k int := 0 ; + o_owner oid ; + s_owner text ; +BEGIN + ------ TESTS PREALABLES ------ + SELECT nspowner::regrole::text + INTO s_owner + FROM pg_catalog.pg_namespace + WHERE nspname = n_schema ; + + -- non existance du schémas + IF NOT FOUND + THEN + RAISE EXCEPTION 'FAP1. Le schéma % n''existe pas.', n_schema ; + END IF ; + + -- absence de permission sur le propriétaire courant du schéma + IF NOT pg_has_role(s_owner::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FAP5. Vous n''êtes pas habilité à modifier le propriétaire du schéma %.', n_schema + USING DETAIL = format('Propriétaire courant : %s.', s_owner) ; + END IF ; + + -- le propriétaire désigné n'existe pas + IF NOT n_owner IN (SELECT rolname::text FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FAP2. Le rôle % n''existe pas.', n_owner ; + -- absence de permission sur le propriétaire désigné + ELSIF NOT pg_has_role(n_owner, 'USAGE') + THEN + RAISE EXCEPTION 'FAP6. Vous n''avez pas la permission d''utiliser le rôle %.', n_owner ; + ELSE + o_owner := quote_ident(n_owner)::regrole::oid ; + END IF ; + + -- le propriétaire désigné n'est pas le propriétaire courant et la fonction + -- a été lancée avec la variante qui ne traite pas le schéma + IF NOT b_setschema + AND NOT quote_ident(n_owner) = s_owner + THEN + RAISE EXCEPTION 'FAP3. Le rôle % n''est pas propriétaire du schéma.', n_owner + USING HINT = format('Lancez asgard_admin_proprietaire(%L, %L) pour changer également le propriétaire du schéma.', + n_schema, n_owner) ; + END IF ; + + ------ PROPRIÉTAIRE DU SCHEMA ------ + IF b_setschema + THEN + EXECUTE format('ALTER SCHEMA %I OWNER TO %I', n_schema, n_owner) ; + RAISE NOTICE '> %', format('ALTER SCHEMA %I OWNER TO %I', n_schema, n_owner) ; + k := k + 1 ; + END IF ; + + ------ PROPRIETAIRES DES OBJETS ------ + -- uniquement ceux qui n'appartiennent pas déjà + -- au rôle identifié + FOR item IN + -- tables, tables étrangères, vues, vues matérialisées, + -- partitions, séquences : + SELECT + relname::text AS n_objet, + relowner AS obj_owner, + relkind IN ('r', 'f', 'p', 'm') AS b, + -- b servira à assurer que les tables soient listées avant les + -- objets qui en dépendent + format('ALTER %s %s OWNER TO %I', kind_lg, pg_class.oid::regclass, n_owner) AS commande + FROM pg_catalog.pg_class, + unnest(ARRAY['r', 'p', 'v', 'm', 'f', 'S'], + ARRAY['TABLE', 'TABLE', 'VIEW', 'MATERIALIZED VIEW', 'FOREIGN TABLE', 'SEQUENCE']) AS l (kind_crt, kind_lg) + WHERE relnamespace = quote_ident(n_schema)::regnamespace + AND relkind IN ('S', 'r', 'p', 'v', 'm', 'f') + AND kind_crt = relkind + AND NOT relowner = o_owner + UNION + -- fonctions et procédures : + -- ... sous la dénomination FUNCTION jusqu'à PG 10, puis en + -- tant que ROUTINE à partir de PG 11, afin que les commandes + -- fonctionnent également avec les procédures. + SELECT + proname::text AS n_objet, + proowner AS obj_owner, + False AS b, + format('ALTER %s %s OWNER TO %I', + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + pg_proc.oid::regprocedure, n_owner) AS commande + FROM pg_catalog.pg_proc + WHERE pronamespace = quote_ident(n_schema)::regnamespace + AND NOT proowner = o_owner + -- à noter que les agrégats (proisagg vaut True) ont + -- leur propre commande ALTER AGGREGATE OWNER TO, mais + -- ALTER FUNCTION OWNER TO fonctionne pour tous les types + -- de fonctions dont les agrégats, et - pour PG 11+ - + -- ALTER ROUTINE OWNER TO fonctionne pour tous les types + -- de fonctions et les procédures. + UNION + -- types et domaines : + SELECT + typname::text AS n_objet, + typowner AS obj_owner, + False AS b, + format('ALTER %s %s.%I OWNER TO %I', kind_lg, typnamespace::regnamespace, + typname, n_owner) AS commande + FROM unnest(ARRAY['true', 'false'], + ARRAY['DOMAIN', 'TYPE']) AS l (kind_crt, kind_lg), + pg_catalog.pg_type + WHERE typnamespace = quote_ident(n_schema)::regnamespace + AND kind_crt::boolean = (typtype = 'd') + AND NOT typowner = o_owner + -- exclusion des types générés automatiquement + AND NOT (pg_type.oid, 'pg_type'::regclass::oid) IN ( + SELECT pg_depend.objid, pg_depend.classid + FROM pg_catalog.pg_depend + WHERE deptype IN ('i', 'a') + ) + UNION + -- conversions : + SELECT + conname::text AS n_objet, + conowner AS obj_owner, + False AS b, + format('ALTER CONVERSION %s.%I OWNER TO %I', connamespace::regnamespace, + conname, n_owner) AS commande + FROM pg_catalog.pg_conversion + WHERE connamespace = quote_ident(n_schema)::regnamespace + AND NOT conowner = o_owner + UNION + -- opérateurs : + SELECT + oprname::text AS n_objet, + oprowner AS obj_owner, + False AS b, + format('ALTER OPERATOR %s OWNER TO %I', pg_operator.oid::regoperator, + n_owner) AS commande + FROM pg_catalog.pg_operator + WHERE oprnamespace = quote_ident(n_schema)::regnamespace + AND NOT oprowner = o_owner + UNION + -- collations : + SELECT + collname::text AS n_objet, + collowner AS obj_owner, + False AS b, + format('ALTER COLLATION %s.%I OWNER TO %I', collnamespace::regnamespace, + collname, n_owner) AS commande + FROM pg_catalog.pg_collation + WHERE collnamespace = quote_ident(n_schema)::regnamespace + AND NOT collowner = o_owner + UNION + -- text search dictionary : + SELECT + dictname::text AS n_objet, + dictowner AS obj_owner, + False AS b, + format('ALTER TEXT SEARCH DICTIONARY %s OWNER TO %I', pg_ts_dict.oid::regdictionary, + n_owner) AS commande + FROM pg_catalog.pg_ts_dict + WHERE dictnamespace = quote_ident(n_schema)::regnamespace + AND NOT dictowner = o_owner + UNION + -- text search configuration : + SELECT + cfgname::text AS n_objet, + cfgowner AS obj_owner, + False AS b, + format('ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %I', pg_ts_config.oid::regconfig, + n_owner) AS commande + FROM pg_catalog.pg_ts_config + WHERE cfgnamespace = quote_ident(n_schema)::regnamespace + AND NOT cfgowner = o_owner + ORDER BY b DESC + LOOP + IF pg_has_role(item.obj_owner, 'USAGE') + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + k := k + 1 ; + ELSE + RAISE EXCEPTION 'FAP4. Vous n''êtes pas habilité à modifier le propriétaire de l''objet %.', item.n_objet + USING DETAIL = 'Propriétaire courant : ' || item.obj_owner::regrole::text || '.' ; + END IF ; + END LOOP ; + ------ RESULTAT ------ + RETURN k ; +END +$_$ ; + +ALTER FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) IS 'ASGARD. Fonction qui modifie le propriétaire d''un schéma et de tous les objets qu''il contient.' ; + + +------ 4.4 - TRANSFORMATION GRANT EN REVOKE ------ + +-- Function: z_asgard.asgard_grant_to_revoke(text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_grant_to_revoke(c_grant text) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction transforme une commande de type GRANT en + son équivalent REVOKE, ou l'inverse. +AVERTISSEMENT : La fonction ne reconnaîtra que les mots clés écrits +en majuscules. +ARGUMENT : une commande de type GRANT/REVOKE présumée valide (chaîne de caractères). +SORTIE : une commande de type REVOKE/GRANT (chaîne de caractères). */ +DECLARE + c_revoke text ; +BEGIN + IF c_grant ~ '^GRANT' + THEN + c_revoke := regexp_replace(c_grant, '^GRANT', 'REVOKE') ; + c_revoke := regexp_replace(c_revoke, '[[:space:]]TO[[:space:]]', ' FROM ') ; + ELSIF c_grant ~ '^REVOKE' + THEN + c_revoke := regexp_replace(c_grant, '^REVOKE', 'GRANT') ; + c_revoke := regexp_replace(c_revoke, '[[:space:]]FROM[[:space:]]', ' TO ') ; + ELSE + RAISE EXCEPTION 'FGR1. Commande GRANT/REVOKE invalide.' ; + END IF ; + RETURN c_revoke ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_grant_to_revoke(text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_grant_to_revoke(text) IS 'ASGARD. Fonction qui transforme une commande GRANT en commande REVOKE.' ; + + +------ 4.5 - INITIALISATION DE GESTION_SCHEMA ------ + +-- Function: z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema( + exceptions text[] default NULL::text[], b_gs boolean default False + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction intègre à la table de gestion des droits + gestion_schema l'ensemble des schémas existants, hors + schémas système et ceux qui sont (optionnellement) listés + en argument. +ARGUMENTS : +- exceptions (optionnel) : un tableau text[] contenant les noms des schémas +à omettre, le cas échéant ; +- b_gs (optionnel) : un booléen indiquant si, dans l'hypothèse où un schéma +serait déjà référencé - nécessairement comme non créé - dans la table de gestion, +c'est le propriétaire du schéma qui doit devenir le "producteur" du schéma +(False) ou le producteur pré-renseigné dans la table de gestion qui doit +devenir le propriétaire du schéma (True). False par défaut. +SORTIE : '__ FIN INTIALISATION.' si la requête s'est exécutée normalement. */ +DECLARE + item record ; + e_mssg text ; + e_detl text ; + e_hint text ; + b_creation boolean ; +BEGIN + + FOR item IN SELECT nspname, nspowner FROM pg_catalog.pg_namespace + WHERE NOT nspname ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + AND (exceptions IS NULL OR NOT nspname = ANY(exceptions)) + LOOP + SELECT creation INTO b_creation + FROM z_asgard.gestion_schema_usr + WHERE item.nspname::text = nom_schema ; + IF b_creation IS NULL + -- schéma non référencé dans gestion_schema + THEN + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) + VALUES (item.nspname::text, replace(item.nspowner::regrole::text, '"', ''), true) ; + RAISE NOTICE '... Schéma % enregistré dans la table de gestion.', item.nspname::text ; + ELSIF NOT b_creation + -- schéma pré-référencé dans gestion_schema + THEN + IF NOT b_gs + THEN + UPDATE z_asgard.gestion_schema_usr + SET creation = true, + producteur = replace(item.nspowner::regrole::text, '"', '') + WHERE item.nspname::text = nom_schema ; + ELSE + UPDATE z_asgard.gestion_schema_usr + SET creation = true + WHERE item.nspname::text = nom_schema ; + END IF ; + RAISE NOTICE '... Schéma % marqué comme créé dans la table de gestion.', item.nspname::text ; + END IF ; + END LOOP ; + + RETURN '__ FIN INITALISATION.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FIG0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) IS 'ASGARD. Fonction qui initialise la table de gestion à partir des schémas existants.' ; + + + +------ 4.6 - DEREFERENCEMENT D'UN SCHEMA ------ + +-- Function: z_asgard_admin.asgard_sortie_gestion_schema(text) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_sortie_gestion_schema(n_schema text) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction permet de supprimer de la table de gestion un + schéma existant (qui échappera alors aux mécanismes de + gestion des droits), en outrepassant les règles qui veulent + que seules les lignes avec creation valant false puisse + être ciblées par des DELETE et que creation ne puisse être + mis à false si le schéma existe. +ARGUMENTS : +- n_schema : nom d'un schéma présumé référencé dans le champ + nom_schema de la table de gestion (sinon la fonction n'aura + pas d'effet). +SORTIE : '__ DEREFERENCEMENT REUSSI.' si la requête s'est exécutée normalement. */ +DECLARE + e_mssg text ; + e_detl text ; + e_hint text ; +BEGIN + + UPDATE z_asgard.gestion_schema_etr + SET ctrl = ARRAY['EXIT', 'x7-A;#rzo'] + WHERE nom_schema = n_schema ; + + DELETE FROM z_asgard.gestion_schema_etr + WHERE nom_schema = n_schema ; + + RETURN '__ DEREFERENCEMENT REUSSI.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FSG0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_sortie_gestion_schema(text) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_sortie_gestion_schema(text) IS 'ASGARD. Fonction qui déréférence un schéma existant de la table de gestion.' ; + + + +------ 4.7 - NETTOYAGE DES RÔLES ------ + +-- Function: z_asgard.asgard_nettoyage_roles() + +CREATE OR REPLACE FUNCTION z_asgard.asgard_nettoyage_roles() + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction active la mise à jour des noms des rôles + désignés dans la table de gestion comme producteur, éditeur et + lecteur, pour prendre en compte les changements de nom + ou suppression qui auraient pu avoir eu lieu. +ARGUMENTS : néant. +SORTIE : '__ NETTOYAGE REUSSI.' si la requête s'est exécutée normalement. */ +DECLARE + e_mssg text ; + e_detl text ; + e_hint text ; +BEGIN + + UPDATE z_asgard.gestion_schema_usr + SET producteur = producteur, + editeur = editeur, + lecteur = lecteur ; + + RETURN '__ NETTOYAGE REUSSI.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FNR0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_nettoyage_roles() + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_nettoyage_roles() IS 'ASGARD. Fonction qui met à jour les noms des rôles référencés dans la table de gestion.' ; + + +------ 4.8 - REINITIALISATION DES PRIVILEGES SUR UN SCHEMA ------ + +-- Function: z_asgard.asgard_initialise_schema(text, boolean, boolean) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_schema( + n_schema text, + b_preserve boolean DEFAULT False, + b_gs boolean default False + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction permet de réinitialiser les droits + sur un schéma selon les privilèges standards associés + aux rôles désignés dans la table de gestion. + Si elle est appliquée à un schéma existant non référencé + dans la table de gestion, elle l'ajoute avec son + propriétaire courant. Elle échoue si le schéma n'existe + pas. +ARGUMENTS : +- n_schema : nom d'un schéma présumé existant ; +- b_preserve (optionnel) : un paramètre booléen. Pour un schéma encore +non référencé (ou pré-référencé comme non-créé) dans la table de gestion une valeur +True signifie que les privilèges des rôles lecteur et éditeur doivent être +ajoutés par dessus les droits actuels. Avec la valeur par défaut False, +les privilèges sont réinitialisés. Ce paramètre est ignoré pour un schéma déjà +référencé comme créé (et les privilèges sont réinitialisés) ; +- b_gs (optionnel) : un booléen indiquant si, dans l'hypothèse où un schéma +serait déjà référencé - nécessairement comme non créé - dans la table de gestion, +c'est le propriétaire du schéma qui doit devenir le "producteur" (False) ou le +producteur de la table de gestion qui doit devenir le propriétaire +du schéma (True). False par défaut. Ce paramètre est ignoré pour un schéma déjà +créé. +SORTIE : '__ REINITIALISATION REUSSIE.' (ou '__INITIALISATION REUSSIE.' pour +un schéma non référencé comme créé avec b_preserve = True) si la requête +s'est exécutée normalement. */ +DECLARE + roles record ; + cree boolean ; + r record ; + c record ; + item record ; + n_owner text ; + k int := 0 ; + n int ; + e_mssg text ; + e_detl text ; + e_hint text ; +BEGIN + ------ TESTS PREALABLES ------ + -- schéma système + IF n_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FIS1. Opération interdite. Le schéma % est un schéma système.', n_schema ; + END IF ; + + -- existence du schéma + SELECT replace(nspowner::regrole::text, '"', '') INTO n_owner + FROM pg_catalog.pg_namespace + WHERE n_schema = nspname::text ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'FIS2. Echec. Le schéma % n''existe pas.', n_schema ; + END IF ; + + -- permission sur le propriétaire + IF NOT pg_has_role(n_owner, 'USAGE') + THEN + RAISE EXCEPTION 'FIS3. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', n_schema + USING HINT = format('Il vous faut être membre du rôle propriétaire %s.', n_owner) ; + END IF ; + + ------ SCHEMA DEJA REFERENCE ? ------ + SELECT + creation + INTO cree + FROM z_asgard.gestion_schema_usr + WHERE nom_schema = n_schema ; + + ------ SCHEMA NON REFERENCE ------ + -- ajouté à gestion_schema + -- le reste est pris en charge par le trigger + -- on_modify_gestion_schema_after + IF NOT FOUND + THEN + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) + VALUES (n_schema, n_owner, true) ; + RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', n_schema ; + + IF b_preserve + THEN + RETURN '__ INITIALISATION REUSSIE.' ; + END IF ; + + ------- SCHEMA PRE-REFERENCE ------ + -- présent dans gestion_schema avec creation valant + -- False. + ELSIF NOT cree + THEN + IF NOT b_gs + THEN + UPDATE z_asgard.gestion_schema_usr + SET creation = true, + producteur = n_owner + WHERE n_schema = nom_schema ; + ELSE + UPDATE z_asgard.gestion_schema_usr + SET creation = true + WHERE n_schema = nom_schema ; + END IF ; + RAISE NOTICE '... Le schéma % a été marqué comme créé dans la table de gestion.', n_schema ; + + IF b_preserve + THEN + RETURN '__ INITIALISATION REUSSIE.' ; + END IF ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur + INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = n_schema ; + + ------ REMISE A PLAT DES PROPRIETAIRES ------ + -- uniquement pour les schémas qui étaient déjà + -- référencés dans gestion_schema (pour les autres, pris en charge + -- par le trigger on_modify_gestion_schema_after) + + -- schéma dont le propriétaire ne serait pas le producteur + IF cree + THEN + IF NOT roles.producteur = n_owner + THEN + -- permission sur le producteur + IF NOT pg_has_role(roles.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FIS4. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', n_schema + USING HINT = format('Il vous faut être membre du rôle producteur %s.') ; + END IF ; + -- propriétaire du schéma + contenu + RAISE NOTICE '(ré)attribution de la propriété du schéma et des objets au rôle producteur du schéma :' ; + PERFORM z_asgard.asgard_admin_proprietaire(n_schema, roles.producteur) ; + + -- schema dont le propriétaire est le producteur + ELSE + -- reprise uniquement des propriétaires du contenu + RAISE NOTICE '(ré)attribution de la propriété des objets au rôle producteur du schéma :' ; + SELECT z_asgard.asgard_admin_proprietaire(n_schema, roles.producteur, False) INTO n ; + IF n = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + END IF ; + + ------ DESTRUCTION DES PRIVILEGES ACTUELS ------ + -- hors privilèges par défaut (définis par ALTER DEFAULT PRIVILEGE) + -- et hors révocations des privilèges par défaut de public sur + -- les types et les fonctions + -- pour le propriétaire, ces commandes ont pour effet + -- de remettre les privilèges par défaut supprimés + + -- public + RAISE NOTICE 'remise à zéro des privilèges manuels du pseudo-rôle public :' ; + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_public( + quote_ident(n_schema)::regnamespace)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + END LOOP ; + IF NOT FOUND + THEN + RAISE NOTICE '> néant' ; + END IF ; + + -- autres rôles + RAISE NOTICE 'remise à zéro des privilèges des autres rôles (pour le producteur, les éventuels privilèges manquants sont réattribués) :' ; + FOR r IN (SELECT rolname FROM pg_roles) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role( + quote_ident(n_schema)::regnamespace, quote_ident(r.rolname)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + k := k + 1 ; + END LOOP ; + END LOOP ; + IF NOT FOUND OR k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + ------ RECREATION DES PRIVILEGES DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.editeur) ; + + EXECUTE format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + + EXECUTE format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + END IF ; + + ------ RECREATION DES PRIVILEGES DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + END IF ; + + ------ RECREATION DES PRIVILEGES SUR LES SCHEMAS D'ASGARD ------ + IF n_schema = 'z_asgard' AND (roles.lecteur IS NULL OR NOT roles.lecteur = 'g_consult') + THEN + -- rétablissement des droits de g_consult + RAISE NOTICE 'rétablissement des privilèges attendus pour g_consult :' ; + + GRANT USAGE ON SCHEMA z_asgard TO g_consult ; + RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard TO g_consult' ; + + GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult' ; + + GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult' ; + + GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult' ; + + GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult' ; + + GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult' ; + + ELSIF n_schema = 'z_asgard_admin' + THEN + -- rétablissement des droits de g_admin_ext + RAISE NOTICE 'rétablissement des privilèges attendus pour g_admin_ext :' ; + + GRANT USAGE ON SCHEMA z_asgard_admin TO g_admin_ext ; + RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard_admin TO g_admin_ext' ; + + GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE z_asgard_admin.gestion_schema TO g_admin_ext ; + RAISE NOTICE '> GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE z_asgard_admin.gestion_schema TO g_admin_ext' ; + + END IF ; + + ------ ACL PAR DEFAUT ------ + k := 0 ; + RAISE NOTICE 'suppression des privilèges par défaut :' ; + FOR item IN ( + SELECT + format( + 'ALTER DEFAULT PRIVILEGES FOR ROLE %s IN SCHEMA %s REVOKE %s ON %s FROM %s', + defaclrole::regrole, + defaclnamespace::regnamespace, + -- impossible que defaclnamespace vaille 0 (privilège portant + -- sur tous les schémas) ici, puisque c'est l'OID de n_schema + privilege, + typ_lg, + CASE WHEN grantee = 0 THEN 'public' ELSE grantee::regrole::text END + ) AS commande, + pg_has_role(defaclrole, 'USAGE') AS utilisable, + defaclrole + FROM pg_default_acl, + aclexplode(defaclacl) AS acl (grantor, grantee, privilege, grantable), + unnest(ARRAY['TABLES', 'SEQUENCES', + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTIONS' ELSE 'ROUTINES' END, + -- à ce stade FUNCTIONS et ROUTINES sont équivalents, mais + -- ROUTINES est préconisé + 'TYPES', 'SCHEMAS'], + ARRAY['r', 'S', 'f', 'T', 'n']) AS t (typ_lg, typ_crt) + WHERE defaclnamespace = quote_ident(n_schema)::regnamespace + AND defaclobjtype = typ_crt + ) + LOOP + IF item.utilisable + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + ELSE + RAISE EXCEPTION 'FIS6. Echec. Vous n''avez pas les privilèges nécessaires pour modifier les privilèges par défaut alloués par le rôle %.', item.defaclrole::regrole::text + USING DETAIL = item.commande, + HINT = 'Tentez de relancer la fonction en tant que super-utilisateur.' ; + END IF ; + k := k + 1 ; + END LOOP ; + IF k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + RETURN '__ REINITIALISATION REUSSIE.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FIS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) IS 'ASGARD. Fonction qui réinitialise les privilèges sur un schéma (et l''ajoute à la table de gestion s''il n''y est pas déjà).' ; + + +------ 4.9 - REINITIALISATION DES PRIVILEGES SUR UN OBJET ------ + +-- Function: z_asgard.asgard_initialise_obj(text, text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_obj( + obj_schema text, + obj_nom text, + obj_typ text + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction permet de réinitialiser les droits + sur un objet selon les privilèges standards associés + aux rôles désignés dans la table de gestion pour son schéma. + +ARGUMENTS : +- "obj_schema" est le nom du schéma contenant l'objet, au format +texte et sans guillemets ; +- "obj_nom" est le nom de l'objet, au format texte et (sauf pour +les fonctions !) sans guillemets ; +- "obj_typ" est le type de l'objet au format text ('table', +'partitioned table' (assimilé à 'table'), 'view', 'materialized view', +'foreign table', 'sequence', 'function', 'aggregate', 'procedure', +'routine', 'type', 'domain'). +SORTIE : '__ REINITIALISATION REUSSIE.' si la requête s'est exécutée +normalement. */ +DECLARE + class_info record ; + roles record ; + obj record ; + r record ; + c record ; + l text ; + k int := 0 ; +BEGIN + + -- pour la suite, on assimile les partitions à des tables + IF obj_typ = 'partitioned table' + THEN + obj_typ := 'table' ; + ELSIF obj_typ = ANY (ARRAY['routine', 'procedure', 'function', 'aggregate']) + THEN + -- à partir de PG 11, les fonctions et procédures sont des routines + IF current_setting('server_version_num')::int >= 110000 + THEN + obj_typ := 'routine' ; + -- pour les versions antérieures, les routines et procédures n'existent + -- théoriquement pas, mais on considère que ces mots-clés désignent + -- des fonctions + ELSE + obj_typ := 'function' ; + END IF ; + END IF ; + + ------ TESTS PREALABLES ------ + -- schéma système + IF obj_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FIO1. Opération interdite. Le schéma % est un schéma système.', obj_schema ; + END IF ; + + -- schéma non référencé + IF NOT obj_schema IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FIO2. Echec. Le schéma % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', obj_schema ; + END IF ; + + -- type invalide + récupération des informations sur le catalogue contenant l'objet + SELECT + xtyp, xclass, xreg, + format('%sname', xprefix) AS xname, + format('%sowner', xprefix) AS xowner, + format('%snamespace', xprefix) AS xschema + INTO class_info + FROM unnest( + ARRAY['table', 'foreign table', 'view', 'materialized view', + 'sequence', 'type', 'domain', 'function', 'routine'], + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_class', 'pg_type', 'pg_type', 'pg_proc', 'pg_proc'], + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'typ', 'typ', + 'pro', 'pro'], + ARRAY['regclass', 'regclass', 'regclass', 'regclass', 'regclass', + 'regtype', 'regtype', 'regprocedure', 'regprocedure'] + ) AS typ (xtyp, xclass, xprefix, xreg) + WHERE typ.xtyp = obj_typ ; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'FIO3. Echec. Le type % n''existe pas ou n''est pas pris en charge.', obj_typ + USING HINT = 'Types acceptés : ''table'', ''partitioned table'', ''view'', ''materialized view'', ''foreign table'', ''sequence'', ''function'', ''aggregate'', ''routine'', ''procedure'', ''type'', ''domain''.' ; + END IF ; + + -- objet inexistant + récupération du propriétaire + EXECUTE 'SELECT ' || class_info.xowner || '::regrole::text AS prop, ' + || class_info.xclass || '.oid, ' + || CASE WHEN class_info.xclass = 'pg_type' + THEN quote_literal(quote_ident(obj_schema) || '.' || quote_ident(obj_nom)) || '::text' + ELSE class_info.xclass || '.oid::' || class_info.xreg || '::text' + END || ' AS appel' + || ' FROM pg_catalog.' || class_info.xclass + || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' + THEN class_info.xclass || '.oid::regprocedure::text = ' + || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + ELSE class_info.xname || ' = ' || quote_literal(obj_nom) + || ' AND ' || class_info.xschema || '::regnamespace::text = ' + || quote_literal(quote_ident(obj_schema)) END + INTO obj ; + + IF obj.prop IS NULL + THEN + RAISE EXCEPTION 'FIO4. Echec. L''objet % n''existe pas.', obj_nom ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj_schema ; + + -- permission sur le producteur + IF NOT pg_has_role(roles.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FIO5. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', obj_schema + USING HINT = format('Il vous faut être membre du rôle producteur %s.', roles.producteur) ; + END IF ; + + ------ REMISE A PLAT DU PROPRIETAIRE ------ + IF NOT obj.prop = quote_ident(roles.producteur) + THEN + -- permission sur le propriétaire de l'objet + IF NOT pg_has_role(obj.prop::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FIO6. Echec. Vous ne disposez pas des permissions nécessaires sur l''objet % pour réaliser cette opération.', obj_nom + USING HINT = format('Il vous faut être membre du rôle propriétaire de l''objet (%s).', obj.prop) ; + END IF ; + + RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', obj_nom ; + l := format('ALTER %s %s OWNER TO %I', obj_typ, obj.appel, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + + ------ DESTRUCTION DES PRIVILEGES ACTUELS ------ + -- hors privilèges par défaut (définis par ALTER DEFAULT PRIVILEGE) + -- et hors révocations des privilèges par défaut de public sur + -- les types et les fonctions + -- pour le propriétaire, ces commandes ont pour effet + -- de remettre les privilèges par défaut supprimés + + -- public + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + RAISE NOTICE 'remise à zéro des privilèges manuels du pseudo-rôle public :' ; + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + END LOOP ; + IF NOT FOUND + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + + -- autres rôles + RAISE NOTICE 'remise à zéro des privilèges des autres rôles (pour le producteur, les éventuels privilèges manquants sont réattribués) :' ; + FOR r IN (SELECT rolname FROM pg_roles) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(r.rolname)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + k := k + 1 ; + END LOOP ; + END LOOP ; + IF NOT FOUND OR k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + ------ RECREATION DES PRIVILEGES DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %I.%I TO %I', + obj_schema, obj_nom, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %I.%I TO %I', + obj_schema, obj_nom, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ RECREATION DES PRIVILEGES DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %I.%I TO %I', + obj_schema, obj_nom, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %I.%I TO %I', + obj_schema, obj_nom, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + RETURN '__ REINITIALISATION REUSSIE.' ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_initialise_obj(text, text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_initialise_obj(text, text, text) IS 'ASGARD. Fonction qui réinitialise les privilèges sur un objet.' ; + + +------ 4.10 - DEPLACEMENT D'OBJET ------ + +-- Function: z_asgard.asgard_deplace_obj(text, text, text, text, int) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_deplace_obj( + obj_schema text, + obj_nom text, + obj_typ text, + schema_cible text, + variante int DEFAULT 1 + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction permet de déplacer un objet vers un nouveau + schéma en spécifiant la gestion voulue sur les droits de + l'objet : transfert ou réinitialisation des privilèges. + Dans le cas d'une table avec un ou plusieurs champs de + type serial, elle prend aussi en charge les privilèges + sur les séquences associées. +ARGUMENTS : +- "obj_schema" est le nom du schéma contenant l'objet, au format +texte et sans guillemets ; +- "obj_nom" est le nom de l'objet, au format texte et sans +guillemets ; +- "obj_typ" est le type de l'objet au format text, parmi 'table', +'partitioned table' (assimilé à 'table'), 'view', 'materialized view', +'foreign table', 'sequence', 'function', 'aggregate', 'procedure', +'routine', 'type' et 'domain' ; +- "schema_cible" est le nom du schéma où doit être déplacé l'objet, +au format texte et sans guillemets ; +- "variante" [optionnel] est un entier qui définit le comportement +attendu par l'utilisateur vis à vis des privilèges : + - 1 (valeur par défaut) | TRANSFERT COMPLET + CONSERVATION : + les privilèges des rôles producteur, éditeur et lecteur de + l'ancien schéma sont transférés sur ceux du nouveau. Si un + éditeur ou lecteur a été désigné pour le nouveau schéma mais + qu'aucun n'était défini pour l'ancien, le rôle reçoit les + privilèges standards pour sa fonction. Le cas échéant, + les privilèges des autres rôles sont conservés ; + - 2 | REINITIALISATION COMPLETE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont supprimés ; + - 3 | TRANSFERT COMPLET + NETTOYAGE : les privilèges des rôles + producteur, éditeur et lecteur de l'ancien schéma sont transférés + sur ceux du nouveau. Si un éditeur ou lecteur a été désigné pour + le nouveau schéma mais qu'aucun n'était défini pour l'ancien, + le rôle reçoit les privilèges standards pour sa fonction. + Les privilèges des autres rôles sont supprimés ; + - 4 | TRANSFERT PRODUCTEUR + CONSERVATION : les privilèges de + l'ancien producteur sont transférés sur le nouveau. Les privilèges + des autres rôles sont conservés tels quels. C'est le comportement + d'une commande ALTER [...] SET SCHEMA (interceptée par l'event + trigger asgard_on_alter_objet) ; + - 5 | TRANSFERT PRODUCTEUR + REINITIALISATION : les privilèges + de l'ancien producteur sont transférés sur le nouveau. Les + nouveaux éditeur et lecteur reçoivent les privilèges standards. + Les privilèges des autres rôles sont supprimés ; + - 6 | REINITIALISATION PARTIELLE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont conservés. +SORTIE : '__ DEPLACEMENT REUSSI.' si la requête s'est exécutée normalement. */ +DECLARE + class_info record ; + roles record ; + roles_cible record ; + obj record ; + r record ; + c record ; + l text ; + c_lecteur text[] ; + c_editeur text[] ; + c_producteur text[] ; + c_n_lecteur text[] ; + c_n_editeur text[] ; + c_autres text[] ; + seq_liste oid[] ; + a text[] ; + s record ; + o oid ; + supported boolean ; +BEGIN + + obj_typ := lower(obj_typ) ; + + -- pour la suite, on assimile les partitions à des tables + IF obj_typ = 'partitioned table' + THEN + obj_typ := 'table' ; + ELSIF obj_typ = ANY (ARRAY['routine', 'procedure', 'function', 'aggregate']) + THEN + -- à partir de PG 11, les fonctions et procédures sont des routines + IF current_setting('server_version_num')::int >= 110000 + THEN + obj_typ := 'routine' ; + -- pour les versions antérieures, les routines et procédures n'existent + -- théoriquement pas, mais on considère que ces mots-clés désignent + -- des fonctions + ELSE + obj_typ := 'function' ; + END IF ; + END IF ; + + ------ TESTS PREALABLES ------ + -- schéma système + IF obj_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FDO1. Opération interdite. Le schéma % est un schéma système.', obj_schema ; + END IF ; + + -- schéma de départ non référencé + IF NOT obj_schema IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FDO2. Echec. Le schéma % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', obj_schema ; + END IF ; + + -- schéma cible non référencé + IF NOT schema_cible IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FDO3. Echec. Le schéma cible % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', schema_cible ; + END IF ; + + -- type invalide + récupération des informations sur le catalogue contenant l'objet + SELECT + xtyp, xclass, xreg, + format('%sname', xprefix) AS xname, + format('%sowner', xprefix) AS xowner, + format('%snamespace', xprefix) AS xschema + INTO class_info + FROM unnest( + ARRAY['table', 'foreign table', 'view', 'materialized view', + 'sequence', 'type', 'domain', 'function', 'routine'], + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_class', 'pg_type', 'pg_type', 'pg_proc', 'pg_proc'], + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'typ', 'typ', + 'pro', 'pro'], + ARRAY['regclass', 'regclass', 'regclass', 'regclass', 'regclass', + 'regtype', 'regtype', 'regprocedure', 'regprocedure'] + ) AS typ (xtyp, xclass, xprefix, xreg) + WHERE typ.xtyp = obj_typ ; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'FDO4. Echec. Le type % n''existe pas ou n''est pas pris en charge.', obj_typ + USING HINT = 'Types acceptés : ''table'', ''partitioned table'', ''view'', ''materialized view'', ''foreign table'', ''sequence'', ''function'', ''aggregate'', ''procedure'', ''routine'', ''type'', ''domain''.' ; + END IF ; + + -- objet inexistant + récupération du propriétaire + EXECUTE 'SELECT ' || class_info.xowner || '::regrole::text AS prop, ' + || class_info.xclass || '.oid, ' + || CASE WHEN class_info.xclass = 'pg_type' + THEN quote_literal(quote_ident(obj_schema) || '.' || quote_ident(obj_nom)) || '::text' + ELSE class_info.xclass || '.oid::' || class_info.xreg || '::text' + END || ' AS appel' + || ' FROM pg_catalog.' || class_info.xclass + || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' + THEN class_info.xclass || '.oid::regprocedure::text = ' + || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + ELSE class_info.xname || ' = ' || quote_literal(obj_nom) + || ' AND ' || class_info.xschema || '::regnamespace::text = ' + || quote_literal(quote_ident(obj_schema)) END + INTO obj ; + + IF obj.prop IS NULL + THEN + RAISE EXCEPTION 'FDO5. Echec. L''objet % n''existe pas.', obj_nom ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + -- schéma de départ : + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj_schema ; + + -- schéma cible : + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles_cible + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = schema_cible ; + + -- permission sur le producteur du schéma cible + IF NOT pg_has_role(roles_cible.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FDO6. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma cible % pour réaliser cette opération.', schema_cible + USING HINT = format('Il vous faut être membre du rôle producteur %s.', roles_cible.producteur) ; + END IF ; + + -- permission sur le propriétaire de l'objet + IF NOT pg_has_role(obj.prop::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FDO7. Echec. Vous ne disposez pas des permissions nécessaires sur l''objet % pour réaliser cette opération.', obj_nom + USING HINT = format('Il vous faut être membre du rôle propriétaire de l''objet (%s).', obj.prop) ; + END IF ; + + ------ MEMORISATION DES PRIVILEGES ACTUELS ------ + -- ancien producteur : + SELECT array_agg(commande) INTO c_producteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.producteur)::regrole) ; + + -- ancien éditeur : + IF roles.editeur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_editeur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_editeur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.editeur)::regrole) ; + END IF ; + + -- ancien lecteur : + IF roles.lecteur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_lecteur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_lecteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.lecteur)::regrole) ; + END IF ; + + -- nouvel éditeur : + IF roles_cible.editeur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_n_editeur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles_cible.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_n_editeur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles_cible.editeur)::regrole) ; + END IF ; + + -- nouveau lecteur : + IF roles_cible.lecteur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_n_lecteur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles_cible.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_n_lecteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles_cible.lecteur)::regrole) ; + END IF ; + + -- autres rôles : + -- pour ces commandes, contrairement aux précédentes, le rôle + -- est inséré dès maintenant (avec "format") + -- public + IF NOT 'public' = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL)) + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(format(commande, 'public')) INTO c_autres + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + END IF ; + -- et le reste + FOR r IN (SELECT rolname FROM pg_roles + WHERE NOT rolname = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL))) + LOOP + SELECT array_agg(format(commande, r.rolname::text)) INTO a + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(r.rolname)::regrole) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END LOOP ; + + ------ PRIVILEGES SUR LES SEQUENCES ASSOCIEES ------ + IF obj_typ = 'table' + THEN + -- dans le cas d'une table, on recherche les séquences + -- utilisées par ses éventuels champs de type serial ou + -- IDENTITY + -- elles sont repérées par le fait qu'il existe + -- une dépendance entre la séquence et un champ de la table : + -- de type DEPENDENCY_AUTO (a) pour la séquence d'un champ serial + -- de type DEPENDENCY_INTERNAL (i) pour la séquence d'un champ IDENDITY + FOR s IN ( + SELECT + pg_class.oid + FROM pg_catalog.pg_depend LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = pg_depend.objid + WHERE pg_depend.classid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refclassid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refobjid = obj.oid + AND pg_depend.refobjsubid > 0 + AND pg_depend.deptype = ANY (ARRAY['a', 'i']) + AND pg_class.relkind = 'S' + ) + LOOP + -- liste des séquences + seq_liste := array_append(seq_liste, s.oid) ; + + -- récupération des privilèges + -- ancien producteur : + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence', quote_ident(roles.producteur)::regrole) ; + IF FOUND + THEN + c_producteur := array_cat(c_producteur, a) ; + a := NULL ; + END IF ; + + -- ancien éditeur : + IF roles.editeur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles.editeur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_editeur := array_cat(c_editeur, a) ; + a := NULL ; + END IF ; + + -- ancien lecteur : + IF roles.lecteur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles.lecteur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_lecteur := array_cat(c_lecteur, a) ; + a := NULL ; + END IF ; + + -- nouvel éditeur : + IF roles_cible.editeur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles_cible.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles_cible.editeur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_n_editeur := array_cat(c_n_editeur, a) ; + a := NULL ; + END IF ; + + -- nouveau lecteur : + IF roles_cible.lecteur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles_cible.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles_cible.lecteur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_n_lecteur := array_cat(c_n_lecteur, a) ; + a := NULL ; + END IF ; + + -- autres rôles : + -- public + IF NOT 'public' = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL)) + THEN + SELECT array_agg(format(commande, 'public')) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END IF ; + -- et le reste + FOR r IN (SELECT rolname FROM pg_roles + WHERE NOT rolname = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL))) + LOOP + SELECT array_agg(format(commande, r.rolname::text)) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(r.rolname)::regrole) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END LOOP ; + END LOOP ; + END IF ; + + ------ DEPLACEMENT DE L'OBJET ------ + EXECUTE format('ALTER %s %s SET SCHEMA %I', obj_typ, obj.appel, schema_cible) ; + + RAISE NOTICE '... Objet déplacé dans le schéma %.', schema_cible ; + + ------ PRIVILEGES DU PRODUCTEUR ------ + -- par défaut, ils ont été transférés + -- lors du changement de propriétaire, il + -- n'y a donc qu'à réinitialiser pour les + -- variantes 2 et 6 + + -- objet, réinitialisation pour 2 et 6 + IF variante IN (2, 6) AND (c_producteur IS NOT NULL) + THEN + RAISE NOTICE 'réinitialisation des privilèges du nouveau producteur, % :', roles_cible.producteur ; + FOREACH l IN ARRAY c_producteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.producteur) ; + RAISE NOTICE '> %', format(l, roles_cible.producteur) ; + END LOOP ; + END IF ; + + ------- PRIVILEGES EDITEUR ------ + -- révocation des privilèges du nouvel éditeur + IF roles_cible.editeur IS NOT NULL + AND (roles.editeur IS NULL OR NOT roles.editeur = roles_cible.editeur) + AND NOT roles.producteur = roles_cible.editeur + AND NOT variante = 4 + AND c_n_editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges pré-existants du nouvel éditeur, % :', roles_cible.editeur ; + FOREACH l IN ARRAY c_n_editeur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.editeur) ; + RAISE NOTICE '> %', format(l, roles_cible.editeur) ; + END LOOP ; + END IF ; + + -- révocation des privilèges de l'ancien éditeur + IF roles.editeur IS NOT NULL AND NOT roles.editeur = roles_cible.producteur + AND (roles_cible.editeur IS NULL OR NOT roles.editeur = roles_cible.editeur OR NOT variante IN (1,3)) + AND NOT variante = 4 + AND c_editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges de l''ancien éditeur, % :', roles.editeur ; + FOREACH l IN ARRAY c_editeur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles.editeur) ; + RAISE NOTICE '> %', format(l, roles.editeur) ; + END LOOP ; + END IF ; + + -- reproduction sur le nouvel éditeur pour les variantes 1 et 3 + IF roles.editeur IS NOT NULL + AND roles_cible.editeur IS NOT NULL + AND variante IN (1, 3) + AND c_editeur IS NOT NULL + AND NOT roles.editeur = roles_cible.editeur + THEN + RAISE NOTICE 'transfert des privilèges de l''ancien éditeur vers le nouvel éditeur, % :', roles_cible.editeur ; + FOREACH l IN ARRAY c_editeur + LOOP + l := replace(l, format('%I.', obj_schema), format('%I.', schema_cible)) ; + EXECUTE format(l, roles_cible.editeur) ; + RAISE NOTICE '> %', format(l, roles_cible.editeur) ; + END LOOP ; + END IF ; + + -- attribution des privilèges standard au nouvel éditeur + -- pour les variantes 2, 5, 6 + -- ou s'il n'y avait pas de lecteur sur l'ancien schéma + IF roles_cible.editeur IS NOT NULL + AND (variante IN (2, 5, 6) OR roles.editeur IS NULL) + AND NOT variante = 4 + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences libres : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + -- sur les séquences des champs serial : + IF seq_liste IS NOT NULL + THEN + FOREACH o IN ARRAY seq_liste + LOOP + l := format('GRANT SELECT, USAGE ON SEQUENCE %s TO %I', + o::regclass, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + END IF ; + + ------- PRIVILEGES LECTEUR ------ + -- révocation des privilèges du nouveau lecteur + IF roles_cible.lecteur IS NOT NULL + AND (roles.lecteur IS NULL OR NOT roles.lecteur = roles_cible.lecteur) + AND NOT roles.producteur = roles_cible.lecteur + AND (roles.editeur IS NULL OR NOT roles.editeur = roles_cible.lecteur) + AND NOT variante = 4 + AND c_n_lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges pré-existants du nouveau lecteur, % :', roles_cible.lecteur ; + FOREACH l IN ARRAY c_n_lecteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.lecteur) ; + RAISE NOTICE '> %', format(l, roles_cible.lecteur) ; + END LOOP ; + END IF ; + + -- révocation des privilèges de l'ancien lecteur + IF roles.lecteur IS NOT NULL AND NOT roles.lecteur = roles_cible.producteur + AND (roles_cible.editeur IS NULL OR NOT roles.lecteur = roles_cible.editeur) + AND (roles_cible.lecteur IS NULL OR NOT roles.lecteur = roles_cible.lecteur OR NOT variante IN (1,3)) + AND NOT variante = 4 + AND c_lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges de l''ancien lecteur, % :', roles.lecteur ; + FOREACH l IN ARRAY c_lecteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles.lecteur) ; + RAISE NOTICE '> %', format(l, roles.lecteur) ; + END LOOP ; + END IF ; + + -- reproduction sur le nouveau lecteur pour les variantes 1 et 3 + IF roles.lecteur IS NOT NULL + AND roles_cible.lecteur IS NOT NULL + AND variante IN (1, 3) + AND c_lecteur IS NOT NULL + AND NOT roles.lecteur = roles_cible.lecteur + THEN + RAISE NOTICE 'transfert des privilèges de l''ancien lecteur vers le nouveau lecteur, % :', roles_cible.lecteur ; + FOREACH l IN ARRAY c_lecteur + LOOP + l := replace(l, format('%I.', obj_schema), format('%I.', schema_cible)) ; + EXECUTE format(l, roles_cible.lecteur) ; + RAISE NOTICE '> %', format(l, roles_cible.lecteur) ; + END LOOP ; + END IF ; + + -- attribution des privilèges standard au nouveau lecteur + -- pour les variantes 2, 5, 6 + -- ou s'il n'y avait pas de lecteur sur l'ancien schéma + IF roles_cible.lecteur IS NOT NULL + AND (variante IN (2, 5, 6) OR roles.lecteur IS NULL) + AND NOT variante = 4 + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences libres : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + -- sur les séquences des champs serial : + IF seq_liste IS NOT NULL + THEN + FOREACH o IN ARRAY seq_liste + LOOP + l := format('GRANT SELECT ON SEQUENCE %s TO %I', o::regclass, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + END IF ; + + ------ AUTRES ROLES ------ + -- pour les variantes 2, 3, 5, remise à zéro + IF variante IN (2, 3, 5) + AND c_autres IS NOT NULL + THEN + RAISE NOTICE 'remise à zéro des privilèges des autres rôles :' ; + FOREACH l IN ARRAY c_autres + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + + RETURN '__ DEPLACEMENT REUSSI.' ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) IS 'ASGARD. Fonction qui prend en charge le déplacement d''un objet dans un nouveau schéma, avec une gestion propre des privilèges.' ; + + +------ 4.11 - OCTROI D'UN RÔLE À TOUS LES RÔLES DE CONNEXION ------ + +-- Function: z_asgard_admin.asgard_all_login_grant_role(text, boolean) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_all_login_grant_role(n_role text, b boolean DEFAULT True) + RETURNS int + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction confère à tous les rôles de connexion du + serveur l'appartenance au rôle donné en argument. +ARGUMENTS : +- n_role : une chaîne de caractères présumée correspondre à un nom de + rôle valide ; +- b : [optionnel] un booléen. Si b vaut False et qu'un rôle de connexion est +déjà membre du rôle considéré par héritage, la fonction ne fait rien. Si +b vaut True (défaut), la fonction ne passera un rôle de connexion que s'il est +lui-même membre du rôle considéré. +SORTIE : un entier correspondant au nombre de rôles pour lesquels +la permission a été accordée. */ +DECLARE + roles record ; + attributeur text ; + utilisateur text := current_user ; + c text ; + n int := 0 ; +BEGIN + ------ TESTS PREALABLES ----- + -- existance du rôle + IF NOT n_role IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FLG1. Echec. Le rôle % n''existe pas', n_role ; + END IF ; + + -- on cherche un rôle dont l'utilisateur est + -- membre et qui, soit a l'attribut CREATEROLE + -- soit a ADMIN OPTION sur le rôle + SELECT rolname INTO attributeur + FROM pg_roles + WHERE pg_has_role(rolname, 'MEMBER') AND rolcreaterole + ORDER BY rolname = current_user DESC ; + IF NOT FOUND + THEN + SELECT grantee INTO attributeur + FROM information_schema.applicable_roles + WHERE is_grantable = 'YES' AND role_name = n_role ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'FLG2. Opération interdite. Permissions insuffisantes pour le rôle %.', n_role + USING HINT = 'Votre rôle doit être membre de ' || n_role + || ' avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + END IF ; + END IF ; + + EXECUTE 'SET ROLE ' || quote_ident(attributeur) ; + + IF b + THEN + FOR roles IN SELECT rolname + FROM pg_roles LEFT JOIN pg_auth_members + ON member = pg_roles.oid AND roleid = n_role::regrole::oid + WHERE rolcanlogin AND member IS NULL + AND NOT rolsuper + LOOP + c := 'GRANT ' || quote_ident(n_role) || ' TO ' || quote_ident(roles.rolname) ; + EXECUTE c ; + RAISE NOTICE '> %', c ; + n := n + 1 ; + END LOOP ; + ELSE + FOR roles IN SELECT rolname FROM pg_roles + WHERE rolcanlogin AND NOT pg_has_role(rolname, n_role, 'MEMBER') + LOOP + c := 'GRANT ' || quote_ident(n_role) || ' TO ' || quote_ident(roles.rolname) ; + EXECUTE c ; + RAISE NOTICE '> %', c ; + n := n + 1 ; + END LOOP ; + END IF ; + + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + + RETURN n ; +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) IS 'ASGARD. Fonction qui confère à tous les rôles de connexion du serveur l''appartenance au rôle donné en argument.' ; + + + +------ 4.12 - IMPORT DE LA NOMENCLATURE DANS GESTION_SCHEMA ------ + +-- Function: z_asgard_admin.asgard_import_nomenclature(text[]) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_import_nomenclature( + domaines text[] default NULL::text[] + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Fonction qui importe dans la table de gestion les schémas manquants + de la nomenclature nationale - ou de certains domaines + de la nomenclature nationale listés en argument - toujours avec + creation valant False, même si le schéma existe (mais n'a pas été + référencé). + Des messages informent l'opérateur des schémas effectivement ajoutés. + Lorsque le schéma est déjà référencé dans la table de gestion, réappliquer + la fonction a pour effet de mettre à jour les champs relatifs à la + nomenclature. +ARGUMENT : domaines (optionnel) : un tableau text[] contenant les noms des +domaines à importer, soit le "niveau 1"/niv1 ou niv1_abr des schémas. Si non renseigné, +toute la nomenclature est importée (hors schémas déjà référencés). +SORTIE : '__ FIN IMPORT NOMENCLATURE.' si la requête s'est exécutée normalement. */ +DECLARE + item record ; + e_mssg text ; + e_detl text ; + e_hint text ; +BEGIN + FOR item IN SELECT * FROM ( + VALUES + ('c', true, 'Données génériques', 'donnee_generique', 'Découpage électoral', 'decoupage_electoral', 'c_don_gen_decoupage_electoral', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Démographie', 'demographie', 'c_don_gen_demographie', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Habillage des cartes', 'habillage', 'c_don_gen_habillage', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Intercommunalité', 'intercommunalite', 'c_don_gen_intercommunalite', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Milieu physique', 'milieu_physique', 'c_don_gen_milieu_physique', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Alimentation en eau potable', 'aep', 'c_eau_aep', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Assainissement', 'assainissement', 'c_eau_assainissement', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Masses d’eau', 'masse_eau', 'c_eau_masse_eau', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Ouvrages', 'ouvrage', 'c_eau_ouvrage', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Pêche', 'peche', 'c_eau_peche', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Surveillance', 'surveillance', 'c_eau_surveillance', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Environnement', 'agri_environnement', 'c_agri_environnement', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Agro-alimentaire', 'agro_alimentaire', 'c_agri_agroalimentaire', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Exploitation & élevage', 'exploitation_elevage', 'c_agri_exploi_elevage', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Parcellaire agricole', 'parcellaire_agricole', 'c_agri_parcellaire_agricole', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Santé animale', 'sante_animale', 'c_agri_sante_animale', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Santé végétale', 'sante_vegetale', 'c_agri_sante_vegetale', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Séismes', 'seisme', 'c_risque_seisme', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Agriculture', 'agriculture', 'Zonages agricoles', 'zonages_agricoles', 'c_agri_zonages_agricoles', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Air & climat', 'air_climat', 'Changement climatique', 'changement_climatique', 'c_air_clim_changement', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Air & climat', 'air_climat', 'Météorologie', 'meteo', 'c_air_clim_meteo', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Air & climat', 'air_climat', 'Qualité de l’air & pollution', 'qualite_pollution', 'c_air_clim_qual_pollu', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Aménagement & urbanisme', 'amenagement_urbanisme', 'Assiettes des servitudes', 'assiette_servitude', 'c_amgt_urb_servitude', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Aménagement & urbanisme', 'amenagement_urbanisme', 'Politique européenne', 'politique_europeenne', 'c_amgt_urb_pol_euro', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Aménagement & urbanisme', 'amenagement_urbanisme', 'Zonages d’aménagement', 'zonages_amenagement', 'c_amgt_urb_zon_amgt', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Aménagement & urbanisme', 'amenagement_urbanisme', 'Zonages d’études', 'zonages_etudes', 'c_amgt_urb_zon_etudes', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Aménagement & urbanisme', 'amenagement_urbanisme', 'Zonages de planification', 'zonages_planification', 'c_amgt_urb_zon_plan', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Enseignement', 'enseignement', 'c_cult_soc_ser_enseignement', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Équipements sportifs et culturels', 'equipement_sportif_culturel', 'c_cult_soc_ser_equip_sport_cult', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Autres établissements', 'erp_autre', 'c_cult_soc_ser_erp_autre', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Patrimoine culturel', 'patrimoine_culturel', 'c_cult_soc_ser_patrim_cult', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Santé & social', 'sante_social', 'c_cult_soc_ser_sante_social', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Culture, société & services', 'culture_societe_service', 'Tourisme', 'tourisme', 'c_cult_soc_ser_tourisme', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Action publique', 'action_publique', 'c_don_gen_action_publique', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Données génériques', 'donnee_generique', 'Découpage administratif', 'administratif', 'c_don_gen_administratif', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Travaux & entretien', 'travail_action', 'c_eau_travail_action', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Autres utilisations', 'utilisation_autre', 'c_eau_utilisation_autre', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Eau', 'eau', 'Zonages eau', 'zonages_eau', 'c_eau_zonages', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Foncier & sol', 'foncier_sol', 'Foncier agricole', 'foncier_agricole', 'c_fon_sol_agricole', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Foncier & sol', 'foncier_sol', 'Mutations foncières', 'mutation_fonciere', 'c_fon_sol_mutation', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Foncier & sol', 'foncier_sol', 'Occupation du sol', 'occupation_sol', 'c_fon_sol_occupation', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Foncier & sol', 'foncier_sol', 'Propriétés foncières', 'propriete_fonciere', 'c_fon_sol_propriete', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Forêt', 'foret', 'Description', 'description', 'c_foret_description', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Forêt', 'foret', 'Défense de la forêt contre les incendies', 'dfci', 'c_foret_dfci', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Forêt', 'foret', 'Gestion', 'gestion', 'c_foret_gestion', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Forêt', 'foret', 'Règlement', 'reglement', 'c_foret_reglement', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Forêt', 'foret', 'Transformation', 'transformation', 'c_foret_transformation', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Accession à la propriété', 'accession_propriete', 'c_hab_vil_access_propriete', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Besoin en logements', 'besoin_en_logement', 'c_hab_vil_besoin_logt', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Construction', 'construction', 'c_hab_vil_construction', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Habitat indigne', 'habitat_indigne', 'c_hab_vil_habitat_indigne', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Occupation des logements', 'occupation_logements', 'c_hab_vil_occupation_logt', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Parc locatif social', 'parc_locatif_social', 'c_hab_vil_parc_loc_social', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Parc de logements', 'parc_logements', 'c_hab_vil_parc_logt', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Politique', 'politique', 'c_hab_vil_politique', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Habitat & politique de la ville', 'habitat_politique_de_la_ville', 'Rénovation', 'renovation', 'c_hab_vil_renovation', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Autres activités', 'autres_activites', 'c_mer_litt_autres_activites', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Chasse maritime', 'chasse_maritime', 'c_mer_litt_chasse_maritime', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Culture marine', 'culture_marine', 'c_mer_litt_culture_marine', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Écologie du littoral', 'ecologie_littoral', 'c_mer_litt_ecol_littoral', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Limites administratives spéciales', 'lim_admin_speciale', 'c_mer_litt_lim_admin_spe', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Lutte anti-pollution', 'lutte_anti_pollution', 'c_mer_litt_lutte_anti_pollu', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Navigation maritime', 'navigation_maritime', 'c_mer_litt_nav_maritime', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Pêche maritime', 'peche_maritime', 'c_mer_litt_peche_maritime', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Mer & littoral', 'mer_littoral', 'Topographie', 'topographie', 'c_mer_litt_topographie', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nature, paysage & biodiversité', 'nature_paysage_biodiversite', 'Chasse', 'chasse', 'c_nat_pays_bio_chasse', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nature, paysage & biodiversité', 'nature_paysage_biodiversite', 'Inventaires nature & biodiversité', 'inventaire_nature_biodiversite', 'c_nat_pays_bio_invent_nat_bio', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nature, paysage & biodiversité', 'nature_paysage_biodiversite', 'Inventaires paysages', 'inventaire_paysage', 'c_nat_pays_bio_invent_pays', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nature, paysage & biodiversité', 'nature_paysage_biodiversite', 'Zonages nature', 'zonage_nature', 'c_nat_pays_bio_zonage_nat', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nature, paysage & biodiversité', 'nature_paysage_biodiversite', 'Zonages paysages', 'zonage_paysage', 'c_nat_pays_bio_zonage_pays', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nuisances', 'nuisance', 'Bruit', 'bruit', 'c_nuis_bruit', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nuisances', 'nuisance', 'Déchets', 'dechet', 'c_nuis_dechet', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nuisances', 'nuisance', 'Nuisances électromagnétiques', 'nuisance_electromagnetique', 'c_nuis_electromag', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Nuisances', 'nuisance', 'Pollution des sols', 'pollution_sol', 'c_nuis_pollu_sol', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Réseaux & énergie', 'reseau_energie_divers', 'Aménagement numérique du territoire', 'amenagement_numerique_territoire', 'c_res_energ_amgt_num_terri', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Réseaux & énergie', 'reseau_energie_divers', 'Autre', 'autre', 'c_res_energ_autre', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Réseaux & énergie', 'reseau_energie_divers', 'Électricité', 'electricite', 'c_res_energ_electricite', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Réseaux & énergie', 'reseau_energie_divers', 'Hydrocarbures', 'hydrocarbure', 'c_res_energ_hydrocarbure', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Réseaux & énergie', 'reseau_energie_divers', 'Télécommunications', 'telecommunication', 'c_res_energ_telecom', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Avalanche', 'avalanche', 'c_risque_avalanche', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Éruptions volcaniques', 'eruption_volcanique', 'c_risque_eruption_volcanique', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Gestion des risques', 'gestion_risque', 'c_risque_gestion_risque', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Inondations', 'inondation', 'c_risque_inondation', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Mouvements de terrain', 'mouvement_terrain', 'c_risque_mouvement_terrain', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Radon', 'radon', 'c_risque_radon', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Risques miniers', 'risque_minier', 'c_risque_minier', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Risques technologiques', 'risque_technologique', 'c_risque_techno', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Zonages risques naturels', 'zonages_risque_naturel', 'c_risque_zonages_naturel', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Risques', 'risque', 'Zonages risques technologiques', 'zonages_risque_technologique', 'c_risque_zonages_techno', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Sites industriels & production', 'site_industriel_production', 'Mines, carrières & granulats', 'mine_carriere_granulats', 'c_indus_prod_mine_carriere_granul', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Sites industriels & production', 'site_industriel_production', 'Sites éoliens', 'site_eolien', 'c_indus_prod_eolien', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Sites industriels & production', 'site_industriel_production', 'Sites industriels', 'site_industriel', 'c_indus_prod_industriel', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Sites industriels & production', 'site_industriel_production', 'Sites de production d’énergie', 'site_production_energie', 'c_indus_prod_prod_energ', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Socio-économie', 'socio_economie', ' ', ' ', 'c_socio_eco', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Sécurité routière', 'securite_routiere', 'c_tr_depl_securite_routiere', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Transport collectif', 'tr_collectif', 'c_tr_depl_collectif', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Transport exceptionnel', 'tr_exceptionnel', 'c_tr_depl_exceptionnel', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Transport de marchandises', 'tr_marchandise', 'c_tr_depl_marchandise', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Transport de matières dangereuses', 'tr_matiere_dangereuse', 'c_tr_depl_mat_dangereuse', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Déplacements', 'transport_deplacement', 'Trafic', 'trafic', 'c_tr_depl_trafic', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Aérien', 'aerien', 'c_tr_infra_aerien', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Circulation douce', 'circulation_douce', 'c_tr_infra_circulation_douce', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Ferroviaire', 'ferroviaire', 'c_tr_infra_ferroviaire', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Fluvial', 'fluvial', 'c_tr_infra_fluvial', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Maritime', 'maritime', 'c_tr_infra_maritime', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Plateformes multimodales', 'plateforme_multimodale', 'c_tr_infra_plateforme_multimod', false, 'g_admin', NULL, 'g_consult'), + ('c', true, 'Infrastructures de transport', 'transport_infrastructure', 'Routier', 'routier', 'c_tr_infra_routier', false, 'g_admin', NULL, 'g_consult') + ) AS t (bloc, nomenclature, niv1, niv1_abr, niv2, niv2_abr, nom_schema, creation, producteur, editeur, lecteur) + WHERE domaines IS NULL OR niv1 = ANY(domaines) OR niv1_abr = ANY(domaines) + LOOP + -- si le schéma n'était pas déjà référencé, il est ajouté + -- (toujours comme non créé, même s'il existe par ailleurs dans la base) + IF NOT item.nom_schema IN (SELECT gestion_schema_usr.nom_schema FROM z_asgard.gestion_schema_usr) + THEN + INSERT INTO z_asgard.gestion_schema_usr + (bloc, nomenclature, niv1, niv1_abr, niv2, niv2_abr, nom_schema, creation, producteur, editeur, lecteur) VALUES + (item.bloc, item.nomenclature, item.niv1, item.niv1_abr, item.niv2, item.niv2_abr, item.nom_schema, item.creation, item.producteur, item.editeur, item.lecteur) ; + RAISE NOTICE 'Le schéma % a été ajouté à la table de gestion.', item.nom_schema ; + + -- sinon les champs de la nomenclature sont simplement mis à jour, le cas échéant + ELSIF item.nom_schema IN (SELECT gestion_schema_usr.nom_schema FROM z_asgard.gestion_schema_usr) + THEN + UPDATE z_asgard.gestion_schema_usr + SET nomenclature = item.nomenclature, + niv1 = item.niv1, + niv1_abr = item.niv1_abr, + niv2 = item.niv2, + niv2_abr = item.niv2_abr + WHERE gestion_schema_usr.nom_schema = item.nom_schema + AND (NOT nomenclature = item.nomenclature + OR NOT coalesce(gestion_schema_usr.niv1, '') = coalesce(item.niv1, '') + OR NOT coalesce(gestion_schema_usr.niv1_abr, '') = coalesce(item.niv1_abr, '') + OR NOT coalesce(gestion_schema_usr.niv2, '') = coalesce(item.niv2, '') + OR NOT coalesce(gestion_schema_usr.niv2_abr, '') = coalesce(item.niv2_abr, '')) ; + IF FOUND + THEN + RAISE NOTICE 'Les champs de la nomenclature ont été mis à jour pour le schéma %.', item.nom_schema ; + END IF ; + + END IF ; + END LOOP ; + + RETURN '__ FIN IMPORT NOMENCLATURE.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FIN0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + + +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_import_nomenclature(text[]) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_import_nomenclature(text[]) IS 'ASGARD. Fonction qui importe dans la table de gestion les schémas manquants de la nomenclature nationale - ou de certains domaines de la nomenclature nationale listés en argument.' ; + + +------ 4.13 - REAFFECTATION DES PRIVILEGES D'UN RÔLE ------ + +-- Function: z_asgard_admin.asgard_reaffecte_role(text, text, boolean, boolean, boolean) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_reaffecte_role( + n_role text, + n_role_cible text DEFAULT NULL, + b_hors_asgard boolean DEFAULT False, + b_privileges boolean DEFAULT True, + b_default_acl boolean DEFAULT FALSE + ) + RETURNS text[] + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction transfère tous les privilèges d'un rôle + à un autre, et en premier lieu ses fonctions de producteur, + éditeur et lecteur. Si aucun rôle cible n'est spécifié, les + privilèges sont simplement supprimés et g_admin devient + producteur des schémas, le cas échéant. +ARGUMENTS : +- n_role : une chaîne de caractères présumée correspondre à un nom de + rôle valide ; +- n_role_cible : une chaîne de caractères présumée correspondre à un + nom de rôle valide ; +- b_hors_asgard : un booléen, valeur par défaut False. Si ce paramètre + vaut True, la propriété et les privilèges sur les objets des schémas + non gérés par ASGARD ou hors schémas (par ex la base), sont pris en + compte. La propriété des objets reviendra à g_admin si aucun + rôle cible n'est spécifié ; +- b_privileges : un booléen, valeur par défaut True. Indique si, dans + l'hypothèse où le rôle cible est spécifié, celui-ci doit recevoir + les privilèges et propriétés du rôle (True) ou seulement ses propriétés + (False) ; +- b_default_acl : un booléen, valeur par défaut False. Indique si les + privilèges par défaut doivent être pris en compte (True) ou non (False). +SORTIE : liste (au format text[]) des bases sur lesquelles le rôle a +encore des droits, sinon NULL. */ +DECLARE + item record ; + n_producteur_cible text := coalesce(n_role_cible, 'g_admin') ; + c record ; + k int ; + utilisateur text ; + l_db text[] ; +BEGIN + + ------ TESTS PREALABLES ----- + -- existance du rôle + IF NOT n_role IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FRR1. Echec. Le rôle % n''existe pas', n_role ; + END IF ; + + -- existance du rôle cible + IF n_role_cible IS NOT NULL AND NOT n_role_cible IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FRR2. Echec. Le rôle % n''existe pas', n_role_cible ; + END IF ; + + + IF NOT b_privileges + THEN + n_role_cible := NULL ; + END IF ; + + ------ FONCTION DE PRODUCTEUR ------ + FOR item IN (SELECT * FROM z_asgard.gestion_schema_usr WHERE producteur = n_role) + LOOP + IF item.editeur = n_producteur_cible + THEN + UPDATE z_asgard.gestion_schema_usr + SET editeur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... L''éditeur du schéma % a été supprimé.', item.nom_schema ; + END IF ; + + IF item.lecteur = n_producteur_cible + THEN + UPDATE z_asgard.gestion_schema_usr + SET lecteur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le lecteur du schéma % a été supprimé.', item.nom_schema ; + END IF ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = n_role_cible + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le producteur du schéma % a été redéfini.', item.nom_schema ; + END LOOP ; + + ------ FONCTION D'EDITEUR ------ + -- seulement si le rôle cible n'est pas déjà producteur du schéma + FOR item IN (SELECT * FROM z_asgard.gestion_schema_usr WHERE editeur = n_role) + LOOP + IF item.producteur = n_role_cible + THEN + RAISE NOTICE 'Le rôle cible est actuellement producteur du schéma %.', item.nom_schema ; + UPDATE z_asgard.gestion_schema_usr + SET editeur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... L''éditeur du schéma % a été supprimé.', item.nom_schema ; + ELSE + + IF item.lecteur = n_role_cible + THEN + UPDATE z_asgard.gestion_schema_usr + SET lecteur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le lecteur du schéma % a été supprimé.', item.nom_schema ; + END IF ; + + UPDATE z_asgard.gestion_schema_usr + SET editeur = n_role_cible + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... L''éditeur du schéma % a été redéfini.', item.nom_schema ; + + END IF ; + END LOOP ; + + ------ FONCTION DE LECTEUR ------ + -- seulement si le rôle cible n'est pas déjà producteur ou éditeur du schéma + FOR item IN (SELECT * FROM z_asgard.gestion_schema_usr WHERE lecteur = n_role) + LOOP + IF item.producteur = n_role_cible + THEN + RAISE NOTICE 'Le rôle cible est actuellement producteur du schéma %.', item.nom_schema ; + UPDATE z_asgard.gestion_schema_usr + SET lecteur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le lecteur du schéma % a été supprimé.', item.nom_schema ; + ELSIF item.editeur = n_role_cible + THEN + RAISE NOTICE 'Le rôle cible est actuellement éditeur du schéma %.', item.nom_schema ; + UPDATE z_asgard.gestion_schema_usr + SET lecteur = NULL + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le lecteur du schéma % a été supprimé.', item.nom_schema ; + ELSE + + UPDATE z_asgard.gestion_schema_usr + SET lecteur = n_role_cible + WHERE nom_schema = item.nom_schema ; + RAISE NOTICE '... Le lecteur du schéma % a été redéfini.', item.nom_schema ; + + END IF ; + END LOOP ; + + ------ PROPRIETES HORS ASGARD ------ + IF b_hors_asgard + THEN + EXECUTE format('REASSIGN OWNED BY %I TO %I', n_role, n_producteur_cible) ; + RAISE NOTICE '> %', format('REASSIGN OWNED BY %I TO %I', n_role, n_producteur_cible) ; + RAISE NOTICE '... Le cas échéant, la propriété des objets hors schémas référencés par ASGARD a été réaffectée.' ; + END IF ; + + ------ PRIVILEGES RESIDUELS SUR LES SCHEMAS D'ASGARD ------- + k := 0 ; + FOR item IN (SELECT * FROM z_asgard.gestion_schema_usr WHERE creation) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role( + quote_ident(item.nom_schema)::regnamespace, quote_ident(n_role)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + + IF n_role_cible IS NOT NULL + THEN + EXECUTE format(c.commande, n_role_cible) ; + RAISE NOTICE '> %', format(c.commande, n_role_cible) ; + END IF ; + + k := k + 1 ; + END LOOP ; + END LOOP ; + IF k > 0 + THEN + IF n_role_cible IS NULL + THEN + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les schémas référencés par ASGARD ont été révoqués.', n_role ; + ELSE + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les schémas référencés par ASGARD ont été réaffectés.', n_role ; + END IF ; + END IF ; + + ------ PRIVILEGES RESIDUELS SUR LES SCHEMAS HORS ASGARD ------ + IF b_hors_asgard + THEN + k := 0 ; + FOR item IN (SELECT * FROM pg_catalog.pg_namespace + LEFT JOIN z_asgard.gestion_schema_usr + ON nspname::text = nom_schema AND creation + WHERE nom_schema IS NULL) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role( + quote_ident(item.nspname::text)::regnamespace, quote_ident(n_role)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + + IF n_role_cible IS NOT NULL + THEN + EXECUTE format(c.commande, n_role_cible) ; + RAISE NOTICE '> %', format(c.commande, n_role_cible) ; + END IF ; + + k := k + 1 ; + END LOOP ; + END LOOP ; + IF k > 0 + THEN + IF n_role_cible IS NULL + THEN + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les schémas non référencés par ASGARD ont été révoqués.', n_role ; + ELSE + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les schémas non référencés par ASGARD ont été réaffectés.', n_role ; + END IF ; + END IF ; + END IF ; + + ------ ACL PAR DEFAUT ------ + IF b_default_acl + THEN + k := 0 ; + FOR item IN ( + SELECT + format( + 'ALTER DEFAULT PRIVILEGES FOR ROLE %s%s REVOKE %s ON %s FROM %I', + defaclrole::regrole, + CASE WHEN defaclnamespace = 0 THEN '' + ELSE format(' IN SCHEMA %s', defaclnamespace::regnamespace) END, + privilege, + typ_lg, + n_role + ) AS revoke_commande, + CASE WHEN n_role_cible IS NOT NULL THEN format( + 'ALTER DEFAULT PRIVILEGES FOR ROLE %s%s GRANT %s ON %s TO %I', + defaclrole::regrole, + CASE WHEN defaclnamespace = 0 THEN '' + ELSE format(' IN SCHEMA %s', defaclnamespace::regnamespace) END, + privilege, + typ_lg, + n_role_cible + ) END AS grant_commande, + pg_has_role(defaclrole, 'USAGE') AS utilisable, + defaclrole + FROM pg_default_acl LEFT JOIN z_asgard.gestion_schema_etr + ON defaclnamespace = oid_schema, + aclexplode(defaclacl) AS acl (grantor, grantee, privilege, grantable), + unnest(ARRAY['TABLES', 'SEQUENCES', 'FUNCTIONS', 'TYPES', 'SCHEMAS'], + ARRAY['r', 'S', 'f', 'T', 'n']) AS t (typ_lg, typ_crt) + WHERE defaclobjtype = typ_crt + AND (oid_schema IS NOT NULL OR b_hors_asgard) + AND grantee = quote_ident(n_role)::regrole + ) + LOOP + IF item.utilisable + THEN + IF n_role_cible IS NOT NULL + THEN + EXECUTE item.grant_commande ; + RAISE NOTICE '> %', item.grant_commande ; + END IF ; + + EXECUTE item.revoke_commande ; + RAISE NOTICE '> %', item.revoke_commande ; + ELSE + RAISE EXCEPTION 'FRR3. Echec. Vous n''avez pas les privilèges nécessaires pour modifier les privilèges par défaut alloués par le rôle %.', item.defaclrole::regrole::text + USING DETAIL = item.revoke_commande, + HINT = 'Tentez de relancer la fonction en tant que super-utilisateur.' ; + END IF ; + k := k + 1 ; + END LOOP ; + IF k > 0 + THEN + IF n_role_cible IS NULL + THEN + RAISE NOTICE '... Les privilèges par défaut du rôle % ont été supprimés.', n_role ; + ELSE + RAISE NOTICE '... Les privilèges par défaut du rôle % ont été transférés.', n_role ; + END IF ; + END IF ; + END IF ; + + ------- OBJETS HORS SCHEMAS ------ + IF b_hors_asgard + THEN + k := 0 ; + FOR c IN ( + -- bases de données + SELECT format('GRANT %s ON DATABASE %I TO %%I', privilege, datname) AS commande + FROM pg_catalog.pg_database, + aclexplode(datacl) AS acl (grantor, grantee, privilege, grantable) + WHERE datacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + UNION + -- tablespaces + SELECT format('GRANT %s ON TABLESPACE %I TO %%I', privilege, spcname) AS commande + FROM pg_catalog.pg_tablespace, + aclexplode(spcacl) AS acl (grantor, grantee, privilege, grantable) + WHERE spcacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + UNION + -- foreign data wrappers + SELECT format('GRANT %s ON FOREIGN DATA WRAPPER %I TO %%I', privilege, fdwname) AS commande + FROM pg_catalog.pg_foreign_data_wrapper, + aclexplode(fdwacl) AS acl (grantor, grantee, privilege, grantable) + WHERE fdwacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + UNION + -- foreign servers + SELECT format('GRANT %s ON FOREIGN SERVER %I TO %%I', privilege, srvname) AS commande + FROM pg_catalog.pg_foreign_server, + aclexplode(srvacl) AS acl (grantor, grantee, privilege, grantable) + WHERE srvacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + UNION + -- langages + SELECT format('GRANT %s ON LANGUAGE %I TO %%I', privilege, lanname) AS commande + FROM pg_catalog.pg_language, + aclexplode(lanacl) AS acl (grantor, grantee, privilege, grantable) + WHERE lanacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + UNION + -- large objects + SELECT format('GRANT %s ON LARGE OBJECT %I TO %%I', privilege, pg_largeobject_metadata.oid::text) AS commande + FROM pg_catalog.pg_largeobject_metadata, + aclexplode(lomacl) AS acl (grantor, grantee, privilege, grantable) + WHERE lomacl IS NOT NULL + AND grantee = quote_ident(n_role)::regrole + ) LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), n_role) ; + + IF n_role_cible IS NOT NULL + THEN + EXECUTE format(c.commande, n_role_cible) ; + RAISE NOTICE '> %', format(c.commande, n_role_cible) ; + END IF ; + + k := k + 1 ; + END LOOP ; + IF k > 0 + THEN + IF n_role_cible IS NULL + THEN + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les objets hors schémas ont été révoqués.', n_role ; + ELSE + RAISE NOTICE '... Les privilèges résiduels du rôle % sur les objets hors schémas ont été réaffectés.', n_role ; + END IF ; + END IF ; + END IF ; + + ------ RESULTAT ------ + SELECT array_agg(DISTINCT pg_database.datname ORDER BY pg_database.datname) + INTO l_db + FROM pg_catalog.pg_shdepend + LEFT JOIN pg_catalog.pg_database + ON pg_shdepend.dbid = pg_database.oid + OR pg_shdepend.classid = 'pg_database'::regclass AND pg_shdepend.objid = pg_database.oid + WHERE refclassid = 'pg_authid'::regclass + AND refobjid = quote_ident(n_role)::regrole ; + + RETURN l_db ; + +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_reaffecte_role(text, text, boolean, boolean, boolean) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_reaffecte_role(text, text, boolean, boolean, boolean) IS 'ASGARD. Fonction qui réaffecte les privilèges et propriétés d''un rôle à un autre.' ; + + +------ 4.14 - REINITIALISATION DES PRIVILEGES SUR TOUS LES SCHEMAS ------ + +-- Function: z_asgard_admin.asgard_initialise_all_schemas(integer) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_initialise_all_schemas(variante integer DEFAULT 0) + RETURNS varchar[] + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction réinitialise les privilèges sur tous les + schémas référencés par ASGARD en une seule commande. + Pour les schémas d'ASGARD, même s'ils n'ont pas été référencés, + les droits nécessaires au bon fonctionnement du système seront + rétablis. +ARGUMENTS : un entier optionnel - 0 par défaut. +Si 1, la fonction ne fera que s'assurer que tous les objets appartiennent +au propriétaire du schéma. Si 2, la fonction ne s'exécutera que sur les +schémas d'ASGARD. +SORTIE : NULL si la requête s'est exécutée normalement, sinon la liste +des schémas qui n'ont pas pu être traités. Se reporter dans ce cas à +l'onglet des messages pour le détail des erreurs. */ +DECLARE + s record ; + l varchar[] ; + b boolean ; + k integer ; + e_mssg text ; + e_detl text ; + e_hint text ; + utilisateur text := current_user::text ; + v_prop oid ; + t text ; +BEGIN + + ------ CONTROLES PREALABLES ------ + -- la fonction est dans z_asgard_admin, donc seuls les membres de + -- g_admin devraient pouvoir y accéder, mais au cas où : + IF NOT pg_has_role('g_admin', 'USAGE') + THEN + RAISE EXCEPTION 'FAS1. Opération interdite. Vous devez être membre de g_admin pour exécuter cette fonction.' ; + END IF ; + + IF NOT utilisateur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + SET ROLE g_admin ; + END IF ; + + -- permission manquante du propriétaire de la vue gestion_schema_usr + -- (en principe g_admin_ext) sur le schéma z_asgard_admin ou la table + -- gestion_schema : + SELECT relowner INTO v_prop + FROM pg_catalog.pg_class + WHERE relname = 'gestion_schema_usr' AND relnamespace = 'z_asgard'::regnamespace::oid ; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'FAS2. Echec. La vue gestion_schema_usr est introuvable.' ; + END IF ; + + IF NOT has_schema_privilege(v_prop, 'z_asgard_admin', 'USAGE') + OR NOT has_table_privilege(v_prop, 'z_asgard_admin.gestion_schema', 'SELECT') + THEN + RAISE NOTICE '(temporaire) droits a minima pour le propriétaire de la vue gestion_schema_usr :' ; + + IF NOT has_schema_privilege(v_prop, 'z_asgard_admin', 'USAGE') + THEN + t := 'GRANT USAGE ON SCHEMA z_asgard_admin TO ' || v_prop::regrole::text ; + EXECUTE t ; + RAISE NOTICE '> %', t ; + END IF ; + + IF NOT has_table_privilege(v_prop, 'z_asgard_admin.gestion_schema', 'SELECT') + THEN + t := 'GRANT SELECT ON TABLE z_asgard_admin.gestion_schema TO ' || v_prop::regrole::text ; + EXECUTE t ; + RAISE NOTICE '> %', t ; + END IF ; + + RAISE NOTICE '---------------------------------' ; + END IF ; + + ------ NETTOYAGE ------ + FOR s IN ( + SELECT 2 AS n, nom_schema, producteur + FROM z_asgard.gestion_schema_usr + WHERE creation AND NOT nom_schema IN ('z_asgard', 'z_asgard_admin') + UNION VALUES (1, 'z_asgard', 'g_admin_ext'), (0, 'z_asgard_admin', 'g_admin') + ORDER BY n, nom_schema + ) + LOOP + b := True ; + + IF s.n < 2 OR variante < 2 + THEN + + ------ CONTROLE DES PRIVILEGES DE G_ADMIN SUR LE PRODUCTEUR ------ + -- si g_admin n'est pas membre du producteur, alors on l'en rend + -- membre, sous réserve que ce ne soit pas un super-utilisateur ou + -- un rôle de connexion (ce dernier cas n'étant pas supposé arriver, + -- sauf désactivation temporaire de triggers ou à avoir donné + -- LOGIN au rôle après l'avoir désigné comme producteur). + IF NOT pg_has_role(s.producteur, 'USAGE') + THEN + -- propriétaire super-utilisateur + IF s.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + RAISE NOTICE '... ECHEC. Schéma % non traité.', s.nom_schema + USING DETAIL = 'Seul un super-utilisateur est habilité à intervenir sur ce schéma. Producteur : ' || s.producteur || '.', + HINT = 'Veuillez relancer la fonction en tant que super-utilisateur.' ; + b := False ; + l := array_append(l, s.nom_schema) ; + + -- propriétaire rôle de connexion + ELSIF s.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolcanlogin) + THEN + RAISE NOTICE '... ECHEC. Schéma % non traité.', s.nom_schema + USING DETAIL = 'Le producteur du schéma est un rôle de connexion. Producteur : ' || s.producteur || '.', + HINT = 'Veuillez relancer la fonction en tant que super-utilisateur ou après avoir désigné un rôle de groupe comme producteur.' ; + b := False ; + l := array_append(l, s.nom_schema) ; + + -- rôle de groupe lambda sur lequel g_admin n'a pas de permission + -- on la lui donne et on continue + ELSE + EXECUTE 'GRANT ' || quote_ident(s.producteur) || ' TO g_admin' ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', s.producteur ; + END IF ; + END IF ; + + IF b + THEN + BEGIN + IF variante = 1 + THEN + -- lancement de la fonction de nettoyage des propriétaires + IF quote_ident(s.producteur) IN (SELECT nspowner::regrole::text FROM pg_catalog.pg_namespace WHERE nspname = s.nom_schema) + THEN + -- version objets seuls si le propriétaire du schéma est bon + RAISE NOTICE '(ré)attribution de la propriété des objets au rôle producteur du schéma :' ; + SELECT z_asgard.asgard_admin_proprietaire(s.nom_schema, s.producteur, False) INTO k ; + IF k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + ELSE + -- version schéma + objets sinon + RAISE NOTICE '(ré)attribution de la propriété du schéma et des objets au rôle producteur du schéma :' ; + PERFORM z_asgard.asgard_admin_proprietaire(s.nom_schema, s.producteur) ; + END IF ; + + ELSE + -- lancement de la fonction de réinitialisation des droits + PERFORM z_asgard.asgard_initialise_schema(s.nom_schema) ; + + END IF ; + + RAISE NOTICE '... Le schéma % a été traité', s.nom_schema ; + + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '... ECHEC. Schéma % non traité.', s.nom_schema ; + RAISE NOTICE 'FAS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + l := array_append(l, s.nom_schema) ; + END ; + END IF ; + + RAISE NOTICE '---------------------------------' ; + END IF ; + + END LOOP ; + + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + + ------ RESULTAT ------ + RETURN l ; + +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_initialise_all_schemas(integer) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_initialise_all_schemas(integer) IS 'ASGARD. Fonction qui réinitialise les droits sur l''ensemble des schémas référencés.' ; + + +------ 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL ------ + +-- Function: z_asgard.asgard_role_trans_acl(regrole) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_role_trans_acl(n_role regrole) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction transforme un nom de rôle pour qu'il soit utilisable + dans une expression régulière de comparaison avec les champs acl + de pg_catalog. +ARGUMENT : un nom de rôle casté en regrole. +SORTIE : sa traduction, en format text. */ +DECLARE + n_role_trans text ; +BEGIN + + IF n_role::text ~ '^["]?[a-zA-Z0-9_]+["]?$' + THEN + -- pour les noms ne comportant que des lettres et + -- des chiffres, même avec des majuscules, on + -- retire les guillemets + n_role_trans := replace(n_role::text, '"', '') ; + ELSE + -- tous les caractères spéciaux vont entre crochets + n_role_trans := regexp_replace(n_role::text, '([^a-zA-Z0-9_]{1})', '[\1]', 'g') ; + -- les antislashs sont doublés + n_role_trans := replace(n_role_trans::text, '\', '\\') ; --' + END IF ; + + RETURN n_role_trans ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_role_trans_acl(regrole) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_role_trans_acl(regrole) IS 'ASGARD. Fonction qui transforme un nom de rôle pour qu''il soit utilisable dans une expression régulière de comparaison avec les champs acl de pg_catalog.' ; + + +------ 4.16 - DIAGNOSTIC DES DROITS NON STANDARDS ------ + +-- Function: z_asgard_admin.asgard_diagnostic(text[]) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_diagnostic(cibles text[] DEFAULT NULL::text[]) + RETURNS TABLE (nom_schema text, nom_objet text, typ_objet text, critique boolean, anomalie text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Pour tous les schémas référencés par ASGARD et + existants dans la base, asgard_diagnostic liste + les écarts avec les droits standards. +ARGUMENT : cibles (optionnel) permet de restreindre le diagnostic +à la liste de schémas spécifiés. +APPEL : SELECT * FROM z_asgard_admin.asgard_diagnostic() ; +SORTIE : une table avec quatre attributs, + - nom_schema = nom du schéma ; + - nom_objet = nom de l'objet concerné ; + - typ_objet = le type d'objet ; + - critique = True si l'anomalie est problématique pour le + bon fonctionnement d'ASGARD, False si elle est bénigne ; + - anomalie = description de l'anomalie. */ +DECLARE + item record ; + catalogue record ; + objet record ; + asgard record ; + s text ; + cibles_trans text ; +BEGIN + + ------ CONTROLES ET PREPARATION ------ + cibles := nullif(nullif(cibles, ARRAY[]::text[]), ARRAY[NULL]::text[]) ; + + IF cibles IS NOT NULL + THEN + + FOREACH s IN ARRAY cibles + LOOP + IF NOT s IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr WHERE gestion_schema_etr.creation) + THEN + RAISE EXCEPTION 'FDD1. Le schéma % n''existe pas ou n''est pas référencé dans la table de gestion d''ASGARD.', s ; + ELSIF s IS NOT NULL + THEN + IF cibles_trans IS NULL + THEN + cibles_trans := quote_literal(s) ; + ELSE + cibles_trans := format('%s, %L', cibles_trans, s) ; + END IF ; + END IF ; + END LOOP ; + + cibles_trans := format('ARRAY[%s]', cibles_trans) ; + cibles_trans := nullif(cibles_trans, 'ARRAY[]') ; + END IF ; + + ------ DIAGNOSTIC ------ + FOR item IN EXECUTE + E'SELECT + gestion_schema_etr.nom_schema, + gestion_schema_etr.oid_schema, + r1.rolname AS producteur, + r1.oid AS oid_producteur, + CASE WHEN editeur = ''public'' THEN ''public'' ELSE r2.rolname END AS editeur, + r2.oid AS oid_editeur, + CASE WHEN lecteur = ''public'' THEN ''public'' ELSE r3.rolname END AS lecteur, + r3.oid AS oid_lecteur + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE gestion_schema_etr.creation' + || CASE WHEN cibles_trans IS NOT NULL + THEN format(' AND gestion_schema_etr.nom_schema = ANY (%s)', cibles_trans) + ELSE '' END + LOOP + FOR catalogue IN ( + SELECT * + FROM + -- liste des objets à traiter + unnest( + -- catalogue de l'objet + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_proc', 'pg_type', 'pg_type', 'pg_conversion', 'pg_operator', 'pg_collation', + 'pg_ts_dict', 'pg_ts_config', 'pg_opfamily', 'pg_opclass', 'pg_statistic_ext', 'pg_namespace', + 'pg_default_acl', 'pg_default_acl', 'pg_default_acl', 'pg_default_acl', 'pg_attribute'], + -- préfixe utilisé pour les attributs du catalogue + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'rel', + 'pro', 'typ', 'typ', 'con', 'opr', 'coll', + 'dict', 'cfg', 'opf', 'opc', 'stx', 'nsp', + 'defacl', 'defacl', 'defacl', 'defacl', 'att'], + -- si dinstinction selon un attribut, nom de cet attribut + ARRAY['relkind', 'relkind', 'relkind', 'relkind', 'relkind', 'relkind', + NULL, 'typtype', 'typtype', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + 'defaclobjtype', 'defaclobjtype', 'defaclobjtype', 'defaclobjtype', NULL], + -- si distinction selon un attribut, valeur de cet attribut + ARRAY['^r$', '^p$', '^v$', '^m$', '^f$', '^S$', + NULL, '^d$', '^[^d]$', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + '^r$', '^S$', '^f$', '^T$', NULL], + -- nom lisible de l'objet + ARRAY['table', 'table partitionnée', 'vue', 'vue matérialisée', 'table étrangère', 'séquence', + 'routine', 'domaine', 'type', 'conversion', 'opérateur', 'collationnement', + 'dictionnaire de recherche plein texte', 'configuration de recherche plein texte', + 'famille d''opérateurs', 'classe d''opérateurs', 'objet statistique étendu', 'schéma', + 'privilège par défaut sur les tables', 'privilège par défaut sur les séquences', + 'privilège par défaut sur les fonctions', 'privilège par défaut sur les types', 'attribut'], + -- contrôle des droits ? + ARRAY[true, true, true, true, true, true, + true, true, true, false, false, false, + false, false, false, false, false, true, + true, true, true, true, true], + -- droits attendus pour le lecteur du schéma sur l'objet + ARRAY['r', 'r', 'r', 'r', 'r', 'r', + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'U', + NULL, NULL, NULL, NULL, NULL], + -- droits attendus pour l'éditeur du schéma sur l'objet + ARRAY['rawd', 'rawd', 'rawd', 'rawd', 'rawd', 'rU', + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'U', + NULL, NULL, NULL, NULL, NULL], + -- droits attendus pour le producteur du schéma sur l'objet + ARRAY['rawdDxt', 'rawdDxt', 'rawdDxt', 'rawdDxt', 'rawdDxt', 'rwU', + 'X', 'U', 'U', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'UC', + 'rawdDxt', 'rwU', 'X', 'U', NULL], + -- droits par défaut de public sur les types et les fonctions + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL, + 'X', 'U', 'U', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL], + -- si non présent dans PG 9.5, version d'apparition + -- sous forme numérique + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, 100000, NULL, + NULL, NULL, NULL, NULL, NULL], + -- géré automatiquement par ASGARD ? + ARRAY[true, true, true, true, true, true, + true, true, true, true, true, true, + true, true, false, false, false, true, + NULL, NULL, NULL, NULL, true] + ) AS l (catalogue, prefixe, attrib_genre, valeur_genre, lib_obj, droits, drt_lecteur, + drt_editeur, drt_producteur, drt_public, min_version, asgard_auto) + ) + LOOP + IF catalogue.min_version IS NULL + OR current_setting('server_version_num')::int >= catalogue.min_version + THEN + FOR objet IN EXECUTE ' + SELECT ' || + CASE WHEN NOT catalogue.catalogue = 'pg_attribute' THEN + catalogue.catalogue || '.oid AS objoid, ' ELSE '' END || + CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN '' + WHEN catalogue.catalogue = 'pg_attribute' + THEN '(z_asgard.asgard_parse_relident(attrelid::regclass))[2] || '' ('' || ' || catalogue.prefixe || 'name || '')'' AS objname, ' + ELSE catalogue.prefixe || 'name::text AS objname, ' END || ' + regexp_replace(' || CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN 'defaclrole' + WHEN catalogue.catalogue = 'pg_attribute' THEN 'NULL' + ELSE catalogue.prefixe || 'owner' END || '::regrole::text, ''^["]?(.*?)["]?$'', ''\1'') AS objowner' || + CASE WHEN catalogue.droits THEN ', ' || catalogue.prefixe || 'acl AS objacl' ELSE '' END || ' + FROM pg_catalog.' || catalogue.catalogue || ' + WHERE ' || CASE WHEN catalogue.catalogue = 'pg_attribute' + THEN 'quote_ident((z_asgard.asgard_parse_relident(attrelid::regclass))[1])::regnamespace::oid = ' || item.oid_schema::text + WHEN catalogue.catalogue = 'pg_namespace' THEN catalogue.prefixe || 'name = ' || quote_literal(item.nom_schema) + ELSE catalogue.prefixe || 'namespace = ' || item.oid_schema::text END || + CASE WHEN catalogue.attrib_genre IS NOT NULL + THEN ' AND ' || catalogue.attrib_genre || ' ~ ' || quote_literal(catalogue.valeur_genre) + ELSE '' END || + CASE WHEN catalogue.catalogue = 'pg_type' + THEN ' AND NOT (pg_type.oid, ''pg_type''::regclass::oid) IN ( + SELECT pg_depend.objid, pg_depend.classid + FROM pg_catalog.pg_depend + WHERE deptype = ANY (ARRAY[''i'', ''a'']) + )' + ELSE '' END + LOOP + -- incohérence propriétaire/producteur + IF NOT objet.objowner = item.producteur + AND NOT catalogue.catalogue = ANY (ARRAY['pg_default_acl', 'pg_attribute']) + THEN + RETURN QUERY + SELECT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + True, + format('le propriétaire (%s) n''est pas le producteur désigné pour le schéma (%s)', + objet.objowner, item.producteur ) ; + END IF ; + + -- présence de privilièges par défaut + IF catalogue.catalogue = 'pg_default_acl' + THEN + RETURN QUERY + SELECT + item.nom_schema::text, + NULL::text, + 'privilège par défaut'::text, + False, + format('%s : %s pour le %s accordé par le rôle %s', + catalogue.lib_obj, + privilege, + CASE WHEN grantee = 0 THEN 'pseudo-rôle public' + ELSE format('rôle %s', grantee::regrole) END, + objet.objowner + ) + FROM aclexplode(objet.objacl) AS acl (grantor, grantee, privilege, grantable) ; + -- droits + ELSIF catalogue.droits + THEN + -- droits à examiner sur les objets d'ASGARD + -- si l'objet courant est un objet d'ASGARD + SELECT * + INTO asgard + FROM ( + VALUES + ('z_asgard_admin', 'z_asgard_admin', 'schéma', 'g_admin_ext', 'U'), + ('z_asgard_admin', 'gestion_schema', 'table', 'g_admin_ext', 'rawd'), + ('z_asgard', 'z_asgard', 'schéma', 'g_consult', 'U'), + ('z_asgard', 'gestion_schema_usr', 'vue', 'g_consult', 'r'), + ('z_asgard', 'gestion_schema_etr', 'vue', 'g_consult', 'r'), + ('z_asgard', 'asgardmenu_metadata', 'vue', 'g_consult', 'r'), + ('z_asgard', 'asgardmanager_metadata', 'vue', 'g_consult', 'r'), + ('z_asgard', 'gestion_schema_read_only', 'vue', 'g_consult', 'r') + ) AS t (a_schema, a_objet, a_type, role, droits) + WHERE a_schema = item.nom_schema AND a_objet = objet.objname::text AND a_type = catalogue.lib_obj ; + + RETURN QUERY + WITH privileges_effectifs AS ( + SELECT + CASE WHEN grantee = 0 THEN 'public' ELSE grantee::regrole::text END AS role_cible, + privilege_effectif, + grantable + FROM aclexplode(objet.objacl) AS acl (grantor, grantee, privilege_effectif, grantable) + WHERE objet.objacl IS NOT NULL + ), + privileges_attendus AS ( + SELECT fonction, f_role, privilege_attendu, f_critique + FROM unnest( + ARRAY['le propriétaire', 'le lecteur du schéma', 'l''éditeur du schéma', 'un rôle d''ASGARD', 'le pseudo-rôle public'], + ARRAY[objet.objowner, item.lecteur, item.editeur, asgard.role, 'public'], + -- dans le cas d'un attribut, objet.objowner ne contient pas le propriétaire mais + -- le nom de la relation. l'enregistrement sera toutefois systématiquement écarté, + -- puisqu'il n'y a pas de droits standards du propriétaire sur les attributs + ARRAY[catalogue.drt_producteur, catalogue.drt_lecteur, catalogue.drt_editeur, asgard.droits, catalogue.drt_public], + ARRAY[False, False, False, True, False] + ) AS t (fonction, f_role, f_droits, f_critique), + z_asgard.asgard_expend_privileges(f_droits) AS b (privilege_attendu) + WHERE f_role IS NOT NULL AND f_droits IS NOT NULL + AND (NOT objet.objacl IS NULL OR NOT fonction = ANY(ARRAY['le propriétaire', 'le pseudo-rôle public'])) + ) + SELECT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + CASE WHEN privilege_effectif IS NULL OR privilege_attendu IS NULL + THEN coalesce(f_critique, False) ELSE False END, + CASE WHEN privilege_effectif IS NULL THEN format('privilège %s manquant pour %s (%s)', privilege_attendu, fonction, f_role) + WHEN privilege_attendu IS NULL THEN format('privilège %s supplémentaire pour le rôle %s%s', privilege_effectif, role_cible, + CASE WHEN grantable THEN ' (avec GRANT OPTION)' ELSE '' END) + WHEN grantable THEN format('le rôle %s est habilité à transmettre le privilège %s (GRANT OPTION)', role_cible, privilege_effectif) + END + FROM privileges_effectifs FULL OUTER JOIN privileges_attendus + ON privilege_effectif = privilege_attendu + AND role_cible = quote_ident(f_role) + WHERE privilege_effectif IS NULL OR privilege_attendu IS NULL OR grantable ; + END IF ; + + -- le producteur du schéma d'une vue ou vue matérialisée + -- n'est ni producteur, ni éditeur, ni lecteur du + -- schéma d'une table source + IF catalogue.lib_obj = ANY(ARRAY['vue', 'vue matérialisée']) + AND NOT item.nom_schema = ANY(ARRAY['z_asgard', 'z_asgard_admin']) + THEN + RETURN QUERY + SELECT + DISTINCT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + False, + format('le producteur du schéma de la %s (%s) n''est pas membre des groupes lecteur, éditeur ou producteur de la %s source %s', + catalogue.lib_obj, item.producteur, liblg, relname) + FROM pg_catalog.pg_rewrite + LEFT JOIN pg_catalog.pg_depend + ON objid = pg_rewrite.oid + LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = refobjid + LEFT JOIN z_asgard.gestion_schema_etr + ON relnamespace::regnamespace::text = quote_ident(gestion_schema_etr.nom_schema) + LEFT JOIN unnest( + ARRAY['table', 'table partitionnée', 'vue', 'vue matérialisée', 'table étrangère', 'séquence'], + ARRAY['r', 'p', 'v', 'm', 'f', 'S'] + ) AS t (liblg, libcrt) + ON relkind = libcrt + WHERE ev_class = objet.objoid + AND rulename = '_RETURN' + AND ev_type = '1' + AND ev_enabled = 'O' + AND is_instead + AND classid = 'pg_rewrite'::regclass::oid + AND refclassid = 'pg_class'::regclass::oid + AND deptype = 'n' + AND NOT refobjid = objet.objoid + AND NOT item.nom_schema = gestion_schema_etr.nom_schema + AND NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_producteur, 'USAGE') + AND (gestion_schema_etr.oid_editeur IS NULL OR NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_editeur, 'USAGE')) + AND (gestion_schema_etr.oid_lecteur IS NULL OR NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_lecteur, 'USAGE')) ; + END IF ; + END LOOP ; + END IF ; + END LOOP ; + END LOOP ; +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_diagnostic(text[]) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_diagnostic(text[]) IS 'ASGARD. Fonction qui liste les écarts vis-à-vis des droits standards sur les schémas actifs référencés par ASGARD.' ; + + +------ 4.17 - EXTRACTION DE NOMS D'OBJETS A PARTIR D'IDENTIFIANTS ------ + +-- Function: z_asgard.asgard_parse_relident(regclass) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_parse_relident(ident regclass) + RETURNS text[] + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction déduit un nom de schéma et un nom de relation + d'un identifiant de relation. Pour PG 9.6+, elle fait double + emploi avec la fonction parse_ident. +ARGUMENT : un identifiant de relation casté en regclass. +SORTIE : une liste de deux éléments : r[1] est le schéma et r[2] la relation. */ +DECLARE + n_schema text ; + n_relation text ; +BEGIN + SELECT + pg_namespace.nspname, + pg_class.relname + INTO n_schema, n_relation + FROM pg_catalog.pg_class + LEFT JOIN pg_catalog.pg_namespace + ON pg_class.relnamespace = pg_namespace.oid + WHERE pg_class.oid = ident ; + IF NOT FOUND + THEN + RETURN NULL ; + ELSE + RETURN ARRAY[n_schema, n_relation] ; + END IF ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_parse_relident(regclass) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_parse_relident(regclass) IS 'ASGARD. Fonction qui retourne le nom du schéma et le nom de la relation à partir d''un identifiant de relation.' ; + + +------ 4.18 - EXPLICITATION DES CODES DE PRIVILÈGES ------ + +-- Function: z_asgard.asgard_expend_privileges(text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_expend_privileges(privileges_codes text) + RETURNS TABLE(privilege text) + LANGUAGE plpgsql + AS $_$ +/* OBJET : Fonction qui explicite les privilèges correspondant + aux codes données en argument. Par exemple + 'SELECT' et 'UPDATE' pour 'rw'. Si un code n'est pas + reconnu, il est ignoré. +ARGUMENT : Les codes des privilèges, concaténés sous la forme d'une +unique chaîne de caractères. +SORTIE : Une table avec un unique champ nommé "privilege". */ +BEGIN + RETURN QUERY + SELECT + p.privilege + FROM unnest( + ARRAY['SELECT', 'INSERT', 'UPDATE', 'DELETE', + 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'USAGE', + 'CREATE', 'EXECUTE', 'CONNECT', 'TEMPORARY'], + ARRAY['r', 'a', 'w', 'd', 'D', 'x', 't', 'U', 'C', 'X', 'c', 'T'] + ) AS p (privilege, prvlg) + WHERE privileges_codes ~ prvlg ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_expend_privileges(text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_expend_privileges(text) IS 'ASGARD. Fonction qui explicite les privilèges correspondant aux codes données en argument.' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +--------------------------------------------- +------ 5 - TRIGGERS SUR GESTION_SCHEMA ------ +--------------------------------------------- +/* 5.1 - TRIGGER BEFORE + 5.2 - TRIGGER AFTER */ + +------ 5.1 - TRIGGER BEFORE ------ + +-- Function: z_asgard_admin.asgard_on_modify_gestion_schema_before() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() RETURNS trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par le trigger asgard_on_modify_gestion_schema_before, + qui valide les informations saisies dans la table de gestion. +CIBLES : z_asgard_admin.gestion_schema. +PORTEE : FOR EACH ROW. +DECLENCHEMENT : BEFORE INSERT, UPDATE, DELETE.*/ +DECLARE + n_role text ; +BEGIN + + ------ INSERT PAR UN UTILISATEUR NON HABILITE ------ + IF TG_OP = 'INSERT' AND NOT has_database_privilege(current_database(), 'CREATE') + -- même si creation vaut faux, seul un rôle habilité à créer des + -- schéma peut ajouter des lignes dans la table de gestion + THEN + RAISE EXCEPTION 'TB1. Vous devez être habilité à créer des schémas pour réaliser cette opération.' ; + END IF ; + + ------ APPLICATION DES VALEURS PAR DEFAUT ------ + -- au tout début car de nombreux tests sont faits par la + -- suite sur "NOT NEW.creation" + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + NEW.creation := coalesce(NEW.creation, False) ; + NEW.nomenclature := coalesce(NEW.nomenclature, False) ; + END IF ; + + ------ EFFACEMENT D'UN ENREGISTREMENT ------ + IF TG_OP = 'DELETE' + THEN + -- on n'autorise pas l'effacement si creation vaut True + -- avec une exception pour les commandes envoyées par la fonction + -- de maintenance asgard_sortie_gestion_schema + IF OLD.creation AND (OLD.ctrl[1] IS NULL OR NOT OLD.ctrl[1] = 'EXIT') + THEN + RAISE EXCEPTION 'TB2. Opération interdite (schéma %). L''effacement n''est autorisé que si creation vaut False.', OLD.nom_schema + USING HINT = 'Pour déréférencer un schéma sans le supprimer, vous pouvez utiliser la fonction z_asgard_admin.asgard_sortie_gestion_schema.' ; + END IF; + + -- on n'autorise pas l'effacement pour les schémas de la nomenclature + IF OLD.nomenclature + THEN + IF OLD.ctrl[1] = 'EXIT' + THEN + RAISE EXCEPTION 'TB26. Opération interdite (schéma %). Le déréférencement n''est pas autorisé pour les schémas de la nomenclature nationale.', OLD.nom_schema + USING HINT = 'Si vous tenez à déréférencer ce schéma, basculez préalablement nomenclature sur False.' ; + ELSE + RAISE EXCEPTION 'TB3. Opération interdite (schéma %). L''effacement n''est pas autorisé pour les schémas de la nomenclature nationale.', OLD.nom_schema + USING HINT = 'Si vous tenez à supprimer de la table de gestion les informations relatives à ce schéma, basculez préalablement nomenclature sur False.' ; + END IF ; + END IF ; + END IF; + + ------ DE-CREATION D'UN SCHEMA ------ + IF TG_OP = 'UPDATE' + THEN + -- si bloc valait déjà d (schéma "mis à la corbeille") + -- on exécute une commande de suppression du schéma. Toute autre modification sur + -- la ligne est ignorée. + IF OLD.bloc = 'd' AND OLD.creation AND NOT NEW.creation AND NEW.ctrl[2] IS NULL + AND OLD.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + -- on bloque tout de même les tentatives de suppression + -- par un utilisateur qui n'aurait pas des droits suffisants (a priori + -- uniquement dans le cas de g_admin avec un schéma appartenant à un + -- super-utilisateur). + -- c'est oid_producteur et pas producteur qui est utilisé au cas + -- où le nom du rôle aurait été modifié entre temps + IF NOT pg_has_role(OLD.oid_producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB23. Opération interdite (schéma %).', OLD.nom_schema + USING DETAIL = 'Seul les membres du rôle producteur ' || OLD.oid_producteur::regrole::text || ' peuvent supprimer ce schéma.' ; + ELSE + EXECUTE 'DROP SCHEMA ' || quote_ident(OLD.nom_schema) || ' CASCADE' ; + RAISE NOTICE '... Le schéma % a été supprimé.', OLD.nom_schema ; + RETURN NULL ; + END IF ; + -- sinon, on n'autorise creation à passer de true à false que si le schéma + -- n'existe plus (permet notamment à l'event trigger qui gère les + -- suppressions de mettre creation à false) + ELSIF OLD.creation and NOT NEW.creation + AND NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + RAISE EXCEPTION 'TB4. Opération interdite (schéma %). Le champ creation ne peut passer de True à False si le schéma existe.', NEW.nom_schema + USING HINT = 'Si vous supprimez physiquement le schéma avec la commande DROP SCHEMA, creation basculera sur False automatiquement.' ; + END IF ; + END IF ; + + IF TG_OP <> 'DELETE' + THEN + ------ PROHIBITION DE LA SAISIE MANUELLE DES OID ------ + -- vérifié grâce au champ ctrl + IF NEW.ctrl[2] IS NULL + OR NOT array_length(NEW.ctrl, 1) >= 2 + OR NEW.ctrl[1] IS NULL + OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT') + OR NOT NEW.ctrl[2] = 'x7-A;#rzo' + -- ctrl NULL ou invalide + THEN + + IF NEW.ctrl[1] = 'EXIT' + THEN + RAISE EXCEPTION 'TB17. Opération interdite (schéma %).', coalesce(NEW.nom_schema, '?') + USING HINT = 'Pour déréférencer un schéma, veuillez utiliser la fonction z_asgard_admin.asgard_sortie_gestion_schema.' ; + END IF ; + + -- réinitialisation du champ ctrl, qui peut contenir des informations + -- issues de commandes antérieures (dans ctrl[1]) + NEW.ctrl := ARRAY['MANUEL', NULL]::text[] ; + + IF TG_OP = 'INSERT' AND ( + NEW.oid_producteur IS NOT NULL + OR NEW.oid_lecteur IS NOT NULL + OR NEW.oid_editeur IS NOT NULL + OR NEW.oid_schema IS NOT NULL + ) + -- cas d'un INSERT manuel pour lequel des OID ont été saisis + -- on les remet à NULL + THEN + NEW.oid_producteur = NULL ; + NEW.oid_editeur = NULL ; + NEW.oid_lecteur = NULL ; + NEW.oid_schema = NULL ; + ELSIF TG_OP = 'UPDATE' + THEN + IF NOT coalesce(NEW.oid_producteur, -1) = coalesce(OLD.oid_producteur, -1) + OR NOT coalesce(NEW.oid_editeur, -1) = coalesce(OLD.oid_editeur, -1) + OR NOT coalesce(NEW.oid_lecteur, -1) = coalesce(OLD.oid_lecteur, -1) + OR NOT coalesce(NEW.oid_schema, -1) = coalesce(OLD.oid_schema, -1) + -- cas d'un UPDATE avec modification des OID + -- on les remet à OLD + THEN + NEW.oid_producteur = OLD.oid_producteur ; + NEW.oid_editeur = OLD.oid_editeur ; + NEW.oid_lecteur = OLD.oid_lecteur ; + NEW.oid_schema = OLD.oid_schema ; + END IF ; + END IF ; + ELSE + -- suppression du mot de passe de contrôle. + -- ctrl[1] est par contre conservé - il sera utilisé + -- par le trigger AFTER pour connaître l'opération + -- à l'origine de son déclenchement. + NEW.ctrl[2] := NULL ; + END IF ; + + ------ REQUETES AUTO A IGNORER ------ + -- les remontées du trigger AFTER (SELF) + -- sont exclues, car les contraintes ont déjà + -- été validées (et pose problèmes avec les + -- contrôles d'OID sur les UPDATE, car ceux-ci + -- ne seront pas nécessairement déjà remplis) ; + -- les requêtes EXIT de même, car c'est un + -- pré-requis à la suppression qui ne fait + -- que modifier le champ ctrl + IF NEW.ctrl[1] IN ('SELF', 'EXIT') + THEN + -- aucune action + RETURN NEW ; + END IF ; + + ------ VERROUILLAGE DES CHAMPS LIES A LA NOMENCLATURE ------ + -- modifiables uniquement par l'ADL + IF TG_OP = 'UPDATE' + THEN + IF (OLD.nomenclature OR NEW.nomenclature) AND NOT pg_has_role('g_admin', 'MEMBER') AND ( + NOT coalesce(OLD.nomenclature, False) = coalesce(NEW.nomenclature, False) + OR NOT coalesce(OLD.niv1, '') = coalesce(NEW.niv1, '') + OR NOT coalesce(OLD.niv1_abr, '') = coalesce(NEW.niv1_abr, '') + OR NOT coalesce(OLD.niv2, '') = coalesce(NEW.niv2, '') + OR NOT coalesce(OLD.niv2_abr, '') = coalesce(NEW.niv2_abr, '') + OR NOT coalesce(OLD.nom_schema, '') = coalesce(NEW.nom_schema, '') + OR NOT coalesce(OLD.bloc, '') = coalesce(NEW.bloc, '') + ) + THEN + RAISE EXCEPTION 'TB18. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seuls les membres de g_admin sont habilités à modifier les champs nomenclature et - pour les schémas de la nomenclature - bloc, niv1, niv1_abr, niv2, niv2_abr et nom_schema.' ; + END IF ; + ELSIF TG_OP = 'INSERT' + THEN + IF NEW.nomenclature AND NOT pg_has_role('g_admin', 'MEMBER') + THEN + RAISE EXCEPTION 'TB19. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seuls les membres de g_admin sont autorisés à ajouter des schémas à la nomenclature (nomenclature = True).' ; + END IF ; + END IF ; + + ------ NETTOYAGE DES CHAÎNES VIDES ------ + -- si l'utilisateur a entré des chaînes vides on met des NULL + NEW.editeur := nullif(NEW.editeur, '') ; + NEW.lecteur := nullif(NEW.lecteur, '') ; + NEW.bloc := nullif(NEW.bloc, '') ; + NEW.niv1 := nullif(NEW.niv1, '') ; + NEW.niv1_abr := nullif(NEW.niv1_abr, '') ; + NEW.niv2 := nullif(NEW.niv2, '') ; + NEW.niv2_abr := nullif(NEW.niv2_abr, '') ; + NEW.nom_schema := nullif(NEW.nom_schema, '') ; + -- si producteur est vide on met par défaut g_admin + NEW.producteur := coalesce(nullif(NEW.producteur, ''), 'g_admin') ; + + ------ NETTOYAGE DES CHAMPS OID ------ + -- pour les rôles de lecteur et éditeur, + -- si le champ de nom est vidé par l'utilisateur, + -- on vide en conséquence l'OID + IF NEW.editeur IS NULL + THEN + NEW.oid_editeur := NULL ; + END IF ; + IF NEW.lecteur IS NULL + THEN + NEW.oid_lecteur := NULL ; + END IF ; + -- si le schéma n'est pas créé, on s'assure que les champs + -- d'OID restent vides + -- à noter que l'event trigger sur DROP SCHEMA vide + -- déjà le champ oid_schema + IF NOT NEW.creation + THEN + NEW.oid_schema := NULL ; + NEW.oid_lecteur := NULL ; + NEW.oid_editeur := NULL ; + NEW.oid_producteur := NULL ; + END IF ; + + ------ VALIDITE DES NOMS DE ROLES ------ + -- dans le cas d'un schéma pré-existant, on s'assure que les rôles qui + -- ne changent pas sont toujours valides (qu'ils existent et que le nom + -- n'a pas été modifié entre temps) + -- si tel est le cas, on les met à jour et on le note dans + -- ctrl, pour que le trigger AFTER sache qu'il ne s'agit + -- pas réellement de nouveaux rôles sur lesquels les droits + -- devraient être réappliqués + IF TG_OP = 'UPDATE' AND NEW.creation + THEN + -- producteur + IF OLD.creation AND OLD.producteur = NEW.producteur + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_producteur ; + IF NOT FOUND + -- le rôle producteur n'existe pas + THEN + -- cas invraisemblable, car un rôle ne peut pas être + -- supprimé alors qu'il est propriétaire d'un schéma, et la + -- commande ALTER SCHEMA OWNER TO aurait été interceptée + -- mais, s'il advient, on repart du propriétaire + -- renseigné dans pg_namespace + SELECT replace(nspowner::regrole::text, '"', ''), nspowner + INTO NEW.producteur, NEW.oid_producteur + FROM pg_catalog.pg_namespace + WHERE pg_namespace.oid = NEW.oid_schema ; + RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', NEW.nom_schema ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; + ELSIF NOT n_role = NEW.producteur + -- libellé obsolète du producteur + THEN + NEW.producteur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle producteur, renommé entre temps.', NEW.nom_schema + USING DETAIL = 'Ancien nom "' || OLD.producteur || '", nouveau nom "' || NEW.producteur || '".' ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; + END IF ; + END IF ; + -- éditeur + IF OLD.creation AND OLD.editeur = NEW.editeur + AND NOT NEW.editeur = 'public' + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_editeur ; + IF NOT FOUND + -- le rôle éditeur n'existe pas + THEN + NEW.editeur := NULL ; + NEW.oid_editeur := NULL ; + RAISE NOTICE '[table de gestion] Schéma %. Le rôle éditeur n''existant plus, il est déréférencé.', NEW.nom_schema + USING DETAIL = 'Ancien nom "' || OLD.editeur || '".' ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; + ELSIF NOT n_role = NEW.editeur + -- libellé obsolète de l'éditeur + THEN + NEW.editeur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle éditeur, renommé entre temps.', NEW.nom_schema + USING DETAIL = 'Ancien nom "' || OLD.editeur || '", nouveau nom "' || NEW.editeur || '".' ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; + END IF ; + END IF ; + -- lecteur + IF OLD.creation AND OLD.lecteur = NEW.lecteur + AND NOT NEW.lecteur = 'public' + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_lecteur ; + IF NOT FOUND + -- le rôle lecteur n'existe pas + THEN + NEW.lecteur := NULL ; + NEW.oid_lecteur := NULL ; + RAISE NOTICE '[table de gestion] Schéma %. Le rôle lecteur n''existant plus, il est déréférencé.', NEW.nom_schema + USING DETAIL = 'Ancien nom "' || OLD.lecteur || '".' ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; + ELSIF NOT n_role = NEW.lecteur + -- libellé obsolète du lecteur + THEN + NEW.lecteur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle lecteur, renommé entre temps.', NEW.nom_schema + USING DETAIL = 'Ancien nom "' || OLD.lecteur || '", nouveau nom "' || NEW.lecteur || '".' ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; + END IF ; + END IF ; + END IF ; + + ------ NON RESPECT DES CONTRAINTES ------ + -- non nullité de nom_schema + IF NEW.nom_schema IS NULL + THEN + RAISE EXCEPTION 'TB8. Saisie incorrecte. Le nom du schéma doit être renseigné (champ nom_schema).' ; + END IF ; + + -- unicité de nom_schema + -- -> contrôlé après les manipulations sur les blocs de + -- la partie suivante. + + -- unicité de oid_schema + IF TG_OP = 'INSERT' AND NEW.oid_schema IN (SELECT gestion_schema_etr.oid_schema FROM z_asgard.gestion_schema_etr + WHERE gestion_schema_etr.oid_schema IS NOT NULL) + THEN + RAISE EXCEPTION 'TB11. Saisie incorrecte (schéma %). Un schéma de même OID est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + THEN + -- cas (très hypothétique) d'une modification d'OID + IF NOT coalesce(NEW.oid_schema, -1) = coalesce(OLD.oid_schema, -1) + AND NEW.oid_schema IN (SELECT gestion_schema_etr.oid_schema FROM z_asgard.gestion_schema_etr + WHERE gestion_schema_etr.oid_schema IS NOT NULL) + THEN + RAISE EXCEPTION 'TB12. Saisie incorrecte (schéma %). Un schéma de même OID est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + END IF ; + END IF ; + + -- non répétition des rôles + IF NOT ((NEW.oid_lecteur IS NULL OR NOT NEW.oid_lecteur = NEW.oid_producteur) + AND (NEW.oid_editeur IS NULL OR NOT NEW.oid_editeur = NEW.oid_producteur) + AND (NEW.oid_lecteur IS NULL OR NEW.oid_editeur IS NULL OR NOT NEW.oid_lecteur = NEW.oid_editeur)) + THEN + RAISE EXCEPTION 'TB13. Saisie incorrecte (schéma %). Les rôles producteur, lecteur et éditeur doivent être distincts.', NEW.nom_schema ; + END IF ; + END IF ; + + ------ COHERENCE BLOC/NOM DU SCHEMA ------ + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF NEW.nom_schema ~ '^d_' + -- cas d'un schéma mis à la corbeille par un changement de nom + -- on rétablit le nom antérieur, la lettre d apparaissant + -- exclusivement dans le bloc + THEN + IF TG_OP = 'INSERT' + -- pour un INSERT, on ne s'intéresse qu'aux cas où + -- le bloc est NULL ou vaut d. Dans tous les autres cas, + -- le bloc prévaudra sur le nom et le schéma n'ira + -- pas à la corbeille de toute façon + THEN + IF NEW.bloc IS NULL + THEN + NEW.bloc := 'd' ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + END IF ; + ELSE + -- pour un UPDATE, on s'intéresse aux cas où le bloc + -- n'a pas changé et aux cas où il a été mis sur 'd' ou + -- (sous certaines conditions) sur NULL. + -- Sinon, le bloc prévaudra sur le nom et le + -- schéma n'ira pas à la corbeille de toute façon + IF NEW.bloc = 'd' AND NOT OLD.bloc = 'd' + -- mise à la corbeille avec action simultanée sur le nom du schéma + -- et le bloc + s'il y a un ancien bloc récupérable + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + -- on ne reprend pas l'ancien nom au cas où autre chose que le préfixe aurait été + -- changé. + + ELSIF NEW.bloc IS NULL AND NOT OLD.bloc = 'd' + -- mise à la corbeille via le nom avec mise à NULL du bloc en + -- parallèle + s'il y a un ancien bloc récupérable + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + + NEW.bloc := 'd' ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc = 'd' + AND OLD.nom_schema ~ '^[a-ce-z]_' + -- s'il y a un ancien préfixe récupérable (cas d'un + -- schéma dont on tente de forcer le bloc à d alors + -- qu'il est déjà dans la corbeille) + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', substring(OLD.nom_schema, '^([a-ce-z]_)')) ; + RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc = 'd' + AND NOT OLD.nom_schema ~ '^[a-z]_' + -- schéma sans bloc de la corbeille sur lequel on tente de forcer + -- un préfixe d + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Suppression du préfixe du schéma sans bloc %.', NEW.nom_schema ; + + ELSIF NEW.bloc IS NULL AND OLD.bloc IS NULL + -- mise à la corbeille d'un schéma sans bloc + THEN + NEW.bloc := 'd' ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc IS NULL + -- mise à la corbeille d'un schéma sans bloc + -- avec modification simultanée du nom et du bloc + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = OLD.bloc AND NOT NEW.bloc = 'd' + -- le bloc ne change pas et contenait une autre + -- valeur que d + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + + NEW.bloc := 'd' ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + END IF ; + + END IF ; + END IF ; + END IF ; + + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF NEW.bloc IS NULL AND NEW.nom_schema ~ '^[a-z]_' + -- si bloc est NULL, mais que le nom du schéma + -- comporte un préfixe, + THEN + IF TG_OP = 'UPDATE' + THEN + IF OLD.bloc IS NOT NULL + AND OLD.nom_schema ~ '^[a-z]_' + AND left(NEW.nom_schema, 1) = left(OLD.nom_schema, 1) + -- sur un UPDATE où le préfixe du schéma n'a pas été modifié, tandis + -- que le bloc a été mis à NULL, on supprime le préfixe du schéma + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^[a-z]_', '') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSE + -- sinon, on met le préfixe du nom du schéma dans bloc + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + END IF ; + ELSE + -- sur un INSERT, + -- on met le préfixe du nom du schéma dans bloc + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + END IF ; + ELSIF NEW.bloc IS NULL + -- si bloc est NULL, et que (sous-entendu) le nom du schéma ne + -- respecte pas la nomenclature, on avertit l'utilisateur + THEN + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSIF NOT NEW.nom_schema ~ ('^'|| NEW.bloc || '_') + AND NOT NEW.bloc = 'd' + -- le bloc est renseigné mais le nom du schéma ne correspond pas + -- (et il ne s'agit pas d'un schéma mis à la corbeille) : + -- Si le nom est de la forme 'a_...', alors : + -- - dans le cas d'un UPDATE avec modification du nom + -- du schéma et pas du bloc, on se fie au nom du schéma + -- et on change le bloc ; + -- - si bloc n'est pas une lettre, on renvoie une erreur ; + -- - dans les autres cas, on se fie au bloc et change le + -- préfixe. + -- Si le nom ne comporte pas de préfixe : + -- - s'il vient d'être sciemment supprimé et que le bloc + -- n'a pas changé, on supprime le bloc ; + -- - sinon, si le bloc est une lettre, on l'ajoute au début du + -- nom (sans doubler l'underscore, si le nom commençait par + -- un underscore) ; + -- - sinon on renvoie une erreur. + THEN + IF NEW.nom_schema ~ '^([a-z])?_' + -- si le nom du schéma contient un préfixe valide + THEN + IF TG_OP = 'UPDATE' + -- sur un UPDATE + THEN + IF NOT NEW.nom_schema = OLD.nom_schema AND NEW.bloc = OLD.bloc + -- si le bloc est le même, mais que le nom du schéma a été modifié + -- on met à jour le bloc selon le nouveau préfixe du schéma + THEN + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- si le nouveau bloc est invalide, on renvoie une erreur + THEN + RAISE EXCEPTION 'TB14. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSE + -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; + RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + END IF ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- (sur un INSERT) + -- si le nouveau bloc est invalide, + -- on renvoie une erreur + THEN + RAISE EXCEPTION 'TB15. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSE + -- (sur un INSERT) + -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; + RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + END IF ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- (si le nom du schéma ne contient pas de préfixe valide) + -- si le nouveau bloc est invalide, on renvoie une erreur + THEN + RAISE EXCEPTION 'TB16. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + -- (si le nom du schéma ne contient pas de préfixe valide) + -- sur un UPDATE + THEN + IF NEW.bloc = OLD.bloc + AND OLD.nom_schema ~ '^([a-z])?_' + -- s'il y avait un bloc, mais que le préfixe vient d'être supprimé + -- dans le nom du schéma : on supprime le bloc + THEN + NEW.bloc := NULL ; + RAISE NOTICE '[table de gestion] Le bloc du schéma % a été supprimé.', NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSE + -- sinon, préfixage du schéma selon le bloc + NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + END IF ; + ELSE + -- sur un INSERT, préfixage du schéma selon le bloc + NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + END IF ; + -- le trigger AFTER se chargera de renommer physiquement le + -- schéma d'autant que de besoin + END IF ; + END IF ; + + ------ NON RESPECT DES CONTRAINTES (SUITE) ------ + -- unicité de nom_schema + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF TG_OP = 'INSERT' AND NEW.nom_schema IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr) + THEN + RAISE EXCEPTION 'TB9. Saisie incorrecte (schéma %). Un schéma de même nom est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + THEN + -- cas d'un changement de nom + IF NOT NEW.nom_schema = OLD.nom_schema + AND NEW.nom_schema IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr) + THEN + RAISE EXCEPTION 'TB10. Saisie incorrecte (schéma %). Un schéma de même nom est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + END IF ; + END IF ; + END IF ; + + ------ MISE À LA CORBEILLE ------ + -- notification de l'utilisateur + IF TG_OP = 'UPDATE' + THEN + -- schéma existant dont bloc bascule sur 'd' + -- ou schéma créé par bascule de creation sur True dans bloc vaut 'd' + IF NEW.creation AND NEW.bloc = 'd' AND (NOT OLD.bloc = 'd' OR OLD.bloc IS NULL) + OR NEW.creation AND NOT OLD.creation AND NEW.bloc = 'd' + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été mis à la corbeille (bloc = ''d'').', NEW.nom_schema + USING HINT = 'Si vous basculez creation sur False, le schéma et son contenu seront automatiquement supprimés.' ; + -- restauration + ELSIF NEW.creation AND OLD.bloc = 'd' AND (NOT NEW.bloc = 'd' OR NEW.bloc IS NULL) + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été retiré de la corbeille (bloc ne vaut plus ''d'').', NEW.nom_schema ; + END IF ; + ELSIF TG_OP = 'INSERT' + THEN + -- nouveau schéma dont bloc vaut 'd' + IF NEW.creation AND NEW.bloc = 'd' + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été mis à la corbeille (bloc = ''d'').', NEW.nom_schema + USING HINT = 'Si vous basculez creation sur False, le schéma et son contenu seront automatiquement supprimés.' ; + END IF ; + END IF ; + + ------ SCHEMAS DES SUPER-UTILISATEURS ------ + -- concerne uniquement les membres de g_admin, qui voient tous + -- les schémas, y compris ceux des super-utilisateurs dont ils + -- ne sont pas membres. Les contrôles suivants bloquent dans ce + -- cas les tentatives de mise à jour des champs nom_schema, + -- producteur, editeur et lecteur, ainsi que les création de schéma + -- via un INSERT ou un UPDATE. + IF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND OLD.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND ( + NOT OLD.nom_schema = NEW.nom_schema + OR NOT OLD.producteur = NEW.producteur AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + OR NOT coalesce(OLD.editeur, '') = coalesce(NEW.editeur, '') AND (NEW.ctrl IS NULL OR NOT 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL))) + OR NOT coalesce(OLD.lecteur, '') = coalesce(NEW.lecteur, '') AND (NEW.ctrl IS NULL OR NOT 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL))) + ) + THEN + IF NOT pg_has_role(OLD.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB20. Opération interdite (schéma %).', OLD.nom_schema + USING DETAIL = 'Seul le rôle producteur ' || OLD.producteur || ' (super-utilisateur) peut modifier ce schéma.' ; + END IF ; + END IF ; + + IF NEW.creation + AND NOT OLD.creation + AND NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB21. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut créer un schéma dont il est identifié comme producteur.' ; + END IF ; + END IF ; + + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.creation + AND NOT OLD.producteur = NEW.producteur AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB24. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut se désigner comme producteur d''un schéma.' ; + END IF ; + END IF ; + + ELSIF TG_OP = 'INSERT' + THEN + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.creation + AND NOT NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + -- on exclut les schémas en cours de référencement, qui sont gérés + -- juste après, avec leur propre message d'erreur + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB22. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut créer un schéma dont il est identifié comme producteur.' ; + END IF ; + END IF ; + + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + -- schéma pré-existant en cours de référencement + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB25. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut référencer dans ASGARD un schéma dont il est identifié comme producteur.' ; + END IF ; + END IF ; + END IF ; + + ------ RETURN ------ + IF TG_OP IN ('UPDATE', 'INSERT') + THEN + RETURN NEW ; + ELSIF TG_OP = 'DELETE' + THEN + RETURN OLD ; + END IF ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() IS 'ASGARD. Fonction appelée par le trigger qui valide les modifications de la table de gestion.'; + + +-- Trigger: asgard_on_modify_gestion_schema_before + +CREATE TRIGGER asgard_on_modify_gestion_schema_before + BEFORE INSERT OR DELETE OR UPDATE + ON z_asgard_admin.gestion_schema + FOR EACH ROW + EXECUTE PROCEDURE z_asgard_admin.asgard_on_modify_gestion_schema_before() ; + +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_before ON z_asgard_admin.gestion_schema IS 'ASGARD. Trigger qui valide les modifications de la table de gestion.'; + + + +------ 5.2 - TRIGGER AFTER ------ + +-- Function: z_asgard_admin.asgard_on_modify_gestion_schema_after() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() RETURNS trigger + LANGUAGE plpgsql + AS $BODY$ +/* OBJET : Fonction exécutée par le trigger asgard_on_modify_gestion_schema_after, + qui répercute physiquement les modifications de la table de gestion. +CIBLES : z_asgard_admin.gestion_schema. +PORTEE : FOR EACH ROW. +DECLENCHEMENT : AFTER INSERT OR UPDATE.*/ +DECLARE + utilisateur text ; + createur text ; + administrateur text ; + e_mssg text ; + e_hint text ; + e_detl text ; + b_superuser boolean ; + b_test boolean ; + l_commande text[] ; + c text ; + c_reverse text ; + a_producteur text ; + a_editeur text ; + a_lecteur text ; + n int ; +BEGIN + + ------ REQUETES AUTO A IGNORER ------ + -- les remontées du trigger lui-même (SELF), + -- ainsi que des event triggers sur les + -- suppressions de schémas (DROP), n'appellent + -- aucune action, elles sont donc exclues dès + -- le départ + -- les remontées des changements de noms sont + -- conservées, pour le cas où la mise en + -- cohérence avec "bloc" aurait conduit à une + -- modification du nom par le trigger BEFORE + -- (géré au point suivant) + -- les remontées des créations et changements + -- de propriétaire (CREATE et OWNER) appellent + -- des opérations sur les droits plus lourdes + -- qui ne permettent pas de les exclure en + -- amont + IF NEW.ctrl[1] IN ('SELF', 'DROP') + THEN + -- aucune action + RETURN NULL ; + END IF ; + + ------ MANIPULATIONS PREALABLES ------ + utilisateur := current_user ; + + -- si besoin pour les futures opérations sur les rôles, + -- récupération du nom d'un rôle dont current_user est membre + -- et qui a l'attribut CREATEROLE. Autant que possible, la + -- requête renvoie current_user lui-même. On exclut d'office les + -- rôles NOINHERIT qui ne pourront pas avoir simultanément les + -- droits du propriétaire de NEW et OLD.producteur + SELECT rolname INTO createur FROM pg_roles + WHERE pg_has_role(rolname, 'MEMBER') AND rolcreaterole AND rolinherit + ORDER BY rolname = current_user DESC ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.producteur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + SELECT rolname INTO a_producteur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_producteur ; + -- pour la suite, on emploira toujours + -- a_producteur à la place de OLD.producteur + -- pour les opérations sur les droits. + -- Il est réputé non NULL pour un schéma + -- pré-existant (OLD.creation vaut True), + -- dans la mesure où un rôle ne peut être + -- supprimé s'il est propriétaire d'un + -- schéma et où tous les changements de + -- propriétaires sont remontés par event + -- triggers (+ contrôles pour assurer la + -- non-modification manuelle des OID). + IF NOT FOUND AND OLD.creation AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur dans la table de gestion est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', OLD.nom_schema ; + SELECT replace(nspowner::regrole::text, '"', '') INTO a_producteur + FROM pg_catalog.pg_namespace + WHERE pg_namespace.oid = NEW.oid_schema ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'TA1. Anomalie critique (schéma %). Le propriétaire du schéma est introuvable.', OLD.nom_schema ; + END IF ; + END IF ; + END IF ; + + ------ MISE EN APPLICATION D'UN CHANGEMENT DE NOM DE SCHEMA ------ + IF NOT NEW.oid_schema::regnamespace::text = quote_ident(NEW.nom_schema) + -- le schéma existe et ne porte pas déjà le nom NEW.nom_schema + THEN + EXECUTE 'ALTER SCHEMA '|| NEW.oid_schema::regnamespace::text || + ' RENAME TO ' || quote_ident(NEW.nom_schema) ; + RAISE NOTICE '... Le schéma % a été renommé.', NEW.nom_schema ; + END IF ; + -- exclusion des remontées d'event trigger correspondant + -- à des changements de noms + IF NEW.ctrl[1] = 'RENAME' + THEN + -- aucune action + RETURN NULL ; + END IF ; + + ------ PREPARATION DU PRODUCTEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister + -- (creation vaut False) ; + -- - d'un schéma pré-existant dont les rôles ne changent pas + -- ou dont le libellé a juste été nettoyé par le trigger + -- BEFORE. + -- ils sont donc exclus au préalable + -- si le moindre rôle a changé, il faudra être membre du + -- groupe propriétaire/producteur pour pouvoir modifier + -- les privilèges en conséquence + b_test := False ; + IF NOT NEW.creation + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND (NEW.producteur = OLD.producteur OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + AND (coalesce(NEW.editeur, '') = coalesce(OLD.editeur, '') OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL))) + AND (coalesce(NEW.lecteur, '') = coalesce(OLD.lecteur, '') OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles) + -- si le producteur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA2. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.producteur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux producteurs.' ; + END IF ; + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE 'CREATE ROLE ' || quote_ident(NEW.producteur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.producteur ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + ELSE + -- si le rôle producteur existe, on vérifie qu'il n'a pas l'option LOGIN + -- les superusers avec LOGIN (comme postgres) sont tolérés + -- paradoxe ou non, dans l'état actuel des choses, cette erreur se + -- déclenche aussi lorsque la modification ne porte que sur les rôles + -- lecteur/éditeur + SELECT rolsuper INTO b_superuser + FROM pg_roles WHERE rolname = NEW.producteur AND rolcanlogin ; + IF NOT b_superuser + THEN + RAISE EXCEPTION 'TA3. Opération interdite (schéma %). Le producteur/propriétaire du schéma ne doit pas être un rôle de connexion.', NEW.nom_schema ; + END IF ; + END IF ; + b_superuser := coalesce(b_superuser, False) ; + + -- mise à jour du champ d'OID du producteur + IF NEW.ctrl[1] IS NULL OR NOT NEW.ctrl[1] IN ('OWNER', 'CREATE') + -- pas dans le cas d'une remontée de commande directe + -- où l'OID du producteur sera déjà renseigné + -- et uniquement s'il a réellement été modifié (ce + -- qui n'est pas le cas si les changements ne portent + -- que sur les rôles lecteur/éditeur) + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_producteur = quote_ident(NEW.producteur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_producteur IS NULL + OR NOT oid_producteur = quote_ident(NEW.producteur)::regrole::oid + ) ; + END IF ; + + -- implémentation des permissions manquantes sur NEW.producteur + IF NOT pg_has_role(utilisateur, NEW.producteur, 'USAGE') + THEN + b_test := True ; + IF createur IS NULL OR b_superuser + THEN + RAISE EXCEPTION 'TA4. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur + USING HINT = 'Votre rôle doit être membre de ' || NEW.producteur + || ' ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + END IF ; + END IF ; + IF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NOT pg_has_role(utilisateur, a_producteur, 'USAGE') + AND NOT (NEW.producteur = OLD.producteur OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + -- les permissions sur OLD.producteur ne sont contrôlées que si le producteur + -- a effectivement été modifié + THEN + b_test := True ; + IF createur IS NULL OR b_superuser + THEN + RAISE EXCEPTION 'TA5. Opération interdite. Permissions insuffisantes pour le rôle %.', a_producteur + USING HINT = 'Votre rôle doit être membre de ' || a_producteur + || ' ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + END IF ; + END IF ; + END IF ; + IF b_test + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + -- par commodité, on rend createur membre à la fois de NEW et (si besoin) + -- de OLD.producteur, même si l'utilisateur avait déjà accès à + -- l'un des deux par ailleurs : + IF NOT pg_has_role(createur, NEW.producteur, 'USAGE') AND NOT b_superuser + THEN + EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO ' || quote_ident(createur) ; + RAISE NOTICE '... Permission accordée à %.', createur || ' sur le rôle ' || NEW.producteur ; + END IF ; + IF TG_OP = 'UPDATE' + THEN + IF NOT pg_has_role(createur, a_producteur, 'USAGE') AND NOT b_superuser + THEN + EXECUTE 'GRANT ' || quote_ident(a_producteur) || ' TO ' || quote_ident(createur) ; + RAISE NOTICE '... Permission accordée à %.', createur || ' sur le rôle ' || a_producteur ; + END IF ; + END IF ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + -- permission de g_admin sur le producteur, s'il y a encore lieu + -- à noter que, dans le cas où le producteur n'a pas été modifié, g_admin + -- devrait déjà avoir une permission sur NEW.producteur, sauf à ce qu'elle + -- lui ait été retirée manuellement entre temps. Les requêtes suivantes + -- génèreraient alors une erreur même dans le cas où la modification ne + -- porte que sur les rôles lecteur/éditeur - ce qui peut-être perçu comme + -- discutable. + IF NOT pg_has_role('g_admin', NEW.producteur, 'USAGE') AND NOT b_superuser + THEN + IF createur IS NOT NULL + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin' ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + ELSE + SELECT grantee INTO administrateur + FROM information_schema.applicable_roles + WHERE is_grantable = 'YES' AND role_name = NEW.producteur ; + IF FOUND + THEN + EXECUTE 'SET ROLE ' || quote_ident(administrateur) ; + EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin' ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + ELSE + RAISE EXCEPTION 'TA6. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur + USING DETAIL = 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin', + HINT = 'Votre rôle doit être membre de ' || NEW.producteur + || ' avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + END IF ; + END IF ; + END IF ; + END IF ; + + ------ PREPARATION DE L'EDITEUR ------ + -- limitée ici à la création du rôle et l'implémentation + -- de son OID. On ne s'intéresse donc pas aux cas : + -- - où il y a pas d'éditeur ; + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- ou dont le libellé a seulement été nettoyé par le + -- trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR NEW.editeur IS NULL + OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.editeur = OLD.editeur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.editeur IN (SELECT rolname FROM pg_catalog.pg_roles) + AND NOT NEW.editeur = 'public' + -- si l'éditeur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA7. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.editeur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; + END IF ; + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE 'CREATE ROLE ' || quote_ident(NEW.editeur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.editeur ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + -- mise à jour du champ d'OID de l'éditeur + IF NEW.editeur = 'public' + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_editeur = 0, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_editeur IS NULL + OR NOT oid_editeur = 0 + ) ; + ELSE + UPDATE z_asgard.gestion_schema_etr + SET oid_editeur = quote_ident(NEW.editeur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_editeur IS NULL + OR NOT oid_editeur = quote_ident(NEW.editeur)::regrole::oid + ) ; + END IF ; + END IF ; + + ------ PREPARATION DU LECTEUR ------ + -- limitée ici à la création du rôle et l'implémentation + -- de son OID. On ne s'intéresse donc pas aux cas : + -- - où il y a pas de lecteur ; + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- ou dont le libellé a seulement été nettoyé par le + -- trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR NEW.lecteur IS NULL + OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.lecteur = OLD.lecteur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.lecteur IN (SELECT rolname FROM pg_catalog.pg_roles) + AND NOT NEW.lecteur = 'public' + -- si le lecteur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA8. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.lecteur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; + END IF ; + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE 'CREATE ROLE ' || quote_ident(NEW.lecteur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.lecteur ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + -- mise à jour du champ d'OID du lecteur + IF NEW.lecteur = 'public' + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_lecteur = 0, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_lecteur IS NULL + OR NOT oid_lecteur = 0 + ) ; + ELSE + UPDATE z_asgard.gestion_schema_etr + SET oid_lecteur = quote_ident(NEW.lecteur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_lecteur IS NULL + OR NOT oid_lecteur = quote_ident(NEW.lecteur)::regrole::oid + ) ; + END IF ; + END IF ; + + ------ CREATION DU SCHEMA ------ + -- on exclut au préalable les cas qui ne + -- correspondent pas à des créations, ainsi que les + -- remontées de l'event trigger sur CREATE SCHEMA, + -- car le schéma existe alors déjà + b_test := False ; + IF NOT NEW.creation OR NEW.ctrl[1] = 'CREATE' + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- le schéma est créé s'il n'existe pas déjà (cas d'ajout + -- d'un schéma pré-existant qui n'était pas référencé dans + -- gestion_schema jusque-là), sinon on alerte juste + -- l'utilisateur + IF NOT NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + IF NOT has_database_privilege(current_database(), 'CREATE') + OR NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + -- si le rôle courant n'a pas les privilèges nécessaires pour + -- créer le schéma, on tente avec le rôle createur [de rôles] + -- pré-identifié, dont on sait au moins qu'il aura les + -- permissions nécessaires sur le rôle producteur - mais pas + -- s'il est habilité à créer des schémas + IF createur IS NOT NULL + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + END IF ; + IF NOT has_database_privilege(current_database(), 'CREATE') + OR NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TA9. Opération interdite. Vous n''êtes pas habilité à créer le schéma %.', NEW.nom_schema + USING HINT = 'Être membre d''un rôle disposant du privilège CREATE sur la base de données est nécessaire pour créer des schémas.' ; + END IF ; + END IF ; + EXECUTE 'CREATE SCHEMA ' || quote_ident(NEW.nom_schema) || ' AUTHORIZATION ' || quote_ident(NEW.producteur) ; + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + RAISE NOTICE '... Le schéma % a été créé.', NEW.nom_schema ; + ELSE + RAISE NOTICE '(schéma % pré-existant)', NEW.nom_schema ; + END IF ; + -- récupération de l'OID du schéma + UPDATE z_asgard.gestion_schema_etr + SET oid_schema = quote_ident(NEW.nom_schema)::regnamespace::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_schema IS NULL + OR NOT oid_schema = quote_ident(NEW.nom_schema)::regnamespace::oid + ) ; + END IF ; + + ------ APPLICATION DES DROITS DU PRODUCTEUR ------ + -- comme précédemment pour la préparation du producteur, + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister + -- (creation vaut False) ; + -- - d'un schéma pré-existant dont le producteur ne change pas + -- ou dont le libellé a juste été nettoyé par le trigger + -- BEFORE ; + -- - d'une remontée de l'event trigger asgard_on_create_schema, + -- car le producteur sera déjà propriétaire du schéma + -- et de son éventuel contenu. Par contre on garde les INSERT, + -- pour les cas de référencements ; + -- - de z_asgard_admin (pour permettre sa saisie initiale + -- dans la table de gestion, étant entendu qu'il est + -- impossible au trigger sur gestion_schema de lancer + -- un ALTER TABLE OWNER TO sur cette même table). + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation + OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL)) + OR NEW.ctrl[1] = 'CREATE' + OR NEW.nom_schema = 'z_asgard_admin' + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.producteur = OLD.producteur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + ELSIF TG_OP = 'UPDATE' + THEN + IF NOT pg_has_role(a_producteur, 'USAGE') + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + END IF ; + END IF ; + + -- changements de propriétaires + IF (NEW.nom_schema, NEW.producteur) + IN (SELECT schema_name, schema_owner FROM information_schema.schemata) + THEN + -- si producteur est déjà propriétaire du schéma (cas d'une remontée de l'event trigger, + -- principalement), on ne change que les propriétaires des objets éventuels + IF quote_ident(NEW.nom_schema)::regnamespace::oid + IN (SELECT refobjid FROM pg_catalog.pg_depend WHERE deptype = 'n') + THEN + -- la commande n'est cependant lancée que s'il existe des dépendances de type + -- DEPENDENCY_NORMAL sur le schéma, ce qui est une condition nécessaire à + -- l'existence d'objets dans le schéma + RAISE NOTICE 'attribution de la propriété des objets au rôle producteur du schéma % :', NEW.nom_schema ; + SELECT z_asgard.asgard_admin_proprietaire(NEW.nom_schema, NEW.producteur, False) + INTO n ; + IF n = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + ELSE + -- sinon schéma + objets + RAISE NOTICE 'attribution de la propriété du schéma et des objets au rôle producteur du schéma % :', NEW.nom_schema ; + PERFORM z_asgard.asgard_admin_proprietaire(NEW.nom_schema, NEW.producteur) ; + END IF ; + + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + ------ APPLICATION DES DROITS DE L'EDITEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- (y compris pour rester vide) ou dont le libellé + -- a seulement été nettoyé par le trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND coalesce(NEW.editeur, '') = coalesce(OLD.editeur, '') + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + END IF ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.editeur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + IF OLD.editeur = 'public' + THEN + a_editeur := 'public' ; + -- récupération des modifications manuelles des + -- droits de OLD.editeur/public, grâce à la fonction + -- asgard_synthese_public + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_public( + quote_ident(NEW.nom_schema)::regnamespace + ) ; + ELSE + SELECT rolname INTO a_editeur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_editeur ; + IF FOUND + THEN + -- récupération des modifications manuelles des + -- droits de OLD.editeur, grâce à la fonction + -- asgard_synthese_role + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_role( + quote_ident(NEW.nom_schema)::regnamespace, + quote_ident(a_editeur)::regrole + ) ; + END IF ; + END IF ; + END IF ; + + IF l_commande IS NOT NULL + -- transfert sur NEW.editeur des droits de + -- OLD.editeur, le cas échéant + THEN + IF NEW.editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression et transfert vers le nouvel éditeur des privilèges de l''ancien éditeur du schéma % :', NEW.nom_schema ; + ELSE + RAISE NOTICE 'suppression des privilèges de l''ancien éditeur du schéma % :', NEW.nom_schema ; + END IF ; + FOREACH c IN ARRAY l_commande + LOOP + IF NEW.editeur IS NOT NULL + THEN + EXECUTE format(c, NEW.editeur) ; + RAISE NOTICE '> %', format(c, NEW.editeur) ; + END IF ; + IF c ~ '^GRANT' + THEN + SELECT z_asgard.asgard_grant_to_revoke(c) INTO c_reverse ; + EXECUTE format(c_reverse, a_editeur) ; + RAISE NOTICE '> %', format(c_reverse, a_editeur) ; + END IF ; + END LOOP ; + + -- sinon, application des privilèges standards de l'éditeur + ELSIF NEW.editeur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma % :', NEW.nom_schema ; + + EXECUTE 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + RAISE NOTICE '> %', 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + + EXECUTE 'GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + RAISE NOTICE '> %', 'GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + + EXECUTE 'GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + RAISE NOTICE '> %', 'GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + + END IF ; + + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + ------ APPLICATION DES DROITS DU LECTEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont le lecteur ne change pas + -- (y compris pour rester vide) ou dont le libellé + -- a seulement été nettoyé par le trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + l_commande := NULL ; + IF NOT NEW.creation OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND coalesce(NEW.lecteur, '') = coalesce(OLD.lecteur, '') + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE 'SET ROLE ' || quote_ident(createur) ; + END IF ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.lecteur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + IF OLD.lecteur = 'public' + THEN + a_lecteur := 'public' ; + -- récupération des modifications manuelles des + -- droits de OLD.lecteur/public, grâce à la fonction + -- asgard_synthese_public + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_public( + quote_ident(NEW.nom_schema)::regnamespace + ) ; + ELSE + SELECT rolname INTO a_lecteur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_lecteur ; + IF FOUND + THEN + -- récupération des modifications manuelles des + -- droits de OLD.lecteur, grâce à la fonction + -- asgard_synthese_role + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_role( + quote_ident(NEW.nom_schema)::regnamespace, + quote_ident(a_lecteur)::regrole + ) ; + END IF ; + END IF ; + END IF ; + + IF l_commande IS NOT NULL + -- transfert sur NEW.lecteur des droits de + -- OLD.lecteur, le cas échéant + THEN + IF NEW.lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression et transfert vers le nouveau lecteur des privilèges de l''ancien lecteur du schéma % :', NEW.nom_schema ; + ELSE + RAISE NOTICE 'suppression des privilèges de l''ancien lecteur du schéma % :', NEW.nom_schema ; + END IF ; + FOREACH c IN ARRAY l_commande + LOOP + IF NEW.lecteur IS NOT NULL + THEN + EXECUTE format(c, NEW.lecteur) ; + RAISE NOTICE '> %', format(c, NEW.lecteur) ; + END IF ; + IF c ~ '^GRANT' + THEN + SELECT z_asgard.asgard_grant_to_revoke(c) INTO c_reverse ; + EXECUTE format(c_reverse, a_lecteur) ; + RAISE NOTICE '> %', format(c_reverse, a_lecteur) ; + END IF ; + END LOOP ; + + -- sinon, application des privilèges standards du lecteur + ELSIF NEW.lecteur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma % :', NEW.nom_schema ; + + EXECUTE 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + RAISE NOTICE '> %', 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + + EXECUTE 'GRANT SELECT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + RAISE NOTICE '> %', 'GRANT SELECT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + + EXECUTE 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + RAISE NOTICE '> %', 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + + END IF ; + + EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + END IF ; + + RETURN NULL ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'TA0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() IS 'ASGARD. Fonction appelée par le trigger qui répercute physiquement les modifications de la table de gestion.' ; + + +-- Trigger: asgard_on_modify_gestion_schema_after + +CREATE TRIGGER asgard_on_modify_gestion_schema_after + AFTER INSERT OR UPDATE + ON z_asgard_admin.gestion_schema + FOR EACH ROW + EXECUTE PROCEDURE z_asgard_admin.asgard_on_modify_gestion_schema_after(); + +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Trigger qui répercute physiquement les modifications de la table de gestion.' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +----------------------------------------------------------- +------ 6 - GESTION DES PERMISSIONS SUR LAYER_STYLES ------ +----------------------------------------------------------- +/* 6.1 - PETITES FONCTIONS UTILITAIRES + 6.2 - FONCTION D'ADMINISTRATION DES PERMISSIONS SUR LAYER_STYLES */ + +------ 6.1 - PETITES FONCTIONS UTILITAIRES ------ + +-- Function: z_asgard.asgard_has_role_usage(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage(role_parent text, role_enfant text DEFAULT current_user) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction détermine si un rôle est membre d'un autre ( + y compris indirectement) et hérite de ses droits. Elle est + équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') + en plus permissif - elle renvoie False quand l'un des rôles + n'existe pas plutôt que d'échouer. +ARGUMENTS : +- role_parent est le nom du rôle dont on souhaite savoir si l'autre +est membre ; +- (optionnel) role_enfant est le nom du rôle dont on souhaite savoir +s'il est membre de l'autre. Si non renseigné, la fonction testera +l'utilisateur courant. +SORTIE : True si la relation entre les rôles est vérifiée. False +si elle ne l'est pas ou si l'un des rôles n'existe pas. */ +BEGIN + + RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; + +EXCEPTION WHEN undefined_object +THEN + RETURN False ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; + + +-- Function: z_asgard.asgard_is_relation_owner(text, text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_relation_owner( + nom_schema text, + nom_relation text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction détermine si un rôle est membre du + propriétaire d'une table, vue ou autre relation. +ARGUMENTS : +- nom_schema est une chaîne de caractères correspondant au nom +du schéma contenant la relation ; +- nom_relation est une chaîne de caractères correspondant au nom +de la relation ; +- (optionnel) nom_role est le nom du rôle dont on veut vérifier +les permissions. Si non renseigné, la fonction testera +l'utilisateur courant. +Tous les arguments sont en écriture naturelle, sans les +guillemets des identifiants PostgreSQL. +SORTIE : True si le rôle est membre du propriétaire de la relation. +False sinon, incluant les cas où le rôle ou la relation n'existe +pas. */ +DECLARE + owner text ; +BEGIN + + SELECT pg_roles.rolname INTO owner + FROM pg_catalog.pg_class + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = relowner + WHERE quote_ident(nom_schema) = relnamespace::regnamespace::text + AND nom_relation = relname ; + + IF NOT FOUND + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(owner, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) IS 'ASGARD. Le rôle est-il membre du propriétaire de la relation considérée ?' ; + + +-- Function: z_asgard.asgard_is_producteur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_producteur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction détermine si le rôle considéré est membre + du rôle producteur d'un schéma donné. +ARGUMENTS : +- nom_schema est une chaîne de caractères correspondant à un +nom de schéma ; +- (optionnel) nom_role est le nom du rôle dont on veut vérifier +les permissions. Si non renseigné, la fonction testera +l'utilisateur courant. +Tous les arguments sont en écriture naturelle, sans les +guillemets des identifiants PostgreSQL. +SORTIE : True si le rôle est membre du rôle producteur du schéma. +False si le schéma n'existe pas ou si le rôle n'est pas membre de +son producteur. */ +DECLARE + producteur text ; +BEGIN + + SELECT gestion_schema_read_only.producteur INTO producteur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF NOT FOUND + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(producteur, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_producteur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_producteur(text, text) IS 'ASGARD. Le rôle est-il membre du producteur du schéma considéré ?' ; + + +-- Function: z_asgard.asgard_is_editeur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_editeur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction détermine si le rôle considéré est membre + du rôle éditeur d'un schéma donné. +ARGUMENTS : +- nom_schema est une chaîne de caractères correspondant à un +nom de schéma ; +- (optionnel) nom_role est le nom du rôle dont on veut vérifier +les permissions. Si non renseigné, la fonction testera +l'utilisateur courant. +Tous les arguments sont en écriture naturelle, sans les +guillemets des identifiants PostgreSQL. +SORTIE : True si le rôle est membre du rôle editeur du schéma. +False si le schéma n'existe pas ou si le rôle n'est pas membre de +son éditeur. */ +DECLARE + editeur text ; +BEGIN + + SELECT gestion_schema_read_only.editeur INTO editeur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF editeur is NULL + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(editeur, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_editeur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_editeur(text, text) IS 'ASGARD. Le rôle est-il membre du éditeur du schéma considéré ?' ; + + +-- Function: z_asgard.asgard_is_lecteur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_lecteur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction détermine si le rôle considéré est membre + du rôle lecteur d'un schéma donné. +ARGUMENTS : +- nom_schema est une chaîne de caractères correspondant à un +nom de schéma ; +- (optionnel) nom_role est le nom du rôle dont on veut vérifier +les permissions. Si non renseigné, la fonction testera +l'utilisateur courant. +Tous les arguments sont en écriture naturelle, sans les +guillemets des identifiants PostgreSQL. +SORTIE : True si le rôle est membre du rôle lecteur du schéma. +False si le schéma n'existe pas ou si le rôle n'est pas membre de +son lecteur. */ +DECLARE + lecteur text ; +BEGIN + + SELECT gestion_schema_read_only.lecteur INTO lecteur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF lecteur IS NULL + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(lecteur, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_lecteur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_lecteur(text, text) IS 'ASGARD. Le rôle est-il membre du lecteur du schéma considéré ?' ; + + +------ 6.2 - FONCTION D'ADMINISTRATION DES PERMISSIONS SUR LAYER_STYLES ------ + +-- Function: z_asgard_admin.asgard_layer_styles(int) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_layer_styles(variante int) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* OBJET : Cette fonction confère à g_consult un accès en + lecture à la table layer_styles du schéma public (table créée + par QGIS pour stocker les styles de couches), ainsi que des + droits d'écriture selon la stratégie spécifiée par le paramètre + "variante". + Elle échoue si la table layer_styles n'existe pas. + Il est possible de relancer la fonction à volonté pour + modifier la stratégie à mettre en oeuvre. + Hormis pour la variante 0, la fonction a pour effet d'activer + la sécurisation niveau ligne sur la table, ce qui pourra + rendre inopérants des accès précédemment définis. + +ARGUMENT : variante est un entier spécifiant les droits à donner en écriture. + - 0 : autorise g_admin à modifier layer_styles. À noter que cette option + n'a d'intérêt que si g_admin n'est pas propriétaire de la table layer_styles ; + - 1 : idem 0 + autorise le producteur d'un schéma à modifier les styles + associés aux tables qu'il contient ; + - 2 : idem 1 + autorise l'éditeur d'un schéma à enregistrer de nouveaux + styles (INSERT) pour les tables du schéma et à modifier (UPDATE et DELETE) + les styles tels que le champ "owner" de layer_styles contient un rôle dont + l'utilisateur est membre (généralement son propre rôle de connexion), tout + cela sous réserve que le style ne soit pas identifié comme style par défaut ; + - 3 : idem 2 sans la condition sur les styles par défaut ; + - 4 : idem 3 sans la condition d'appartenance au rôle "owner" du style ; + - 5 : idem 2, mais les mêmes autorisations sont également données au + lecteur du schéma ; + - 99 : supprime tous les droits accordés par les autres stratégies (y + compris l'accès en lecture de g_consult). + +SORTIE : '__ FIN ATTRIBUTION PERMISSIONS.' (ou '__ FIN SUPPRESSION PERMISSIONS.' +pour la variante 99) si l'opération s'est déroulée comme prévu. */ + +BEGIN + + IF NOT 'layer_styles' IN (SELECT relname FROM pg_catalog.pg_class WHERE relnamespace = 'public'::regnamespace) + THEN + RAISE EXCEPTION 'ALS01. La table layer_styles n''existe pas.' + USING ERRCODE = '42P01' ; + END IF ; + + IF NOT z_asgard.asgard_is_relation_owner('public', 'layer_styles') + THEN + RAISE EXCEPTION 'ALS02. Vous devez être membre du rôle propriétaire de la table layer_styles pour réaliser cette opération.' + USING ERRCODE = '42501' ; + END IF ; + + ------ NETTOYAGE ------ + + -- suppression des droits + IF NOT z_asgard.asgard_is_relation_owner('public', 'layer_styles', 'g_consult') + THEN + REVOKE SELECT, INSERT, UPDATE, DELETE ON layer_styles FROM g_consult ; + REVOKE SELECT, USAGE ON SEQUENCE layer_styles_id_seq FROM g_consult ; + END IF ; + IF NOT z_asgard.asgard_is_relation_owner('public', 'layer_styles', 'g_admin') + THEN + REVOKE SELECT, INSERT, UPDATE, DELETE ON layer_styles FROM g_admin ; + REVOKE SELECT, USAGE ON SEQUENCE layer_styles_id_seq FROM g_admin ; + END IF ; + + -- suppression des politiques de sécurité + DROP POLICY IF EXISTS asgard_layer_styles_public_select ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_producteur_insert ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_producteur_update ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_producteur_delete ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_editeur_insert ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_editeur_update ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_editeur_delete ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_lecteur_insert ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_lecteur_update ON layer_styles ; + DROP POLICY IF EXISTS asgard_layer_styles_lecteur_delete ON layer_styles ; + + -- désactivation de la sécurisation niveau ligne + ALTER TABLE layer_styles DISABLE ROW LEVEL SECURITY ; + + IF variante = 99 + THEN + RETURN '__ FIN SUPPRESSION PERMISSIONS.' ; + END IF ; + + ------ NOUVELLE STRATEGIE ------- + + IF variante = 0 + THEN + -- droits de lecture pour g_consult + GRANT SELECT ON layer_styles TO g_consult ; + GRANT SELECT ON SEQUENCE layer_styles_id_seq TO g_consult ; + + -- droits d'édition pour g_admin + GRANT SELECT, INSERT, UPDATE, DELETE ON layer_styles TO g_admin ; + GRANT SELECT, USAGE ON SEQUENCE layer_styles_id_seq TO g_admin ; + + RETURN '__ FIN ATTRIBUTION PERMISSIONS.' ; + END IF ; + + -- activation de la sécurisation niveau ligne + ALTER TABLE layer_styles ENABLE ROW LEVEL SECURITY ; + + -- droits d'édition pour g_admin + GRANT SELECT, INSERT, UPDATE, DELETE ON layer_styles TO g_admin ; + GRANT SELECT, USAGE ON SEQUENCE layer_styles_id_seq TO g_admin ; + -- NB : g_admin a l'attribut BYPASSRLS, donc pourra quoi qu'il + -- arrive accéder à toutes les lignes de la table. + + -- droits étendus pour g_consult + GRANT SELECT, INSERT, UPDATE, DELETE ON layer_styles TO g_consult ; + GRANT SELECT, USAGE ON SEQUENCE layer_styles_id_seq TO g_consult ; + + -- définition des politiques de sécurité : + -- - accès en lecture pour tous + CREATE POLICY asgard_layer_styles_public_select ON layer_styles + FOR SELECT USING (True) ; + + -- - accès en écriture pour les membres du rôle producteur + CREATE POLICY asgard_layer_styles_producteur_insert ON layer_styles + FOR INSERT + WITH CHECK (z_asgard.asgard_is_producteur(f_table_schema)) ; + CREATE POLICY asgard_layer_styles_producteur_update ON layer_styles + FOR UPDATE + USING (z_asgard.asgard_is_producteur(f_table_schema)) + WITH CHECK (z_asgard.asgard_is_producteur(f_table_schema)) ; + CREATE POLICY asgard_layer_styles_producteur_delete ON layer_styles + FOR DELETE + USING (z_asgard.asgard_is_producteur(f_table_schema)) ; + IF variante = 1 + THEN + RETURN '__ FIN ATTRIBUTION PERMISSIONS.' ; + END IF ; + + -- - accès en écriture pour les membres du rôle éditeur + IF variante IN (2, 5) + THEN + CREATE POLICY asgard_layer_styles_editeur_insert ON layer_styles + FOR INSERT + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + CREATE POLICY asgard_layer_styles_editeur_update ON layer_styles + FOR UPDATE + USING (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + CREATE POLICY asgard_layer_styles_editeur_delete ON layer_styles + FOR DELETE + USING (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + ELSIF variante = 3 + THEN + CREATE POLICY asgard_layer_styles_editeur_insert ON layer_styles + FOR INSERT + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner)) ; + CREATE POLICY asgard_layer_styles_editeur_update ON layer_styles + FOR UPDATE + USING (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner)) + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner)) ; + CREATE POLICY asgard_layer_styles_editeur_delete ON layer_styles + FOR DELETE + USING (z_asgard.asgard_is_editeur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner)) ; + ELSIF variante = 4 + THEN + CREATE POLICY asgard_layer_styles_editeur_insert ON layer_styles + FOR INSERT + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema)) ; + CREATE POLICY asgard_layer_styles_editeur_update ON layer_styles + FOR UPDATE + USING (z_asgard.asgard_is_editeur(f_table_schema)) + WITH CHECK (z_asgard.asgard_is_editeur(f_table_schema)) ; + CREATE POLICY asgard_layer_styles_editeur_delete ON layer_styles + FOR DELETE + USING (z_asgard.asgard_is_editeur(f_table_schema)) ; + END IF ; + + IF variante < 5 + THEN + RETURN '__ FIN ATTRIBUTION PERMISSIONS.' ; + END IF ; + + -- - accès en écriture pour les membres du rôle lecteur + CREATE POLICY asgard_layer_styles_lecteur_insert ON layer_styles + FOR INSERT + WITH CHECK (z_asgard.asgard_is_lecteur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + CREATE POLICY asgard_layer_styles_lecteur_update ON layer_styles + FOR UPDATE + USING (z_asgard.asgard_is_lecteur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) + WITH CHECK (z_asgard.asgard_is_lecteur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + CREATE POLICY asgard_layer_styles_lecteur_delete ON layer_styles + FOR DELETE + USING (z_asgard.asgard_is_lecteur(f_table_schema) AND z_asgard.asgard_has_role_usage(owner) AND NOT useasdefault) ; + + RETURN '__ FIN ATTRIBUTION PERMISSIONS.' ; +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_layer_styles(int) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_layer_styles(int) IS 'ASGARD. Fonction qui définit des permissions sur la table layer_styles de QGIS.' ; + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From f894ef88413d52e6a5c4e15ae362e5593cb57330 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:57:48 +0200 Subject: [PATCH 05/32] Create asgard--1.3.2--1.4.0.sql Initialisation. --- asgard--1.3.2--1.4.0.sql | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 asgard--1.3.2--1.4.0.sql diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql new file mode 100644 index 0000000..e5eff13 --- /dev/null +++ b/asgard--1.3.2--1.4.0.sql @@ -0,0 +1,86 @@ +\echo Use "ALTER EXTENSION plume_pg UPDATE TO '1.4.0'" to load this file. \quit +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- ASGARD - Système de gestion des droits pour PostgreSQL, version 1.4.0 +-- > Script de mise à jour depuis la version 1.3.2. +-- +-- Copyright République Française, 2020-2022. +-- Secrétariat général du Ministère de la transition écologique, du +-- Ministère de la cohésion des territoires et des relations avec les +-- collectivités territoriales et du Ministère de la Mer. +-- Service du numérique. +-- +-- contributrice pour cette version : Leslie Lemaire (SNUM/UNI/DRC). +-- +-- mél : drc.uni.snum.sg@developpement-durable.gouv.fr +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Note de version : +-- https://snum.scenari-community.org/Asgard/Documentation/#SEC_1-4-0 +-- +-- Documentation : +-- https://snum.scenari-community.org/Asgard/Documentation/ +-- +-- GitHub : +-- https://github.com/MTES-MCT/asgard-postgresql +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Ce logiciel est un programme informatique complémentaire au système de +-- gestion de base de données PosgreSQL ("https://www.postgresql.org/"). Il +-- met à disposition un cadre méthodologique et des outils pour la gestion +-- des droits sur les serveurs PostgreSQL. +-- +-- Ce logiciel est régi par la licence CeCILL-B soumise au droit français +-- et respectant les principes de diffusion des logiciels libres. Vous +-- pouvez utiliser, modifier et/ou redistribuer ce programme sous les +-- conditions de la licence CeCILL-B telle que diffusée par le CEA, le +-- CNRS et l'INRIA sur le site "http://www.cecill.info". +-- Lien SPDX : "https://spdx.org/licenses/CECILL-B.html". +-- +-- En contrepartie de l'accessibilité au code source et des droits de copie, +-- de modification et de redistribution accordés par cette licence, il n'est +-- offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +-- seule une responsabilité restreinte pèse sur l'auteur du programme, le +-- titulaire des droits patrimoniaux et les concédants successifs. +-- +-- A cet égard l'attention de l'utilisateur est attirée sur les risques +-- associés au chargement, à l'utilisation, à la modification et/ou au +-- développement et à la reproduction du logiciel par l'utilisateur étant +-- donné sa spécificité de logiciel libre, qui peut le rendre complexe à +-- manipuler et qui le réserve donc à des développeurs et des professionnels +-- avertis possédant des connaissances informatiques approfondies. Les +-- utilisateurs sont donc invités à charger et tester l'adéquation du +-- logiciel à leurs besoins dans des conditions permettant d'assurer la +-- sécurité de leurs systèmes et ou de leurs données et, plus généralement, +-- à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +-- +-- Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +-- pris connaissance de la licence CeCILL-B, et que vous en avez accepté +-- les termes. +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- +-- Cette extension ne peut être installée que par un super-utilisateur +-- (création de déclencheurs sur évènement). +-- +-- Elle n'est pas compatible avec les versions 9.4 ou antérieures de +-- PostgreSQL. +-- +-- schémas contenant les objets : z_asgard et z_asgard_admin. +-- +-- objets créés par le script : néant. +-- +-- objets modifiés par le script : néant. +-- +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +-- MOT DE PASSE DE CONTRÔLE : 'x7-A;#rzo' + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1ed28a63cf1e7eb48cb27ca1a4478be67fb11038 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:58:30 +0200 Subject: [PATCH 06/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout d'un test confirmant que les schémas référencés le restent lors de la montée de version. --- recette/asgard_recette.sql | 92 +++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index 7df401b..46eefeb 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -1,6 +1,6 @@ -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- --- ASGARD - Système de gestion des droits pour PostgreSQL, version 1.3.2 +-- ASGARD - Système de gestion des droits pour PostgreSQL, version 1.4.0 -- > Script de recette. -- -- Copyright République Française, 2020-2022. @@ -16569,3 +16569,93 @@ $_$; COMMENT ON FUNCTION z_asgard_recette.t089b() IS 'ASGARD recette. TEST : Prise en charge des commandes sur tous les types de routines.' ; +-- FUNCTION: z_asgard_recette.t090() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t090() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + DROP EXTENSION asgard ; + CREATE EXTENSION asgard VERSION '1.2.4' ; + + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_admin ; + ASSERT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #1' ; + + ALTER EXTENSION asgard UPDATE ; + ASSERT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #2' ; + + SET ROLE g_admin ; + ASSERT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #3' ; + + RESET ROLE ; + DROP SCHEMA c_bibliotheque ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$; + +COMMENT ON FUNCTION z_asgard_recette.t090() IS 'ASGARD recette. TEST : Préservation du référencement des schémas lors des montées de version.' ; + + +-- FUNCTION: z_asgard_recette.t090b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t090b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + DROP EXTENSION asgard ; + CREATE EXTENSION asgard VERSION '1.2.4' ; + + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION g_admin ; + ASSERT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #1' ; + + ALTER EXTENSION asgard UPDATE ; + ASSERT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #2' ; + + SET ROLE g_admin ; + ASSERT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation), + 'échec assertion #3' ; + + RESET ROLE ; + DROP SCHEMA "c_Bibliothèque" ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$; + +COMMENT ON FUNCTION z_asgard_recette.t090b() IS 'ASGARD recette. TEST : Préservation du référencement des schémas lors des montées de version.' ; From dc1619a75a74655fc4ee1d386bcc705e673b6d27 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 17 May 2022 19:49:52 +0200 Subject: [PATCH 07/32] Building v1.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accès en lecture aux objets d'Asgard à public au lieu de g_consult. Prise en charge des objets statistiques étendus, des classes d'opérateurs et des familles d'opérateurs. Correction d'une coquille dans la fonction z_asgard_admin.asgard_on_create_objet() (comparaison d'un nom d'objet avec un identifiant). Suppression de toutes les douteuses commandes replace('"', '') parfois utilisées pour transformer des identifiants d'objets en noms. Suppression définitive de la fonction z_asgard.asgard_role_trans_acl(regrole). La suppression de schémas via des commandes DROP OWNED est désormais correctement répercutée dans la table de gestion (modification du déclencheur sur évènement asgard_on_drop_schema). z_asgard.asgard_initialise_obj(text, text, text) et z_asgard.asgard_deplace_obj(text, text, text, text, int) deviennent plus permissives sur la saisie des noms de fonctions. Il est désormais possible d'utiliser les formes abrégées des types - int au lieu de integer, par exemple - ou encore de mettre des espaces avant ou après les virgules qui séparent les types. z_asgard.asgard_deplace_obj(text, text, text, text, int) vérifie maintenant qu'il n'existe pas déjà dans le schéma d'arrivée un objet de même nom que l'objet à transférer + index et séquences liées pour les tables. --- asgard--1.3.2--1.4.0.sql | 4858 +++++++++++++++++++++++++++++++++++- asgard--1.4.0.sql | 1588 +++++++----- recette/asgard_recette.sql | 2242 ++++++++++------- 3 files changed, 7223 insertions(+), 1465 deletions(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index e5eff13..a617aa6 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -72,15 +72,4871 @@ -- -- objets créés par le script : néant. -- --- objets modifiés par le script : néant. +-- objets modifiés par le script (parfois seulement leur descriptif) : +-- - Schema: z_asgard +-- - View: z_asgard.gestion_schema_usr +-- - View: z_asgard.gestion_schema_etr +-- - View: z_asgard.asgardmenu_metadata +-- - View: z_asgard.asgardmanager_metadata +-- - View: z_asgard.gestion_schema_read_only +-- - Function: z_asgard_admin.asgard_on_alter_schema() +-- - Event Trigger: asgard_on_alter_schema +-- - Function: z_asgard_admin.asgard_on_create_schema() +-- - Event Trigger: asgard_on_create_schema +-- - Function: z_asgard_admin.asgard_on_drop_schema() +-- - Event Trigger: asgard_on_drop_schema +-- - Function: z_asgard_admin.asgard_on_create_objet() +-- - Event Trigger: asgard_on_create_objet +-- - Function: z_asgard_admin.asgard_on_alter_objet() +-- - Event Trigger: asgard_on_alter_objet +-- - Function: z_asgard.asgard_admin_proprietaire(text, text, boolean) +-- - Function: z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) +-- - Function: z_asgard.asgard_initialise_schema(text, boolean, boolean) +-- - Function: z_asgard.asgard_initialise_obj(text, text, text) +-- - Function: z_asgard.asgard_deplace_obj(text, text, text, text, int) +-- - Function: z_asgard_admin.asgard_all_login_grant_role(text, boolean) +-- - Function: z_asgard_admin.asgard_diagnostic(text[]) +-- - Function: z_asgard_admin.asgard_on_modify_gestion_schema_before() +-- - Trigger: asgard_on_modify_gestion_schema_before +-- - Function: z_asgard_admin.asgard_on_modify_gestion_schema_after() +-- - Trigger: asgard_on_modify_gestion_schema_after +-- - Function: z_asgard.asgard_has_role_usage(text, text) +-- - Function: z_asgard.asgard_is_relation_owner(text, text) +-- - Function: z_asgard.asgard_is_producteur(text, text) +-- - Function: z_asgard.asgard_is_editeur(text, text) +-- - Function: z_asgard.asgard_is_lecteur(text, text) +-- +-- objets supprimés par le script : +-- - Function: z_asgard.asgard_role_trans_acl(regrole) -- -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/* 2 - PREPARATION DES OBJETS + 3 - CREATION DES EVENT TRIGGERS + 4 - FONCTIONS UTILITAIRES + 6 - GESTION DES PERMISSIONS SUR LAYER_STYLES */ + -- MOT DE PASSE DE CONTRÔLE : 'x7-A;#rzo' -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +---------------------------------------- +------ 2 - PREPARATION DES OBJETS ------ +---------------------------------------- + +/* 2.1 - CREATION DES SCHEMAS + 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA + 2.6 - VUE POUR ASGARDMENU + 2.7 - VUE POUR ASGARDMANAGER + 2.8 - VERSION LECTURE SEULE DE GESTION_SCHEMA_USR */ + +-- Le cas échéant, le lecteur et l'éditeur de z_asgard sont révoqués : +UPDATE z_asgard_admin.gestion_schema + SET lecteur = NULL, + editeur = NULL + WHERE nom_schema = 'z_asgard' ; + +------ 2.1 - CREATION DES SCHEMAS ------ + +-- Schema: z_asgard + +REVOKE USAGE ON SCHEMA z_asgard FROM g_consult ; +GRANT USAGE ON SCHEMA z_asgard TO public ; + + +------ 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA ------ + +-- View: z_asgard.gestion_schema_usr + +REVOKE SELECT ON TABLE z_asgard.gestion_schema_usr FROM g_consult ; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public ; + +-- View: z_asgard.gestion_schema_etr + +REVOKE SELECT ON TABLE z_asgard.gestion_schema_etr FROM g_consult ; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public ; + +------ View: z_asgard.asgardmenu_metadata ------ + +REVOKE SELECT ON TABLE z_asgard.asgardmenu_metadata FROM g_consult ; +GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public ; + +------ View: z_asgard.asgardmanager_metadata ------ + +REVOKE SELECT ON TABLE z_asgard.asgardmanager_metadata FROM g_consult ; +GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public ; + +------ View: z_asgard.gestion_schema_read_only ------ + +REVOKE SELECT ON TABLE z_asgard.gestion_schema_read_only FROM g_consult ; +GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +--------------------------------------------- +------ 3 - CREATION DES EVENT TRIGGERS ------ +--------------------------------------------- + +/* 3.1 - EVENT TRIGGER SUR ALTER SCHEMA + 3.2 - EVENT TRIGGER SUR CREATE SCHEMA + 3.3 - EVENT TRIGGER SUR DROP SCHEMA + 3.4 - EVENT TRIGGER SUR CREATE OBJET + 3.5 - EVENT TRIGGER SUR ALTER OBJET */ + + +------ 3.1 - EVENT TRIGGER SUR ALTER SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_alter_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_schema() + RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_alter_schema, +qui répercute sur la table de gestion d'Asgard les changements de noms et +propriétaires réalisés par des commandes ALTER SCHEMA directes. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ +DECLARE + obj record ; + objname text ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EAS1. Schéma z_asgard inaccessible.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EAS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; + END IF ; + + + FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() + WHERE object_type = 'schema' + LOOP + + ------ RENAME ------ + UPDATE z_asgard.gestion_schema_etr + SET nom_schema = nspname, + ctrl = ARRAY['RENAME', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + WHERE oid_schema = obj.objid + AND obj.objid = pg_namespace.oid + AND NOT nom_schema = nspname + RETURNING nspname INTO objname ; + IF FOUND + THEN + RAISE NOTICE '... Le nom du schéma % a été mis à jour dans la table de gestion.', objname ; + END IF ; + + ------ OWNER TO ------ + UPDATE z_asgard.gestion_schema_etr + SET producteur = rolname, + oid_producteur = nspowner, + ctrl = ARRAY['OWNER', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE oid_schema = obj.objid + AND obj.objid = pg_namespace.oid + AND NOT oid_producteur = nspowner + RETURNING nspname INTO objname ; + IF FOUND + THEN + RAISE NOTICE '... Le producteur du schéma % a été mis à jour dans la table de gestion.', objname ; + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EAS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_alter_schema() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_alter_schema, qui répercute sur la table de gestion d''Asgard les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; + + +-- Event Trigger: asgard_on_alter_schema + +COMMENT ON EVENT TRIGGER asgard_on_alter_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; + + +------ 3.2 - EVENT TRIGGER SUR CREATE SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_create_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_schema() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_create_schema, +qui répercute sur la table de gestion d'Asgard les créations de schémas réalisées +par des commandes CREATE SCHEMA directes. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ +DECLARE + obj record ; + objname text ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'ECS1. Schéma z_asgard inaccessible.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'INSERT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'ECS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; + END IF ; + + + FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() + WHERE object_type = 'schema' + LOOP + + ------ SCHEMA PRE-ENREGISTRE DANS GESTION_SCHEMA ------ + UPDATE z_asgard.gestion_schema_etr + SET oid_schema = obj.objid, + producteur = rolname, + oid_producteur = nspowner, + creation = True, + ctrl = ARRAY['CREATE', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE quote_ident(nom_schema) = obj.object_identity + AND obj.objid = pg_namespace.oid + AND NOT creation + RETURNING nspname INTO objname ; + -- creation vaut true si et seulement si la création a été initiée via la table + -- de gestion dans ce cas, il n'est pas nécessaire de réintervenir dessus + IF FOUND + THEN + RAISE NOTICE '... Le schéma % apparaît désormais comme "créé" dans la table de gestion.', objname ; + + ------ SCHEMA NON REPERTORIE DANS GESTION_SCHEMA ------ + ELSIF NOT obj.object_identity IN (SELECT quote_ident(nom_schema) FROM z_asgard.gestion_schema_etr) + THEN + INSERT INTO z_asgard.gestion_schema_etr (oid_schema, nom_schema, producteur, oid_producteur, creation, ctrl)( + SELECT + obj.objid, + nspname, + rolname, + nspowner, + True, + ARRAY['CREATE', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE obj.objid = pg_namespace.oid + ) + RETURNING nom_schema INTO objname ; + RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', objname ; + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'ECS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_create_schema() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_create_schema, qui répercute sur la table de gestion d''Asgard les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; + + +-- Event Trigger: asgard_on_create_schema + +COMMENT ON EVENT TRIGGER asgard_on_create_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; + + +------ 3.3 - EVENT TRIGGER SUR DROP SCHEMA ------ + +-- Function: z_asgard_admin.asgard_on_drop_schema() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_drop_schema() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_drop_schema, +qui répercute sur la table de gestion d'Asgard les suppressions de schémas +réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la +suppression d'une extension. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ +DECLARE + obj record ; + objname text ; + e_mssg text ; + e_hint text ; + e_detl text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EDS1. Schéma z_asgard inaccessible.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EDS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; + END IF ; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + WHERE object_type = 'schema' + LOOP + ------ ENREGISTREMENT DE LA SUPPRESSION ------ + UPDATE z_asgard.gestion_schema_etr + SET (creation, oid_schema, ctrl) = (False, NULL, ARRAY['DROP', 'x7-A;#rzo']) + WHERE quote_ident(nom_schema) = obj.object_identity + RETURNING nom_schema INTO objname ; + IF FOUND THEN + RAISE NOTICE '... La suppression du schéma % a été enregistrée dans la table de gestion (creation = False).', objname ; + END IF ; + + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EDS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_drop_schema() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_drop_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_drop_schema, qui répercute sur la table de gestion d''Asgard les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la suppression d''une extension.' ; + + +-- Event Trigger: asgard_on_drop_schema + +DROP EVENT TRIGGER asgard_on_drop_schema ; + +CREATE EVENT TRIGGER asgard_on_drop_schema ON SQL_DROP + WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION', 'DROP OWNED') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_drop_schema() ; + +COMMENT ON EVENT TRIGGER asgard_on_drop_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la suppression d''une extension.' ; + + +------ 3.4 - EVENT TRIGGER SUR CREATE OBJET ------ + +-- Function: z_asgard_admin.asgard_on_create_objet() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_objet() + RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_create_objet, +qui applique aux nouveaux objets créés les droits pré-définis pour le schéma +dans la table de gestion d'Asgard. + + Elle est activée par toutes les commandes CREATE portant sur des objets qui + dépendent d'un schéma et ont un propriétaire. + + Elle ignore les objets dont le schéma n'est pas référencé par Asgard. + +*/ +DECLARE + obj record ; + roles record ; + src record ; + proprietaire text ; + xowner text ; + e_mssg text ; + e_hint text ; + e_detl text ; + l text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'ECO1. Schéma z_asgard inaccessible.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'ECO2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; + END IF ; + + + FOR obj IN SELECT DISTINCT + classid, objid, object_type, schema_name, object_identity + FROM pg_event_trigger_ddl_commands() + WHERE schema_name IS NOT NULL + ORDER BY object_type DESC + LOOP + + -- récupération des rôles de la table de gestion pour le schéma de l'objet + -- on se base sur les OID et non les noms pour se prémunir contre les changements + -- de libellés ; des jointures sur pg_roles permettent de vérifier que les rôles + -- n'ont pas été supprimés entre temps + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj.schema_name ; + + -- on ne traite que les schémas qui sont gérés par ASGARD + -- ce qui implique un rôle producteur non nul + IF roles.producteur IS NOT NULL + THEN + -- récupération du nom du champ contenant le propriétaire + -- courant de l'objet + SELECT attname::text INTO xowner + FROM pg_catalog.pg_attribute + WHERE attrelid = obj.classid AND attname ~ 'owner' ; + -- pourrait ne rien renvoyer pour certains pseudo-objets + -- comme les "table constraint" + + IF FOUND + THEN + -- récupération du propriétaire courant de l'objet + -- génère une erreur si la requête ne renvoie rien + EXECUTE format('SELECT rolname + FROM %2$s LEFT JOIN pg_catalog.pg_roles + ON %1$s = pg_roles.oid + WHERE %2$s.oid = %3$s', + xowner, obj.classid::regclass, obj.objid) + INTO STRICT proprietaire ; + + -- si le propriétaire courant n'est pas le producteur + IF NOT roles.producteur = proprietaire + THEN + + ------ PROPRIETAIRE DE L'OBJET (DROITS DU PRODUCTEUR) ------ + RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', + obj.object_identity ; + l := format('ALTER %s %s OWNER TO %I', + CASE WHEN obj.object_type = 'statistics object' + THEN 'statistics' ELSE obj.object_type END, + obj.object_identity, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + + ------ PROPRIETAIRE DE LA FAMILLE D'OPERATEURS IMPLICITE ------ + -- Lorsque le paramètre FAMILY n'est pas spécifié à la + -- création d'une classe d'opérateurs, une famille de + -- même nom que la classe d'opérateurs est créée... en + -- passant entre les mailles du filets du déclencheur. + IF obj.object_type = 'operator class' AND EXISTS ( + SELECT * FROM pg_catalog.pg_opclass + LEFT JOIN pg_catalog.pg_opfamily + ON pg_opfamily.oid = opcfamily + WHERE obj.objid = pg_opclass.oid + AND opfname = opcname + AND opfmethod = opcmethod + AND opfnamespace = opcnamespace + AND NOT opfowner = quote_ident(roles.producteur)::regrole + ) + THEN + l := format('ALTER operator family %s OWNER TO %I', + obj.object_identity, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + + END IF ; + + ------ DROITS DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + -- sur les tables : + IF obj.object_type IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %s TO %I', + obj.object_identity, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + + -- sur les séquences : + ELSIF obj.object_type IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %s TO %I', + obj.object_identity, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ DROITS DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + -- sur les tables : + IF obj.object_type IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %s TO %I', + obj.object_identity, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + + -- sur les séquences : + ELSIF obj.object_type IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %s TO %I', + obj.object_identity, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ VERIFICATION DES DROITS SUR LES SOURCES DES VUES ------- + IF obj.object_type IN ('view', 'materialized view') + THEN + FOR src IN ( + SELECT + DISTINCT + nom_schema, + relnamespace, + relname, + liblg, + relowner, + oid_producteur, + oid_editeur, + oid_lecteur + FROM pg_catalog.pg_rewrite + LEFT JOIN pg_catalog.pg_depend + ON objid = pg_rewrite.oid + LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = refobjid + LEFT JOIN z_asgard.gestion_schema_etr + ON relnamespace::regnamespace::text = quote_ident(gestion_schema_etr.nom_schema) + LEFT JOIN unnest( + ARRAY['Table', 'Table partitionnée', 'Vue', 'Vue matérialisée', 'Table étrangère', 'Séquence'], + ARRAY['r', 'p', 'v', 'm', 'f', 'S'] + ) AS t (liblg, libcrt) + ON relkind = libcrt + WHERE ev_class = obj.objid + AND rulename = '_RETURN' + AND ev_type = '1' + AND ev_enabled = 'O' + AND is_instead + AND classid = 'pg_rewrite'::regclass::oid + AND refclassid = 'pg_class'::regclass::oid + AND deptype = 'n' + AND NOT refobjid = obj.objid + AND NOT has_table_privilege(roles.producteur, refobjid, 'SELECT') + ) + LOOP + IF src.oid_producteur IS NOT NULL + -- l'utilisateur courant a suffisamment de droits pour voir le schéma de la source + -- dans sa table de gestion + THEN + RAISE WARNING 'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', + format('%s %s', CASE WHEN obj.object_type = 'materialized view' + THEN 'matérialisée ' ELSE '' END, obj.object_identity) + USING DETAIL = format('%s source %I.%I, producteur %s, éditeur %s, lecteur %s.', + src.liblg, src.nom_schema, src.relname, src.oid_producteur::regrole, + coalesce(src.oid_editeur::regrole::text, 'non défini'), + coalesce(src.oid_lecteur::regrole::text, 'non défini') + ), + HINT = CASE WHEN src.oid_lecteur IS NULL + THEN format('Pour faire du producteur de la vue %s le lecteur du schéma source, vous pouvez lancer la commande suivante : UPDATE z_asgard.gestion_schema_usr SET lecteur = %L WHERE nom_schema = %L.', + CASE WHEN obj.object_type = 'materialized view' THEN 'matérialisée ' ELSE '' END, + roles.producteur, src.nom_schema) + ELSE format('Pour faire du producteur de la vue %s le lecteur du schéma source, vous pouvez lancer la commande suivante : GRANT %s TO %I.', + CASE WHEN obj.object_type = 'materialized view' THEN 'matérialisée ' ELSE '' END, + src.oid_lecteur::regrole, roles.producteur) + END ; + ELSE + RAISE WARNING 'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', + format('%s %s', CASE WHEN obj.object_type = 'materialized view' + THEN 'matérialisée ' ELSE '' END, obj.object_identity) + USING DETAIL = format('%s source %s.%I, propriétaire %s.', src.liblg, + src.relnamespace::regnamespace, src.relname, src.relowner::regrole) ; + END IF ; + END LOOP ; + END IF ; + END IF ; + END IF ; + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'ECO0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_create_objet() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_objet() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_create_objet, qui applique aux nouveaux objets créés les droits pré-définis pour le schéma dans la table de gestion d''Asgard.' ; + + +-- Event Trigger: asgard_on_create_objet + +DROP EVENT TRIGGER asgard_on_create_objet ; + +DO +$$ +BEGIN + IF current_setting('server_version_num')::int < 100000 + THEN + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; + ELSIF current_setting('server_version_num')::int < 110000 + THEN + -- + CREATE STATISTICS pour PG 10 + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY', 'CREATE STATISTICS') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; + ELSE + -- + CREATE PROCEDURE pour PG 11+ + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY', 'CREATE STATISTICS', 'CREATE PROCEDURE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; + END IF ; +END +$$ ; + +COMMENT ON EVENT TRIGGER asgard_on_create_objet IS 'ASGARD. Déclencheur sur évènement qui applique aux nouveaux objets créés les droits pré-définis pour le schéma dans la table de gestion d''Asgard.' ; + + +------ 3.5 - EVENT TRIGGER SUR ALTER OBJET ------ + +-- Function: z_asgard_admin.asgard_on_alter_objet() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_objet() RETURNS event_trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_alter_objet, +qui assure que le producteur d'un schéma reste propriétaire de tous les +objets qui en dépendent. + + Elle est activée par toutes les commandes ALTER portant sur des objets qui + dépendent d'un schéma et ont un propriétaire, mais n'aura réellement d'effet + que pour celles qui affectent la cohérence des propriétaires : + + * Les ALTER ... SET SCHEMA lorsque le schéma cible a un producteur différent + de celui du schéma d'origine. Elle modifie alors le propriétaire de l'objet + selon le producteur du nouveau schéma. + * Les ALTER ... OWNER TO, dont elle inhibe l'effet en rendant la propriété de + l'objet au producteur du schéma. + + Elle n'agit pas sur les privilèges. Elle ignore les objets dont le schéma + (après exécution de la commande) n'est pas référencé par Asgard. + +*/ +DECLARE + obj record ; + n_producteur regrole ; + a_producteur regrole ; + l text ; + e_mssg text ; + e_hint text ; + e_detl text ; + xowner text ; +BEGIN + ------ CONTROLES DES PRIVILEGES ------ + IF NOT has_schema_privilege('z_asgard', 'USAGE') + THEN + RAISE EXCEPTION 'EAO1. Schéma z_asgard inaccessible.' ; + END IF ; + + IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + THEN + RAISE EXCEPTION 'EAO2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; + END IF ; + + FOR obj IN SELECT DISTINCT + classid, objid, object_type, schema_name, object_identity + FROM pg_event_trigger_ddl_commands() + WHERE schema_name IS NOT NULL + ORDER BY object_type DESC + LOOP + + -- récupération du rôle identifié comme producteur pour le schéma de l'objet + -- (à l'issue de la commande) + -- on se base sur l'OID et non le nom pour se prémunir contre les changements + -- de libellés + SELECT oid_producteur::regrole INTO n_producteur + FROM z_asgard.gestion_schema_etr + WHERE nom_schema = obj.schema_name ; + + IF FOUND + THEN + -- récupération du nom du champ contenant le propriétaire + -- de l'objet + SELECT attname::text INTO xowner + FROM pg_catalog.pg_attribute + WHERE attrelid = obj.classid AND attname ~ 'owner' ; + -- ne renvoie rien pour certains pseudo-objets comme les + -- "table constraint" + + IF FOUND + THEN + -- récupération du propriétaire courant de l'objet + -- génère une erreur si la requête ne renvoie rien + EXECUTE format('SELECT %s::regrole FROM %s WHERE oid = %s', + xowner, obj.classid::regclass, obj.objid) + INTO STRICT a_producteur ; + + -- si les deux rôles sont différents + IF NOT n_producteur = a_producteur + THEN + ------ MODIFICATION DU PROPRIETAIRE ------ + -- l'objet est attribué au propriétaire désigné pour le schéma + -- (n_producteur) + RAISE NOTICE 'attribution de la propriété de % au rôle producteur du schéma :', + obj.object_identity ; + l := format('ALTER %s %s OWNER TO %s', + CASE WHEN obj.object_type = 'statistics object' + THEN 'statistics' ELSE obj.object_type END, + obj.object_identity, n_producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + END IF ; + END LOOP ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'EAO0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_alter_objet() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_objet() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_alter_objet, qui assure que le producteur d''un schéma reste propriétaire de tous les objets qui en dépendent.' ; + + +-- Event Trigger: asgard_on_alter_objet + +DROP EVENT TRIGGER asgard_on_alter_objet ; + +DO +$$ +BEGIN + IF current_setting('server_version_num')::int < 100000 + THEN + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; + ELSIF current_setting('server_version_num')::int < 110000 + THEN + -- + ALTER STATISTICS, ALTER OPERATOR CLASS, ALTER OPERATOR FAMILY + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', + 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; + ELSE + -- + ALTER PROCEDURE, ALTER ROUTINE + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', + 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY', 'ALTER PROCEDURE', + 'ALTER ROUTINE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; + END IF ; +END +$$ ; + +COMMENT ON EVENT TRIGGER asgard_on_alter_objet IS 'ASGARD. Déclencheur sur évènement qui assure que le producteur d''un schéma reste propriétaire de tous les objets qui en dépendent.' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +--------------------------------------- +------ 4 - FONCTIONS UTILITAIRES ------ +--------------------------------------- + +/* 4.3 - MODIFICATION DU PROPRIETAIRE D'UN SCHEMA ET SON CONTENU + 4.5 - INITIALISATION DE GESTION_SCHEMA + 4.8 - REINITIALISATION DES PRIVILEGES SUR UN SCHEMA + 4.9 - REINITIALISATION DES PRIVILEGES SUR UN OBJET + 4.10 - DEPLACEMENT D'OBJET + 4.11 - OCTROI D'UN RÔLE À TOUS LES RÔLES DE CONNEXION + 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL + 4.16 - DIAGNOSTIC DES DROITS NON STANDARDS */ + + +------ 4.3 - MODIFICATION DU PROPRIETAIRE D'UN SCHEMA ET SON CONTENU ------ + +-- Function: z_asgard.asgard_admin_proprietaire(text, text, boolean) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_admin_proprietaire( + n_schema text, n_owner text, b_setschema boolean DEFAULT True + ) + RETURNS int + LANGUAGE plpgsql + AS $_$ +/* Attribue un schéma et tous les objets qu'il contient au propriétaire désigné. + + Elle n'intervient que sur les objets qui n'appartiennent pas déjà au + rôle considéré. + + Parameters + ---------- + n_schema : text + Chaîne de caractères correspondant au nom du schéma à considérer. + n_owner : text + Chaîne de caractères correspondant au nom du rôle qui doit être + propriétaire des objets. + b_setschema : boolean, default True + Booléen qui indique si la fonction doit changer le propriétaire + du schéma ou seulement des objets qu'il contient. + + Returns + ------- + int + Nombre d'objets effectivement traités. Les commandes lancées sont + notifiées au fur et à mesure. + +*/ +DECLARE + item record ; + k int := 0 ; + o_owner oid ; + s_owner text ; +BEGIN + ------ TESTS PREALABLES ------ + SELECT nspowner::regrole::text + INTO s_owner + FROM pg_catalog.pg_namespace + WHERE nspname = n_schema ; + + -- non existance du schémas + IF NOT FOUND + THEN + RAISE EXCEPTION 'FAP1. Le schéma % n''existe pas.', n_schema ; + END IF ; + + -- absence de permission sur le propriétaire courant du schéma + IF NOT pg_has_role(s_owner::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FAP5. Vous n''êtes pas habilité à modifier le propriétaire du schéma %.', n_schema + USING DETAIL = format('Propriétaire courant : %s.', s_owner) ; + END IF ; + + -- le propriétaire désigné n'existe pas + IF NOT n_owner IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FAP2. Le rôle % n''existe pas.', n_owner ; + -- absence de permission sur le propriétaire désigné + ELSIF NOT pg_has_role(n_owner, 'USAGE') + THEN + RAISE EXCEPTION 'FAP6. Vous n''avez pas la permission d''utiliser le rôle %.', n_owner ; + ELSE + o_owner := quote_ident(n_owner)::regrole::oid ; + END IF ; + + -- le propriétaire désigné n'est pas le propriétaire courant et la fonction + -- a été lancée avec la variante qui ne traite pas le schéma + IF NOT b_setschema + AND NOT quote_ident(n_owner) = s_owner + THEN + RAISE EXCEPTION 'FAP3. Le rôle % n''est pas propriétaire du schéma.', n_owner + USING HINT = format('Lancez asgard_admin_proprietaire(%L, %L) pour changer également le propriétaire du schéma.', + n_schema, n_owner) ; + END IF ; + + ------ PROPRIÉTAIRE DU SCHEMA ------ + IF b_setschema + THEN + EXECUTE format('ALTER SCHEMA %I OWNER TO %I', n_schema, n_owner) ; + RAISE NOTICE '> %', format('ALTER SCHEMA %I OWNER TO %I', n_schema, n_owner) ; + k := k + 1 ; + END IF ; + + ------ PROPRIETAIRES DES OBJETS ------ + -- uniquement ceux qui n'appartiennent pas déjà + -- au rôle identifié + FOR item IN + -- tables, tables étrangères, vues, vues matérialisées, + -- partitions, séquences : + SELECT + relname::text AS n_objet, + relowner AS obj_owner, + relkind IN ('r', 'f', 'p', 'm') AS b, + -- b servira à assurer que les tables soient listées avant les + -- objets qui en dépendent + format('ALTER %s %s OWNER TO %I', + kind_lg, pg_class.oid::regclass, n_owner) AS commande + FROM pg_catalog.pg_class, + unnest(ARRAY['r', 'p', 'v', 'm', 'f', 'S'], + ARRAY['TABLE', 'TABLE', 'VIEW', 'MATERIALIZED VIEW', 'FOREIGN TABLE', 'SEQUENCE']) AS l (kind_crt, kind_lg) + WHERE relnamespace = quote_ident(n_schema)::regnamespace + AND relkind IN ('S', 'r', 'p', 'v', 'm', 'f') + AND kind_crt = relkind + AND NOT relowner = o_owner + UNION + -- fonctions et procédures : + -- ... sous la dénomination FUNCTION jusqu'à PG 10, puis en + -- tant que ROUTINE à partir de PG 11, afin que les commandes + -- fonctionnent également avec les procédures. + SELECT + proname::text AS n_objet, + proowner AS obj_owner, + False AS b, + format('ALTER %s %s OWNER TO %I', + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTION' ELSE 'ROUTINE' END, + pg_proc.oid::regprocedure, n_owner) AS commande + FROM pg_catalog.pg_proc + WHERE pronamespace = quote_ident(n_schema)::regnamespace + AND NOT proowner = o_owner + -- à noter que les agrégats (proisagg vaut True) ont + -- leur propre commande ALTER AGGREGATE OWNER TO, mais + -- ALTER FUNCTION OWNER TO fonctionne pour tous les types + -- de fonctions dont les agrégats, et - pour PG 11+ - + -- ALTER ROUTINE OWNER TO fonctionne pour tous les types + -- de fonctions et les procédures. + UNION + -- types et domaines : + SELECT + typname::text AS n_objet, + typowner AS obj_owner, + False AS b, + format('ALTER %s %I.%I OWNER TO %I', + kind_lg, n_schema, typname, n_owner) AS commande + FROM unnest(ARRAY['true', 'false'], + ARRAY['DOMAIN', 'TYPE']) AS l (kind_crt, kind_lg), + pg_catalog.pg_type + WHERE typnamespace = quote_ident(n_schema)::regnamespace + AND kind_crt::boolean = (typtype = 'd') + AND NOT typowner = o_owner + -- exclusion des types générés automatiquement + AND NOT (pg_type.oid, 'pg_type'::regclass::oid) IN ( + SELECT pg_depend.objid, pg_depend.classid + FROM pg_catalog.pg_depend + WHERE deptype IN ('i', 'a') + ) + UNION + -- conversions : + SELECT + conname::text AS n_objet, + conowner AS obj_owner, + False AS b, + format('ALTER CONVERSION %I.%I OWNER TO %I', + n_schema, conname, n_owner) AS commande + FROM pg_catalog.pg_conversion + WHERE connamespace = quote_ident(n_schema)::regnamespace + AND NOT conowner = o_owner + UNION + -- opérateurs : + SELECT + oprname::text AS n_objet, + oprowner AS obj_owner, + False AS b, + format('ALTER OPERATOR %s OWNER TO %I', + pg_operator.oid::regoperator, n_owner) AS commande + FROM pg_catalog.pg_operator + WHERE oprnamespace = quote_ident(n_schema)::regnamespace + AND NOT oprowner = o_owner + UNION + -- collations : + SELECT + collname::text AS n_objet, + collowner AS obj_owner, + False AS b, + format('ALTER COLLATION %I.%I OWNER TO %I', + n_schema, collname, n_owner) AS commande + FROM pg_catalog.pg_collation + WHERE collnamespace = quote_ident(n_schema)::regnamespace + AND NOT collowner = o_owner + UNION + -- text search dictionary : + SELECT + dictname::text AS n_objet, + dictowner AS obj_owner, + False AS b, + format('ALTER TEXT SEARCH DICTIONARY %s OWNER TO %I', + pg_ts_dict.oid::regdictionary, n_owner) AS commande + FROM pg_catalog.pg_ts_dict + WHERE dictnamespace = quote_ident(n_schema)::regnamespace + AND NOT dictowner = o_owner + UNION + -- text search configuration : + SELECT + cfgname::text AS n_objet, + cfgowner AS obj_owner, + False AS b, + format('ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %I', + pg_ts_config.oid::regconfig, n_owner) AS commande + FROM pg_catalog.pg_ts_config + WHERE cfgnamespace = quote_ident(n_schema)::regnamespace + AND NOT cfgowner = o_owner + -- operator family : + UNION + SELECT + opfname::text AS n_objet, + opfowner AS obj_owner, + False AS b, + format('ALTER OPERATOR FAMILY %I.%I USING %I OWNER TO %I', + n_schema, opfname, amname, n_owner) AS commande + FROM pg_catalog.pg_opfamily + LEFT JOIN pg_catalog.pg_am ON pg_am.oid = opfmethod + WHERE opfnamespace = quote_ident(n_schema)::regnamespace + AND NOT opfowner = o_owner + -- operator class : + UNION + SELECT + opcname::text AS n_objet, + opcowner AS obj_owner, + False AS b, + format('ALTER OPERATOR CLASS %I.%I USING %I OWNER TO %I', + n_schema, opcname, amname, n_owner) AS commande + FROM pg_catalog.pg_opclass + LEFT JOIN pg_catalog.pg_am ON pg_am.oid = opcmethod + WHERE opcnamespace = quote_ident(n_schema)::regnamespace + AND NOT opcowner = o_owner + ORDER BY b DESC + LOOP + IF pg_has_role(item.obj_owner, 'USAGE') + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + k := k + 1 ; + ELSE + RAISE EXCEPTION 'FAP4. Vous n''êtes pas habilité à modifier le propriétaire de l''objet %.', item.n_objet + USING DETAIL = format('Propriétaire courant : %s.', item.obj_owner::regrole) ; + END IF ; + END LOOP ; + + ------ CATALOGUES CONDITIONNELS ------ + -- soit ceux qui n'existent pas sous toutes les versions de PostgreSQL + IF current_setting('server_version_num')::int >= 100000 + THEN + FOR item IN + -- extended planner statistics : + SELECT + stxname::text AS n_objet, + stxowner AS obj_owner, + format('ALTER STATISTICS %I.%I OWNER TO %I', + n_schema, stxname, n_owner) AS commande + FROM pg_catalog.pg_statistic_ext + WHERE stxnamespace = quote_ident(n_schema)::regnamespace + AND NOT stxowner = o_owner + LOOP + IF pg_has_role(item.obj_owner, 'USAGE') + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + k := k + 1 ; + ELSE + RAISE EXCEPTION 'FAP4. Vous n''êtes pas habilité à modifier le propriétaire de l''objet %.', item.n_objet + USING DETAIL = format('Propriétaire courant : %s.', item.obj_owner::regrole) ; + END IF ; + END LOOP ; + END IF ; + + ------ RESULTAT ------ + RETURN k ; +END +$_$ ; + +ALTER FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) IS 'ASGARD. Attribue un schéma et tous les objets qu''il contient au propriétaire désigné.' ; + + +------ 4.5 - INITIALISATION DE GESTION_SCHEMA ------ + +-- Function: z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema( + exceptions text[] default NULL::text[], b_gs boolean default False + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* Enregistre dans la table de gestion d'Asgard l'ensemble des schémas +existants encore non référencés, hors schémas système et ceux qui sont +(optionnellement) listés en argument. + + Parameters + ---------- + exceptions : text[], optional + Liste des noms des schémas à omettre, le cas échéant. + b_gs : boolean, default False + Un booléen indiquant si, dans l'hypothèse où un schéma serait + marqué comme non créé dans la table de gestion, c'est le propriétaire + actuel du schéma qui doit être déclaré comme son producteur (False, + comportement par défaut) ou si c'est le producteur pré-renseigné dans + la table de gestion qui doit devenir le propriétaire du schéma (True). + Ce paramètre est ignoré pour un schéma déjà marqué comme créé. Il vise + un cas anecdotique où le champ creation de la table de gestion n'est + pas cohérent avec l'état réel du schéma. La fonction rétablira alors + le lien entre le schéma et l'enregistrement portant son nom dans la + table de gestion. + + Returns + ------- + text + '__ FIN INTIALISATION.' si la requête s'est exécutée normalement. + +*/ +DECLARE + item record ; + e_mssg text ; + e_detl text ; + e_hint text ; + b_creation boolean ; +BEGIN + + FOR item IN SELECT nspname, nspowner, rolname + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE NOT nspname ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + AND (exceptions IS NULL OR NOT nspname = ANY(exceptions)) + LOOP + SELECT creation INTO b_creation + FROM z_asgard.gestion_schema_usr + WHERE item.nspname = nom_schema ; + IF b_creation IS NULL + -- schéma non référencé dans gestion_schema + THEN + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) + VALUES (item.nspname, item.rolname, True) ; + RAISE NOTICE '... Schéma % enregistré dans la table de gestion.', item.nspname ; + ELSIF NOT b_creation + -- schéma pré-référencé dans gestion_schema + THEN + IF NOT b_gs + THEN + UPDATE z_asgard.gestion_schema_usr + SET creation = True, + producteur = item.rolname + WHERE item.nspname = nom_schema ; + ELSE + UPDATE z_asgard.gestion_schema_usr + SET creation = True + WHERE item.nspname = nom_schema ; + END IF ; + RAISE NOTICE '... Schéma % marqué comme créé dans la table de gestion.', item.nspname ; + END IF ; + END LOOP ; + + RETURN '__ FIN INITALISATION.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FIG0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$ ; + +ALTER FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) IS 'ASGARD. Enregistre dans la table de gestion d''Asgard l''ensemble des schémas existants encore non référencés, hors schémas système et ceux qui sont (optionnellement) listés en argument.' ; + + +------ 4.8 - REINITIALISATION DES PRIVILEGES SUR UN SCHEMA ------ + +-- Function: z_asgard.asgard_initialise_schema(text, boolean, boolean) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_schema( + n_schema text, + b_preserve boolean DEFAULT False, + b_gs boolean default False + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* Réinitialise les droits sur un schéma et ses objets selon les privilèges +standards du producteur, de l'éditeur et du lecteur désignés dans la table +de gestion d'Asgard. + + Elle a notamment pour effet de révoquer tout privilège accordé à + d'autres rôles que le producteur et les éventuels éditeur et lecteur. + + Si cette fonction est appliquée à un schéma existant non référencé + dans la table de gestion, elle l'y ajoute, avec son propriétaire + courant comme producteur. + + La fonction échouera si le schéma n'existe pas. + + Parameters + ---------- + n_schema : text + Nom d'un schéma présumé existant. + b_preserve : boolean, default False + Pour un schéma encore non référencé ou pré-référencé comme non créé + dans la table de gestion, une valeur True signifie que les privilèges + des rôles lecteur et éditeur doivent être ajoutés par dessus les droits + actuels. Avec la valeur par défaut False, les privilèges sont + réinitialisés avant application des droits standards. Ce paramètre est + ignoré pour un schéma déjà référencé comme créé - les privilèges sont + alors quoi qu'il arrive réinitialisés. + b_gs : boolean, default False + Un booléen indiquant si, dans l'hypothèse où le schéma serait + marqué comme non créé dans la table de gestion, c'est le propriétaire + actuel du schéma qui doit être déclaré comme son producteur (False, + comportement par défaut) ou si c'est le producteur pré-renseigné dans + la table de gestion qui doit devenir le propriétaire du schéma (True). + Ce paramètre est ignoré pour un schéma déjà marqué comme créé. Il vise + un cas anecdotique où le champ creation de la table de gestion n'est + pas cohérent avec l'état réel du schéma. La fonction rétablira alors + le lien entre le schéma et l'enregistrement portant son nom dans la + table de gestion. + + Returns + ------- + text + '__ REINITIALISATION REUSSIE.' (ou '__INITIALISATION REUSSIE.' pour + un schéma non référencé comme créé avec b_preserve = True) si la + requête s'est exécutée normalement. + +*/ +DECLARE + roles record ; + cree boolean ; + r record ; + c record ; + item record ; + n_owner text ; + k int := 0 ; + n int ; + e_mssg text ; + e_detl text ; + e_hint text ; +BEGIN + ------ TESTS PREALABLES ------ + -- schéma système + IF n_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FIS1. Opération interdite. Le schéma % est un schéma système.', n_schema ; + END IF ; + + -- existence du schéma + SELECT rolname INTO n_owner + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE n_schema = nspname ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'FIS2. Echec. Le schéma % n''existe pas.', n_schema ; + END IF ; + + -- permission sur le propriétaire + IF NOT pg_has_role(n_owner, 'USAGE') + THEN + RAISE EXCEPTION 'FIS3. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', n_schema + USING HINT = format('Il vous faut être membre du rôle propriétaire %s.', n_owner) ; + END IF ; + + ------ SCHEMA DEJA REFERENCE ? ------ + SELECT + creation + INTO cree + FROM z_asgard.gestion_schema_usr + WHERE nom_schema = n_schema ; + + ------ SCHEMA NON REFERENCE ------ + -- ajouté à gestion_schema + -- le reste est pris en charge par le trigger + -- on_modify_gestion_schema_after + IF NOT FOUND + THEN + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) + VALUES (n_schema, n_owner, true) ; + RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', n_schema ; + + IF b_preserve + THEN + RETURN '__ INITIALISATION REUSSIE.' ; + END IF ; + + ------- SCHEMA PRE-REFERENCE ------ + -- présent dans gestion_schema avec creation valant + -- False. + ELSIF NOT cree + THEN + IF NOT b_gs + THEN + UPDATE z_asgard.gestion_schema_usr + SET creation = true, + producteur = n_owner + WHERE n_schema = nom_schema ; + ELSE + UPDATE z_asgard.gestion_schema_usr + SET creation = true + WHERE n_schema = nom_schema ; + END IF ; + RAISE NOTICE '... Le schéma % a été marqué comme créé dans la table de gestion.', n_schema ; + + IF b_preserve + THEN + RETURN '__ INITIALISATION REUSSIE.' ; + END IF ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur + INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = n_schema ; + + ------ REMISE A PLAT DES PROPRIETAIRES ------ + -- uniquement pour les schémas qui étaient déjà + -- référencés dans gestion_schema (pour les autres, pris en charge + -- par le trigger on_modify_gestion_schema_after) + + -- schéma dont le propriétaire ne serait pas le producteur + IF cree + THEN + IF NOT roles.producteur = n_owner + THEN + -- permission sur le producteur + IF NOT pg_has_role(roles.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FIS4. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', n_schema + USING HINT = format('Il vous faut être membre du rôle producteur %s.') ; + END IF ; + -- propriétaire du schéma + contenu + RAISE NOTICE '(ré)attribution de la propriété du schéma et des objets au rôle producteur du schéma :' ; + PERFORM z_asgard.asgard_admin_proprietaire(n_schema, roles.producteur) ; + + -- schema dont le propriétaire est le producteur + ELSE + -- reprise uniquement des propriétaires du contenu + RAISE NOTICE '(ré)attribution de la propriété des objets au rôle producteur du schéma :' ; + SELECT z_asgard.asgard_admin_proprietaire(n_schema, roles.producteur, False) INTO n ; + IF n = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + END IF ; + + ------ DESTRUCTION DES PRIVILEGES ACTUELS ------ + -- hors privilèges par défaut (définis par ALTER DEFAULT PRIVILEGE) + -- et hors révocations des privilèges par défaut de public sur + -- les types et les fonctions + -- pour le propriétaire, ces commandes ont pour effet + -- de remettre les privilèges par défaut supprimés + + -- public + RAISE NOTICE 'remise à zéro des privilèges manuels du pseudo-rôle public :' ; + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_public( + quote_ident(n_schema)::regnamespace)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + END LOOP ; + IF NOT FOUND + THEN + RAISE NOTICE '> néant' ; + END IF ; + + -- autres rôles + RAISE NOTICE 'remise à zéro des privilèges des autres rôles (pour le producteur, les éventuels privilèges manquants sont réattribués) :' ; + FOR r IN (SELECT rolname FROM pg_roles) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role( + quote_ident(n_schema)::regnamespace, quote_ident(r.rolname)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + k := k + 1 ; + END LOOP ; + END LOOP ; + IF NOT FOUND OR k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + ------ RECREATION DES PRIVILEGES DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.editeur) ; + + EXECUTE format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + + EXECUTE format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.editeur) ; + END IF ; + + ------ RECREATION DES PRIVILEGES DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', n_schema, roles.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', n_schema, roles.lecteur) ; + END IF ; + + ------ RECREATION DES PRIVILEGES SUR LES SCHEMAS D'ASGARD ------ + IF n_schema = 'z_asgard' + THEN + -- rétablissement des droits de public + RAISE NOTICE 'rétablissement des privilèges attendus pour le pseudo-rôle public :' ; + + GRANT USAGE ON SCHEMA z_asgard TO public ; + RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard TO public' ; + + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public ; + RAISE NOTICE '> GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public' ; + + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public ; + RAISE NOTICE '> GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public' ; + + GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public' ; + + GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public' ; + + GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public' ; + + ELSIF n_schema = 'z_asgard_admin' + THEN + -- rétablissement des droits de g_admin_ext + RAISE NOTICE 'rétablissement des privilèges attendus pour g_admin_ext :' ; + + GRANT USAGE ON SCHEMA z_asgard_admin TO g_admin_ext ; + RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard_admin TO g_admin_ext' ; + + GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE z_asgard_admin.gestion_schema TO g_admin_ext ; + RAISE NOTICE '> GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE z_asgard_admin.gestion_schema TO g_admin_ext' ; + + END IF ; + + ------ ACL PAR DEFAUT ------ + k := 0 ; + RAISE NOTICE 'suppression des privilèges par défaut :' ; + FOR item IN ( + SELECT + format( + 'ALTER DEFAULT PRIVILEGES FOR ROLE %s IN SCHEMA %s REVOKE %s ON %s FROM %s', + defaclrole::regrole, + defaclnamespace::regnamespace, + -- impossible que defaclnamespace vaille 0 (privilège portant + -- sur tous les schémas) ici, puisque c'est l'OID de n_schema + privilege, + typ_lg, + CASE WHEN grantee = 0 THEN 'public' ELSE grantee::regrole::text END + ) AS commande, + pg_has_role(defaclrole, 'USAGE') AS utilisable, + defaclrole + FROM pg_default_acl, + aclexplode(defaclacl) AS acl (grantor, grantee, privilege, grantable), + unnest(ARRAY['TABLES', 'SEQUENCES', + CASE WHEN current_setting('server_version_num')::int < 110000 + THEN 'FUNCTIONS' ELSE 'ROUTINES' END, + -- à ce stade FUNCTIONS et ROUTINES sont équivalents, mais + -- ROUTINES est préconisé + 'TYPES', 'SCHEMAS'], + ARRAY['r', 'S', 'f', 'T', 'n']) AS t (typ_lg, typ_crt) + WHERE defaclnamespace = quote_ident(n_schema)::regnamespace + AND defaclobjtype = typ_crt + ) + LOOP + IF item.utilisable + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + ELSE + RAISE EXCEPTION 'FIS6. Echec. Vous n''avez pas les privilèges nécessaires pour modifier les privilèges par défaut alloués par le rôle %.', item.defaclrole::regrole::text + USING DETAIL = item.commande, + HINT = 'Tentez de relancer la fonction en tant que super-utilisateur.' ; + END IF ; + k := k + 1 ; + END LOOP ; + IF k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + RETURN '__ REINITIALISATION REUSSIE.' ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'FIS0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$_$ ; + +ALTER FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) IS 'ASGARD. Réinitialise les droits sur un schéma et ses objets selon les privilèges standards du producteur, de l''éditeur et du lecteur désignés dans la table de gestion d''Asgard.' ; + + +------ 4.9 - REINITIALISATION DES PRIVILEGES SUR UN OBJET ------ + +-- Function: z_asgard.asgard_initialise_obj(text, text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_obj( + obj_schema text, + obj_nom text, + obj_typ text + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* Réinitialise les droits sur un objet selon les privilèges standards +associés aux rôles désignés dans la table de gestion pour son schéma. + + Parameters + ---------- + obj_schema : text + Le nom du schéma contenant l'objet. + obj_nom : text + Le nom de l'objet. À écrire sans les guillemets des identifiants + PostgreSQL SAUF pour les fonctions, dont le nom doit impérativement + être entre guillemets s'il ne respecte pas les conventions de + nommage des identifiants PostgreSQL. + obj_typ : str + Le type de l'objet, parmi 'table', 'partitioned table' (assimilé + à 'table'), 'view', 'materialized view', 'foreign table', 'sequence', + 'function', 'aggregate', 'procedure', 'routine', 'type' et 'domain'. + + Returns + ------- + text + '__ REINITIALISATION REUSSIE.' si la requête s'est exécutée normalement. + +*/ +DECLARE + class_info record ; + roles record ; + obj record ; + r record ; + c record ; + l text ; + k int := 0 ; +BEGIN + + -- pour la suite, on assimile les partitions à des tables + IF obj_typ = 'partitioned table' + THEN + obj_typ := 'table' ; + ELSIF obj_typ = ANY (ARRAY['routine', 'procedure', 'function', 'aggregate']) + THEN + -- à partir de PG 11, les fonctions et procédures sont des routines + IF current_setting('server_version_num')::int >= 110000 + THEN + obj_typ := 'routine' ; + -- pour les versions antérieures, les routines et procédures n'existent + -- théoriquement pas, mais on considère que ces mots-clés désignent + -- des fonctions + ELSE + obj_typ := 'function' ; + END IF ; + END IF ; + + ------ TESTS PREALABLES ------ + -- schéma système + IF obj_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FIO1. Opération interdite. Le schéma % est un schéma système.', obj_schema ; + END IF ; + + -- schéma non référencé + IF NOT obj_schema IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FIO2. Echec. Le schéma % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', obj_schema ; + END IF ; + + -- type invalide + récupération des informations sur le catalogue contenant l'objet + SELECT + xtyp, xclass, xreg, + format('%sname', xprefix) AS xname, + format('%sowner', xprefix) AS xowner, + format('%snamespace', xprefix) AS xschema + INTO class_info + FROM unnest( + ARRAY['table', 'foreign table', 'view', 'materialized view', + 'sequence', 'type', 'domain', 'function', 'routine'], + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_class', 'pg_type', 'pg_type', 'pg_proc', 'pg_proc'], + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'typ', 'typ', + 'pro', 'pro'], + ARRAY['regclass', 'regclass', 'regclass', 'regclass', 'regclass', + 'regtype', 'regtype', 'regprocedure', 'regprocedure'] + ) AS typ (xtyp, xclass, xprefix, xreg) + WHERE typ.xtyp = obj_typ ; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'FIO3. Echec. Le type % n''existe pas ou n''est pas pris en charge.', obj_typ + USING HINT = 'Types acceptés : ''table'', ''partitioned table'', ''view'', ''materialized view'', ''foreign table'', ''sequence'', ''function'', ''aggregate'', ''routine'', ''procedure'', ''type'', ''domain''.' ; + END IF ; + + -- objet inexistant + récupération du propriétaire + EXECUTE 'SELECT ' || class_info.xowner || '::regrole::text AS prop, ' + || class_info.xclass || '.oid, ' + || CASE WHEN class_info.xclass = 'pg_type' + THEN quote_literal(quote_ident(obj_schema) || '.' || quote_ident(obj_nom)) || '::text' + ELSE class_info.xclass || '.oid::' || class_info.xreg || '::text' + END || ' AS appel' + || ' FROM pg_catalog.' || class_info.xclass + || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' + THEN class_info.xclass || '.oid = ' + || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + || '::regprocedure' + ELSE class_info.xname || ' = ' || quote_literal(obj_nom) + || ' AND ' || class_info.xschema || '::regnamespace::text = ' + || quote_literal(quote_ident(obj_schema)) END + INTO obj ; + + IF obj.prop IS NULL + THEN + RAISE EXCEPTION 'FIO4. Echec. L''objet % n''existe pas.', obj_nom ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj_schema ; + + -- permission sur le producteur + IF NOT pg_has_role(roles.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FIO5. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma % pour réaliser cette opération.', obj_schema + USING HINT = format('Il vous faut être membre du rôle producteur %s.', roles.producteur) ; + END IF ; + + ------ REMISE A PLAT DU PROPRIETAIRE ------ + IF NOT obj.prop = quote_ident(roles.producteur) + THEN + -- permission sur le propriétaire de l'objet + IF NOT pg_has_role(obj.prop::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FIO6. Echec. Vous ne disposez pas des permissions nécessaires sur l''objet % pour réaliser cette opération.', obj_nom + USING HINT = format('Il vous faut être membre du rôle propriétaire de l''objet (%s).', obj.prop) ; + END IF ; + + RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', obj_nom ; + l := format('ALTER %s %s OWNER TO %I', obj_typ, obj.appel, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + + ------ DESTRUCTION DES PRIVILEGES ACTUELS ------ + -- hors privilèges par défaut (définis par ALTER DEFAULT PRIVILEGE) + -- et hors révocations des privilèges par défaut de public sur + -- les types et les fonctions + -- pour le propriétaire, ces commandes ont pour effet + -- de remettre les privilèges par défaut supprimés + + -- public + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + RAISE NOTICE 'remise à zéro des privilèges manuels du pseudo-rôle public :' ; + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), 'public') ; + END LOOP ; + IF NOT FOUND + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + + -- autres rôles + RAISE NOTICE 'remise à zéro des privilèges des autres rôles (pour le producteur, les éventuels privilèges manquants sont réattribués) :' ; + FOR r IN (SELECT rolname FROM pg_roles) + LOOP + FOR c IN (SELECT * FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(r.rolname)::regrole)) + LOOP + EXECUTE format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + RAISE NOTICE '> %', format(z_asgard.asgard_grant_to_revoke(c.commande), r.rolname) ; + k := k + 1 ; + END LOOP ; + END LOOP ; + IF NOT FOUND OR k = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + + ------ RECREATION DES PRIVILEGES DE L'EDITEUR ------ + IF roles.editeur IS NOT NULL + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %I.%I TO %I', + obj_schema, obj_nom, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %I.%I TO %I', + obj_schema, obj_nom, roles.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + ------ RECREATION DES PRIVILEGES DU LECTEUR ------ + IF roles.lecteur IS NOT NULL + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %I.%I TO %I', + obj_schema, obj_nom, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %I.%I TO %I', + obj_schema, obj_nom, roles.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; + + RETURN '__ REINITIALISATION REUSSIE.' ; +END +$_$; + +ALTER FUNCTION z_asgard.asgard_initialise_obj(text, text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_initialise_obj(text, text, text) IS 'ASGARD. Réinitialise les privilèges sur un objet.' ; + + +------ 4.10 - DEPLACEMENT D'OBJET ------ + +-- Function: z_asgard.asgard_deplace_obj(text, text, text, text, int) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_deplace_obj( + obj_schema text, + obj_nom text, + obj_typ text, + schema_cible text, + variante int DEFAULT 1 + ) + RETURNS text + LANGUAGE plpgsql + AS $_$ +/* Déplace un objet vers un nouveau schéma, en transférant ou réinitialisant +les privilèges selon la variante choisie. + + Lorsque des séquences sont associées aux champs de la table, la fonction + gère également leurs privilèges. + + Parameters + ---------- + obj_schema : text + Le nom du schéma contenant l'objet. + obj_nom : text + Le nom de l'objet. À écrire sans les guillemets des identifiants + PostgreSQL SAUF pour les fonctions, dont le nom doit impérativement + être entre guillemets s'il ne respecte pas les conventions de + nommage des identifiants PostgreSQL. + obj_typ : str + Le type de l'objet, parmi 'table', 'partitioned table' (assimilé + à 'table'), 'view', 'materialized view', 'foreign table', 'sequence', + 'function', 'aggregate', 'procedure', 'routine', 'type' et 'domain'. + schema_cible : str + Le nom du schéma où doit être déplacé l'objet. + variante : int, default 1 + Un entier qui définit le comportement attendu par l'utilisateur + vis-à-vis des privilèges : + + * 1 (valeur par défaut) | TRANSFERT COMPLET + CONSERVATION : + les privilèges des rôles producteur, éditeur et lecteur de + l'ancien schéma sont transférés sur ceux du nouveau. Si un + éditeur ou lecteur a été désigné pour le nouveau schéma mais + qu'aucun n'était défini pour l'ancien, le rôle reçoit les + privilèges standards pour sa fonction. Le cas échéant, + les privilèges des autres rôles sont conservés. + * 2 | REINITIALISATION COMPLETE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont supprimés. + * 3 | TRANSFERT COMPLET + NETTOYAGE : les privilèges des rôles + producteur, éditeur et lecteur de l'ancien schéma sont transférés + sur ceux du nouveau. Si un éditeur ou lecteur a été désigné pour + le nouveau schéma mais qu'aucun n'était défini pour l'ancien, + le rôle reçoit les privilèges standards pour sa fonction. + Les privilèges des autres rôles sont supprimés. + * 4 | TRANSFERT PRODUCTEUR + CONSERVATION : les privilèges de + l'ancien producteur sont transférés sur le nouveau. Les privilèges + des autres rôles sont conservés tels quels. C'est le comportement + d'une commande ALTER [...] SET SCHEMA (interceptée par le déclencheur + sur évènement asgard_on_alter_objet). + * 5 | TRANSFERT PRODUCTEUR + REINITIALISATION : les privilèges + de l'ancien producteur sont transférés sur le nouveau. Les + nouveaux éditeur et lecteur reçoivent les privilèges standards. + Les privilèges des autres rôles sont supprimés. + * 6 | REINITIALISATION PARTIELLE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont conservés. + + Returns + ------- + text + '__ DEPLACEMENT REUSSI.' si la requête s'est exécutée normalement. + +*/ +DECLARE + class_info record ; + roles record ; + roles_cible record ; + obj record ; + r record ; + c record ; + l text ; + c_lecteur text[] ; + c_editeur text[] ; + c_producteur text[] ; + c_n_lecteur text[] ; + c_n_editeur text[] ; + c_autres text[] ; + seq_liste oid[] ; + a text[] ; + s record ; + o oid ; + supported boolean ; + duplicate oid ; +BEGIN + + obj_typ := lower(obj_typ) ; + + -- pour la suite, on assimile les partitions à des tables + IF obj_typ = 'partitioned table' + THEN + obj_typ := 'table' ; + ELSIF obj_typ = ANY (ARRAY['routine', 'procedure', 'function', 'aggregate']) + THEN + -- à partir de PG 11, les fonctions et procédures sont des routines + IF current_setting('server_version_num')::int >= 110000 + THEN + obj_typ := 'routine' ; + -- pour les versions antérieures, les routines et procédures n'existent + -- théoriquement pas, mais on considère que ces mots-clés désignent + -- des fonctions + ELSE + obj_typ := 'function' ; + END IF ; + END IF ; + + ------ TESTS PREALABLES ------ + -- schéma système + IF obj_schema ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + THEN + RAISE EXCEPTION 'FDO1. Opération interdite. Le schéma % est un schéma système.', obj_schema ; + END IF ; + + -- schéma de départ non référencé + IF NOT obj_schema IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FDO2. Echec. Le schéma % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', obj_schema ; + END IF ; + + -- schéma cible non référencé + IF NOT schema_cible IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE creation) + THEN + RAISE EXCEPTION 'FDO3. Echec. Le schéma cible % n''est pas référencé dans la table de gestion (ou marqué comme non créé).', schema_cible ; + END IF ; + + -- type invalide + récupération des informations sur le catalogue contenant l'objet + SELECT + xtyp, xclass, xreg, + format('%sname', xprefix) AS xname, + format('%sowner', xprefix) AS xowner, + format('%snamespace', xprefix) AS xschema + INTO class_info + FROM unnest( + ARRAY['table', 'foreign table', 'view', 'materialized view', + 'sequence', 'type', 'domain', 'function', 'routine'], + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_class', 'pg_type', 'pg_type', 'pg_proc', 'pg_proc'], + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'typ', 'typ', + 'pro', 'pro'], + ARRAY['regclass', 'regclass', 'regclass', 'regclass', 'regclass', + 'regtype', 'regtype', 'regprocedure', 'regprocedure'] + ) AS typ (xtyp, xclass, xprefix, xreg) + WHERE typ.xtyp = obj_typ ; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'FDO4. Echec. Le type % n''existe pas ou n''est pas pris en charge.', obj_typ + USING HINT = 'Types acceptés : ''table'', ''partitioned table'', ''view'', ''materialized view'', ''foreign table'', ''sequence'', ''function'', ''aggregate'', ''procedure'', ''routine'', ''type'', ''domain''.' ; + END IF ; + + -- objet inexistant + récupération du propriétaire + EXECUTE 'SELECT ' || class_info.xowner || '::regrole::text AS prop, ' + || class_info.xclass || '.oid, ' + || CASE WHEN class_info.xclass = 'pg_type' + THEN quote_literal(quote_ident(obj_schema) || '.' || quote_ident(obj_nom)) || '::text' + ELSE class_info.xclass || '.oid::' || class_info.xreg || '::text' + END || ' AS appel,' + || class_info.xname || ' AS objname' + || CASE WHEN class_info.xclass = 'pg_proc' + THEN ', pg_catalog.oidvectortypes(proargtypes) AS proargtypes' ELSE '' END + || ' FROM pg_catalog.' || class_info.xclass + || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' + THEN class_info.xclass || '.oid = ' + || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + || '::regprocedure' + ELSE class_info.xname || ' = ' || quote_literal(obj_nom) + || ' AND ' || class_info.xschema || '::regnamespace::text = ' + || quote_literal(quote_ident(obj_schema)) END + INTO obj ; + + IF obj.prop IS NULL + THEN + RAISE EXCEPTION 'FDO5. Echec. L''objet % n''existe pas.', obj_nom ; + END IF ; + + -- il existe déjà un objet de même définition dans le schéma cible + IF class_info.xclass = 'pg_proc' THEN + EXECUTE format(' + SELECT oid FROM pg_catalog.pg_proc + WHERE pronamespace = %L::regnamespace + AND proname = %L + AND pg_catalog.oidvectortypes(proargtypes) = %L', + quote_ident(schema_cible), obj.objname, obj.proargtypes) + INTO duplicate ; + ELSE + EXECUTE format(' + SELECT oid FROM pg_catalog.%s + WHERE %s = %L::regnamespace + AND %s = %L', + class_info.xclass, + class_info.xschema, quote_ident(schema_cible), + class_info.xname, obj.objname) + INTO duplicate ; + END IF ; + + IF duplicate IS NOT NULL + THEN + RAISE EXCEPTION 'FDO8. Opération interdite. Il existe déjà un objet de même définition dans le schéma cible.' ; + END IF ; + + ------ RECUPERATION DES ROLES ------ + -- schéma de départ : + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = obj_schema ; + + -- schéma cible : + SELECT + r1.rolname AS producteur, + CASE WHEN editeur = 'public' THEN 'public' ELSE r2.rolname END AS editeur, + CASE WHEN lecteur = 'public' THEN 'public' ELSE r3.rolname END AS lecteur, + creation INTO roles_cible + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE nom_schema = schema_cible ; + + -- permission sur le producteur du schéma cible + IF NOT pg_has_role(roles_cible.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'FDO6. Echec. Vous ne disposez pas des permissions nécessaires sur le schéma cible % pour réaliser cette opération.', schema_cible + USING HINT = format('Il vous faut être membre du rôle producteur %s.', roles_cible.producteur) ; + END IF ; + + -- permission sur le propriétaire de l'objet + IF NOT pg_has_role(obj.prop::regrole::oid, 'USAGE') + THEN + RAISE EXCEPTION 'FDO7. Echec. Vous ne disposez pas des permissions nécessaires sur l''objet % pour réaliser cette opération.', obj_nom + USING HINT = format('Il vous faut être membre du rôle propriétaire de l''objet (%s).', obj.prop) ; + END IF ; + + ------ MEMORISATION DES PRIVILEGES ACTUELS ------ + -- ancien producteur : + SELECT array_agg(commande) INTO c_producteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.producteur)::regrole) ; + + -- ancien éditeur : + IF roles.editeur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_editeur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_editeur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.editeur)::regrole) ; + END IF ; + + -- ancien lecteur : + IF roles.lecteur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_lecteur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_lecteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles.lecteur)::regrole) ; + END IF ; + + -- nouvel éditeur : + IF roles_cible.editeur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_n_editeur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles_cible.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_n_editeur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles_cible.editeur)::regrole) ; + END IF ; + + -- nouveau lecteur : + IF roles_cible.lecteur = 'public' + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(commande) INTO c_n_lecteur + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + ELSIF roles_cible.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO c_n_lecteur + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(roles_cible.lecteur)::regrole) ; + END IF ; + + -- autres rôles : + -- pour ces commandes, contrairement aux précédentes, le rôle + -- est inséré dès maintenant (avec "format") + -- public + IF NOT 'public' = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL)) + THEN + IF obj_typ IN ('table', 'view', 'materialized view', 'sequence', + 'foreign table', 'partitioned table') + THEN + SELECT array_agg(format(commande, 'public')) INTO c_autres + FROM z_asgard.asgard_synthese_public_obj(obj.oid, obj_typ) ; + END IF ; + END IF ; + -- et le reste + FOR r IN (SELECT rolname FROM pg_roles + WHERE NOT rolname = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL))) + LOOP + SELECT array_agg(format(commande, r.rolname::text)) INTO a + FROM z_asgard.asgard_synthese_role_obj( + obj.oid, obj_typ, quote_ident(r.rolname)::regrole) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END LOOP ; + + ------ BREF CONTRÔLE DES INDEX ------ + -- il s'agit seulement de vérifier qu'il n'existe pas déjà d'index + -- de même nom dans le schéma cible + + -- 1. index qui dépendent d'une contrainte, tels les index + -- des clés primaires + FOR s IN ( + SELECT + pg_class.oid, + pg_class.relname + FROM pg_catalog.pg_constraint + LEFT JOIN pg_catalog.pg_class ON pg_class.oid = pg_constraint.conindid + WHERE pg_constraint.conrelid = obj.oid + AND pg_constraint.conindid IS NOT NULL + ) + LOOP + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO9. Opération interdite. Il existe dans le schéma cible une relation de même nom que l''index associé %.', s.relname ; + END IF ; + END LOOP ; + + -- 2. autres index (qui dépendent directement de la table) + FOR s IN ( + SELECT + pg_class.oid, + pg_class.relname + FROM pg_catalog.pg_depend LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = pg_depend.objid + WHERE pg_depend.classid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refclassid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refobjid = obj.oid + AND pg_depend.refobjsubid > 0 + AND pg_depend.deptype = ANY (ARRAY['a', 'i']) + AND pg_class.relkind = 'i' + ) + LOOP + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO10. Opération interdite. Il existe dans le schéma cible une relation de même nom que l''index associé %.', s.relname ; + END IF ; + END LOOP ; + + ------ PRIVILEGES SUR LES SEQUENCES ASSOCIEES ------ + IF obj_typ = 'table' + THEN + -- dans le cas d'une table, on recherche les séquences + -- utilisées par ses éventuels champs de type serial ou + -- IDENTITY + -- elles sont repérées par le fait qu'il existe + -- une dépendance entre la séquence et un champ de la table : + -- de type DEPENDENCY_AUTO (a) pour la séquence d'un champ serial + -- de type DEPENDENCY_INTERNAL (i) pour la séquence d'un champ IDENDITY + FOR s IN ( + SELECT + pg_class.oid, + pg_class.relname + FROM pg_catalog.pg_depend LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = pg_depend.objid + WHERE pg_depend.classid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refclassid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refobjid = obj.oid + AND pg_depend.refobjsubid > 0 + AND pg_depend.deptype = ANY (ARRAY['a', 'i']) + AND pg_class.relkind = 'S' + ) + LOOP + -- il existe déjà une séquence de même nom dans le schéma cible + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO11. Opération interdite. Il existe dans le schéma cible une relation de même nom que la séquence associée %.', s.relname ; + END IF ; + + -- liste des séquences + seq_liste := array_append(seq_liste, s.oid) ; + + -- récupération des privilèges + -- ancien producteur : + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence', quote_ident(roles.producteur)::regrole) ; + IF FOUND + THEN + c_producteur := array_cat(c_producteur, a) ; + a := NULL ; + END IF ; + + -- ancien éditeur : + IF roles.editeur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles.editeur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_editeur := array_cat(c_editeur, a) ; + a := NULL ; + END IF ; + + -- ancien lecteur : + IF roles.lecteur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles.lecteur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_lecteur := array_cat(c_lecteur, a) ; + a := NULL ; + END IF ; + + -- nouvel éditeur : + IF roles_cible.editeur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles_cible.editeur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles_cible.editeur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_n_editeur := array_cat(c_n_editeur, a) ; + a := NULL ; + END IF ; + + -- nouveau lecteur : + IF roles_cible.lecteur = 'public' + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + ELSIF roles_cible.lecteur IS NOT NULL + THEN + SELECT array_agg(commande) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(roles_cible.lecteur)::regrole) ; + END IF ; + IF a IS NOT NULL + THEN + c_n_lecteur := array_cat(c_n_lecteur, a) ; + a := NULL ; + END IF ; + + -- autres rôles : + -- public + IF NOT 'public' = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL)) + THEN + SELECT array_agg(format(commande, 'public')) INTO a + FROM z_asgard.asgard_synthese_public_obj(s.oid, 'sequence'::text) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END IF ; + -- et le reste + FOR r IN (SELECT rolname FROM pg_roles + WHERE NOT rolname = ANY (array_remove(ARRAY[roles.producteur, roles.lecteur, roles.editeur, + roles_cible.producteur, roles_cible.lecteur, roles_cible.editeur], NULL))) + LOOP + SELECT array_agg(format(commande, r.rolname::text)) INTO a + FROM z_asgard.asgard_synthese_role_obj( + s.oid, 'sequence'::text, quote_ident(r.rolname)::regrole) ; + IF FOUND + THEN + c_autres := array_cat(c_autres, a) ; + a := NULL ; + END IF ; + END LOOP ; + END LOOP ; + END IF ; + + ------ DEPLACEMENT DE L'OBJET ------ + EXECUTE format('ALTER %s %s SET SCHEMA %I', obj_typ, obj.appel, schema_cible) ; + + RAISE NOTICE '... Objet déplacé dans le schéma %.', schema_cible ; + + ------ PRIVILEGES DU PRODUCTEUR ------ + -- par défaut, ils ont été transférés + -- lors du changement de propriétaire, il + -- n'y a donc qu'à réinitialiser pour les + -- variantes 2 et 6 + + -- objet, réinitialisation pour 2 et 6 + IF variante IN (2, 6) AND (c_producteur IS NOT NULL) + THEN + RAISE NOTICE 'réinitialisation des privilèges du nouveau producteur, % :', roles_cible.producteur ; + FOREACH l IN ARRAY c_producteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.producteur) ; + RAISE NOTICE '> %', format(l, roles_cible.producteur) ; + END LOOP ; + END IF ; + + ------- PRIVILEGES EDITEUR ------ + -- révocation des privilèges du nouvel éditeur + IF roles_cible.editeur IS NOT NULL + AND (roles.editeur IS NULL OR NOT roles.editeur = roles_cible.editeur) + AND NOT roles.producteur = roles_cible.editeur + AND NOT variante = 4 + AND c_n_editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges pré-existants du nouvel éditeur, % :', roles_cible.editeur ; + FOREACH l IN ARRAY c_n_editeur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.editeur) ; + RAISE NOTICE '> %', format(l, roles_cible.editeur) ; + END LOOP ; + END IF ; + + -- révocation des privilèges de l'ancien éditeur + IF roles.editeur IS NOT NULL AND NOT roles.editeur = roles_cible.producteur + AND (roles_cible.editeur IS NULL OR NOT roles.editeur = roles_cible.editeur OR NOT variante IN (1,3)) + AND NOT variante = 4 + AND c_editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges de l''ancien éditeur, % :', roles.editeur ; + FOREACH l IN ARRAY c_editeur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles.editeur) ; + RAISE NOTICE '> %', format(l, roles.editeur) ; + END LOOP ; + END IF ; + + -- reproduction sur le nouvel éditeur pour les variantes 1 et 3 + IF roles.editeur IS NOT NULL + AND roles_cible.editeur IS NOT NULL + AND variante IN (1, 3) + AND c_editeur IS NOT NULL + AND NOT roles.editeur = roles_cible.editeur + THEN + RAISE NOTICE 'transfert des privilèges de l''ancien éditeur vers le nouvel éditeur, % :', roles_cible.editeur ; + FOREACH l IN ARRAY c_editeur + LOOP + l := replace(l, format('%I.', obj_schema), format('%I.', schema_cible)) ; + EXECUTE format(l, roles_cible.editeur) ; + RAISE NOTICE '> %', format(l, roles_cible.editeur) ; + END LOOP ; + END IF ; + + -- attribution des privilèges standard au nouvel éditeur + -- pour les variantes 2, 5, 6 + -- ou s'il n'y avait pas de lecteur sur l'ancien schéma + IF roles_cible.editeur IS NOT NULL + AND (variante IN (2, 5, 6) OR roles.editeur IS NULL) + AND NOT variante = 4 + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences libres : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma :' ; + l := format('GRANT SELECT, USAGE ON SEQUENCE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + -- sur les séquences des champs serial : + IF seq_liste IS NOT NULL + THEN + FOREACH o IN ARRAY seq_liste + LOOP + l := format('GRANT SELECT, USAGE ON SEQUENCE %s TO %I', + o::regclass, roles_cible.editeur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + END IF ; + + ------- PRIVILEGES LECTEUR ------ + -- révocation des privilèges du nouveau lecteur + IF roles_cible.lecteur IS NOT NULL + AND (roles.lecteur IS NULL OR NOT roles.lecteur = roles_cible.lecteur) + AND NOT roles.producteur = roles_cible.lecteur + AND (roles.editeur IS NULL OR NOT roles.editeur = roles_cible.lecteur) + AND NOT variante = 4 + AND c_n_lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges pré-existants du nouveau lecteur, % :', roles_cible.lecteur ; + FOREACH l IN ARRAY c_n_lecteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles_cible.lecteur) ; + RAISE NOTICE '> %', format(l, roles_cible.lecteur) ; + END LOOP ; + END IF ; + + -- révocation des privilèges de l'ancien lecteur + IF roles.lecteur IS NOT NULL AND NOT roles.lecteur = roles_cible.producteur + AND (roles_cible.editeur IS NULL OR NOT roles.lecteur = roles_cible.editeur) + AND (roles_cible.lecteur IS NULL OR NOT roles.lecteur = roles_cible.lecteur OR NOT variante IN (1,3)) + AND NOT variante = 4 + AND c_lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression des privilèges de l''ancien lecteur, % :', roles.lecteur ; + FOREACH l IN ARRAY c_lecteur + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE format(l, roles.lecteur) ; + RAISE NOTICE '> %', format(l, roles.lecteur) ; + END LOOP ; + END IF ; + + -- reproduction sur le nouveau lecteur pour les variantes 1 et 3 + IF roles.lecteur IS NOT NULL + AND roles_cible.lecteur IS NOT NULL + AND variante IN (1, 3) + AND c_lecteur IS NOT NULL + AND NOT roles.lecteur = roles_cible.lecteur + THEN + RAISE NOTICE 'transfert des privilèges de l''ancien lecteur vers le nouveau lecteur, % :', roles_cible.lecteur ; + FOREACH l IN ARRAY c_lecteur + LOOP + l := replace(l, format('%I.', obj_schema), format('%I.', schema_cible)) ; + EXECUTE format(l, roles_cible.lecteur) ; + RAISE NOTICE '> %', format(l, roles_cible.lecteur) ; + END LOOP ; + END IF ; + + -- attribution des privilèges standard au nouveau lecteur + -- pour les variantes 2, 5, 6 + -- ou s'il n'y avait pas de lecteur sur l'ancien schéma + IF roles_cible.lecteur IS NOT NULL + AND (variante IN (2, 5, 6) OR roles.lecteur IS NULL) + AND NOT variante = 4 + THEN + -- sur les tables : + IF obj_typ IN ('table', 'view', 'materialized view', 'foreign table') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON TABLE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + -- sur les séquences libres : + ELSIF obj_typ IN ('sequence') + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma :' ; + l := format('GRANT SELECT ON SEQUENCE %I.%I TO %I', + schema_cible, obj_nom, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + -- sur les séquences des champs serial : + IF seq_liste IS NOT NULL + THEN + FOREACH o IN ARRAY seq_liste + LOOP + l := format('GRANT SELECT ON SEQUENCE %s TO %I', o::regclass, roles_cible.lecteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + END IF ; + + ------ AUTRES ROLES ------ + -- pour les variantes 2, 3, 5, remise à zéro + IF variante IN (2, 3, 5) + AND c_autres IS NOT NULL + THEN + RAISE NOTICE 'remise à zéro des privilèges des autres rôles :' ; + FOREACH l IN ARRAY c_autres + LOOP + l := z_asgard.asgard_grant_to_revoke(replace(l, format('%I.', obj_schema), format('%I.', schema_cible))) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END LOOP ; + END IF ; + + RETURN '__ DEPLACEMENT REUSSI.' ; +END +$_$ ; + +ALTER FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) IS 'ASGARD. Déplace un objet vers un nouveau schéma, en transférant ou réinitialisant les privilèges selon la variante choisie.' ; + + +------ 4.11 - OCTROI D'UN RÔLE À TOUS LES RÔLES DE CONNEXION ------ + +-- Function: z_asgard_admin.asgard_all_login_grant_role(text, boolean) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_all_login_grant_role(n_role text, b boolean DEFAULT True) + RETURNS int + LANGUAGE plpgsql + AS $_$ +/* Confère à tous les rôles de connexion du serveur l'appartenance au rôle +donné en argument. + + Parameters + ---------- + n_role : text + Une chaîne de caractères présumée correspondre à un nom de + rôle valide. + b : boolean, default True + Si b vaut False et qu'un rôle de connexion est déjà membre + du rôle considéré par héritage, la fonction ne fait rien. Si + b vaut True (défaut), la fonction ne passera un rôle de connexion + que s'il est lui-même membre du rôle considéré. + + Returns + ------- + int + Le nombre de rôles pour lesquels la permission a été accordée. + +*/ +DECLARE + roles record ; + attributeur text ; + utilisateur text := current_user ; + c text ; + n int := 0 ; +BEGIN + ------ TESTS PREALABLES ----- + -- existance du rôle + IF NOT n_role IN (SELECT rolname FROM pg_catalog.pg_roles) + THEN + RAISE EXCEPTION 'FLG1. Echec. Le rôle % n''existe pas.', n_role ; + END IF ; + + -- on cherche un rôle dont l'utilisateur est + -- membre et qui, soit a l'attribut CREATEROLE + -- soit a ADMIN OPTION sur le rôle + SELECT rolname INTO attributeur + FROM pg_roles + WHERE pg_has_role(rolname, 'MEMBER') AND rolcreaterole + ORDER BY rolname = current_user DESC ; + IF NOT FOUND + THEN + SELECT grantee INTO attributeur + FROM information_schema.applicable_roles + WHERE is_grantable = 'YES' AND role_name = n_role ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'FLG2. Opération interdite. Permissions insuffisantes pour le rôle %.', n_role + USING HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + n_role) ; + END IF ; + END IF ; + + EXECUTE format('SET ROLE %I', attributeur) ; + + IF b + THEN + FOR roles IN SELECT rolname + FROM pg_roles LEFT JOIN pg_auth_members + ON member = pg_roles.oid AND roleid = n_role::regrole::oid + WHERE rolcanlogin AND member IS NULL + AND NOT rolsuper + LOOP + c := format('GRANT %s TO %s', n_role, roles.rolname) ; + EXECUTE c ; + RAISE NOTICE '> %', c ; + n := n + 1 ; + END LOOP ; + ELSE + FOR roles IN SELECT rolname FROM pg_roles + WHERE rolcanlogin AND NOT pg_has_role(rolname, n_role, 'MEMBER') + LOOP + c := format('GRANT %s TO %s', n_role, roles.rolname) ; + EXECUTE c ; + RAISE NOTICE '> %', c ; + n := n + 1 ; + END LOOP ; + END IF ; + + EXECUTE format('SET ROLE %I', utilisateur) ; + + RETURN n ; +END +$_$; + +ALTER FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) IS 'ASGARD. Confère à tous les rôles de connexion du serveur l''appartenance au rôle donné en argument.' ; + + +------ 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL ------ + +-- Function: z_asgard.asgard_role_trans_acl(regrole) + +DROP FUNCTION z_asgard.asgard_role_trans_acl(regrole) ; + + +------ 4.16 - DIAGNOSTIC DES DROITS NON STANDARDS ------ + +-- Function: z_asgard_admin.asgard_diagnostic(text[]) + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_diagnostic(cibles text[] DEFAULT NULL::text[]) + RETURNS TABLE (nom_schema text, nom_objet text, typ_objet text, critique boolean, anomalie text) + LANGUAGE plpgsql + AS $_$ +/* Pour tous les schémas actifs référencés par Asgard, liste les écarts +entre les droits effectifs et les droits standards. + + Cette fonction peut avoir une durée d'exécution conséquente + si elle est appliquée à un grand nombre de schémas. + + Les "anomalies" détectée peuvent être parfaitement justifiées + si elles résultent d'une personnalisation volontaire des + droits sur certains objets. + + Parameters + ---------- + cibles : text[], optional + Permet de restreindre le diagnostic à la liste de schémas + spécifiés. + + Returns + ------- + table (nom_schema : text, nom_objet : text, typ_objet : text, + critique : boolean, anomalie : text) + Une table avec quatre attributs : + + * "nom_schema" est le nom du schéma. + * "nom_objet" est le nom de l'objet concerné. + * "typ_objet" est le type d'objet. + * "critique" vaut True si l'anomalie est problématique pour + le bon fonctionnement d'Asgard (et doit être corrigée au + plus tôt), False si elle est bénigne. + * "anomalie" est une description de l'anomalie. + + Examples + -------- + SELECT * FROM z_asgard_admin.asgard_diagnostic() ; + SELECT * FROM z_asgard_admin.asgard_diagnostic(ARRAY['schema_1', 'schema_2']) ; + +*/ +DECLARE + item record ; + catalogue record ; + objet record ; + asgard record ; + s text ; + cibles_trans text ; +BEGIN + + ------ CONTROLES ET PREPARATION ------ + cibles := nullif(nullif(cibles, ARRAY[]::text[]), ARRAY[NULL]::text[]) ; + + IF cibles IS NOT NULL + THEN + + FOREACH s IN ARRAY cibles + LOOP + IF NOT s IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr WHERE gestion_schema_etr.creation) + THEN + RAISE EXCEPTION 'FDD1. Le schéma % n''existe pas ou n''est pas référencé dans la table de gestion d''ASGARD.', s ; + ELSIF s IS NOT NULL + THEN + IF cibles_trans IS NULL + THEN + cibles_trans := quote_literal(s) ; + ELSE + cibles_trans := format('%s, %L', cibles_trans, s) ; + END IF ; + END IF ; + END LOOP ; + + cibles_trans := format('ARRAY[%s]', cibles_trans) ; + cibles_trans := nullif(cibles_trans, 'ARRAY[]') ; + END IF ; + + ------ DIAGNOSTIC ------ + FOR item IN EXECUTE + E'SELECT + gestion_schema_etr.nom_schema, + gestion_schema_etr.oid_schema, + r1.rolname AS producteur, + r1.oid AS oid_producteur, + CASE WHEN editeur = ''public'' THEN ''public'' ELSE r2.rolname END AS editeur, + r2.oid AS oid_editeur, + CASE WHEN lecteur = ''public'' THEN ''public'' ELSE r3.rolname END AS lecteur, + r3.oid AS oid_lecteur + FROM z_asgard.gestion_schema_etr + LEFT JOIN pg_catalog.pg_roles AS r1 ON r1.oid = oid_producteur + LEFT JOIN pg_catalog.pg_roles AS r2 ON r2.oid = oid_editeur + LEFT JOIN pg_catalog.pg_roles AS r3 ON r3.oid = oid_lecteur + WHERE gestion_schema_etr.creation' + || CASE WHEN cibles_trans IS NOT NULL + THEN format(' AND gestion_schema_etr.nom_schema = ANY (%s)', cibles_trans) + ELSE '' END + LOOP + FOR catalogue IN ( + SELECT * + FROM + -- liste des objets à traiter + unnest( + -- catalogue de l'objet + ARRAY['pg_class', 'pg_class', 'pg_class', 'pg_class', 'pg_class', 'pg_class', + 'pg_proc', 'pg_type', 'pg_type', 'pg_conversion', 'pg_operator', 'pg_collation', + 'pg_ts_dict', 'pg_ts_config', 'pg_opfamily', 'pg_opclass', 'pg_statistic_ext', 'pg_namespace', + 'pg_default_acl', 'pg_default_acl', 'pg_default_acl', 'pg_default_acl', 'pg_attribute'], + -- préfixe utilisé pour les attributs du catalogue + ARRAY['rel', 'rel', 'rel', 'rel', 'rel', 'rel', + 'pro', 'typ', 'typ', 'con', 'opr', 'coll', + 'dict', 'cfg', 'opf', 'opc', 'stx', 'nsp', + 'defacl', 'defacl', 'defacl', 'defacl', 'att'], + -- si dinstinction selon un attribut, nom de cet attribut + ARRAY['relkind', 'relkind', 'relkind', 'relkind', 'relkind', 'relkind', + NULL, 'typtype', 'typtype', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + 'defaclobjtype', 'defaclobjtype', 'defaclobjtype', 'defaclobjtype', NULL], + -- si distinction selon un attribut, valeur de cet attribut + ARRAY['^r$', '^p$', '^v$', '^m$', '^f$', '^S$', + NULL, '^d$', '^[^d]$', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + '^r$', '^S$', '^f$', '^T$', NULL], + -- nom lisible de l'objet + ARRAY['table', 'table partitionnée', 'vue', 'vue matérialisée', 'table étrangère', 'séquence', + 'routine', 'domaine', 'type', 'conversion', 'opérateur', 'collationnement', + 'dictionnaire de recherche plein texte', 'configuration de recherche plein texte', + 'famille d''opérateurs', 'classe d''opérateurs', 'objet statistique étendu', 'schéma', + 'privilège par défaut sur les tables', 'privilège par défaut sur les séquences', + 'privilège par défaut sur les fonctions', 'privilège par défaut sur les types', 'attribut'], + -- contrôle des droits ? + ARRAY[true, true, true, true, true, true, + true, true, true, false, false, false, + false, false, false, false, false, true, + true, true, true, true, true], + -- droits attendus pour le lecteur du schéma sur l'objet + ARRAY['r', 'r', 'r', 'r', 'r', 'r', + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'U', + NULL, NULL, NULL, NULL, NULL], + -- droits attendus pour l'éditeur du schéma sur l'objet + ARRAY['rawd', 'rawd', 'rawd', 'rawd', 'rawd', 'rU', + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'U', + NULL, NULL, NULL, NULL, NULL], + -- droits attendus pour le producteur du schéma sur l'objet + ARRAY['rawdDxt', 'rawdDxt', 'rawdDxt', 'rawdDxt', 'rawdDxt', 'rwU', + 'X', 'U', 'U', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, 'UC', + 'rawdDxt', 'rwU', 'X', 'U', NULL], + -- droits par défaut de public sur les types et les fonctions + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL, + 'X', 'U', 'U', NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL], + -- si non présent dans PG 9.5, version d'apparition + -- sous forme numérique + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, 100000, NULL, + NULL, NULL, NULL, NULL, NULL], + -- géré automatiquement par ASGARD ? + ARRAY[true, true, true, true, true, true, + true, true, true, true, true, true, + true, true, false, false, false, true, + NULL, NULL, NULL, NULL, true] + ) AS l (catalogue, prefixe, attrib_genre, valeur_genre, lib_obj, droits, drt_lecteur, + drt_editeur, drt_producteur, drt_public, min_version, asgard_auto) + ) + LOOP + IF catalogue.min_version IS NULL + OR current_setting('server_version_num')::int >= catalogue.min_version + THEN + FOR objet IN EXECUTE ' + SELECT ' || + CASE WHEN NOT catalogue.catalogue = 'pg_attribute' THEN + catalogue.catalogue || '.oid AS objoid, ' ELSE '' END || + CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN '' + WHEN catalogue.catalogue = 'pg_attribute' + THEN '(z_asgard.asgard_parse_relident(attrelid::regclass))[2] || '' ('' || ' || + catalogue.prefixe || 'name || '')'' AS objname, ' + ELSE catalogue.prefixe || 'name::text AS objname, ' END || ' + rolname AS objowner' || + CASE WHEN catalogue.droits THEN ', ' || catalogue.prefixe || 'acl AS objacl' ELSE '' END || ' + FROM pg_catalog.' || catalogue.catalogue || ' + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = ' || + CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN 'defaclrole' + WHEN catalogue.catalogue = 'pg_attribute' THEN 'NULL' + ELSE catalogue.prefixe || 'owner' END || ' + WHERE ' || CASE WHEN catalogue.catalogue = 'pg_attribute' + THEN 'quote_ident((z_asgard.asgard_parse_relident(attrelid::regclass))[1])::regnamespace::oid = ' || + item.oid_schema::text + WHEN catalogue.catalogue = 'pg_namespace' THEN catalogue.prefixe || 'name = ' || + quote_literal(item.nom_schema) + ELSE catalogue.prefixe || 'namespace = ' || item.oid_schema::text END || + CASE WHEN catalogue.attrib_genre IS NOT NULL + THEN ' AND ' || catalogue.attrib_genre || ' ~ ' || quote_literal(catalogue.valeur_genre) + ELSE '' END || + CASE WHEN catalogue.catalogue = 'pg_type' + THEN ' AND NOT (pg_type.oid, ''pg_type''::regclass::oid) IN ( + SELECT pg_depend.objid, pg_depend.classid + FROM pg_catalog.pg_depend + WHERE deptype = ANY (ARRAY[''i'', ''a'']) + )' + ELSE '' END + LOOP + -- incohérence propriétaire/producteur + IF NOT objet.objowner = item.producteur + AND NOT catalogue.catalogue = ANY (ARRAY['pg_default_acl', 'pg_attribute']) + THEN + RETURN QUERY + SELECT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + True, + format('le propriétaire (%s) n''est pas le producteur désigné pour le schéma (%s)', + objet.objowner, item.producteur ) ; + END IF ; + + -- présence de privilièges par défaut + IF catalogue.catalogue = 'pg_default_acl' + THEN + RETURN QUERY + SELECT + item.nom_schema::text, + NULL::text, + 'privilège par défaut'::text, + False, + format('%s : %s pour le %s accordé par le rôle %s', + catalogue.lib_obj, + privilege, + CASE WHEN grantee = 0 THEN 'pseudo-rôle public' + ELSE format('rôle %s', grantee::regrole) END, + objet.objowner + ) + FROM aclexplode(objet.objacl) AS acl (grantor, grantee, privilege, grantable) ; + -- droits + ELSIF catalogue.droits + THEN + -- droits à examiner sur les objets d'ASGARD + -- si l'objet courant est un objet d'ASGARD + SELECT * + INTO asgard + FROM ( + VALUES + ('z_asgard_admin', 'z_asgard_admin', 'schéma', 'g_admin_ext', 'U'), + ('z_asgard_admin', 'gestion_schema', 'table', 'g_admin_ext', 'rawd'), + ('z_asgard', 'z_asgard', 'schéma', 'public', 'U'), + ('z_asgard', 'gestion_schema_usr', 'vue', 'public', 'rawd'), + ('z_asgard', 'gestion_schema_etr', 'vue', 'public', 'rawd'), + ('z_asgard', 'asgardmenu_metadata', 'vue', 'public', 'r'), + ('z_asgard', 'asgardmanager_metadata', 'vue', 'public', 'r'), + ('z_asgard', 'gestion_schema_read_only', 'vue', 'public', 'r') + ) AS t (a_schema, a_objet, a_type, role, droits) + WHERE a_schema = item.nom_schema AND a_objet = objet.objname::text AND a_type = catalogue.lib_obj ; + + RETURN QUERY + WITH privileges_effectifs AS ( + SELECT + CASE WHEN grantee = 0 THEN 'public' ELSE grantee::regrole::text END AS role_cible, + privilege_effectif, + grantable + FROM aclexplode(objet.objacl) AS acl (grantor, grantee, privilege_effectif, grantable) + WHERE objet.objacl IS NOT NULL + ), + privileges_attendus AS ( + SELECT fonction, f_role, privilege_attendu, f_critique + FROM unnest( + ARRAY['le propriétaire', 'le lecteur du schéma', 'l''éditeur du schéma', 'un rôle d''ASGARD', 'le pseudo-rôle public'], + ARRAY[objet.objowner, item.lecteur, item.editeur, asgard.role, 'public'], + -- dans le cas d'un attribut, objet.objowner ne contient pas le propriétaire mais + -- le nom de la relation. l'enregistrement sera toutefois systématiquement écarté, + -- puisqu'il n'y a pas de droits standards du propriétaire sur les attributs + ARRAY[catalogue.drt_producteur, catalogue.drt_lecteur, catalogue.drt_editeur, asgard.droits, catalogue.drt_public], + ARRAY[False, False, False, True, False] + ) AS t (fonction, f_role, f_droits, f_critique), + z_asgard.asgard_expend_privileges(f_droits) AS b (privilege_attendu) + WHERE f_role IS NOT NULL AND f_droits IS NOT NULL + AND (NOT objet.objacl IS NULL OR NOT fonction = ANY(ARRAY['le propriétaire', 'le pseudo-rôle public'])) + ) + SELECT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + CASE WHEN privilege_effectif IS NULL OR privilege_attendu IS NULL + THEN coalesce(f_critique, False) ELSE False END, + CASE WHEN privilege_effectif IS NULL THEN format('privilège %s manquant pour %s (%s)', privilege_attendu, fonction, f_role) + WHEN privilege_attendu IS NULL THEN format('privilège %s supplémentaire pour le rôle %s%s', privilege_effectif, role_cible, + CASE WHEN grantable THEN ' (avec GRANT OPTION)' ELSE '' END) + WHEN grantable THEN format('le rôle %s est habilité à transmettre le privilège %s (GRANT OPTION)', role_cible, privilege_effectif) + END + FROM privileges_effectifs FULL OUTER JOIN privileges_attendus + ON privilege_effectif = privilege_attendu + AND role_cible = quote_ident(f_role) + WHERE privilege_effectif IS NULL OR privilege_attendu IS NULL OR grantable ; + END IF ; + + -- le producteur du schéma d'une vue ou vue matérialisée + -- n'est ni producteur, ni éditeur, ni lecteur du + -- schéma d'une table source + IF catalogue.lib_obj = ANY(ARRAY['vue', 'vue matérialisée']) + AND NOT item.nom_schema = ANY(ARRAY['z_asgard', 'z_asgard_admin']) + THEN + RETURN QUERY + SELECT + DISTINCT + item.nom_schema::text, + objet.objname::text, + catalogue.lib_obj, + False, + format('le producteur du schéma de la %s (%s) n''est pas membre des groupes lecteur, éditeur ou producteur de la %s source %s', + catalogue.lib_obj, item.producteur, liblg, relname) + FROM pg_catalog.pg_rewrite + LEFT JOIN pg_catalog.pg_depend + ON objid = pg_rewrite.oid + LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = refobjid + LEFT JOIN z_asgard.gestion_schema_etr + ON relnamespace::regnamespace::text = quote_ident(gestion_schema_etr.nom_schema) + LEFT JOIN unnest( + ARRAY['table', 'table partitionnée', 'vue', 'vue matérialisée', 'table étrangère', 'séquence'], + ARRAY['r', 'p', 'v', 'm', 'f', 'S'] + ) AS t (liblg, libcrt) + ON relkind = libcrt + WHERE ev_class = objet.objoid + AND rulename = '_RETURN' + AND ev_type = '1' + AND ev_enabled = 'O' + AND is_instead + AND classid = 'pg_rewrite'::regclass::oid + AND refclassid = 'pg_class'::regclass::oid + AND deptype = 'n' + AND NOT refobjid = objet.objoid + AND NOT item.nom_schema = gestion_schema_etr.nom_schema + AND NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_producteur, 'USAGE') + AND (gestion_schema_etr.oid_editeur IS NULL OR NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_editeur, 'USAGE')) + AND (gestion_schema_etr.oid_lecteur IS NULL OR NOT pg_has_role(item.oid_producteur, gestion_schema_etr.oid_lecteur, 'USAGE')) ; + END IF ; + END LOOP ; + END IF ; + END LOOP ; + END LOOP ; +END +$_$ ; + +ALTER FUNCTION z_asgard_admin.asgard_diagnostic(text[]) + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_diagnostic(text[]) IS 'ASGARD. Pour tous les schémas actifs référencés par Asgard, liste les écarts entre les droits effectifs et les droits standards.' ; + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +--------------------------------------------- +------ 5 - TRIGGERS SUR GESTION_SCHEMA ------ +--------------------------------------------- + +/* 5.1 - TRIGGER BEFORE + 5.2 - TRIGGER AFTER */ + +------ 5.1 - TRIGGER BEFORE ------ + +-- Function: z_asgard_admin.asgard_on_modify_gestion_schema_before() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() + RETURNS trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_before +sur z_asgard_admin.gestion_schema, qui valide et normalise les informations +saisies dans la table de gestion avant leur enregistrement. + +*/ +DECLARE + n_role text ; +BEGIN + + ------ INSERT PAR UN UTILISATEUR NON HABILITE ------ + IF TG_OP = 'INSERT' AND NOT has_database_privilege(current_database(), 'CREATE') + -- même si creation vaut faux, seul un rôle habilité à créer des + -- schéma peut ajouter des lignes dans la table de gestion + THEN + RAISE EXCEPTION 'TB1. Vous devez être habilité à créer des schémas pour réaliser cette opération.' ; + END IF ; + + ------ APPLICATION DES VALEURS PAR DEFAUT ------ + -- au tout début car de nombreux tests sont faits par la + -- suite sur "NOT NEW.creation" + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + NEW.creation := coalesce(NEW.creation, False) ; + NEW.nomenclature := coalesce(NEW.nomenclature, False) ; + END IF ; + + ------ EFFACEMENT D'UN ENREGISTREMENT ------ + IF TG_OP = 'DELETE' + THEN + -- on n'autorise pas l'effacement si creation vaut True + -- avec une exception pour les commandes envoyées par la fonction + -- de maintenance asgard_sortie_gestion_schema + IF OLD.creation AND (OLD.ctrl[1] IS NULL OR NOT OLD.ctrl[1] = 'EXIT') + THEN + RAISE EXCEPTION 'TB2. Opération interdite (schéma %). L''effacement n''est autorisé que si creation vaut False.', OLD.nom_schema + USING HINT = 'Pour déréférencer un schéma sans le supprimer, vous pouvez utiliser la fonction z_asgard_admin.asgard_sortie_gestion_schema.' ; + END IF; + + -- on n'autorise pas l'effacement pour les schémas de la nomenclature + IF OLD.nomenclature + THEN + IF OLD.ctrl[1] = 'EXIT' + THEN + RAISE EXCEPTION 'TB26. Opération interdite (schéma %). Le déréférencement n''est pas autorisé pour les schémas de la nomenclature nationale.', OLD.nom_schema + USING HINT = 'Si vous tenez à déréférencer ce schéma, basculez préalablement nomenclature sur False.' ; + ELSE + RAISE EXCEPTION 'TB3. Opération interdite (schéma %). L''effacement n''est pas autorisé pour les schémas de la nomenclature nationale.', OLD.nom_schema + USING HINT = 'Si vous tenez à supprimer de la table de gestion les informations relatives à ce schéma, basculez préalablement nomenclature sur False.' ; + END IF ; + END IF ; + END IF; + + ------ DE-CREATION D'UN SCHEMA ------ + IF TG_OP = 'UPDATE' + THEN + -- si bloc valait déjà d (schéma "mis à la corbeille") + -- on exécute une commande de suppression du schéma. Toute autre modification sur + -- la ligne est ignorée. + IF OLD.bloc = 'd' AND OLD.creation AND NOT NEW.creation AND NEW.ctrl[2] IS NULL + AND OLD.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + -- on bloque tout de même les tentatives de suppression + -- par un utilisateur qui n'aurait pas des droits suffisants (a priori + -- uniquement dans le cas de g_admin avec un schéma appartenant à un + -- super-utilisateur). + -- c'est oid_producteur et pas producteur qui est utilisé au cas + -- où le nom du rôle aurait été modifié entre temps + IF NOT pg_has_role(OLD.oid_producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB23. Opération interdite (schéma %).', OLD.nom_schema + USING DETAIL = format('Seuls les membres du rôle producteur %s peuvent supprimer ce schéma.', OLD.oid_producteur::regrole) ; + ELSE + EXECUTE format('DROP SCHEMA %I CASCADE', OLD.nom_schema) ; + RAISE NOTICE '... Le schéma % a été supprimé.', OLD.nom_schema ; + RETURN NULL ; + END IF ; + -- sinon, on n'autorise creation à passer de true à false que si le schéma + -- n'existe plus (permet notamment à l'event trigger qui gère les + -- suppressions de mettre creation à false) + ELSIF OLD.creation and NOT NEW.creation + AND NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + RAISE EXCEPTION 'TB4. Opération interdite (schéma %). Le champ creation ne peut passer de True à False si le schéma existe.', NEW.nom_schema + USING HINT = 'Si vous supprimez physiquement le schéma avec la commande DROP SCHEMA, creation basculera sur False automatiquement.' ; + END IF ; + END IF ; + + IF TG_OP <> 'DELETE' + THEN + ------ PROHIBITION DE LA SAISIE MANUELLE DES OID ------ + -- vérifié grâce au champ ctrl + IF NEW.ctrl[2] IS NULL + OR NOT array_length(NEW.ctrl, 1) >= 2 + OR NEW.ctrl[1] IS NULL + OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT') + OR NOT NEW.ctrl[2] = 'x7-A;#rzo' + -- ctrl NULL ou invalide + THEN + + IF NEW.ctrl[1] = 'EXIT' + THEN + RAISE EXCEPTION 'TB17. Opération interdite (schéma %).', coalesce(NEW.nom_schema, '?') + USING HINT = 'Pour déréférencer un schéma, veuillez utiliser la fonction z_asgard_admin.asgard_sortie_gestion_schema.' ; + END IF ; + + -- réinitialisation du champ ctrl, qui peut contenir des informations + -- issues de commandes antérieures (dans ctrl[1]) + NEW.ctrl := ARRAY['MANUEL', NULL]::text[] ; + + IF TG_OP = 'INSERT' AND ( + NEW.oid_producteur IS NOT NULL + OR NEW.oid_lecteur IS NOT NULL + OR NEW.oid_editeur IS NOT NULL + OR NEW.oid_schema IS NOT NULL + ) + -- cas d'un INSERT manuel pour lequel des OID ont été saisis + -- on les remet à NULL + THEN + NEW.oid_producteur = NULL ; + NEW.oid_editeur = NULL ; + NEW.oid_lecteur = NULL ; + NEW.oid_schema = NULL ; + ELSIF TG_OP = 'UPDATE' + THEN + IF NOT coalesce(NEW.oid_producteur, -1) = coalesce(OLD.oid_producteur, -1) + OR NOT coalesce(NEW.oid_editeur, -1) = coalesce(OLD.oid_editeur, -1) + OR NOT coalesce(NEW.oid_lecteur, -1) = coalesce(OLD.oid_lecteur, -1) + OR NOT coalesce(NEW.oid_schema, -1) = coalesce(OLD.oid_schema, -1) + -- cas d'un UPDATE avec modification des OID + -- on les remet à OLD + THEN + NEW.oid_producteur = OLD.oid_producteur ; + NEW.oid_editeur = OLD.oid_editeur ; + NEW.oid_lecteur = OLD.oid_lecteur ; + NEW.oid_schema = OLD.oid_schema ; + END IF ; + END IF ; + ELSE + -- suppression du mot de passe de contrôle. + -- ctrl[1] est par contre conservé - il sera utilisé + -- par le trigger AFTER pour connaître l'opération + -- à l'origine de son déclenchement. + NEW.ctrl[2] := NULL ; + END IF ; + + ------ REQUETES AUTO A IGNORER ------ + -- les remontées du trigger AFTER (SELF) + -- sont exclues, car les contraintes ont déjà + -- été validées (et pose problèmes avec les + -- contrôles d'OID sur les UPDATE, car ceux-ci + -- ne seront pas nécessairement déjà remplis) ; + -- les requêtes EXIT de même, car c'est un + -- pré-requis à la suppression qui ne fait + -- que modifier le champ ctrl + IF NEW.ctrl[1] IN ('SELF', 'EXIT') + THEN + -- aucune action + RETURN NEW ; + END IF ; + + ------ VERROUILLAGE DES CHAMPS LIES A LA NOMENCLATURE ------ + -- modifiables uniquement par l'ADL + IF TG_OP = 'UPDATE' + THEN + IF (OLD.nomenclature OR NEW.nomenclature) AND NOT pg_has_role('g_admin', 'MEMBER') AND ( + NOT coalesce(OLD.nomenclature, False) = coalesce(NEW.nomenclature, False) + OR NOT coalesce(OLD.niv1, '') = coalesce(NEW.niv1, '') + OR NOT coalesce(OLD.niv1_abr, '') = coalesce(NEW.niv1_abr, '') + OR NOT coalesce(OLD.niv2, '') = coalesce(NEW.niv2, '') + OR NOT coalesce(OLD.niv2_abr, '') = coalesce(NEW.niv2_abr, '') + OR NOT coalesce(OLD.nom_schema, '') = coalesce(NEW.nom_schema, '') + OR NOT coalesce(OLD.bloc, '') = coalesce(NEW.bloc, '') + ) + THEN + RAISE EXCEPTION 'TB18. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seuls les membres de g_admin sont habilités à modifier les champs nomenclature et - pour les schémas de la nomenclature - bloc, niv1, niv1_abr, niv2, niv2_abr et nom_schema.' ; + END IF ; + ELSIF TG_OP = 'INSERT' + THEN + IF NEW.nomenclature AND NOT pg_has_role('g_admin', 'MEMBER') + THEN + RAISE EXCEPTION 'TB19. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = 'Seuls les membres de g_admin sont autorisés à ajouter des schémas à la nomenclature (nomenclature = True).' ; + END IF ; + END IF ; + + ------ NETTOYAGE DES CHAÎNES VIDES ------ + -- si l'utilisateur a entré des chaînes vides on met des NULL + NEW.editeur := nullif(NEW.editeur, '') ; + NEW.lecteur := nullif(NEW.lecteur, '') ; + NEW.bloc := nullif(NEW.bloc, '') ; + NEW.niv1 := nullif(NEW.niv1, '') ; + NEW.niv1_abr := nullif(NEW.niv1_abr, '') ; + NEW.niv2 := nullif(NEW.niv2, '') ; + NEW.niv2_abr := nullif(NEW.niv2_abr, '') ; + NEW.nom_schema := nullif(NEW.nom_schema, '') ; + -- si producteur est vide on met par défaut g_admin + NEW.producteur := coalesce(nullif(NEW.producteur, ''), 'g_admin') ; + + ------ NETTOYAGE DES CHAMPS OID ------ + -- pour les rôles de lecteur et éditeur, + -- si le champ de nom est vidé par l'utilisateur, + -- on vide en conséquence l'OID + IF NEW.editeur IS NULL + THEN + NEW.oid_editeur := NULL ; + END IF ; + IF NEW.lecteur IS NULL + THEN + NEW.oid_lecteur := NULL ; + END IF ; + -- si le schéma n'est pas créé, on s'assure que les champs + -- d'OID restent vides + -- à noter que l'event trigger sur DROP SCHEMA vide + -- déjà le champ oid_schema + IF NOT NEW.creation + THEN + NEW.oid_schema := NULL ; + NEW.oid_lecteur := NULL ; + NEW.oid_editeur := NULL ; + NEW.oid_producteur := NULL ; + END IF ; + + ------ VALIDITE DES NOMS DE ROLES ------ + -- dans le cas d'un schéma pré-existant, on s'assure que les rôles qui + -- ne changent pas sont toujours valides (qu'ils existent et que le nom + -- n'a pas été modifié entre temps) + -- si tel est le cas, on les met à jour et on le note dans + -- ctrl, pour que le trigger AFTER sache qu'il ne s'agit + -- pas réellement de nouveaux rôles sur lesquels les droits + -- devraient être réappliqués + IF TG_OP = 'UPDATE' AND NEW.creation + THEN + -- producteur + IF OLD.creation AND OLD.producteur = NEW.producteur + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_producteur ; + IF NOT FOUND + -- le rôle producteur n'existe pas + THEN + -- cas invraisemblable, car un rôle ne peut pas être + -- supprimé alors qu'il est propriétaire d'un schéma, et la + -- commande ALTER SCHEMA OWNER TO aurait été interceptée + -- mais, s'il advient, on repart du propriétaire + -- renseigné dans pg_namespace + SELECT rolname, nspowner + INTO NEW.producteur, NEW.oid_producteur + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE pg_namespace.oid = NEW.oid_schema ; + RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', NEW.nom_schema ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; + ELSIF NOT n_role = NEW.producteur + -- libellé obsolète du producteur + THEN + NEW.producteur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle producteur, renommé entre temps.', NEW.nom_schema + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.producteur, NEW.producteur) ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; + END IF ; + END IF ; + -- éditeur + IF OLD.creation AND OLD.editeur = NEW.editeur + AND NOT NEW.editeur = 'public' + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_editeur ; + IF NOT FOUND + -- le rôle éditeur n'existe pas + THEN + NEW.editeur := NULL ; + NEW.oid_editeur := NULL ; + RAISE NOTICE '[table de gestion] Schéma %. Le rôle éditeur n''existant plus, il est déréférencé.', NEW.nom_schema + USING DETAIL = format('Ancien nom "%s".', OLD.editeur) ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; + ELSIF NOT n_role = NEW.editeur + -- libellé obsolète de l'éditeur + THEN + NEW.editeur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle éditeur, renommé entre temps.', NEW.nom_schema + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.editeur, NEW.editeur) ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; + END IF ; + END IF ; + -- lecteur + IF OLD.creation AND OLD.lecteur = NEW.lecteur + AND NOT NEW.lecteur = 'public' + THEN + SELECT rolname INTO n_role + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = NEW.oid_lecteur ; + IF NOT FOUND + -- le rôle lecteur n'existe pas + THEN + NEW.lecteur := NULL ; + NEW.oid_lecteur := NULL ; + RAISE NOTICE '[table de gestion] Schéma %. Le rôle lecteur n''existant plus, il est déréférencé.', NEW.nom_schema + USING DETAIL = format('Ancien nom "%s".', OLD.lecteur) ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; + ELSIF NOT n_role = NEW.lecteur + -- libellé obsolète du lecteur + THEN + NEW.lecteur := n_role ; + RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle lecteur, renommé entre temps.', NEW.nom_schema + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.lecteur, NEW.lecteur) ; + NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; + END IF ; + END IF ; + END IF ; + + ------ NON RESPECT DES CONTRAINTES ------ + -- non nullité de nom_schema + IF NEW.nom_schema IS NULL + THEN + RAISE EXCEPTION 'TB8. Saisie incorrecte. Le nom du schéma doit être renseigné (champ nom_schema).' ; + END IF ; + + -- unicité de nom_schema + -- -> contrôlé après les manipulations sur les blocs de + -- la partie suivante. + + -- unicité de oid_schema + IF TG_OP = 'INSERT' AND NEW.oid_schema IN (SELECT gestion_schema_etr.oid_schema FROM z_asgard.gestion_schema_etr + WHERE gestion_schema_etr.oid_schema IS NOT NULL) + THEN + RAISE EXCEPTION 'TB11. Saisie incorrecte (schéma %). Un schéma de même OID est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + THEN + -- cas (très hypothétique) d'une modification d'OID + IF NOT coalesce(NEW.oid_schema, -1) = coalesce(OLD.oid_schema, -1) + AND NEW.oid_schema IN (SELECT gestion_schema_etr.oid_schema FROM z_asgard.gestion_schema_etr + WHERE gestion_schema_etr.oid_schema IS NOT NULL) + THEN + RAISE EXCEPTION 'TB12. Saisie incorrecte (schéma %). Un schéma de même OID est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + END IF ; + END IF ; + + -- non répétition des rôles + IF NOT ((NEW.oid_lecteur IS NULL OR NOT NEW.oid_lecteur = NEW.oid_producteur) + AND (NEW.oid_editeur IS NULL OR NOT NEW.oid_editeur = NEW.oid_producteur) + AND (NEW.oid_lecteur IS NULL OR NEW.oid_editeur IS NULL OR NOT NEW.oid_lecteur = NEW.oid_editeur)) + THEN + RAISE EXCEPTION 'TB13. Saisie incorrecte (schéma %). Les rôles producteur, lecteur et éditeur doivent être distincts.', NEW.nom_schema ; + END IF ; + END IF ; + + ------ COHERENCE BLOC/NOM DU SCHEMA ------ + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF NEW.nom_schema ~ '^d_' + -- cas d'un schéma mis à la corbeille par un changement de nom + -- on rétablit le nom antérieur, la lettre d apparaissant + -- exclusivement dans le bloc + THEN + IF TG_OP = 'INSERT' + -- pour un INSERT, on ne s'intéresse qu'aux cas où + -- le bloc est NULL ou vaut d. Dans tous les autres cas, + -- le bloc prévaudra sur le nom et le schéma n'ira + -- pas à la corbeille de toute façon + THEN + IF NEW.bloc IS NULL + THEN + NEW.bloc := 'd' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + END IF ; + ELSE + -- pour un UPDATE, on s'intéresse aux cas où le bloc + -- n'a pas changé et aux cas où il a été mis sur 'd' ou + -- (sous certaines conditions) sur NULL. + -- Sinon, le bloc prévaudra sur le nom et le + -- schéma n'ira pas à la corbeille de toute façon + IF NEW.bloc = 'd' AND NOT OLD.bloc = 'd' + -- mise à la corbeille avec action simultanée sur le nom du schéma + -- et le bloc + s'il y a un ancien bloc récupérable + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; + -- on ne reprend pas l'ancien nom au cas où autre chose que le préfixe aurait été + -- changé. + + ELSIF NEW.bloc IS NULL AND NOT OLD.bloc = 'd' + -- mise à la corbeille via le nom avec mise à NULL du bloc en + -- parallèle + s'il y a un ancien bloc récupérable + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; + + NEW.bloc := 'd' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc = 'd' + AND OLD.nom_schema ~ '^[a-ce-z]_' + -- s'il y a un ancien préfixe récupérable (cas d'un + -- schéma dont on tente de forcer le bloc à d alors + -- qu'il est déjà dans la corbeille) + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', substring(OLD.nom_schema, '^([a-ce-z]_)')) ; + RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc = 'd' + AND NOT OLD.nom_schema ~ '^[a-z]_' + -- schéma sans bloc de la corbeille sur lequel on tente de forcer + -- un préfixe d + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Suppression du préfixe du schéma sans bloc %.', NEW.nom_schema ; + + ELSIF NEW.bloc IS NULL AND OLD.bloc IS NULL + -- mise à la corbeille d'un schéma sans bloc + THEN + NEW.bloc := 'd' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = 'd' AND OLD.bloc IS NULL + -- mise à la corbeille d'un schéma sans bloc + -- avec modification simultanée du nom et du bloc + THEN + NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + + ELSIF NEW.bloc = OLD.bloc AND NOT NEW.bloc = 'd' + -- le bloc ne change pas et contenait une autre + -- valeur que d + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; + + NEW.bloc := 'd' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + END IF ; + + END IF ; + END IF ; + END IF ; + + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF NEW.bloc IS NULL AND NEW.nom_schema ~ '^[a-z]_' + -- si bloc est NULL, mais que le nom du schéma + -- comporte un préfixe, + THEN + IF TG_OP = 'UPDATE' + THEN + IF OLD.bloc IS NOT NULL + AND OLD.nom_schema ~ '^[a-z]_' + AND left(NEW.nom_schema, 1) = left(OLD.nom_schema, 1) + -- sur un UPDATE où le préfixe du schéma n'a pas été modifié, tandis + -- que le bloc a été mis à NULL, on supprime le préfixe du schéma + THEN + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^[a-z]_', '') ; + RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSE + -- sinon, on met le préfixe du nom du schéma dans bloc + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + END IF ; + ELSE + -- sur un INSERT, + -- on met le préfixe du nom du schéma dans bloc + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + END IF ; + ELSIF NEW.bloc IS NULL + -- si bloc est NULL, et que (sous-entendu) le nom du schéma ne + -- respecte pas la nomenclature, on avertit l'utilisateur + THEN + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSIF NOT NEW.nom_schema ~ ('^'|| NEW.bloc || '_') + AND NOT NEW.bloc = 'd' + -- le bloc est renseigné mais le nom du schéma ne correspond pas + -- (et il ne s'agit pas d'un schéma mis à la corbeille) : + -- Si le nom est de la forme 'a_...', alors : + -- - dans le cas d'un UPDATE avec modification du nom + -- du schéma et pas du bloc, on se fie au nom du schéma + -- et on change le bloc ; + -- - si bloc n'est pas une lettre, on renvoie une erreur ; + -- - dans les autres cas, on se fie au bloc et change le + -- préfixe. + -- Si le nom ne comporte pas de préfixe : + -- - s'il vient d'être sciemment supprimé et que le bloc + -- n'a pas changé, on supprime le bloc ; + -- - sinon, si le bloc est une lettre, on l'ajoute au début du + -- nom (sans doubler l'underscore, si le nom commençait par + -- un underscore) ; + -- - sinon on renvoie une erreur. + THEN + IF NEW.nom_schema ~ '^([a-z])?_' + -- si le nom du schéma contient un préfixe valide + THEN + IF TG_OP = 'UPDATE' + -- sur un UPDATE + THEN + IF NOT NEW.nom_schema = OLD.nom_schema AND NEW.bloc = OLD.bloc + -- si le bloc est le même, mais que le nom du schéma a été modifié + -- on met à jour le bloc selon le nouveau préfixe du schéma + THEN + NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- si le nouveau bloc est invalide, on renvoie une erreur + THEN + RAISE EXCEPTION 'TB14. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSE + -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; + END IF ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- (sur un INSERT) + -- si le nouveau bloc est invalide, + -- on renvoie une erreur + THEN + RAISE EXCEPTION 'TB15. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSE + -- (sur un INSERT) + -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc + NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; + END IF ; + ELSIF NOT NEW.bloc ~ '^[a-z]$' + -- (si le nom du schéma ne contient pas de préfixe valide) + -- si le nouveau bloc est invalide, on renvoie une erreur + THEN + RAISE EXCEPTION 'TB16. Saisie invalide (schéma %). Le bloc doit être une lettre minuscule ou rien.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + -- (si le nom du schéma ne contient pas de préfixe valide) + -- sur un UPDATE + THEN + IF NEW.bloc = OLD.bloc + AND OLD.nom_schema ~ '^([a-z])?_' + -- s'il y avait un bloc, mais que le préfixe vient d'être supprimé + -- dans le nom du schéma : on supprime le bloc + THEN + NEW.bloc := NULL ; + RAISE NOTICE '[table de gestion] Le bloc du schéma % a été supprimé.', NEW.nom_schema ; + RAISE NOTICE '[table de gestion] Le nom du schéma % ne respecte pas la nomenclature.', NEW.nom_schema + USING HINT = 'Si vous saisissez un préfixe dans le champ bloc, il sera automatiquement ajouté au nom du schéma.' ; + ELSE + -- sinon, préfixage du schéma selon le bloc + NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; + END IF ; + ELSE + -- sur un INSERT, préfixage du schéma selon le bloc + NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; + END IF ; + -- le trigger AFTER se chargera de renommer physiquement le + -- schéma d'autant que de besoin + END IF ; + END IF ; + + ------ NON RESPECT DES CONTRAINTES (SUITE) ------ + -- unicité de nom_schema + IF TG_OP IN ('INSERT', 'UPDATE') + THEN + IF TG_OP = 'INSERT' AND NEW.nom_schema IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr) + THEN + RAISE EXCEPTION 'TB9. Saisie incorrecte (schéma %). Un schéma de même nom est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + ELSIF TG_OP = 'UPDATE' + THEN + -- cas d'un changement de nom + IF NOT NEW.nom_schema = OLD.nom_schema + AND NEW.nom_schema IN (SELECT gestion_schema_etr.nom_schema FROM z_asgard.gestion_schema_etr) + THEN + RAISE EXCEPTION 'TB10. Saisie incorrecte (schéma %). Un schéma de même nom est déjà répertorié dans la table de gestion.', NEW.nom_schema ; + END IF ; + END IF ; + END IF ; + + ------ MISE À LA CORBEILLE ------ + -- notification de l'utilisateur + IF TG_OP = 'UPDATE' + THEN + -- schéma existant dont bloc bascule sur 'd' + -- ou schéma créé par bascule de creation sur True dans bloc vaut 'd' + IF NEW.creation AND NEW.bloc = 'd' AND (NOT OLD.bloc = 'd' OR OLD.bloc IS NULL) + OR NEW.creation AND NOT OLD.creation AND NEW.bloc = 'd' + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été mis à la corbeille (bloc = ''d'').', NEW.nom_schema + USING HINT = 'Si vous basculez creation sur False, le schéma et son contenu seront automatiquement supprimés.' ; + -- restauration + ELSIF NEW.creation AND OLD.bloc = 'd' AND (NOT NEW.bloc = 'd' OR NEW.bloc IS NULL) + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été retiré de la corbeille (bloc ne vaut plus ''d'').', NEW.nom_schema ; + END IF ; + ELSIF TG_OP = 'INSERT' + THEN + -- nouveau schéma dont bloc vaut 'd' + IF NEW.creation AND NEW.bloc = 'd' + THEN + RAISE NOTICE '[table de gestion] Le schéma % a été mis à la corbeille (bloc = ''d'').', NEW.nom_schema + USING HINT = 'Si vous basculez creation sur False, le schéma et son contenu seront automatiquement supprimés.' ; + END IF ; + END IF ; + + ------ SCHEMAS DES SUPER-UTILISATEURS ------ + -- concerne uniquement les membres de g_admin, qui voient tous + -- les schémas, y compris ceux des super-utilisateurs dont ils + -- ne sont pas membres. Les contrôles suivants bloquent dans ce + -- cas les tentatives de mise à jour des champs nom_schema, + -- producteur, editeur et lecteur, ainsi que les création de schéma + -- via un INSERT ou un UPDATE. + IF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND OLD.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND ( + NOT OLD.nom_schema = NEW.nom_schema + OR NOT OLD.producteur = NEW.producteur AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + OR NOT coalesce(OLD.editeur, '') = coalesce(NEW.editeur, '') AND (NEW.ctrl IS NULL OR NOT 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL))) + OR NOT coalesce(OLD.lecteur, '') = coalesce(NEW.lecteur, '') AND (NEW.ctrl IS NULL OR NOT 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL))) + ) + THEN + IF NOT pg_has_role(OLD.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB20. Opération interdite (schéma %).', OLD.nom_schema + USING DETAIL = format('Seul le rôle producteur %s (super-utilisateur) peut modifier ce schéma.', OLD.producteur) ; + END IF ; + END IF ; + + IF NEW.creation + AND NOT OLD.creation + AND NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB21. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = format('Seul le super-utilisateur %s peut créer un schéma dont il est identifié comme producteur.', NEW.producteur) ; + END IF ; + END IF ; + + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.creation + AND NOT OLD.producteur = NEW.producteur AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB24. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = format('Seul le super-utilisateur %s peut se désigner comme producteur d''un schéma.', NEW.producteur) ; + END IF ; + END IF ; + + ELSIF TG_OP = 'INSERT' + THEN + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.creation + AND NOT NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + -- on exclut les schémas en cours de référencement, qui sont gérés + -- juste après, avec leur propre message d'erreur + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB22. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = format('Seul le super-utilisateur %s peut créer un schéma dont il est identifié comme producteur.', NEW.producteur) ; + END IF ; + END IF ; + + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + AND NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + -- schéma pré-existant en cours de référencement + THEN + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TB25. Opération interdite (schéma %).', NEW.nom_schema + USING DETAIL = format('Seul le super-utilisateur %s peut référencer dans ASGARD un schéma dont il est identifié comme producteur.', NEW.producteur) ; + END IF ; + END IF ; + END IF ; + + ------ RETURN ------ + IF TG_OP IN ('UPDATE', 'INSERT') + THEN + RETURN NEW ; + ELSIF TG_OP = 'DELETE' + THEN + RETURN OLD ; + END IF ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_before sur z_asgard_admin.gestion_schema, qui valide et normalise les informations saisies dans la table de gestion avant leur enregistrement.' ; + + +-- Trigger: asgard_on_modify_gestion_schema_before + +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_before ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui valide et normalise les informations saisies dans la table de gestion avant leur enregistrement.' ; + + +------ 5.2 - TRIGGER AFTER ------ + +-- Function: z_asgard_admin.asgard_on_modify_gestion_schema_after() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() + RETURNS trigger + LANGUAGE plpgsql + AS $BODY$ +/* Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_after +sur z_asgard_admin.gestion_schema, qui répercute physiquement les modifications +de la table de gestion. + +*/ +DECLARE + utilisateur text ; + createur text ; + administrateur text ; + e_mssg text ; + e_hint text ; + e_detl text ; + b_superuser boolean ; + b_test boolean ; + l_commande text[] ; + c text ; + c_reverse text ; + a_producteur text ; + a_editeur text ; + a_lecteur text ; + n int ; +BEGIN + + ------ REQUETES AUTO A IGNORER ------ + -- les remontées du trigger lui-même (SELF), + -- ainsi que des event triggers sur les + -- suppressions de schémas (DROP), n'appellent + -- aucune action, elles sont donc exclues dès + -- le départ + -- les remontées des changements de noms sont + -- conservées, pour le cas où la mise en + -- cohérence avec "bloc" aurait conduit à une + -- modification du nom par le trigger BEFORE + -- (géré au point suivant) + -- les remontées des créations et changements + -- de propriétaire (CREATE et OWNER) appellent + -- des opérations sur les droits plus lourdes + -- qui ne permettent pas de les exclure en + -- amont + IF NEW.ctrl[1] IN ('SELF', 'DROP') + THEN + -- aucune action + RETURN NULL ; + END IF ; + + ------ MANIPULATIONS PREALABLES ------ + utilisateur := current_user ; + + -- si besoin pour les futures opérations sur les rôles, + -- récupération du nom d'un rôle dont current_user est membre + -- et qui a l'attribut CREATEROLE. Autant que possible, la + -- requête renvoie current_user lui-même. On exclut d'office les + -- rôles NOINHERIT qui ne pourront pas avoir simultanément les + -- droits du propriétaire de NEW et OLD.producteur + SELECT rolname INTO createur FROM pg_roles + WHERE pg_has_role(rolname, 'MEMBER') AND rolcreaterole AND rolinherit + ORDER BY rolname = current_user DESC ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.producteur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + SELECT rolname INTO a_producteur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_producteur ; + -- pour la suite, on emploira toujours + -- a_producteur à la place de OLD.producteur + -- pour les opérations sur les droits. + -- Il est réputé non NULL pour un schéma + -- pré-existant (OLD.creation vaut True), + -- dans la mesure où un rôle ne peut être + -- supprimé s'il est propriétaire d'un + -- schéma et où tous les changements de + -- propriétaires sont remontés par event + -- triggers (+ contrôles pour assurer la + -- non-modification manuelle des OID). + IF NOT FOUND AND OLD.creation AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur dans la table de gestion est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', OLD.nom_schema ; + SELECT rolname INTO a_producteur + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE pg_namespace.oid = NEW.oid_schema ; + IF NOT FOUND + THEN + RAISE EXCEPTION 'TA1. Anomalie critique (schéma %). Le propriétaire du schéma est introuvable.', OLD.nom_schema ; + END IF ; + END IF ; + END IF ; + + ------ MISE EN APPLICATION D'UN CHANGEMENT DE NOM DE SCHEMA ------ + IF NOT NEW.oid_schema::regnamespace::text = quote_ident(NEW.nom_schema) + -- le schéma existe et ne porte pas déjà le nom NEW.nom_schema + THEN + EXECUTE format('ALTER SCHEMA %s RENAME TO %I', NEW.oid_schema::regnamespace, NEW.nom_schema) ; + RAISE NOTICE '... Le schéma % a été renommé.', NEW.nom_schema ; + END IF ; + -- exclusion des remontées d'event trigger correspondant + -- à des changements de noms + IF NEW.ctrl[1] = 'RENAME' + THEN + -- aucune action + RETURN NULL ; + END IF ; + + ------ PREPARATION DU PRODUCTEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister + -- (creation vaut False) ; + -- - d'un schéma pré-existant dont les rôles ne changent pas + -- ou dont le libellé a juste été nettoyé par le trigger + -- BEFORE. + -- ils sont donc exclus au préalable + -- si le moindre rôle a changé, il faudra être membre du + -- groupe propriétaire/producteur pour pouvoir modifier + -- les privilèges en conséquence + b_test := False ; + IF NOT NEW.creation + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND (NEW.producteur = OLD.producteur OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + AND (coalesce(NEW.editeur, '') = coalesce(OLD.editeur, '') OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL))) + AND (coalesce(NEW.lecteur, '') = coalesce(OLD.lecteur, '') OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL))) + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles) + -- si le producteur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA2. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.producteur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux producteurs.' ; + END IF ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.producteur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.producteur ; + EXECUTE format('SET ROLE %I', utilisateur) ; + ELSE + -- si le rôle producteur existe, on vérifie qu'il n'a pas l'option LOGIN + -- les superusers avec LOGIN (comme postgres) sont tolérés + -- paradoxe ou non, dans l'état actuel des choses, cette erreur se + -- déclenche aussi lorsque la modification ne porte que sur les rôles + -- lecteur/éditeur + SELECT rolsuper INTO b_superuser + FROM pg_roles WHERE rolname = NEW.producteur AND rolcanlogin ; + IF NOT b_superuser + THEN + RAISE EXCEPTION 'TA3. Opération interdite (schéma %). Le producteur/propriétaire du schéma ne doit pas être un rôle de connexion.', NEW.nom_schema ; + END IF ; + END IF ; + b_superuser := coalesce(b_superuser, False) ; + + -- mise à jour du champ d'OID du producteur + IF NEW.ctrl[1] IS NULL OR NOT NEW.ctrl[1] IN ('OWNER', 'CREATE') + -- pas dans le cas d'une remontée de commande directe + -- où l'OID du producteur sera déjà renseigné + -- et uniquement s'il a réellement été modifié (ce + -- qui n'est pas le cas si les changements ne portent + -- que sur les rôles lecteur/éditeur) + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_producteur = quote_ident(NEW.producteur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_producteur IS NULL + OR NOT oid_producteur = quote_ident(NEW.producteur)::regrole::oid + ) ; + END IF ; + + -- implémentation des permissions manquantes sur NEW.producteur + IF NOT pg_has_role(utilisateur, NEW.producteur, 'USAGE') + THEN + b_test := True ; + IF createur IS NULL OR b_superuser + THEN + RAISE EXCEPTION 'TA4. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur + USING HINT = format('Votre rôle doit être membre de %s ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + NEW.producteur) ; + END IF ; + END IF ; + IF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NOT pg_has_role(utilisateur, a_producteur, 'USAGE') + AND NOT (NEW.producteur = OLD.producteur OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) + -- les permissions sur OLD.producteur ne sont contrôlées que si le producteur + -- a effectivement été modifié + THEN + b_test := True ; + IF createur IS NULL OR b_superuser + THEN + RAISE EXCEPTION 'TA5. Opération interdite. Permissions insuffisantes pour le rôle %.', a_producteur + USING HINT = format('Votre rôle doit être membre de %s ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + a_producteur) ; + END IF ; + END IF ; + END IF ; + IF b_test + THEN + EXECUTE format('SET ROLE %I', createur) ; + -- par commodité, on rend createur membre à la fois de NEW et (si besoin) + -- de OLD.producteur, même si l'utilisateur avait déjà accès à + -- l'un des deux par ailleurs : + IF NOT pg_has_role(createur, NEW.producteur, 'USAGE') AND NOT b_superuser + THEN + EXECUTE format('GRANT %I TO %I', NEW.producteur, createur) ; + RAISE NOTICE USING MESSAGE = format('... Permission accordée à %s sur le rôle %s.', createur, NEW.producteur) ; + END IF ; + IF TG_OP = 'UPDATE' + THEN + IF NOT pg_has_role(createur, a_producteur, 'USAGE') AND NOT b_superuser + THEN + EXECUTE format('GRANT %I TO %I', a_producteur, createur) ; + RAISE NOTICE USING MESSAGE = format('... Permission accordée à %s sur le rôle %s.', createur, a_producteur) ; + END IF ; + END IF ; + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + -- permission de g_admin sur le producteur, s'il y a encore lieu + -- à noter que, dans le cas où le producteur n'a pas été modifié, g_admin + -- devrait déjà avoir une permission sur NEW.producteur, sauf à ce qu'elle + -- lui ait été retirée manuellement entre temps. Les requêtes suivantes + -- génèreraient alors une erreur même dans le cas où la modification ne + -- porte que sur les rôles lecteur/éditeur - ce qui peut-être perçu comme + -- discutable. + IF NOT pg_has_role('g_admin', NEW.producteur, 'USAGE') AND NOT b_superuser + THEN + IF createur IS NOT NULL + THEN + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + EXECUTE format('SET ROLE %I', utilisateur) ; + ELSE + SELECT grantee INTO administrateur + FROM information_schema.applicable_roles + WHERE is_grantable = 'YES' AND role_name = NEW.producteur ; + IF FOUND + THEN + EXECUTE format('SET ROLE %I', administrateur) ; + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + EXECUTE format('SET ROLE %I', utilisateur) ; + ELSE + RAISE EXCEPTION 'TA6. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur + USING DETAIL = format('GRANT %I TO g_admin', NEW.producteur), + HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + NEW.producteur) ; + END IF ; + END IF ; + END IF ; + END IF ; + + ------ PREPARATION DE L'EDITEUR ------ + -- limitée ici à la création du rôle et l'implémentation + -- de son OID. On ne s'intéresse donc pas aux cas : + -- - où il y a pas d'éditeur ; + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- ou dont le libellé a seulement été nettoyé par le + -- trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR NEW.editeur IS NULL + OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.editeur = OLD.editeur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.editeur IN (SELECT rolname FROM pg_catalog.pg_roles) + AND NOT NEW.editeur = 'public' + -- si l'éditeur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA7. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.editeur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; + END IF ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.editeur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.editeur ; + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + -- mise à jour du champ d'OID de l'éditeur + IF NEW.editeur = 'public' + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_editeur = 0, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_editeur IS NULL + OR NOT oid_editeur = 0 + ) ; + ELSE + UPDATE z_asgard.gestion_schema_etr + SET oid_editeur = quote_ident(NEW.editeur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_editeur IS NULL + OR NOT oid_editeur = quote_ident(NEW.editeur)::regrole::oid + ) ; + END IF ; + END IF ; + + ------ PREPARATION DU LECTEUR ------ + -- limitée ici à la création du rôle et l'implémentation + -- de son OID. On ne s'intéresse donc pas aux cas : + -- - où il y a pas de lecteur ; + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- ou dont le libellé a seulement été nettoyé par le + -- trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR NEW.lecteur IS NULL + OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.lecteur = OLD.lecteur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + IF NOT NEW.lecteur IN (SELECT rolname FROM pg_catalog.pg_roles) + AND NOT NEW.lecteur = 'public' + -- si le lecteur désigné n'existe pas, on le crée + -- ou renvoie une erreur si les privilèges de l'utilisateur + -- sont insuffisants + THEN + IF createur IS NULL + THEN + RAISE EXCEPTION 'TA8. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.lecteur + USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; + END IF ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.lecteur) ; + RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.lecteur ; + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + -- mise à jour du champ d'OID du lecteur + IF NEW.lecteur = 'public' + THEN + UPDATE z_asgard.gestion_schema_etr + SET oid_lecteur = 0, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_lecteur IS NULL + OR NOT oid_lecteur = 0 + ) ; + ELSE + UPDATE z_asgard.gestion_schema_etr + SET oid_lecteur = quote_ident(NEW.lecteur)::regrole::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_lecteur IS NULL + OR NOT oid_lecteur = quote_ident(NEW.lecteur)::regrole::oid + ) ; + END IF ; + END IF ; + + ------ CREATION DU SCHEMA ------ + -- on exclut au préalable les cas qui ne + -- correspondent pas à des créations, ainsi que les + -- remontées de l'event trigger sur CREATE SCHEMA, + -- car le schéma existe alors déjà + b_test := False ; + IF NOT NEW.creation OR NEW.ctrl[1] = 'CREATE' + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- le schéma est créé s'il n'existe pas déjà (cas d'ajout + -- d'un schéma pré-existant qui n'était pas référencé dans + -- gestion_schema jusque-là), sinon on alerte juste + -- l'utilisateur + IF NOT NEW.nom_schema IN (SELECT nspname FROM pg_catalog.pg_namespace) + THEN + IF NOT has_database_privilege(current_database(), 'CREATE') + OR NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + -- si le rôle courant n'a pas les privilèges nécessaires pour + -- créer le schéma, on tente avec le rôle createur [de rôles] + -- pré-identifié, dont on sait au moins qu'il aura les + -- permissions nécessaires sur le rôle producteur - mais pas + -- s'il est habilité à créer des schémas + IF createur IS NOT NULL + THEN + EXECUTE format('SET ROLE %I', createur) ; + END IF ; + IF NOT has_database_privilege(current_database(), 'CREATE') + OR NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + RAISE EXCEPTION 'TA9. Opération interdite. Vous n''êtes pas habilité à créer le schéma %.', NEW.nom_schema + USING HINT = 'Être membre d''un rôle disposant du privilège CREATE sur la base de données est nécessaire pour créer des schémas.' ; + END IF ; + END IF ; + EXECUTE format('CREATE SCHEMA %I AUTHORIZATION %I', NEW.nom_schema, NEW.producteur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; + RAISE NOTICE '... Le schéma % a été créé.', NEW.nom_schema ; + ELSE + RAISE NOTICE '(schéma % pré-existant)', NEW.nom_schema ; + END IF ; + -- récupération de l'OID du schéma + UPDATE z_asgard.gestion_schema_etr + SET oid_schema = quote_ident(NEW.nom_schema)::regnamespace::oid, + ctrl = ARRAY['SELF', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema AND ( + oid_schema IS NULL + OR NOT oid_schema = quote_ident(NEW.nom_schema)::regnamespace::oid + ) ; + END IF ; + + ------ APPLICATION DES DROITS DU PRODUCTEUR ------ + -- comme précédemment pour la préparation du producteur, + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister + -- (creation vaut False) ; + -- - d'un schéma pré-existant dont le producteur ne change pas + -- ou dont le libellé a juste été nettoyé par le trigger + -- BEFORE ; + -- - d'une remontée de l'event trigger asgard_on_create_schema, + -- car le producteur sera déjà propriétaire du schéma + -- et de son éventuel contenu. Par contre on garde les INSERT, + -- pour les cas de référencements ; + -- - de z_asgard_admin (pour permettre sa saisie initiale + -- dans la table de gestion, étant entendu qu'il est + -- impossible au trigger sur gestion_schema de lancer + -- un ALTER TABLE OWNER TO sur cette même table). + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation + OR 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL)) + OR NEW.ctrl[1] = 'CREATE' + OR NEW.nom_schema = 'z_asgard_admin' + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation AND NEW.producteur = OLD.producteur + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE format('SET ROLE %I', createur) ; + ELSIF TG_OP = 'UPDATE' + THEN + IF NOT pg_has_role(a_producteur, 'USAGE') + THEN + EXECUTE format('SET ROLE %I', createur) ; + END IF ; + END IF ; + + -- changements de propriétaires + IF (NEW.nom_schema, NEW.producteur) + IN (SELECT schema_name, schema_owner FROM information_schema.schemata) + THEN + -- si producteur est déjà propriétaire du schéma (cas d'une remontée de l'event trigger, + -- principalement), on ne change que les propriétaires des objets éventuels + IF quote_ident(NEW.nom_schema)::regnamespace::oid + IN (SELECT refobjid FROM pg_catalog.pg_depend WHERE deptype = 'n') + THEN + -- la commande n'est cependant lancée que s'il existe des dépendances de type + -- DEPENDENCY_NORMAL sur le schéma, ce qui est une condition nécessaire à + -- l'existence d'objets dans le schéma + RAISE NOTICE 'attribution de la propriété des objets au rôle producteur du schéma % :', NEW.nom_schema ; + SELECT z_asgard.asgard_admin_proprietaire(NEW.nom_schema, NEW.producteur, False) + INTO n ; + IF n = 0 + THEN + RAISE NOTICE '> néant' ; + END IF ; + END IF ; + ELSE + -- sinon schéma + objets + RAISE NOTICE 'attribution de la propriété du schéma et des objets au rôle producteur du schéma % :', NEW.nom_schema ; + PERFORM z_asgard.asgard_admin_proprietaire(NEW.nom_schema, NEW.producteur) ; + END IF ; + + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + ------ APPLICATION DES DROITS DE L'EDITEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont l'éditeur ne change pas + -- (y compris pour rester vide) ou dont le libellé + -- a seulement été nettoyé par le trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + IF NOT NEW.creation OR 'CLEAN editeur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND coalesce(NEW.editeur, '') = coalesce(OLD.editeur, '') + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE format('SET ROLE %I', createur) ; + END IF ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.editeur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + IF OLD.editeur = 'public' + THEN + a_editeur := 'public' ; + -- récupération des modifications manuelles des + -- droits de OLD.editeur/public, grâce à la fonction + -- asgard_synthese_public + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_public( + quote_ident(NEW.nom_schema)::regnamespace + ) ; + ELSE + SELECT rolname INTO a_editeur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_editeur ; + IF FOUND + THEN + -- récupération des modifications manuelles des + -- droits de OLD.editeur, grâce à la fonction + -- asgard_synthese_role + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_role( + quote_ident(NEW.nom_schema)::regnamespace, + quote_ident(a_editeur)::regrole + ) ; + END IF ; + END IF ; + END IF ; + + IF l_commande IS NOT NULL + -- transfert sur NEW.editeur des droits de + -- OLD.editeur, le cas échéant + THEN + IF NEW.editeur IS NOT NULL + THEN + RAISE NOTICE 'suppression et transfert vers le nouvel éditeur des privilèges de l''ancien éditeur du schéma % :', NEW.nom_schema ; + ELSE + RAISE NOTICE 'suppression des privilèges de l''ancien éditeur du schéma % :', NEW.nom_schema ; + END IF ; + FOREACH c IN ARRAY l_commande + LOOP + IF NEW.editeur IS NOT NULL + THEN + EXECUTE format(c, NEW.editeur) ; + RAISE NOTICE '> %', format(c, NEW.editeur) ; + END IF ; + IF c ~ '^GRANT' + THEN + SELECT z_asgard.asgard_grant_to_revoke(c) INTO c_reverse ; + EXECUTE format(c_reverse, a_editeur) ; + RAISE NOTICE '> %', format(c_reverse, a_editeur) ; + END IF ; + END LOOP ; + + -- sinon, application des privilèges standards de l'éditeur + ELSIF NEW.editeur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma % :', NEW.nom_schema ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + + EXECUTE format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + + EXECUTE format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + + END IF ; + + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + ------ APPLICATION DES DROITS DU LECTEUR ------ + -- on ne s'intéresse pas aux cas : + -- - d'un schéma qui n'a pas/plus vocation à exister ; + -- - d'un schéma pré-existant dont le lecteur ne change pas + -- (y compris pour rester vide) ou dont le libellé + -- a seulement été nettoyé par le trigger BEFORE. + -- ils sont donc exclus au préalable + b_test := False ; + l_commande := NULL ; + IF NOT NEW.creation OR 'CLEAN lecteur' = ANY(array_remove(NEW.ctrl, NULL)) + THEN + b_test := True ; + ELSIF TG_OP = 'UPDATE' + THEN + IF OLD.creation + AND coalesce(NEW.lecteur, '') = coalesce(OLD.lecteur, '') + THEN + b_test := True ; + END IF ; + END IF ; + + IF NOT b_test + THEN + -- si besoin, on bascule sur le rôle createur. À ce stade, + -- il est garanti que soit l'utilisateur courant soit + -- createur (pour le cas d'un utilisateur courant + -- NOINHERIT) aura les privilèges nécessaires + IF NOT pg_has_role(NEW.producteur, 'USAGE') + THEN + EXECUTE format('SET ROLE %I', createur) ; + END IF ; + + IF TG_OP = 'UPDATE' + THEN + -- la validité de OLD.lecteur n'ayant + -- pas été contrôlée par le trigger BEFORE, + -- on le fait maintenant + IF OLD.lecteur = 'public' + THEN + a_lecteur := 'public' ; + -- récupération des modifications manuelles des + -- droits de OLD.lecteur/public, grâce à la fonction + -- asgard_synthese_public + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_public( + quote_ident(NEW.nom_schema)::regnamespace + ) ; + ELSE + SELECT rolname INTO a_lecteur + FROM pg_catalog.pg_roles + WHERE pg_roles.oid = OLD.oid_lecteur ; + IF FOUND + THEN + -- récupération des modifications manuelles des + -- droits de OLD.lecteur, grâce à la fonction + -- asgard_synthese_role + SELECT array_agg(commande) INTO l_commande + FROM z_asgard.asgard_synthese_role( + quote_ident(NEW.nom_schema)::regnamespace, + quote_ident(a_lecteur)::regrole + ) ; + END IF ; + END IF ; + END IF ; + + IF l_commande IS NOT NULL + -- transfert sur NEW.lecteur des droits de + -- OLD.lecteur, le cas échéant + THEN + IF NEW.lecteur IS NOT NULL + THEN + RAISE NOTICE 'suppression et transfert vers le nouveau lecteur des privilèges de l''ancien lecteur du schéma % :', NEW.nom_schema ; + ELSE + RAISE NOTICE 'suppression des privilèges de l''ancien lecteur du schéma % :', NEW.nom_schema ; + END IF ; + FOREACH c IN ARRAY l_commande + LOOP + IF NEW.lecteur IS NOT NULL + THEN + EXECUTE format(c, NEW.lecteur) ; + RAISE NOTICE '> %', format(c, NEW.lecteur) ; + END IF ; + IF c ~ '^GRANT' + THEN + SELECT z_asgard.asgard_grant_to_revoke(c) INTO c_reverse ; + EXECUTE format(c_reverse, a_lecteur) ; + RAISE NOTICE '> %', format(c_reverse, a_lecteur) ; + END IF ; + END LOOP ; + + -- sinon, application des privilèges standards du lecteur + ELSIF NEW.lecteur IS NOT NULL + THEN + RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma % :', NEW.nom_schema ; + + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + + EXECUTE format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + + END IF ; + + EXECUTE format('SET ROLE %I', utilisateur) ; + END IF ; + + RETURN NULL ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE EXCEPTION 'TA0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_after sur z_asgard_admin.gestion_schema, qui répercute physiquement les modifications de la table de gestion.' ; + + +-- Trigger: asgard_on_modify_gestion_schema_after + +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui répercute physiquement les modifications de la table de gestion.' ; + + -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +----------------------------------------------------------- +------ 6 - GESTION DES PERMISSIONS SUR LAYER_STYLES ------ +----------------------------------------------------------- +/* 6.1 - PETITES FONCTIONS UTILITAIRES + 6.2 - FONCTION D'ADMINISTRATION DES PERMISSIONS SUR LAYER_STYLES */ + +------ 6.1 - PETITES FONCTIONS UTILITAIRES ------ + +-- Function: z_asgard.asgard_has_role_usage(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( + role_parent text, + role_enfant text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. + + Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') + en plus permissif - elle renvoie False quand l'un des rôles + n'existe pas plutôt que d'échouer. + + Parameters + ---------- + role_parent : text + Nom du rôle dont on souhaite savoir si l'autre est membre. + role_enfant : text, optional + Nom du rôle dont on souhaite savoir s'il est membre de l'autre. + Si non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si la relation entre les rôles est vérifiée. False + si elle ne l'est pas ou si l'un des rôles n'existe pas. + +*/ +BEGIN + + RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; + +EXCEPTION WHEN undefined_object +THEN + RETURN False ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; + + +-- Function: z_asgard.asgard_is_relation_owner(text, text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_relation_owner( + nom_schema text, + nom_relation text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si un rôle est membre du propriétaire d'une table, vue ou autre relation. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant au nom du schéma dont + dépend la relation. + nom_relation : text + Chaîne de caractères correspondant au nom de la relation. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si non + renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du propriétaire de la relation. + False sinon, incluant les cas où le rôle ou la relation n'existe + pas. + +*/ +DECLARE + owner text ; +BEGIN + + SELECT pg_roles.rolname INTO owner + FROM pg_catalog.pg_class + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = relowner + WHERE quote_ident(nom_schema) = relnamespace::regnamespace::text + AND nom_relation = relname ; + + IF NOT FOUND + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(owner, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) IS 'ASGARD. Détermine si un rôle est membre du propriétaire d''une table, vue ou autre relation.' ; + + +-- Function: z_asgard.asgard_is_producteur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_producteur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si le rôle considéré est membre du rôle producteur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle producteur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son producteur. + +*/ +DECLARE + producteur text ; +BEGIN + + SELECT gestion_schema_read_only.producteur INTO producteur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF producteur IS NULL + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(producteur, nom_role) ; + +END +$_$ ; + +ALTER FUNCTION z_asgard.asgard_is_producteur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_producteur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle producteur d''un schéma donné.' ; + + +-- Function: z_asgard.asgard_is_editeur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_editeur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si le rôle considéré est membre du rôle éditeur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle éditeur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son éditeur. + +*/ +DECLARE + editeur text ; +BEGIN + + SELECT gestion_schema_read_only.editeur INTO editeur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF editeur is NULL + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(editeur, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_editeur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_editeur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle éditeur d''un schéma donné.' ; + + +-- Function: z_asgard.asgard_is_lecteur(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_is_lecteur( + schema_cible text, + nom_role text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si le rôle considéré est membre du rôle lecteur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle lecteur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son lecteur. + +*/ +DECLARE + lecteur text ; +BEGIN + + SELECT gestion_schema_read_only.lecteur INTO lecteur + FROM z_asgard.gestion_schema_read_only + WHERE gestion_schema_read_only.nom_schema = schema_cible ; + + IF lecteur IS NULL + THEN + RETURN False ; + END IF ; + + RETURN z_asgard.asgard_has_role_usage(lecteur, nom_role) ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_is_lecteur(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_is_lecteur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle lecteur d''un schéma donné.' ; + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index 7474107..40d5c16 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -261,7 +261,7 @@ CREATE SCHEMA z_asgard COMMENT ON SCHEMA z_asgard IS 'ASGARD. Utilitaires pour la gestion des droits.' ; -GRANT USAGE ON SCHEMA z_asgard TO g_consult ; +GRANT USAGE ON SCHEMA z_asgard TO public ; ------ 2.2 - TABLE GESTION_SCHEMA ------ @@ -371,7 +371,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( ALTER VIEW z_asgard.gestion_schema_usr OWNER TO g_admin_ext; -GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult ; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public ; COMMENT ON VIEW z_asgard.gestion_schema_usr IS 'ASGARD. Vue pour la gestion courante des schémas - création et administration des droits.' ; @@ -425,7 +425,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( ALTER VIEW z_asgard.gestion_schema_etr OWNER TO g_admin_ext; -GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult ; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public ; COMMENT ON VIEW z_asgard.gestion_schema_etr IS 'ASGARD. Vue technique pour l''alimentation de la table z_asgard_admin.gestion_schema par les déclencheurs.' ; @@ -478,7 +478,7 @@ CREATE OR REPLACE VIEW z_asgard.asgardmenu_metadata AS ( ALTER VIEW z_asgard.asgardmenu_metadata OWNER TO g_admin_ext ; -GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult ; +GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public ; COMMENT ON VIEW z_asgard.asgardmenu_metadata IS 'ASGARD. Données utiles à l''extension QGIS AsgardMenu.' ; COMMENT ON COLUMN z_asgard.asgardmenu_metadata.id IS 'Identifiant entier unique.' ; @@ -516,7 +516,7 @@ CREATE OR REPLACE VIEW z_asgard.asgardmanager_metadata AS ( ALTER VIEW z_asgard.asgardmanager_metadata OWNER TO g_admin_ext ; -GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult ; +GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public ; COMMENT ON VIEW z_asgard.asgardmanager_metadata IS 'ASGARD. Données utiles à l''extension QGIS AsgardManager.' ; COMMENT ON COLUMN z_asgard.asgardmanager_metadata.id IS 'Identifiant entier unique.' ; @@ -550,7 +550,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_read_only AS ( ALTER VIEW z_asgard.gestion_schema_read_only OWNER TO g_admin_ext; -GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult ; +GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public ; COMMENT ON VIEW z_asgard.gestion_schema_read_only IS 'ASGARD. Vue de consultation des droits définis sur les schémas. Accessible à tous en lecture seule.' ; @@ -593,18 +593,21 @@ COMMENT ON COLUMN z_asgard.gestion_schema_read_only.lecteur IS 'Rôle désigné -- Function: z_asgard_admin.asgard_on_alter_schema() -CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_schema() RETURNS event_trigger +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_schema() + RETURNS event_trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par l'event trigger asgard_on_alter_schema qui - répercute dans la table z_asgard_admin.gestion_schema (via la vue - z_asgard.gestion_schema_etr) les modifications de noms - et propriétaires des schémas réalisées par des commandes - ALTER SCHEMA directes. -DECLENCHEMENT : ON DDL COMMAND END. -CONDITION : WHEN TAG IN ('ALTER SCHEMA') */ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_alter_schema, +qui répercute sur la table de gestion d'Asgard les changements de noms et +propriétaires réalisés par des commandes ALTER SCHEMA directes. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ DECLARE obj record ; + objname text ; e_mssg text ; e_hint text ; e_detl text ; @@ -612,50 +615,48 @@ BEGIN ------ CONTROLES DES PRIVILEGES ------ IF NOT has_schema_privilege('z_asgard', 'USAGE') THEN - RAISE EXCEPTION 'EAS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EAS1. Schéma z_asgard inaccessible.' ; END IF ; IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') - OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') THEN - RAISE EXCEPTION 'EAS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EAS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; END IF ; FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() - WHERE object_type = 'schema' + WHERE object_type = 'schema' LOOP ------ RENAME ------ UPDATE z_asgard.gestion_schema_etr - SET nom_schema = replace(obj.object_identity, '"', ''), + SET nom_schema = nspname, ctrl = ARRAY['RENAME', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace WHERE oid_schema = obj.objid - AND NOT quote_ident(nom_schema) = obj.object_identity ; + AND obj.objid = pg_namespace.oid + AND NOT nom_schema = nspname + RETURNING nspname INTO objname ; IF FOUND THEN - RAISE NOTICE '... Le nom du schéma % a été mis à jour dans la table de gestion.', replace(obj.object_identity, '"', '') ; + RAISE NOTICE '... Le nom du schéma % a été mis à jour dans la table de gestion.', objname ; END IF ; ------ OWNER TO ------ UPDATE z_asgard.gestion_schema_etr - SET (producteur, oid_producteur, ctrl) = ( - SELECT - replace(nspowner::regrole::text, '"', ''), - nspowner, - ARRAY['OWNER', 'x7-A;#rzo'] - FROM pg_catalog.pg_namespace - WHERE obj.objid = pg_namespace.oid - ) - WHERE oid_schema = obj.objid - AND NOT oid_producteur = ( - SELECT nspowner - FROM pg_catalog.pg_namespace - WHERE obj.objid = pg_namespace.oid - ) ; + SET producteur = rolname, + oid_producteur = nspowner, + ctrl = ARRAY['OWNER', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE oid_schema = obj.objid + AND obj.objid = pg_namespace.oid + AND NOT oid_producteur = nspowner + RETURNING nspname INTO objname ; IF FOUND THEN - RAISE NOTICE '... Le producteur du schéma % a été mis à jour dans la table de gestion.', replace(obj.object_identity, '"', '') ; + RAISE NOTICE '... Le producteur du schéma % a été mis à jour dans la table de gestion.', objname ; END IF ; END LOOP ; @@ -667,14 +668,14 @@ EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'EAS0 > %', e_mssg USING DETAIL = e_detl, HINT = e_hint ; - + END $BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_alter_schema() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_alter_schema, qui répercute sur la table de gestion d''Asgard les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; -- Event Trigger: asgard_on_alter_schema @@ -683,7 +684,7 @@ CREATE EVENT TRIGGER asgard_on_alter_schema ON DDL_COMMAND_END WHEN TAG IN ('ALTER SCHEMA') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_schema() ; -COMMENT ON EVENT TRIGGER asgard_on_alter_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; +COMMENT ON EVENT TRIGGER asgard_on_alter_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les changements de noms et propriétaires réalisés par des commandes ALTER SCHEMA directes.' ; @@ -694,14 +695,17 @@ COMMENT ON EVENT TRIGGER asgard_on_alter_schema IS 'ASGARD. Event trigger qui r CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_schema() RETURNS event_trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par l'event trigger asgard_on_create_schema qui - répercute dans la table z_asgard_admin.gestion_schema (via la vue - z_asgard.gestion_schema_etr) les créations de schémas - réalisées par des commandes CREATE SCHEMA directes. -DECLENCHEMENT : ON DDL COMMAND END. -CONDITION : WHEN TAG IN ('CREATE SCHEMA') */ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_create_schema, +qui répercute sur la table de gestion d'Asgard les créations de schémas réalisées +par des commandes CREATE SCHEMA directes. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ DECLARE obj record ; + objname text ; e_mssg text ; e_hint text ; e_detl text ; @@ -709,39 +713,39 @@ BEGIN ------ CONTROLES DES PRIVILEGES ------ IF NOT has_schema_privilege('z_asgard', 'USAGE') THEN - RAISE EXCEPTION 'ECS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'ECS1. Schéma z_asgard inaccessible.' ; END IF ; IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') - OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'INSERT') - OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'INSERT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') THEN - RAISE EXCEPTION 'ECS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'ECS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; END IF ; FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() - WHERE object_type = 'schema' + WHERE object_type = 'schema' LOOP ------ SCHEMA PRE-ENREGISTRE DANS GESTION_SCHEMA ------ UPDATE z_asgard.gestion_schema_etr - SET (oid_schema, producteur, oid_producteur, creation, ctrl) = ( - SELECT - obj.objid, - replace(nspowner::regrole::text, '"', ''), - nspowner, - true, - ARRAY['CREATE', 'x7-A;#rzo'] - FROM pg_catalog.pg_namespace - WHERE obj.objid = pg_namespace.oid - ) + SET oid_schema = obj.objid, + producteur = rolname, + oid_producteur = nspowner, + creation = True, + ctrl = ARRAY['CREATE', 'x7-A;#rzo'] + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner WHERE quote_ident(nom_schema) = obj.object_identity - AND NOT creation ; -- creation vaut true si et seulement si la création a été initiée via la table - -- de gestion dans ce cas, il n'est pas nécessaire de réintervenir dessus + AND obj.objid = pg_namespace.oid + AND NOT creation + RETURNING nspname INTO objname ; + -- creation vaut true si et seulement si la création a été initiée via la table + -- de gestion dans ce cas, il n'est pas nécessaire de réintervenir dessus IF FOUND THEN - RAISE NOTICE '... Le schéma % apparaît désormais comme "créé" dans la table de gestion.', replace(obj.object_identity, '"', '') ; + RAISE NOTICE '... Le schéma % apparaît désormais comme "créé" dans la table de gestion.', objname ; ------ SCHEMA NON REPERTORIE DANS GESTION_SCHEMA ------ ELSIF NOT obj.object_identity IN (SELECT quote_ident(nom_schema) FROM z_asgard.gestion_schema_etr) @@ -749,15 +753,17 @@ BEGIN INSERT INTO z_asgard.gestion_schema_etr (oid_schema, nom_schema, producteur, oid_producteur, creation, ctrl)( SELECT obj.objid, - replace(obj.object_identity, '"', ''), - replace(nspowner::regrole::text, '"', ''), + nspname, + rolname, nspowner, - true, + True, ARRAY['CREATE', 'x7-A;#rzo'] FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner WHERE obj.objid = pg_namespace.oid - ) ; - RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', replace(obj.object_identity, '"', '') ; + ) + RETURNING nom_schema INTO objname ; + RAISE NOTICE '... Le schéma % a été enregistré dans la table de gestion.', objname ; END IF ; END LOOP ; @@ -776,7 +782,7 @@ $BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_create_schema() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_create_schema, qui répercute sur la table de gestion d''Asgard les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; -- Event Trigger: asgard_on_create_schema @@ -785,9 +791,7 @@ CREATE EVENT TRIGGER asgard_on_create_schema ON DDL_COMMAND_END WHEN TAG IN ('CREATE SCHEMA') EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_schema() ; -COMMENT ON EVENT TRIGGER asgard_on_create_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; - - +COMMENT ON EVENT TRIGGER asgard_on_create_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les créations de schémas réalisées par des commandes CREATE SCHEMA directes.' ; ------ 3.3 - EVENT TRIGGER SUR DROP SCHEMA ------ @@ -797,15 +801,18 @@ COMMENT ON EVENT TRIGGER asgard_on_create_schema IS 'ASGARD. Event trigger qui r CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_drop_schema() RETURNS event_trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par l'event trigger asgard_on_drop_schema qui - répercute dans la table z_asgard_admin.gestion_schema (via la vue - z_asgard.gestion_schema_etr) les suppressions de schémas - réalisées par des commandes DROP SCHEMA directes ou exécutées - dans le cadre de la désinstallation d'une extension. -DECLENCHEMENT : ON SQL DROP. -CONDITION : WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION') */ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_drop_schema, +qui répercute sur la table de gestion d'Asgard les suppressions de schémas +réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la +suppression d'une extension. + + Elle n'écrit pas directement dans la table z_asgard_admin.gestion_schema, + mais dans la vue modifiable z_asgard.gestion_schema_etr. + +*/ DECLARE obj record ; + objname text ; e_mssg text ; e_hint text ; e_detl text ; @@ -813,25 +820,25 @@ BEGIN ------ CONTROLES DES PRIVILEGES ------ IF NOT has_schema_privilege('z_asgard', 'USAGE') THEN - RAISE EXCEPTION 'EDS1. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EDS1. Schéma z_asgard inaccessible.' ; END IF ; IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'UPDATE') - OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') + OR NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') THEN - RAISE EXCEPTION 'EDS2. Vous devez être membre du groupe éditeur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EDS2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; END IF ; - FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() WHERE object_type = 'schema' LOOP ------ ENREGISTREMENT DE LA SUPPRESSION ------ UPDATE z_asgard.gestion_schema_etr SET (creation, oid_schema, ctrl) = (False, NULL, ARRAY['DROP', 'x7-A;#rzo']) - WHERE quote_ident(nom_schema) = obj.object_identity ; + WHERE quote_ident(nom_schema) = obj.object_identity + RETURNING nom_schema INTO objname ; IF FOUND THEN - RAISE NOTICE '... La suppression du schéma % a été enregistrée dans la table de gestion (creation = False).', replace(obj.object_identity, '"', ''); + RAISE NOTICE '... La suppression du schéma % a été enregistrée dans la table de gestion (creation = False).', objname ; END IF ; END LOOP ; @@ -845,21 +852,21 @@ EXCEPTION WHEN OTHERS THEN HINT = e_hint ; END -$BODY$; +$BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_drop_schema() OWNER TO g_admin; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_drop_schema() IS 'ASGARD. Fonction appelée par l''event trigger qui répercute sur la table de gestion les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou exécutées dans le cadre de la désinstallation d''une extension.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_drop_schema() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_drop_schema, qui répercute sur la table de gestion d''Asgard les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la suppression d''une extension.' ; -- Event Trigger: asgard_on_drop_schema CREATE EVENT TRIGGER asgard_on_drop_schema ON SQL_DROP - WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION') + WHEN TAG IN ('DROP SCHEMA', 'DROP EXTENSION', 'DROP OWNED') EXECUTE PROCEDURE z_asgard_admin.asgard_on_drop_schema() ; -COMMENT ON EVENT TRIGGER asgard_on_drop_schema IS 'ASGARD. Event trigger qui répercute sur la table de gestion les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou exécutées dans le cadre de la désinstallation d''une extension.' ; +COMMENT ON EVENT TRIGGER asgard_on_drop_schema IS 'ASGARD. Déclencheur sur évènement qui répercute sur la table de gestion d''Asgard les suppressions de schémas réalisées par des commandes DROP SCHEMA directes ou dans le cadre de la suppression d''une extension.' ; @@ -867,22 +874,20 @@ COMMENT ON EVENT TRIGGER asgard_on_drop_schema IS 'ASGARD. Event trigger qui ré -- Function: z_asgard_admin.asgard_on_create_objet() -CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_objet() RETURNS event_trigger +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_create_objet() + RETURNS event_trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par l'event trigger asgard_on_create_objet qui - veille à attribuer aux nouveaux objets créés les droits prévus - pour le schéma dans la table de gestion. -AVERTISSEMENT : Les commandes CREATE OPERATOR CLASS, CREATE OPERATOR FAMILY -et CREATE STATISTICS ne sont pas prises en charge pour l'heure. -DECLENCHEMENT : ON DDL COMMAND END. -CONDITION : WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', -'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', -'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', -'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', -'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE'). -À partir de PostgreSQL 11, 'CREATE PROCEDURE' déclenche également l'exécution -de la présente fonction. */ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_create_objet, +qui applique aux nouveaux objets créés les droits pré-définis pour le schéma +dans la table de gestion d'Asgard. + + Elle est activée par toutes les commandes CREATE portant sur des objets qui + dépendent d'un schéma et ont un propriétaire. + + Elle ignore les objets dont le schéma n'est pas référencé par Asgard. + +*/ DECLARE obj record ; roles record ; @@ -897,16 +902,17 @@ BEGIN ------ CONTROLES DES PRIVILEGES ------ IF NOT has_schema_privilege('z_asgard', 'USAGE') THEN - RAISE EXCEPTION 'ECO1. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'ECO1. Schéma z_asgard inaccessible.' ; END IF ; IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') THEN - RAISE EXCEPTION 'ECO2. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'ECO2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; END IF ; - FOR obj IN SELECT DISTINCT classid, objid, object_type, schema_name, object_identity + FOR obj IN SELECT DISTINCT + classid, objid, object_type, schema_name, object_identity FROM pg_event_trigger_ddl_commands() WHERE schema_name IS NOT NULL ORDER BY object_type DESC @@ -939,24 +945,52 @@ BEGIN -- comme les "table constraint" IF FOUND - THEN - + THEN -- récupération du propriétaire courant de l'objet -- génère une erreur si la requête ne renvoie rien - EXECUTE format('SELECT %s::regrole::text FROM %s WHERE oid = %s', - xowner, obj.classid::regclass, obj.objid) + EXECUTE format('SELECT rolname + FROM %2$s LEFT JOIN pg_catalog.pg_roles + ON %1$s = pg_roles.oid + WHERE %2$s.oid = %3$s', + xowner, obj.classid::regclass, obj.objid) INTO STRICT proprietaire ; -- si le propriétaire courant n'est pas le producteur - IF NOT roles.producteur::text = proprietaire + IF NOT roles.producteur = proprietaire THEN ------ PROPRIETAIRE DE L'OBJET (DROITS DU PRODUCTEUR) ------ - RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', obj.object_identity ; - l := format('ALTER %s %s OWNER TO %I', obj.object_type, - obj.object_identity, roles.producteur) ; + RAISE NOTICE 'réattribution de la propriété de % au rôle producteur du schéma :', + obj.object_identity ; + l := format('ALTER %s %s OWNER TO %I', + CASE WHEN obj.object_type = 'statistics object' + THEN 'statistics' ELSE obj.object_type END, + obj.object_identity, roles.producteur) ; EXECUTE l ; RAISE NOTICE '> %', l ; + + ------ PROPRIETAIRE DE LA FAMILLE D'OPERATEURS IMPLICITE ------ + -- Lorsque le paramètre FAMILY n'est pas spécifié à la + -- création d'une classe d'opérateurs, une famille de + -- même nom que la classe d'opérateurs est créée... en + -- passant entre les mailles du filets du déclencheur. + IF obj.object_type = 'operator class' AND EXISTS ( + SELECT * FROM pg_catalog.pg_opclass + LEFT JOIN pg_catalog.pg_opfamily + ON pg_opfamily.oid = opcfamily + WHERE obj.objid = pg_opclass.oid + AND opfname = opcname + AND opfmethod = opcmethod + AND opfnamespace = opcnamespace + AND NOT opfowner = quote_ident(roles.producteur)::regrole + ) + THEN + l := format('ALTER operator family %s OWNER TO %I', + obj.object_identity, roles.producteur) ; + EXECUTE l ; + RAISE NOTICE '> %', l ; + END IF ; + END IF ; ------ DROITS DE L'EDITEUR ------ @@ -1064,19 +1098,17 @@ BEGIN src.oid_lecteur::regrole, roles.producteur) END ; ELSE - RAISE WARNING'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', + RAISE WARNING 'Le producteur du schéma de la vue % ne dispose pas des droits nécessaires pour accéder à ses données sources.', format('%s %s', CASE WHEN obj.object_type = 'materialized view' THEN 'matérialisée ' ELSE '' END, obj.object_identity) USING DETAIL = format('%s source %s.%I, propriétaire %s.', src.liblg, src.relnamespace::regnamespace, src.relname, src.relowner::regrole) ; END IF ; END LOOP ; - END IF ; - + END IF ; END IF ; - END IF; - - END LOOP; + END IF ; + END LOOP ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, @@ -1087,40 +1119,55 @@ EXCEPTION WHEN OTHERS THEN HINT = e_hint ; END -$BODY$; +$BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_create_objet() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_objet() IS 'ASGARD. Fonction appelée par l''event trigger qui applique les droits pré-définis sur les nouveaux objets.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_create_objet() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_create_objet, qui applique aux nouveaux objets créés les droits pré-définis pour le schéma dans la table de gestion d''Asgard.' ; -- Event Trigger: asgard_on_create_objet + DO $$ BEGIN - IF current_setting('server_version_num')::int < 110000 + IF current_setting('server_version_num')::int < 100000 + THEN + CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END + WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', + 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', + 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', + 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; + ELSIF current_setting('server_version_num')::int < 110000 THEN + -- + CREATE STATISTICS pour PG 10 CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', - 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE') - EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet(); + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY', 'CREATE STATISTICS') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; ELSE + -- + CREATE PROCEDURE pour PG 11+ CREATE EVENT TRIGGER asgard_on_create_objet ON DDL_COMMAND_END WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'CREATE VIEW', 'CREATE MATERIALIZED VIEW', 'SELECT INTO', 'CREATE SEQUENCE', 'CREATE FOREIGN TABLE', 'CREATE FUNCTION', 'CREATE OPERATOR', 'CREATE AGGREGATE', 'CREATE COLLATION', 'CREATE CONVERSION', 'CREATE DOMAIN', 'CREATE TEXT SEARCH CONFIGURATION', - 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE PROCEDURE') - EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet(); + 'CREATE TEXT SEARCH DICTIONARY', 'CREATE TYPE', 'CREATE OPERATOR CLASS', + 'CREATE OPERATOR FAMILY', 'CREATE STATISTICS', 'CREATE PROCEDURE') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_create_objet() ; END IF ; END $$ ; -COMMENT ON EVENT TRIGGER asgard_on_create_objet IS 'ASGARD. Event trigger qui applique les droits pré-définis sur les nouveaux objets.' ; +COMMENT ON EVENT TRIGGER asgard_on_create_objet IS 'ASGARD. Déclencheur sur évènement qui applique aux nouveaux objets créés les droits pré-définis pour le schéma dans la table de gestion d''Asgard.' ; @@ -1131,25 +1178,24 @@ COMMENT ON EVENT TRIGGER asgard_on_create_objet IS 'ASGARD. Event trigger qui ap CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_alter_objet() RETURNS event_trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par l'event trigger asgard_on_alter_objet, qui - assure que le propriétaire de l'objet reste le propriétaire du - schéma qui le contient après l'exécution d'une commande ALTER. - Elle vise en particulier les SET SCHEMA (lorsque le schéma - cible a un producteur différent de celui du schéma d'origine, elle - modifie le propriétaire de l'objet en conséquence) et les - OWNER TO (elle inhibe leur effet en rendant la propriété de - l'objet au producteur du schéma). - Elle n'agit pas sur les privilèges. -AVERTISSEMENT : Les commandes ALTER OPERATOR CLASS, ALTER OPERATOR FAMILY -et ALTER STATISTICS ne sont pas pris en charge pour l'heure. -DECLENCHEMENT : ON DDL COMMAND END. -CONDITION : WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', -'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', -'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', -'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', -'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE'). -À partir de PostgreSQL 11, 'ALTER PROCEDURE' et 'ALTER ROUTINE' déclenchent -également l'exécution de la présente fonction. */ +/* Fonction exécutée par le déclencheur sur évènement asgard_on_alter_objet, +qui assure que le producteur d'un schéma reste propriétaire de tous les +objets qui en dépendent. + + Elle est activée par toutes les commandes ALTER portant sur des objets qui + dépendent d'un schéma et ont un propriétaire, mais n'aura réellement d'effet + que pour celles qui affectent la cohérence des propriétaires : + + * Les ALTER ... SET SCHEMA lorsque le schéma cible a un producteur différent + de celui du schéma d'origine. Elle modifie alors le propriétaire de l'objet + selon le producteur du nouveau schéma. + * Les ALTER ... OWNER TO, dont elle inhibe l'effet en rendant la propriété de + l'objet au producteur du schéma. + + Elle n'agit pas sur les privilèges. Elle ignore les objets dont le schéma + (après exécution de la commande) n'est pas référencé par Asgard. + +*/ DECLARE obj record ; n_producteur regrole ; @@ -1163,18 +1209,19 @@ BEGIN ------ CONTROLES DES PRIVILEGES ------ IF NOT has_schema_privilege('z_asgard', 'USAGE') THEN - RAISE EXCEPTION 'EAO1. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EAO1. Schéma z_asgard inaccessible.' ; END IF ; IF NOT has_table_privilege('z_asgard.gestion_schema_etr', 'SELECT') THEN - RAISE EXCEPTION 'EAO2. Vous devez être membre du groupe lecteur du schéma z_asgard pour réaliser cette opération.' ; + RAISE EXCEPTION 'EAO2. Permissions insuffisantes pour la vue z_asgard.gestion_schema_etr.' ; END IF ; - FOR obj IN SELECT DISTINCT classid, objid, object_type, schema_name, object_identity - FROM pg_event_trigger_ddl_commands() - WHERE schema_name IS NOT NULL - ORDER BY object_type DESC + FOR obj IN SELECT DISTINCT + classid, objid, object_type, schema_name, object_identity + FROM pg_event_trigger_ddl_commands() + WHERE schema_name IS NOT NULL + ORDER BY object_type DESC LOOP -- récupération du rôle identifié comme producteur pour le schéma de l'objet @@ -1199,8 +1246,8 @@ BEGIN THEN -- récupération du propriétaire courant de l'objet -- génère une erreur si la requête ne renvoie rien - EXECUTE format('SELECT %s::regrole::text FROM %s WHERE oid = %s', xowner, - obj.classid::regclass, obj.objid) + EXECUTE format('SELECT %s::regrole FROM %s WHERE oid = %s', + xowner, obj.classid::regclass, obj.objid) INTO STRICT a_producteur ; -- si les deux rôles sont différents @@ -1209,9 +1256,12 @@ BEGIN ------ MODIFICATION DU PROPRIETAIRE ------ -- l'objet est attribué au propriétaire désigné pour le schéma -- (n_producteur) - RAISE NOTICE 'attribution de la propriété de % au rôle producteur du schéma :', obj.object_identity ; - l := format('ALTER %s %s OWNER TO %s', obj.object_type, - obj.object_identity, n_producteur) ; + RAISE NOTICE 'attribution de la propriété de % au rôle producteur du schéma :', + obj.object_identity ; + l := format('ALTER %s %s OWNER TO %s', + CASE WHEN obj.object_type = 'statistics object' + THEN 'statistics' ELSE obj.object_type END, + obj.object_identity, n_producteur) ; EXECUTE l ; RAISE NOTICE '> %', l ; END IF ; @@ -1229,12 +1279,12 @@ EXCEPTION WHEN OTHERS THEN HINT = e_hint ; END -$BODY$; +$BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_alter_objet() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_objet() IS 'ASGARD. Fonction appelée par l''event trigger qui assure que le producteur d''un schéma reste propriétaire de tous les objets qu''il contient.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_objet() IS 'ASGARD. Fonction exécutée par le déclencheur sur évènement asgard_on_alter_objet, qui assure que le producteur d''un schéma reste propriétaire de tous les objets qui en dépendent.' ; -- Event Trigger: asgard_on_alter_objet @@ -1242,7 +1292,7 @@ COMMENT ON FUNCTION z_asgard_admin.asgard_on_alter_objet() IS 'ASGARD. Fonction DO $$ BEGIN - IF current_setting('server_version_num')::int < 110000 + IF current_setting('server_version_num')::int < 100000 THEN CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', @@ -1250,21 +1300,34 @@ BEGIN 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE') - EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet(); + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; + ELSIF current_setting('server_version_num')::int < 110000 + THEN + -- + ALTER STATISTICS, ALTER OPERATOR CLASS, ALTER OPERATOR FAMILY + CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END + WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', + 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', + 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', + 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', + 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY') + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; ELSE + -- + ALTER PROCEDURE, ALTER ROUTINE CREATE EVENT TRIGGER asgard_on_alter_objet ON DDL_COMMAND_END WHEN TAG IN ('ALTER TABLE', 'ALTER VIEW', 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER PROCEDURE', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', + 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY', 'ALTER PROCEDURE', 'ALTER ROUTINE') - EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet(); + EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; END IF ; END $$ ; -COMMENT ON EVENT TRIGGER asgard_on_alter_objet IS 'ASGARD. Event trigger qui assure que le producteur d''un schéma reste propriétaire de tous les objets qu''il contient.' ; +COMMENT ON EVENT TRIGGER asgard_on_alter_objet IS 'ASGARD. Déclencheur sur évènement qui assure que le producteur d''un schéma reste propriétaire de tous les objets qui en dépendent.' ; -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1755,28 +1818,34 @@ COMMENT ON FUNCTION z_asgard.asgard_synthese_public_obj(oid, text) IS 'ASGARD. F -- Function: z_asgard.asgard_admin_proprietaire(text, text, boolean) CREATE OR REPLACE FUNCTION z_asgard.asgard_admin_proprietaire( - n_schema text, n_owner text, b_setschema boolean DEFAULT True - ) + n_schema text, n_owner text, b_setschema boolean DEFAULT True + ) RETURNS int LANGUAGE plpgsql AS $_$ -/* OBJET : Gestion des droits. Cette fonction permet d''attribuer - un schéma et tous les objets qu'il contient à un [nouveau] - propriétaire. -AVERTISSEMENT : Les objets de type operator class, operator family -et extended planner statistic ne sont pas pris en charge pour l'heure. -ARGUMENTS : -- "n_schema" est une chaîne de caractères correspondant au nom du - schéma à considérer ; -- "n_owner" est une chaîne de caractères correspondant au nom du - rôle (rôle de groupe ou rôle de connexion) qui doit être - propriétaire des objets ; -- "b_setschema" est un paramètre booléen optionnel (vrai par défaut) - qui indique si la fonction doit changer le propriétaire du schéma - ou seulement des objets qu'il contient. -RESULTAT : la fonction renvoie un entier correspondant au nombre -d''objets effectivement traités. Les commandes lancées sont notifiées -au fur et à mesure. */ +/* Attribue un schéma et tous les objets qu'il contient au propriétaire désigné. + + Elle n'intervient que sur les objets qui n'appartiennent pas déjà au + rôle considéré. + + Parameters + ---------- + n_schema : text + Chaîne de caractères correspondant au nom du schéma à considérer. + n_owner : text + Chaîne de caractères correspondant au nom du rôle qui doit être + propriétaire des objets. + b_setschema : boolean, default True + Booléen qui indique si la fonction doit changer le propriétaire + du schéma ou seulement des objets qu'il contient. + + Returns + ------- + int + Nombre d'objets effectivement traités. Les commandes lancées sont + notifiées au fur et à mesure. + +*/ DECLARE item record ; k int := 0 ; @@ -1799,11 +1868,11 @@ BEGIN IF NOT pg_has_role(s_owner::regrole::oid, 'USAGE') THEN RAISE EXCEPTION 'FAP5. Vous n''êtes pas habilité à modifier le propriétaire du schéma %.', n_schema - USING DETAIL = format('Propriétaire courant : %s.', s_owner) ; + USING DETAIL = format('Propriétaire courant : %s.', s_owner) ; END IF ; -- le propriétaire désigné n'existe pas - IF NOT n_owner IN (SELECT rolname::text FROM pg_catalog.pg_roles) + IF NOT n_owner IN (SELECT rolname FROM pg_catalog.pg_roles) THEN RAISE EXCEPTION 'FAP2. Le rôle % n''existe pas.', n_owner ; -- absence de permission sur le propriétaire désigné @@ -1844,7 +1913,8 @@ BEGIN relkind IN ('r', 'f', 'p', 'm') AS b, -- b servira à assurer que les tables soient listées avant les -- objets qui en dépendent - format('ALTER %s %s OWNER TO %I', kind_lg, pg_class.oid::regclass, n_owner) AS commande + format('ALTER %s %s OWNER TO %I', + kind_lg, pg_class.oid::regclass, n_owner) AS commande FROM pg_catalog.pg_class, unnest(ARRAY['r', 'p', 'v', 'm', 'f', 'S'], ARRAY['TABLE', 'TABLE', 'VIEW', 'MATERIALIZED VIEW', 'FOREIGN TABLE', 'SEQUENCE']) AS l (kind_crt, kind_lg) @@ -1880,8 +1950,8 @@ BEGIN typname::text AS n_objet, typowner AS obj_owner, False AS b, - format('ALTER %s %s.%I OWNER TO %I', kind_lg, typnamespace::regnamespace, - typname, n_owner) AS commande + format('ALTER %s %I.%I OWNER TO %I', + kind_lg, n_schema, typname, n_owner) AS commande FROM unnest(ARRAY['true', 'false'], ARRAY['DOMAIN', 'TYPE']) AS l (kind_crt, kind_lg), pg_catalog.pg_type @@ -1900,8 +1970,8 @@ BEGIN conname::text AS n_objet, conowner AS obj_owner, False AS b, - format('ALTER CONVERSION %s.%I OWNER TO %I', connamespace::regnamespace, - conname, n_owner) AS commande + format('ALTER CONVERSION %I.%I OWNER TO %I', + n_schema, conname, n_owner) AS commande FROM pg_catalog.pg_conversion WHERE connamespace = quote_ident(n_schema)::regnamespace AND NOT conowner = o_owner @@ -1911,8 +1981,8 @@ BEGIN oprname::text AS n_objet, oprowner AS obj_owner, False AS b, - format('ALTER OPERATOR %s OWNER TO %I', pg_operator.oid::regoperator, - n_owner) AS commande + format('ALTER OPERATOR %s OWNER TO %I', + pg_operator.oid::regoperator, n_owner) AS commande FROM pg_catalog.pg_operator WHERE oprnamespace = quote_ident(n_schema)::regnamespace AND NOT oprowner = o_owner @@ -1922,8 +1992,8 @@ BEGIN collname::text AS n_objet, collowner AS obj_owner, False AS b, - format('ALTER COLLATION %s.%I OWNER TO %I', collnamespace::regnamespace, - collname, n_owner) AS commande + format('ALTER COLLATION %I.%I OWNER TO %I', + n_schema, collname, n_owner) AS commande FROM pg_catalog.pg_collation WHERE collnamespace = quote_ident(n_schema)::regnamespace AND NOT collowner = o_owner @@ -1933,8 +2003,8 @@ BEGIN dictname::text AS n_objet, dictowner AS obj_owner, False AS b, - format('ALTER TEXT SEARCH DICTIONARY %s OWNER TO %I', pg_ts_dict.oid::regdictionary, - n_owner) AS commande + format('ALTER TEXT SEARCH DICTIONARY %s OWNER TO %I', + pg_ts_dict.oid::regdictionary, n_owner) AS commande FROM pg_catalog.pg_ts_dict WHERE dictnamespace = quote_ident(n_schema)::regnamespace AND NOT dictowner = o_owner @@ -1944,11 +2014,35 @@ BEGIN cfgname::text AS n_objet, cfgowner AS obj_owner, False AS b, - format('ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %I', pg_ts_config.oid::regconfig, - n_owner) AS commande + format('ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %I', + pg_ts_config.oid::regconfig, n_owner) AS commande FROM pg_catalog.pg_ts_config WHERE cfgnamespace = quote_ident(n_schema)::regnamespace AND NOT cfgowner = o_owner + -- operator family : + UNION + SELECT + opfname::text AS n_objet, + opfowner AS obj_owner, + False AS b, + format('ALTER OPERATOR FAMILY %I.%I USING %I OWNER TO %I', + n_schema, opfname, amname, n_owner) AS commande + FROM pg_catalog.pg_opfamily + LEFT JOIN pg_catalog.pg_am ON pg_am.oid = opfmethod + WHERE opfnamespace = quote_ident(n_schema)::regnamespace + AND NOT opfowner = o_owner + -- operator class : + UNION + SELECT + opcname::text AS n_objet, + opcowner AS obj_owner, + False AS b, + format('ALTER OPERATOR CLASS %I.%I USING %I OWNER TO %I', + n_schema, opcname, amname, n_owner) AS commande + FROM pg_catalog.pg_opclass + LEFT JOIN pg_catalog.pg_am ON pg_am.oid = opcmethod + WHERE opcnamespace = quote_ident(n_schema)::regnamespace + AND NOT opcowner = o_owner ORDER BY b DESC LOOP IF pg_has_role(item.obj_owner, 'USAGE') @@ -1958,9 +2052,37 @@ BEGIN k := k + 1 ; ELSE RAISE EXCEPTION 'FAP4. Vous n''êtes pas habilité à modifier le propriétaire de l''objet %.', item.n_objet - USING DETAIL = 'Propriétaire courant : ' || item.obj_owner::regrole::text || '.' ; + USING DETAIL = format('Propriétaire courant : %s.', item.obj_owner::regrole) ; END IF ; END LOOP ; + + ------ CATALOGUES CONDITIONNELS ------ + -- soit ceux qui n'existent pas sous toutes les versions de PostgreSQL + IF current_setting('server_version_num')::int >= 100000 + THEN + FOR item IN + -- extended planner statistics : + SELECT + stxname::text AS n_objet, + stxowner AS obj_owner, + format('ALTER STATISTICS %I.%I OWNER TO %I', + n_schema, stxname, n_owner) AS commande + FROM pg_catalog.pg_statistic_ext + WHERE stxnamespace = quote_ident(n_schema)::regnamespace + AND NOT stxowner = o_owner + LOOP + IF pg_has_role(item.obj_owner, 'USAGE') + THEN + EXECUTE item.commande ; + RAISE NOTICE '> %', item.commande ; + k := k + 1 ; + ELSE + RAISE EXCEPTION 'FAP4. Vous n''êtes pas habilité à modifier le propriétaire de l''objet %.', item.n_objet + USING DETAIL = format('Propriétaire courant : %s.', item.obj_owner::regrole) ; + END IF ; + END LOOP ; + END IF ; + ------ RESULTAT ------ RETURN k ; END @@ -1969,7 +2091,7 @@ $_$ ; ALTER FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) IS 'ASGARD. Fonction qui modifie le propriétaire d''un schéma et de tous les objets qu''il contient.' ; +COMMENT ON FUNCTION z_asgard.asgard_admin_proprietaire(text, text, boolean) IS 'ASGARD. Attribue un schéma et tous les objets qu''il contient au propriétaire désigné.' ; ------ 4.4 - TRANSFORMATION GRANT EN REVOKE ------ @@ -2015,24 +2137,37 @@ COMMENT ON FUNCTION z_asgard.asgard_grant_to_revoke(text) IS 'ASGARD. Fonction q -- Function: z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema( - exceptions text[] default NULL::text[], b_gs boolean default False - ) + exceptions text[] default NULL::text[], b_gs boolean default False + ) RETURNS text LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction intègre à la table de gestion des droits - gestion_schema l'ensemble des schémas existants, hors - schémas système et ceux qui sont (optionnellement) listés - en argument. -ARGUMENTS : -- exceptions (optionnel) : un tableau text[] contenant les noms des schémas -à omettre, le cas échéant ; -- b_gs (optionnel) : un booléen indiquant si, dans l'hypothèse où un schéma -serait déjà référencé - nécessairement comme non créé - dans la table de gestion, -c'est le propriétaire du schéma qui doit devenir le "producteur" du schéma -(False) ou le producteur pré-renseigné dans la table de gestion qui doit -devenir le propriétaire du schéma (True). False par défaut. -SORTIE : '__ FIN INTIALISATION.' si la requête s'est exécutée normalement. */ +/* Enregistre dans la table de gestion d'Asgard l'ensemble des schémas +existants encore non référencés, hors schémas système et ceux qui sont +(optionnellement) listés en argument. + + Parameters + ---------- + exceptions : text[], optional + Liste des noms des schémas à omettre, le cas échéant. + b_gs : boolean, default False + Un booléen indiquant si, dans l'hypothèse où un schéma serait + marqué comme non créé dans la table de gestion, c'est le propriétaire + actuel du schéma qui doit être déclaré comme son producteur (False, + comportement par défaut) ou si c'est le producteur pré-renseigné dans + la table de gestion qui doit devenir le propriétaire du schéma (True). + Ce paramètre est ignoré pour un schéma déjà marqué comme créé. Il vise + un cas anecdotique où le champ creation de la table de gestion n'est + pas cohérent avec l'état réel du schéma. La fonction rétablira alors + le lien entre le schéma et l'enregistrement portant son nom dans la + table de gestion. + + Returns + ------- + text + '__ FIN INTIALISATION.' si la requête s'est exécutée normalement. + +*/ DECLARE item record ; e_mssg text ; @@ -2041,35 +2176,37 @@ DECLARE b_creation boolean ; BEGIN - FOR item IN SELECT nspname, nspowner FROM pg_catalog.pg_namespace - WHERE NOT nspname ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', - '^public$', '^information_schema$', '^topology$']) - AND (exceptions IS NULL OR NOT nspname = ANY(exceptions)) + FOR item IN SELECT nspname, nspowner, rolname + FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE NOT nspname ~ ANY(ARRAY['^pg_toast', '^pg_temp', '^pg_catalog$', + '^public$', '^information_schema$', '^topology$']) + AND (exceptions IS NULL OR NOT nspname = ANY(exceptions)) LOOP SELECT creation INTO b_creation FROM z_asgard.gestion_schema_usr - WHERE item.nspname::text = nom_schema ; + WHERE item.nspname = nom_schema ; IF b_creation IS NULL -- schéma non référencé dans gestion_schema THEN INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) - VALUES (item.nspname::text, replace(item.nspowner::regrole::text, '"', ''), true) ; - RAISE NOTICE '... Schéma % enregistré dans la table de gestion.', item.nspname::text ; + VALUES (item.nspname, item.rolname, True) ; + RAISE NOTICE '... Schéma % enregistré dans la table de gestion.', item.nspname ; ELSIF NOT b_creation -- schéma pré-référencé dans gestion_schema THEN IF NOT b_gs THEN UPDATE z_asgard.gestion_schema_usr - SET creation = true, - producteur = replace(item.nspowner::regrole::text, '"', '') - WHERE item.nspname::text = nom_schema ; + SET creation = True, + producteur = item.rolname + WHERE item.nspname = nom_schema ; ELSE UPDATE z_asgard.gestion_schema_usr - SET creation = true - WHERE item.nspname::text = nom_schema ; + SET creation = True + WHERE item.nspname = nom_schema ; END IF ; - RAISE NOTICE '... Schéma % marqué comme créé dans la table de gestion.', item.nspname::text ; + RAISE NOTICE '... Schéma % marqué comme créé dans la table de gestion.', item.nspname ; END IF ; END LOOP ; @@ -2084,13 +2221,12 @@ EXCEPTION WHEN OTHERS THEN HINT = e_hint ; END -$_$; +$_$ ; ALTER FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) IS 'ASGARD. Fonction qui initialise la table de gestion à partir des schémas existants.' ; - +COMMENT ON FUNCTION z_asgard_admin.asgard_initialisation_gestion_schema(text[], boolean) IS 'ASGARD. Enregistre dans la table de gestion d''Asgard l''ensemble des schémas existants encore non référencés, hors schémas système et ceux qui sont (optionnellement) listés en argument.' ; ------ 4.6 - DEREFERENCEMENT D'UN SCHEMA ------ @@ -2194,37 +2330,58 @@ COMMENT ON FUNCTION z_asgard.asgard_nettoyage_roles() IS 'ASGARD. Fonction qui m -- Function: z_asgard.asgard_initialise_schema(text, boolean, boolean) CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_schema( - n_schema text, - b_preserve boolean DEFAULT False, - b_gs boolean default False - ) + n_schema text, + b_preserve boolean DEFAULT False, + b_gs boolean default False + ) RETURNS text LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction permet de réinitialiser les droits - sur un schéma selon les privilèges standards associés - aux rôles désignés dans la table de gestion. - Si elle est appliquée à un schéma existant non référencé - dans la table de gestion, elle l'ajoute avec son - propriétaire courant. Elle échoue si le schéma n'existe - pas. -ARGUMENTS : -- n_schema : nom d'un schéma présumé existant ; -- b_preserve (optionnel) : un paramètre booléen. Pour un schéma encore -non référencé (ou pré-référencé comme non-créé) dans la table de gestion une valeur -True signifie que les privilèges des rôles lecteur et éditeur doivent être -ajoutés par dessus les droits actuels. Avec la valeur par défaut False, -les privilèges sont réinitialisés. Ce paramètre est ignoré pour un schéma déjà -référencé comme créé (et les privilèges sont réinitialisés) ; -- b_gs (optionnel) : un booléen indiquant si, dans l'hypothèse où un schéma -serait déjà référencé - nécessairement comme non créé - dans la table de gestion, -c'est le propriétaire du schéma qui doit devenir le "producteur" (False) ou le -producteur de la table de gestion qui doit devenir le propriétaire -du schéma (True). False par défaut. Ce paramètre est ignoré pour un schéma déjà -créé. -SORTIE : '__ REINITIALISATION REUSSIE.' (ou '__INITIALISATION REUSSIE.' pour -un schéma non référencé comme créé avec b_preserve = True) si la requête -s'est exécutée normalement. */ +/* Réinitialise les droits sur un schéma et ses objets selon les privilèges +standards du producteur, de l'éditeur et du lecteur désignés dans la table +de gestion d'Asgard. + + Elle a notamment pour effet de révoquer tout privilège accordé à + d'autres rôles que le producteur et les éventuels éditeur et lecteur. + + Si cette fonction est appliquée à un schéma existant non référencé + dans la table de gestion, elle l'y ajoute, avec son propriétaire + courant comme producteur. + + La fonction échouera si le schéma n'existe pas. + + Parameters + ---------- + n_schema : text + Nom d'un schéma présumé existant. + b_preserve : boolean, default False + Pour un schéma encore non référencé ou pré-référencé comme non créé + dans la table de gestion, une valeur True signifie que les privilèges + des rôles lecteur et éditeur doivent être ajoutés par dessus les droits + actuels. Avec la valeur par défaut False, les privilèges sont + réinitialisés avant application des droits standards. Ce paramètre est + ignoré pour un schéma déjà référencé comme créé - les privilèges sont + alors quoi qu'il arrive réinitialisés. + b_gs : boolean, default False + Un booléen indiquant si, dans l'hypothèse où le schéma serait + marqué comme non créé dans la table de gestion, c'est le propriétaire + actuel du schéma qui doit être déclaré comme son producteur (False, + comportement par défaut) ou si c'est le producteur pré-renseigné dans + la table de gestion qui doit devenir le propriétaire du schéma (True). + Ce paramètre est ignoré pour un schéma déjà marqué comme créé. Il vise + un cas anecdotique où le champ creation de la table de gestion n'est + pas cohérent avec l'état réel du schéma. La fonction rétablira alors + le lien entre le schéma et l'enregistrement portant son nom dans la + table de gestion. + + Returns + ------- + text + '__ REINITIALISATION REUSSIE.' (ou '__INITIALISATION REUSSIE.' pour + un schéma non référencé comme créé avec b_preserve = True) si la + requête s'est exécutée normalement. + +*/ DECLARE roles record ; cree boolean ; @@ -2247,9 +2404,10 @@ BEGIN END IF ; -- existence du schéma - SELECT replace(nspowner::regrole::text, '"', '') INTO n_owner + SELECT rolname INTO n_owner FROM pg_catalog.pg_namespace - WHERE n_schema = nspname::text ; + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner + WHERE n_schema = nspname ; IF NOT FOUND THEN RAISE EXCEPTION 'FIS2. Echec. Le schéma % n''existe pas.', n_schema ; @@ -2420,28 +2578,28 @@ BEGIN END IF ; ------ RECREATION DES PRIVILEGES SUR LES SCHEMAS D'ASGARD ------ - IF n_schema = 'z_asgard' AND (roles.lecteur IS NULL OR NOT roles.lecteur = 'g_consult') + IF n_schema = 'z_asgard' THEN - -- rétablissement des droits de g_consult - RAISE NOTICE 'rétablissement des privilèges attendus pour g_consult :' ; + -- rétablissement des droits de public + RAISE NOTICE 'rétablissement des privilèges attendus pour le pseudo-rôle public :' ; - GRANT USAGE ON SCHEMA z_asgard TO g_consult ; - RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard TO g_consult' ; + GRANT USAGE ON SCHEMA z_asgard TO public ; + RAISE NOTICE '> GRANT USAGE ON SCHEMA z_asgard TO public' ; - GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult ; - RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_usr TO g_consult' ; + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public ; + RAISE NOTICE '> GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public' ; - GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult ; - RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_etr TO g_consult' ; + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public ; + RAISE NOTICE '> GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public' ; - GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult ; - RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO g_consult' ; + GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmenu_metadata TO public' ; - GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult ; - RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO g_consult' ; + GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.asgardmanager_metadata TO public' ; - GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult ; - RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO g_consult' ; + GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public ; + RAISE NOTICE '> GRANT SELECT ON TABLE z_asgard.gestion_schema_read_only TO public' ; ELSIF n_schema = 'z_asgard_admin' THEN @@ -2513,12 +2671,12 @@ EXCEPTION WHEN OTHERS THEN HINT = e_hint ; END -$_$; +$_$ ; ALTER FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) IS 'ASGARD. Fonction qui réinitialise les privilèges sur un schéma (et l''ajoute à la table de gestion s''il n''y est pas déjà).' ; +COMMENT ON FUNCTION z_asgard.asgard_initialise_schema(text, boolean, boolean) IS 'ASGARD. Réinitialise les droits sur un schéma et ses objets selon les privilèges standards du producteur, de l''éditeur et du lecteur désignés dans la table de gestion d''Asgard.' ; ------ 4.9 - REINITIALISATION DES PRIVILEGES SUR UN OBJET ------ @@ -2533,21 +2691,29 @@ CREATE OR REPLACE FUNCTION z_asgard.asgard_initialise_obj( RETURNS text LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction permet de réinitialiser les droits - sur un objet selon les privilèges standards associés - aux rôles désignés dans la table de gestion pour son schéma. - -ARGUMENTS : -- "obj_schema" est le nom du schéma contenant l'objet, au format -texte et sans guillemets ; -- "obj_nom" est le nom de l'objet, au format texte et (sauf pour -les fonctions !) sans guillemets ; -- "obj_typ" est le type de l'objet au format text ('table', -'partitioned table' (assimilé à 'table'), 'view', 'materialized view', -'foreign table', 'sequence', 'function', 'aggregate', 'procedure', -'routine', 'type', 'domain'). -SORTIE : '__ REINITIALISATION REUSSIE.' si la requête s'est exécutée -normalement. */ +/* Réinitialise les droits sur un objet selon les privilèges standards +associés aux rôles désignés dans la table de gestion pour son schéma. + + Parameters + ---------- + obj_schema : text + Le nom du schéma contenant l'objet. + obj_nom : text + Le nom de l'objet. À écrire sans les guillemets des identifiants + PostgreSQL SAUF pour les fonctions, dont le nom doit impérativement + être entre guillemets s'il ne respecte pas les conventions de + nommage des identifiants PostgreSQL. + obj_typ : str + Le type de l'objet, parmi 'table', 'partitioned table' (assimilé + à 'table'), 'view', 'materialized view', 'foreign table', 'sequence', + 'function', 'aggregate', 'procedure', 'routine', 'type' et 'domain'. + + Returns + ------- + text + '__ REINITIALISATION REUSSIE.' si la requête s'est exécutée normalement. + +*/ DECLARE class_info record ; roles record ; @@ -2624,8 +2790,9 @@ BEGIN END || ' AS appel' || ' FROM pg_catalog.' || class_info.xclass || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' - THEN class_info.xclass || '.oid::regprocedure::text = ' + THEN class_info.xclass || '.oid = ' || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + || '::regprocedure' ELSE class_info.xname || ' = ' || quote_literal(obj_nom) || ' AND ' || class_info.xschema || '::regnamespace::text = ' || quote_literal(quote_ident(obj_schema)) END @@ -2762,7 +2929,7 @@ $_$; ALTER FUNCTION z_asgard.asgard_initialise_obj(text, text, text) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_initialise_obj(text, text, text) IS 'ASGARD. Fonction qui réinitialise les privilèges sur un objet.' ; +COMMENT ON FUNCTION z_asgard.asgard_initialise_obj(text, text, text) IS 'ASGARD. Réinitialise les privilèges sur un objet.' ; ------ 4.10 - DEPLACEMENT D'OBJET ------ @@ -2770,63 +2937,75 @@ COMMENT ON FUNCTION z_asgard.asgard_initialise_obj(text, text, text) IS 'ASGARD. -- Function: z_asgard.asgard_deplace_obj(text, text, text, text, int) CREATE OR REPLACE FUNCTION z_asgard.asgard_deplace_obj( - obj_schema text, - obj_nom text, - obj_typ text, - schema_cible text, - variante int DEFAULT 1 - ) + obj_schema text, + obj_nom text, + obj_typ text, + schema_cible text, + variante int DEFAULT 1 + ) RETURNS text LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction permet de déplacer un objet vers un nouveau - schéma en spécifiant la gestion voulue sur les droits de - l'objet : transfert ou réinitialisation des privilèges. - Dans le cas d'une table avec un ou plusieurs champs de - type serial, elle prend aussi en charge les privilèges - sur les séquences associées. -ARGUMENTS : -- "obj_schema" est le nom du schéma contenant l'objet, au format -texte et sans guillemets ; -- "obj_nom" est le nom de l'objet, au format texte et sans -guillemets ; -- "obj_typ" est le type de l'objet au format text, parmi 'table', -'partitioned table' (assimilé à 'table'), 'view', 'materialized view', -'foreign table', 'sequence', 'function', 'aggregate', 'procedure', -'routine', 'type' et 'domain' ; -- "schema_cible" est le nom du schéma où doit être déplacé l'objet, -au format texte et sans guillemets ; -- "variante" [optionnel] est un entier qui définit le comportement -attendu par l'utilisateur vis à vis des privilèges : - - 1 (valeur par défaut) | TRANSFERT COMPLET + CONSERVATION : - les privilèges des rôles producteur, éditeur et lecteur de - l'ancien schéma sont transférés sur ceux du nouveau. Si un - éditeur ou lecteur a été désigné pour le nouveau schéma mais - qu'aucun n'était défini pour l'ancien, le rôle reçoit les - privilèges standards pour sa fonction. Le cas échéant, - les privilèges des autres rôles sont conservés ; - - 2 | REINITIALISATION COMPLETE : les nouveaux - producteur, éditeur et lecteur reçoivent les privilèges - standard. Les privilèges des autres rôles sont supprimés ; - - 3 | TRANSFERT COMPLET + NETTOYAGE : les privilèges des rôles - producteur, éditeur et lecteur de l'ancien schéma sont transférés - sur ceux du nouveau. Si un éditeur ou lecteur a été désigné pour - le nouveau schéma mais qu'aucun n'était défini pour l'ancien, - le rôle reçoit les privilèges standards pour sa fonction. - Les privilèges des autres rôles sont supprimés ; - - 4 | TRANSFERT PRODUCTEUR + CONSERVATION : les privilèges de - l'ancien producteur sont transférés sur le nouveau. Les privilèges - des autres rôles sont conservés tels quels. C'est le comportement - d'une commande ALTER [...] SET SCHEMA (interceptée par l'event - trigger asgard_on_alter_objet) ; - - 5 | TRANSFERT PRODUCTEUR + REINITIALISATION : les privilèges - de l'ancien producteur sont transférés sur le nouveau. Les - nouveaux éditeur et lecteur reçoivent les privilèges standards. - Les privilèges des autres rôles sont supprimés ; - - 6 | REINITIALISATION PARTIELLE : les nouveaux - producteur, éditeur et lecteur reçoivent les privilèges - standard. Les privilèges des autres rôles sont conservés. -SORTIE : '__ DEPLACEMENT REUSSI.' si la requête s'est exécutée normalement. */ +/* Déplace un objet vers un nouveau schéma, en transférant ou réinitialisant +les privilèges selon la variante choisie. + + Lorsque des séquences sont associées aux champs de la table, la fonction + gère également leurs privilèges. + + Parameters + ---------- + obj_schema : text + Le nom du schéma contenant l'objet. + obj_nom : text + Le nom de l'objet. À écrire sans les guillemets des identifiants + PostgreSQL SAUF pour les fonctions, dont le nom doit impérativement + être entre guillemets s'il ne respecte pas les conventions de + nommage des identifiants PostgreSQL. + obj_typ : str + Le type de l'objet, parmi 'table', 'partitioned table' (assimilé + à 'table'), 'view', 'materialized view', 'foreign table', 'sequence', + 'function', 'aggregate', 'procedure', 'routine', 'type' et 'domain'. + schema_cible : str + Le nom du schéma où doit être déplacé l'objet. + variante : int, default 1 + Un entier qui définit le comportement attendu par l'utilisateur + vis-à-vis des privilèges : + + * 1 (valeur par défaut) | TRANSFERT COMPLET + CONSERVATION : + les privilèges des rôles producteur, éditeur et lecteur de + l'ancien schéma sont transférés sur ceux du nouveau. Si un + éditeur ou lecteur a été désigné pour le nouveau schéma mais + qu'aucun n'était défini pour l'ancien, le rôle reçoit les + privilèges standards pour sa fonction. Le cas échéant, + les privilèges des autres rôles sont conservés. + * 2 | REINITIALISATION COMPLETE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont supprimés. + * 3 | TRANSFERT COMPLET + NETTOYAGE : les privilèges des rôles + producteur, éditeur et lecteur de l'ancien schéma sont transférés + sur ceux du nouveau. Si un éditeur ou lecteur a été désigné pour + le nouveau schéma mais qu'aucun n'était défini pour l'ancien, + le rôle reçoit les privilèges standards pour sa fonction. + Les privilèges des autres rôles sont supprimés. + * 4 | TRANSFERT PRODUCTEUR + CONSERVATION : les privilèges de + l'ancien producteur sont transférés sur le nouveau. Les privilèges + des autres rôles sont conservés tels quels. C'est le comportement + d'une commande ALTER [...] SET SCHEMA (interceptée par le déclencheur + sur évènement asgard_on_alter_objet). + * 5 | TRANSFERT PRODUCTEUR + REINITIALISATION : les privilèges + de l'ancien producteur sont transférés sur le nouveau. Les + nouveaux éditeur et lecteur reçoivent les privilèges standards. + Les privilèges des autres rôles sont supprimés. + * 6 | REINITIALISATION PARTIELLE : les nouveaux + producteur, éditeur et lecteur reçoivent les privilèges + standard. Les privilèges des autres rôles sont conservés. + + Returns + ------- + text + '__ DEPLACEMENT REUSSI.' si la requête s'est exécutée normalement. + +*/ DECLARE class_info record ; roles record ; @@ -2846,6 +3025,7 @@ DECLARE s record ; o oid ; supported boolean ; + duplicate oid ; BEGIN obj_typ := lower(obj_typ) ; @@ -2912,28 +3092,57 @@ BEGIN RAISE EXCEPTION 'FDO4. Echec. Le type % n''existe pas ou n''est pas pris en charge.', obj_typ USING HINT = 'Types acceptés : ''table'', ''partitioned table'', ''view'', ''materialized view'', ''foreign table'', ''sequence'', ''function'', ''aggregate'', ''procedure'', ''routine'', ''type'', ''domain''.' ; END IF ; - + -- objet inexistant + récupération du propriétaire EXECUTE 'SELECT ' || class_info.xowner || '::regrole::text AS prop, ' || class_info.xclass || '.oid, ' || CASE WHEN class_info.xclass = 'pg_type' THEN quote_literal(quote_ident(obj_schema) || '.' || quote_ident(obj_nom)) || '::text' ELSE class_info.xclass || '.oid::' || class_info.xreg || '::text' - END || ' AS appel' + END || ' AS appel,' + || class_info.xname || ' AS objname' + || CASE WHEN class_info.xclass = 'pg_proc' + THEN ', pg_catalog.oidvectortypes(proargtypes) AS proargtypes' ELSE '' END || ' FROM pg_catalog.' || class_info.xclass || ' WHERE ' || CASE WHEN class_info.xclass = 'pg_proc' - THEN class_info.xclass || '.oid::regprocedure::text = ' + THEN class_info.xclass || '.oid = ' || quote_literal(quote_ident(obj_schema) || '.' || obj_nom) + || '::regprocedure' ELSE class_info.xname || ' = ' || quote_literal(obj_nom) || ' AND ' || class_info.xschema || '::regnamespace::text = ' || quote_literal(quote_ident(obj_schema)) END INTO obj ; - + IF obj.prop IS NULL THEN RAISE EXCEPTION 'FDO5. Echec. L''objet % n''existe pas.', obj_nom ; END IF ; + -- il existe déjà un objet de même définition dans le schéma cible + IF class_info.xclass = 'pg_proc' THEN + EXECUTE format(' + SELECT oid FROM pg_catalog.pg_proc + WHERE pronamespace = %L::regnamespace + AND proname = %L + AND pg_catalog.oidvectortypes(proargtypes) = %L', + quote_ident(schema_cible), obj.objname, obj.proargtypes) + INTO duplicate ; + ELSE + EXECUTE format(' + SELECT oid FROM pg_catalog.%s + WHERE %s = %L::regnamespace + AND %s = %L', + class_info.xclass, + class_info.xschema, quote_ident(schema_cible), + class_info.xname, obj.objname) + INTO duplicate ; + END IF ; + + IF duplicate IS NOT NULL + THEN + RAISE EXCEPTION 'FDO8. Opération interdite. Il existe déjà un objet de même définition dans le schéma cible.' ; + END IF ; + ------ RECUPERATION DES ROLES ------ -- schéma de départ : SELECT @@ -3072,6 +3281,53 @@ BEGIN END IF ; END LOOP ; + ------ BREF CONTRÔLE DES INDEX ------ + -- il s'agit seulement de vérifier qu'il n'existe pas déjà d'index + -- de même nom dans le schéma cible + + -- 1. index qui dépendent d'une contrainte, tels les index + -- des clés primaires + FOR s IN ( + SELECT + pg_class.oid, + pg_class.relname + FROM pg_catalog.pg_constraint + LEFT JOIN pg_catalog.pg_class ON pg_class.oid = pg_constraint.conindid + WHERE pg_constraint.conrelid = obj.oid + AND pg_constraint.conindid IS NOT NULL + ) + LOOP + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO9. Opération interdite. Il existe dans le schéma cible une relation de même nom que l''index associé %.', s.relname ; + END IF ; + END LOOP ; + + -- 2. autres index (qui dépendent directement de la table) + FOR s IN ( + SELECT + pg_class.oid, + pg_class.relname + FROM pg_catalog.pg_depend LEFT JOIN pg_catalog.pg_class + ON pg_class.oid = pg_depend.objid + WHERE pg_depend.classid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refclassid = 'pg_catalog.pg_class'::regclass::oid + AND pg_depend.refobjid = obj.oid + AND pg_depend.refobjsubid > 0 + AND pg_depend.deptype = ANY (ARRAY['a', 'i']) + AND pg_class.relkind = 'i' + ) + LOOP + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO10. Opération interdite. Il existe dans le schéma cible une relation de même nom que l''index associé %.', s.relname ; + END IF ; + END LOOP ; + ------ PRIVILEGES SUR LES SEQUENCES ASSOCIEES ------ IF obj_typ = 'table' THEN @@ -3084,7 +3340,8 @@ BEGIN -- de type DEPENDENCY_INTERNAL (i) pour la séquence d'un champ IDENDITY FOR s IN ( SELECT - pg_class.oid + pg_class.oid, + pg_class.relname FROM pg_catalog.pg_depend LEFT JOIN pg_catalog.pg_class ON pg_class.oid = pg_depend.objid WHERE pg_depend.classid = 'pg_catalog.pg_class'::regclass::oid @@ -3095,6 +3352,14 @@ BEGIN AND pg_class.relkind = 'S' ) LOOP + -- il existe déjà une séquence de même nom dans le schéma cible + IF EXISTS (SELECT oid FROM pg_catalog.pg_class + WHERE pg_class.relname = s.relname + AND relnamespace = quote_ident(schema_cible)::regnamespace) + THEN + RAISE EXCEPTION 'FDO11. Opération interdite. Il existe dans le schéma cible une relation de même nom que la séquence associée %.', s.relname ; + END IF ; + -- liste des séquences seq_liste := array_append(seq_liste, s.oid) ; @@ -3417,12 +3682,12 @@ BEGIN RETURN '__ DEPLACEMENT REUSSI.' ; END -$_$; +$_$ ; ALTER FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) IS 'ASGARD. Fonction qui prend en charge le déplacement d''un objet dans un nouveau schéma, avec une gestion propre des privilèges.' ; +COMMENT ON FUNCTION z_asgard.asgard_deplace_obj(text, text, text, text, int) IS 'ASGARD. Déplace un objet vers un nouveau schéma, en transférant ou réinitialisant les privilèges selon la variante choisie.' ; ------ 4.11 - OCTROI D'UN RÔLE À TOUS LES RÔLES DE CONNEXION ------ @@ -3433,17 +3698,26 @@ CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_all_login_grant_role(n_role tex RETURNS int LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction confère à tous les rôles de connexion du - serveur l'appartenance au rôle donné en argument. -ARGUMENTS : -- n_role : une chaîne de caractères présumée correspondre à un nom de - rôle valide ; -- b : [optionnel] un booléen. Si b vaut False et qu'un rôle de connexion est -déjà membre du rôle considéré par héritage, la fonction ne fait rien. Si -b vaut True (défaut), la fonction ne passera un rôle de connexion que s'il est -lui-même membre du rôle considéré. -SORTIE : un entier correspondant au nombre de rôles pour lesquels -la permission a été accordée. */ +/* Confère à tous les rôles de connexion du serveur l'appartenance au rôle +donné en argument. + + Parameters + ---------- + n_role : text + Une chaîne de caractères présumée correspondre à un nom de + rôle valide. + b : boolean, default True + Si b vaut False et qu'un rôle de connexion est déjà membre + du rôle considéré par héritage, la fonction ne fait rien. Si + b vaut True (défaut), la fonction ne passera un rôle de connexion + que s'il est lui-même membre du rôle considéré. + + Returns + ------- + int + Le nombre de rôles pour lesquels la permission a été accordée. + +*/ DECLARE roles record ; attributeur text ; @@ -3455,7 +3729,7 @@ BEGIN -- existance du rôle IF NOT n_role IN (SELECT rolname FROM pg_catalog.pg_roles) THEN - RAISE EXCEPTION 'FLG1. Echec. Le rôle % n''existe pas', n_role ; + RAISE EXCEPTION 'FLG1. Echec. Le rôle % n''existe pas.', n_role ; END IF ; -- on cherche un rôle dont l'utilisateur est @@ -3473,38 +3747,38 @@ BEGIN IF NOT FOUND THEN RAISE EXCEPTION 'FLG2. Opération interdite. Permissions insuffisantes pour le rôle %.', n_role - USING HINT = 'Votre rôle doit être membre de ' || n_role - || ' avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + USING HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + n_role) ; END IF ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(attributeur) ; + EXECUTE format('SET ROLE %I', attributeur) ; IF b THEN FOR roles IN SELECT rolname - FROM pg_roles LEFT JOIN pg_auth_members - ON member = pg_roles.oid AND roleid = n_role::regrole::oid - WHERE rolcanlogin AND member IS NULL - AND NOT rolsuper + FROM pg_roles LEFT JOIN pg_auth_members + ON member = pg_roles.oid AND roleid = n_role::regrole::oid + WHERE rolcanlogin AND member IS NULL + AND NOT rolsuper LOOP - c := 'GRANT ' || quote_ident(n_role) || ' TO ' || quote_ident(roles.rolname) ; + c := format('GRANT %s TO %s', n_role, roles.rolname) ; EXECUTE c ; RAISE NOTICE '> %', c ; n := n + 1 ; END LOOP ; ELSE FOR roles IN SELECT rolname FROM pg_roles - WHERE rolcanlogin AND NOT pg_has_role(rolname, n_role, 'MEMBER') + WHERE rolcanlogin AND NOT pg_has_role(rolname, n_role, 'MEMBER') LOOP - c := 'GRANT ' || quote_ident(n_role) || ' TO ' || quote_ident(roles.rolname) ; + c := format('GRANT %s TO %s', n_role, roles.rolname) ; EXECUTE c ; RAISE NOTICE '> %', c ; n := n + 1 ; END LOOP ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; RETURN n ; END @@ -3513,8 +3787,7 @@ $_$; ALTER FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) IS 'ASGARD. Fonction qui confère à tous les rôles de connexion du serveur l''appartenance au rôle donné en argument.' ; - +COMMENT ON FUNCTION z_asgard_admin.asgard_all_login_grant_role(text, boolean) IS 'ASGARD. Confère à tous les rôles de connexion du serveur l''appartenance au rôle donné en argument.' ; ------ 4.12 - IMPORT DE LA NOMENCLATURE DANS GESTION_SCHEMA ------ @@ -4271,45 +4544,10 @@ ALTER FUNCTION z_asgard_admin.asgard_initialise_all_schemas(integer) COMMENT ON FUNCTION z_asgard_admin.asgard_initialise_all_schemas(integer) IS 'ASGARD. Fonction qui réinitialise les droits sur l''ensemble des schémas référencés.' ; ------- 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL ------ +------ 4.15 - TRANSFORMATION D'UN NOM DE RÔLE POUR COMPARAISON AVEC LES CHAMPS ACL ------ [supprimé version 1.4.0] -- Function: z_asgard.asgard_role_trans_acl(regrole) -CREATE OR REPLACE FUNCTION z_asgard.asgard_role_trans_acl(n_role regrole) - RETURNS text - LANGUAGE plpgsql - AS $_$ -/* OBJET : Cette fonction transforme un nom de rôle pour qu'il soit utilisable - dans une expression régulière de comparaison avec les champs acl - de pg_catalog. -ARGUMENT : un nom de rôle casté en regrole. -SORTIE : sa traduction, en format text. */ -DECLARE - n_role_trans text ; -BEGIN - - IF n_role::text ~ '^["]?[a-zA-Z0-9_]+["]?$' - THEN - -- pour les noms ne comportant que des lettres et - -- des chiffres, même avec des majuscules, on - -- retire les guillemets - n_role_trans := replace(n_role::text, '"', '') ; - ELSE - -- tous les caractères spéciaux vont entre crochets - n_role_trans := regexp_replace(n_role::text, '([^a-zA-Z0-9_]{1})', '[\1]', 'g') ; - -- les antislashs sont doublés - n_role_trans := replace(n_role_trans::text, '\', '\\') ; --' - END IF ; - - RETURN n_role_trans ; -END -$_$; - -ALTER FUNCTION z_asgard.asgard_role_trans_acl(regrole) - OWNER TO g_admin_ext ; - -COMMENT ON FUNCTION z_asgard.asgard_role_trans_acl(regrole) IS 'ASGARD. Fonction qui transforme un nom de rôle pour qu''il soit utilisable dans une expression régulière de comparaison avec les champs acl de pg_catalog.' ; - ------ 4.16 - DIAGNOSTIC DES DROITS NON STANDARDS ------ @@ -4319,19 +4557,42 @@ CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_diagnostic(cibles text[] DEFAUL RETURNS TABLE (nom_schema text, nom_objet text, typ_objet text, critique boolean, anomalie text) LANGUAGE plpgsql AS $_$ -/* OBJET : Pour tous les schémas référencés par ASGARD et - existants dans la base, asgard_diagnostic liste - les écarts avec les droits standards. -ARGUMENT : cibles (optionnel) permet de restreindre le diagnostic -à la liste de schémas spécifiés. -APPEL : SELECT * FROM z_asgard_admin.asgard_diagnostic() ; -SORTIE : une table avec quatre attributs, - - nom_schema = nom du schéma ; - - nom_objet = nom de l'objet concerné ; - - typ_objet = le type d'objet ; - - critique = True si l'anomalie est problématique pour le - bon fonctionnement d'ASGARD, False si elle est bénigne ; - - anomalie = description de l'anomalie. */ +/* Pour tous les schémas actifs référencés par Asgard, liste les écarts +entre les droits effectifs et les droits standards. + + Cette fonction peut avoir une durée d'exécution conséquente + si elle est appliquée à un grand nombre de schémas. + + Les "anomalies" détectée peuvent être parfaitement justifiées + si elles résultent d'une personnalisation volontaire des + droits sur certains objets. + + Parameters + ---------- + cibles : text[], optional + Permet de restreindre le diagnostic à la liste de schémas + spécifiés. + + Returns + ------- + table (nom_schema : text, nom_objet : text, typ_objet : text, + critique : boolean, anomalie : text) + Une table avec quatre attributs : + + * "nom_schema" est le nom du schéma. + * "nom_objet" est le nom de l'objet concerné. + * "typ_objet" est le type d'objet. + * "critique" vaut True si l'anomalie est problématique pour + le bon fonctionnement d'Asgard (et doit être corrigée au + plus tôt), False si elle est bénigne. + * "anomalie" est une description de l'anomalie. + + Examples + -------- + SELECT * FROM z_asgard_admin.asgard_diagnostic() ; + SELECT * FROM z_asgard_admin.asgard_diagnostic(ARRAY['schema_1', 'schema_2']) ; + +*/ DECLARE item record ; catalogue record ; @@ -4468,16 +4729,21 @@ BEGIN catalogue.catalogue || '.oid AS objoid, ' ELSE '' END || CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN '' WHEN catalogue.catalogue = 'pg_attribute' - THEN '(z_asgard.asgard_parse_relident(attrelid::regclass))[2] || '' ('' || ' || catalogue.prefixe || 'name || '')'' AS objname, ' + THEN '(z_asgard.asgard_parse_relident(attrelid::regclass))[2] || '' ('' || ' || + catalogue.prefixe || 'name || '')'' AS objname, ' ELSE catalogue.prefixe || 'name::text AS objname, ' END || ' - regexp_replace(' || CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN 'defaclrole' - WHEN catalogue.catalogue = 'pg_attribute' THEN 'NULL' - ELSE catalogue.prefixe || 'owner' END || '::regrole::text, ''^["]?(.*?)["]?$'', ''\1'') AS objowner' || + rolname AS objowner' || CASE WHEN catalogue.droits THEN ', ' || catalogue.prefixe || 'acl AS objacl' ELSE '' END || ' FROM pg_catalog.' || catalogue.catalogue || ' + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = ' || + CASE WHEN catalogue.catalogue = 'pg_default_acl' THEN 'defaclrole' + WHEN catalogue.catalogue = 'pg_attribute' THEN 'NULL' + ELSE catalogue.prefixe || 'owner' END || ' WHERE ' || CASE WHEN catalogue.catalogue = 'pg_attribute' - THEN 'quote_ident((z_asgard.asgard_parse_relident(attrelid::regclass))[1])::regnamespace::oid = ' || item.oid_schema::text - WHEN catalogue.catalogue = 'pg_namespace' THEN catalogue.prefixe || 'name = ' || quote_literal(item.nom_schema) + THEN 'quote_ident((z_asgard.asgard_parse_relident(attrelid::regclass))[1])::regnamespace::oid = ' || + item.oid_schema::text + WHEN catalogue.catalogue = 'pg_namespace' THEN catalogue.prefixe || 'name = ' || + quote_literal(item.nom_schema) ELSE catalogue.prefixe || 'namespace = ' || item.oid_schema::text END || CASE WHEN catalogue.attrib_genre IS NOT NULL THEN ' AND ' || catalogue.attrib_genre || ' ~ ' || quote_literal(catalogue.valeur_genre) @@ -4532,12 +4798,12 @@ BEGIN VALUES ('z_asgard_admin', 'z_asgard_admin', 'schéma', 'g_admin_ext', 'U'), ('z_asgard_admin', 'gestion_schema', 'table', 'g_admin_ext', 'rawd'), - ('z_asgard', 'z_asgard', 'schéma', 'g_consult', 'U'), - ('z_asgard', 'gestion_schema_usr', 'vue', 'g_consult', 'r'), - ('z_asgard', 'gestion_schema_etr', 'vue', 'g_consult', 'r'), - ('z_asgard', 'asgardmenu_metadata', 'vue', 'g_consult', 'r'), - ('z_asgard', 'asgardmanager_metadata', 'vue', 'g_consult', 'r'), - ('z_asgard', 'gestion_schema_read_only', 'vue', 'g_consult', 'r') + ('z_asgard', 'z_asgard', 'schéma', 'public', 'U'), + ('z_asgard', 'gestion_schema_usr', 'vue', 'public', 'rawd'), + ('z_asgard', 'gestion_schema_etr', 'vue', 'public', 'rawd'), + ('z_asgard', 'asgardmenu_metadata', 'vue', 'public', 'r'), + ('z_asgard', 'asgardmanager_metadata', 'vue', 'public', 'r'), + ('z_asgard', 'gestion_schema_read_only', 'vue', 'public', 'r') ) AS t (a_schema, a_objet, a_type, role, droits) WHERE a_schema = item.nom_schema AND a_objet = objet.objname::text AND a_type = catalogue.lib_obj ; @@ -4628,12 +4894,12 @@ BEGIN END LOOP ; END LOOP ; END -$_$; +$_$ ; ALTER FUNCTION z_asgard_admin.asgard_diagnostic(text[]) OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_diagnostic(text[]) IS 'ASGARD. Fonction qui liste les écarts vis-à-vis des droits standards sur les schémas actifs référencés par ASGARD.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_diagnostic(text[]) IS 'ASGARD. Pour tous les schémas actifs référencés par Asgard, liste les écarts entre les droits effectifs et les droits standards.' ; ------ 4.17 - EXTRACTION DE NOMS D'OBJETS A PARTIR D'IDENTIFIANTS ------ @@ -4676,6 +4942,7 @@ ALTER FUNCTION z_asgard.asgard_parse_relident(regclass) COMMENT ON FUNCTION z_asgard.asgard_parse_relident(regclass) IS 'ASGARD. Fonction qui retourne le nom du schéma et le nom de la relation à partir d''un identifiant de relation.' ; + ------ 4.18 - EXPLICITATION DES CODES DE PRIVILÈGES ------ -- Function: z_asgard.asgard_expend_privileges(text) @@ -4711,6 +4978,7 @@ ALTER FUNCTION z_asgard.asgard_expend_privileges(text) COMMENT ON FUNCTION z_asgard.asgard_expend_privileges(text) IS 'ASGARD. Fonction qui explicite les privilèges correspondant aux codes données en argument.' ; + -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -4724,14 +4992,15 @@ COMMENT ON FUNCTION z_asgard.asgard_expend_privileges(text) IS 'ASGARD. Fonction -- Function: z_asgard_admin.asgard_on_modify_gestion_schema_before() -CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() RETURNS trigger +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() + RETURNS trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par le trigger asgard_on_modify_gestion_schema_before, - qui valide les informations saisies dans la table de gestion. -CIBLES : z_asgard_admin.gestion_schema. -PORTEE : FOR EACH ROW. -DECLENCHEMENT : BEFORE INSERT, UPDATE, DELETE.*/ +/* Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_before +sur z_asgard_admin.gestion_schema, qui valide et normalise les informations +saisies dans la table de gestion avant leur enregistrement. + +*/ DECLARE n_role text ; BEGIN @@ -4797,9 +5066,9 @@ BEGIN IF NOT pg_has_role(OLD.oid_producteur, 'USAGE') THEN RAISE EXCEPTION 'TB23. Opération interdite (schéma %).', OLD.nom_schema - USING DETAIL = 'Seul les membres du rôle producteur ' || OLD.oid_producteur::regrole::text || ' peuvent supprimer ce schéma.' ; + USING DETAIL = format('Seuls les membres du rôle producteur %s peuvent supprimer ce schéma.', OLD.oid_producteur::regrole) ; ELSE - EXECUTE 'DROP SCHEMA ' || quote_ident(OLD.nom_schema) || ' CASCADE' ; + EXECUTE format('DROP SCHEMA %I CASCADE', OLD.nom_schema) ; RAISE NOTICE '... Le schéma % a été supprimé.', OLD.nom_schema ; RETURN NULL ; END IF ; @@ -4974,9 +5243,10 @@ BEGIN -- commande ALTER SCHEMA OWNER TO aurait été interceptée -- mais, s'il advient, on repart du propriétaire -- renseigné dans pg_namespace - SELECT replace(nspowner::regrole::text, '"', ''), nspowner + SELECT rolname, nspowner INTO NEW.producteur, NEW.oid_producteur FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner WHERE pg_namespace.oid = NEW.oid_schema ; RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', NEW.nom_schema ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; @@ -4985,7 +5255,7 @@ BEGIN THEN NEW.producteur := n_role ; RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle producteur, renommé entre temps.', NEW.nom_schema - USING DETAIL = 'Ancien nom "' || OLD.producteur || '", nouveau nom "' || NEW.producteur || '".' ; + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.producteur, NEW.producteur) ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN producteur') ; END IF ; END IF ; @@ -5002,14 +5272,14 @@ BEGIN NEW.editeur := NULL ; NEW.oid_editeur := NULL ; RAISE NOTICE '[table de gestion] Schéma %. Le rôle éditeur n''existant plus, il est déréférencé.', NEW.nom_schema - USING DETAIL = 'Ancien nom "' || OLD.editeur || '".' ; + USING DETAIL = format('Ancien nom "%s".', OLD.editeur) ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; ELSIF NOT n_role = NEW.editeur -- libellé obsolète de l'éditeur THEN NEW.editeur := n_role ; RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle éditeur, renommé entre temps.', NEW.nom_schema - USING DETAIL = 'Ancien nom "' || OLD.editeur || '", nouveau nom "' || NEW.editeur || '".' ; + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.editeur, NEW.editeur) ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN editeur') ; END IF ; END IF ; @@ -5026,14 +5296,14 @@ BEGIN NEW.lecteur := NULL ; NEW.oid_lecteur := NULL ; RAISE NOTICE '[table de gestion] Schéma %. Le rôle lecteur n''existant plus, il est déréférencé.', NEW.nom_schema - USING DETAIL = 'Ancien nom "' || OLD.lecteur || '".' ; + USING DETAIL = format('Ancien nom "%s".', OLD.lecteur) ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; ELSIF NOT n_role = NEW.lecteur -- libellé obsolète du lecteur THEN NEW.lecteur := n_role ; RAISE NOTICE '[table de gestion] Schéma %. Mise à jour du libellé du rôle lecteur, renommé entre temps.', NEW.nom_schema - USING DETAIL = 'Ancien nom "' || OLD.lecteur || '", nouveau nom "' || NEW.lecteur || '".' ; + USING DETAIL = format('Ancien nom "%s", nouveau nom "%s".', OLD.lecteur, NEW.lecteur) ; NEW.ctrl := array_append(NEW.ctrl, 'CLEAN lecteur') ; END IF ; END IF ; @@ -5092,7 +5362,8 @@ BEGIN IF NEW.bloc IS NULL THEN NEW.bloc := 'd' ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; @@ -5113,7 +5384,8 @@ BEGIN -- et le bloc + s'il y a un ancien bloc récupérable THEN NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; - RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; -- on ne reprend pas l'ancien nom au cas où autre chose que le préfixe aurait été -- changé. @@ -5122,10 +5394,12 @@ BEGIN -- parallèle + s'il y a un ancien bloc récupérable THEN NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; - RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; NEW.bloc := 'd' ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; ELSIF NEW.bloc = 'd' AND OLD.bloc = 'd' AND OLD.nom_schema ~ '^[a-ce-z]_' @@ -5148,7 +5422,8 @@ BEGIN -- mise à la corbeille d'un schéma sans bloc THEN NEW.bloc := 'd' ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; NEW.nom_schema := substring(NEW.nom_schema, '^d_(.*)$') ; RAISE NOTICE '[table de gestion] Le préfixe du schéma % a été supprimé.', NEW.nom_schema ; @@ -5165,10 +5440,12 @@ BEGIN -- valeur que d THEN NEW.nom_schema := regexp_replace(NEW.nom_schema, '^(d)_', OLD.bloc || '_') ; - RAISE NOTICE '[table de gestion] Restauration du préfixe du schéma %.', NEW.nom_schema || ' d''après son ancien bloc (' || OLD.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Restauration du préfixe du schéma %s d''après son ancien bloc (%s).', + NEW.nom_schema, OLD.bloc) ; NEW.bloc := 'd' ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; END IF ; END IF ; @@ -5196,13 +5473,15 @@ BEGIN ELSE -- sinon, on met le préfixe du nom du schéma dans bloc NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; END IF ; ELSE -- sur un INSERT, -- on met le préfixe du nom du schéma dans bloc NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; END IF ; ELSIF NEW.bloc IS NULL -- si bloc est NULL, et que (sous-entendu) le nom du schéma ne @@ -5240,7 +5519,8 @@ BEGIN -- on met à jour le bloc selon le nouveau préfixe du schéma THEN NEW.bloc := substring(NEW.nom_schema, '^([a-z])_') ; - RAISE NOTICE '[table de gestion] Mise à jour du bloc pour le schéma %.', NEW.nom_schema || ' (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du bloc pour le schéma %s (%s).', + NEW.nom_schema, NEW.bloc) ; ELSIF NOT NEW.bloc ~ '^[a-z]$' -- si le nouveau bloc est invalide, on renvoie une erreur THEN @@ -5248,7 +5528,8 @@ BEGIN ELSE -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; - RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; END IF ; ELSIF NOT NEW.bloc ~ '^[a-z]$' -- (sur un INSERT) @@ -5260,7 +5541,8 @@ BEGIN -- (sur un INSERT) -- si le bloc est valide, on met à jour le préfixe du schéma d'après le bloc NEW.nom_schema := regexp_replace(NEW.nom_schema, '^([a-z])?_', NEW.bloc || '_') ; - RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; END IF ; ELSIF NOT NEW.bloc ~ '^[a-z]$' -- (si le nom du schéma ne contient pas de préfixe valide) @@ -5283,12 +5565,14 @@ BEGIN ELSE -- sinon, préfixage du schéma selon le bloc NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; - RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; END IF ; ELSE -- sur un INSERT, préfixage du schéma selon le bloc NEW.nom_schema := NEW.bloc || '_' || NEW.nom_schema ; - RAISE NOTICE '[table de gestion] Mise à jour du préfixe du schéma %.', NEW.nom_schema || ' d''après son bloc (' || NEW.bloc || ')' ; + RAISE NOTICE USING MESSAGE = format('[table de gestion] Mise à jour du préfixe du schéma %s d''après son bloc (%s)', + NEW.nom_schema, NEW.bloc) ; END IF ; -- le trigger AFTER se chargera de renommer physiquement le -- schéma d'autant que de besoin @@ -5360,7 +5644,7 @@ BEGIN IF NOT pg_has_role(OLD.producteur, 'USAGE') THEN RAISE EXCEPTION 'TB20. Opération interdite (schéma %).', OLD.nom_schema - USING DETAIL = 'Seul le rôle producteur ' || OLD.producteur || ' (super-utilisateur) peut modifier ce schéma.' ; + USING DETAIL = format('Seul le rôle producteur %s (super-utilisateur) peut modifier ce schéma.', OLD.producteur) ; END IF ; END IF ; @@ -5371,7 +5655,7 @@ BEGIN IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN RAISE EXCEPTION 'TB21. Opération interdite (schéma %).', NEW.nom_schema - USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut créer un schéma dont il est identifié comme producteur.' ; + USING DETAIL = format('Seul le super-utilisateur %s peut créer un schéma dont il est identifié comme producteur.', NEW.producteur) ; END IF ; END IF ; @@ -5382,7 +5666,7 @@ BEGIN IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN RAISE EXCEPTION 'TB24. Opération interdite (schéma %).', NEW.nom_schema - USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut se désigner comme producteur d''un schéma.' ; + USING DETAIL = format('Seul le super-utilisateur %s peut se désigner comme producteur d''un schéma.', NEW.producteur) ; END IF ; END IF ; @@ -5397,7 +5681,7 @@ BEGIN IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN RAISE EXCEPTION 'TB22. Opération interdite (schéma %).', NEW.nom_schema - USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut créer un schéma dont il est identifié comme producteur.' ; + USING DETAIL = format('Seul le super-utilisateur %s peut créer un schéma dont il est identifié comme producteur.', NEW.producteur) ; END IF ; END IF ; @@ -5408,7 +5692,7 @@ BEGIN IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN RAISE EXCEPTION 'TB25. Opération interdite (schéma %).', NEW.nom_schema - USING DETAIL = 'Seul le super-utilisateur ' || NEW.producteur || ' peut référencer dans ASGARD un schéma dont il est identifié comme producteur.' ; + USING DETAIL = format('Seul le super-utilisateur %s peut référencer dans ASGARD un schéma dont il est identifié comme producteur.', NEW.producteur) ; END IF ; END IF ; END IF ; @@ -5428,7 +5712,7 @@ $BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() IS 'ASGARD. Fonction appelée par le trigger qui valide les modifications de la table de gestion.'; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_before() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_before sur z_asgard_admin.gestion_schema, qui valide et normalise les informations saisies dans la table de gestion avant leur enregistrement.' ; -- Trigger: asgard_on_modify_gestion_schema_before @@ -5439,7 +5723,7 @@ CREATE TRIGGER asgard_on_modify_gestion_schema_before FOR EACH ROW EXECUTE PROCEDURE z_asgard_admin.asgard_on_modify_gestion_schema_before() ; -COMMENT ON TRIGGER asgard_on_modify_gestion_schema_before ON z_asgard_admin.gestion_schema IS 'ASGARD. Trigger qui valide les modifications de la table de gestion.'; +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_before ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui valide et normalise les informations saisies dans la table de gestion avant leur enregistrement.' ; @@ -5447,14 +5731,15 @@ COMMENT ON TRIGGER asgard_on_modify_gestion_schema_before ON z_asgard_admin.gest -- Function: z_asgard_admin.asgard_on_modify_gestion_schema_after() -CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() RETURNS trigger +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() + RETURNS trigger LANGUAGE plpgsql AS $BODY$ -/* OBJET : Fonction exécutée par le trigger asgard_on_modify_gestion_schema_after, - qui répercute physiquement les modifications de la table de gestion. -CIBLES : z_asgard_admin.gestion_schema. -PORTEE : FOR EACH ROW. -DECLENCHEMENT : AFTER INSERT OR UPDATE.*/ +/* Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_after +sur z_asgard_admin.gestion_schema, qui répercute physiquement les modifications +de la table de gestion. + +*/ DECLARE utilisateur text ; createur text ; @@ -5530,8 +5815,9 @@ BEGIN IF NOT FOUND AND OLD.creation AND (NEW.ctrl IS NULL OR NOT 'CLEAN producteur' = ANY(array_remove(NEW.ctrl, NULL))) THEN RAISE NOTICE '[table de gestion] ANOMALIE. Schéma %. L''OID actuellement renseigné pour le producteur dans la table de gestion est invalide. Poursuite avec l''OID du propriétaire courant du schéma.', OLD.nom_schema ; - SELECT replace(nspowner::regrole::text, '"', '') INTO a_producteur + SELECT rolname INTO a_producteur FROM pg_catalog.pg_namespace + LEFT JOIN pg_catalog.pg_roles ON pg_roles.oid = nspowner WHERE pg_namespace.oid = NEW.oid_schema ; IF NOT FOUND THEN @@ -5544,8 +5830,7 @@ BEGIN IF NOT NEW.oid_schema::regnamespace::text = quote_ident(NEW.nom_schema) -- le schéma existe et ne porte pas déjà le nom NEW.nom_schema THEN - EXECUTE 'ALTER SCHEMA '|| NEW.oid_schema::regnamespace::text || - ' RENAME TO ' || quote_ident(NEW.nom_schema) ; + EXECUTE format('ALTER SCHEMA %s RENAME TO %I', NEW.oid_schema::regnamespace, NEW.nom_schema) ; RAISE NOTICE '... Le schéma % a été renommé.', NEW.nom_schema ; END IF ; -- exclusion des remontées d'event trigger correspondant @@ -5594,10 +5879,10 @@ BEGIN RAISE EXCEPTION 'TA2. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.producteur USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux producteurs.' ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(createur) ; - EXECUTE 'CREATE ROLE ' || quote_ident(NEW.producteur) ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.producteur) ; RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.producteur ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; ELSE -- si le rôle producteur existe, on vérifie qu'il n'a pas l'option LOGIN -- les superusers avec LOGIN (comme postgres) sont tolérés @@ -5637,8 +5922,8 @@ BEGIN IF createur IS NULL OR b_superuser THEN RAISE EXCEPTION 'TA4. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur - USING HINT = 'Votre rôle doit être membre de ' || NEW.producteur - || ' ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + USING HINT = format('Votre rôle doit être membre de %s ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + NEW.producteur) ; END IF ; END IF ; IF TG_OP = 'UPDATE' @@ -5652,31 +5937,31 @@ BEGIN IF createur IS NULL OR b_superuser THEN RAISE EXCEPTION 'TA5. Opération interdite. Permissions insuffisantes pour le rôle %.', a_producteur - USING HINT = 'Votre rôle doit être membre de ' || a_producteur - || ' ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + USING HINT = format('Votre rôle doit être membre de %s ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + a_producteur) ; END IF ; END IF ; END IF ; IF b_test THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; -- par commodité, on rend createur membre à la fois de NEW et (si besoin) -- de OLD.producteur, même si l'utilisateur avait déjà accès à -- l'un des deux par ailleurs : IF NOT pg_has_role(createur, NEW.producteur, 'USAGE') AND NOT b_superuser THEN - EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO ' || quote_ident(createur) ; - RAISE NOTICE '... Permission accordée à %.', createur || ' sur le rôle ' || NEW.producteur ; + EXECUTE format('GRANT %I TO %I', NEW.producteur, createur) ; + RAISE NOTICE USING MESSAGE = format('... Permission accordée à %s sur le rôle %s.', createur, NEW.producteur) ; END IF ; IF TG_OP = 'UPDATE' THEN IF NOT pg_has_role(createur, a_producteur, 'USAGE') AND NOT b_superuser THEN - EXECUTE 'GRANT ' || quote_ident(a_producteur) || ' TO ' || quote_ident(createur) ; - RAISE NOTICE '... Permission accordée à %.', createur || ' sur le rôle ' || a_producteur ; + EXECUTE format('GRANT %I TO %I', a_producteur, createur) ; + RAISE NOTICE USING MESSAGE = format('... Permission accordée à %s sur le rôle %s.', createur, a_producteur) ; END IF ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; -- permission de g_admin sur le producteur, s'il y a encore lieu @@ -5690,25 +5975,25 @@ BEGIN THEN IF createur IS NOT NULL THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; - EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin' ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; ELSE SELECT grantee INTO administrateur FROM information_schema.applicable_roles WHERE is_grantable = 'YES' AND role_name = NEW.producteur ; IF FOUND THEN - EXECUTE 'SET ROLE ' || quote_ident(administrateur) ; - EXECUTE 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin' ; + EXECUTE format('SET ROLE %I', administrateur) ; + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; ELSE RAISE EXCEPTION 'TA6. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur - USING DETAIL = 'GRANT ' || quote_ident(NEW.producteur) || ' TO g_admin', - HINT = 'Votre rôle doit être membre de ' || NEW.producteur - || ' avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.' ; + USING DETAIL = format('GRANT %I TO g_admin', NEW.producteur), + HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', + NEW.producteur) ; END IF ; END IF ; END IF ; @@ -5749,10 +6034,10 @@ BEGIN RAISE EXCEPTION 'TA7. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.editeur USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(createur) ; - EXECUTE 'CREATE ROLE ' || quote_ident(NEW.editeur) ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.editeur) ; RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.editeur ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; -- mise à jour du champ d'OID de l'éditeur @@ -5811,10 +6096,10 @@ BEGIN RAISE EXCEPTION 'TA8. Opération interdite. Vous n''êtes pas habilité à créer le rôle %.', NEW.lecteur USING HINT = 'Être membre d''un rôle disposant des attributs CREATEROLE et INHERIT est nécessaire pour créer de nouveaux éditeurs.' ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(createur) ; - EXECUTE 'CREATE ROLE ' || quote_ident(NEW.lecteur) ; + EXECUTE format('SET ROLE %I', createur) ; + EXECUTE format('CREATE ROLE %I', NEW.lecteur) ; RAISE NOTICE '... Le rôle de groupe % a été créé.', NEW.lecteur ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; -- mise à jour du champ d'OID du lecteur @@ -5873,7 +6158,7 @@ BEGIN -- s'il est habilité à créer des schémas IF createur IS NOT NULL THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; END IF ; IF NOT has_database_privilege(current_database(), 'CREATE') OR NOT pg_has_role(NEW.producteur, 'USAGE') @@ -5882,8 +6167,8 @@ BEGIN USING HINT = 'Être membre d''un rôle disposant du privilège CREATE sur la base de données est nécessaire pour créer des schémas.' ; END IF ; END IF ; - EXECUTE 'CREATE SCHEMA ' || quote_ident(NEW.nom_schema) || ' AUTHORIZATION ' || quote_ident(NEW.producteur) ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('CREATE SCHEMA %I AUTHORIZATION %I', NEW.nom_schema, NEW.producteur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; RAISE NOTICE '... Le schéma % a été créé.', NEW.nom_schema ; ELSE RAISE NOTICE '(schéma % pré-existant)', NEW.nom_schema ; @@ -5938,12 +6223,12 @@ BEGIN -- NOINHERIT) aura les privilèges nécessaires IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; ELSIF TG_OP = 'UPDATE' THEN IF NOT pg_has_role(a_producteur, 'USAGE') THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; END IF ; END IF ; @@ -5973,7 +6258,7 @@ BEGIN PERFORM z_asgard.asgard_admin_proprietaire(NEW.nom_schema, NEW.producteur) ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; ------ APPLICATION DES DROITS DE L'EDITEUR ------ @@ -6004,7 +6289,7 @@ BEGIN -- NOINHERIT) aura les privilèges nécessaires IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; END IF ; IF TG_OP = 'UPDATE' @@ -6070,18 +6355,18 @@ BEGIN THEN RAISE NOTICE 'application des privilèges standards pour le rôle éditeur du schéma % :', NEW.nom_schema ; - EXECUTE 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; - RAISE NOTICE '> %', 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; - EXECUTE 'GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; - RAISE NOTICE '> %', 'GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + EXECUTE format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; - EXECUTE 'GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; - RAISE NOTICE '> %', 'GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.editeur) ; + EXECUTE format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; + RAISE NOTICE '> %', format('GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.editeur) ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; ------ APPLICATION DES DROITS DU LECTEUR ------ @@ -6113,7 +6398,7 @@ BEGIN -- NOINHERIT) aura les privilèges nécessaires IF NOT pg_has_role(NEW.producteur, 'USAGE') THEN - EXECUTE 'SET ROLE ' || quote_ident(createur) ; + EXECUTE format('SET ROLE %I', createur) ; END IF ; IF TG_OP = 'UPDATE' @@ -6179,18 +6464,18 @@ BEGIN THEN RAISE NOTICE 'application des privilèges standards pour le rôle lecteur du schéma % :', NEW.nom_schema ; - EXECUTE 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; - RAISE NOTICE '> %', 'GRANT USAGE ON SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT USAGE ON SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; - EXECUTE 'GRANT SELECT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; - RAISE NOTICE '> %', 'GRANT SELECT ON ALL TABLES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; - EXECUTE 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; - RAISE NOTICE '> %', 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA ' || quote_ident(NEW.nom_schema) || ' TO ' || quote_ident(NEW.lecteur) ; + EXECUTE format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; + RAISE NOTICE '> %', format('GRANT SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', NEW.nom_schema, NEW.lecteur) ; END IF ; - EXECUTE 'SET ROLE ' || quote_ident(utilisateur) ; + EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; RETURN NULL ; @@ -6209,7 +6494,7 @@ $BODY$ ; ALTER FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() OWNER TO g_admin ; -COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() IS 'ASGARD. Fonction appelée par le trigger qui répercute physiquement les modifications de la table de gestion.' ; +COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_on_modify_gestion_schema_after sur z_asgard_admin.gestion_schema, qui répercute physiquement les modifications de la table de gestion.' ; -- Trigger: asgard_on_modify_gestion_schema_after @@ -6218,14 +6503,13 @@ CREATE TRIGGER asgard_on_modify_gestion_schema_after AFTER INSERT OR UPDATE ON z_asgard_admin.gestion_schema FOR EACH ROW - EXECUTE PROCEDURE z_asgard_admin.asgard_on_modify_gestion_schema_after(); + EXECUTE PROCEDURE z_asgard_admin.asgard_on_modify_gestion_schema_after() ; -COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Trigger qui répercute physiquement les modifications de la table de gestion.' ; +COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui répercute physiquement les modifications de la table de gestion.' ; -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ----------------------------------------------------------- ------ 6 - GESTION DES PERMISSIONS SUR LAYER_STYLES ------ ----------------------------------------------------------- @@ -6236,23 +6520,34 @@ COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gesti -- Function: z_asgard.asgard_has_role_usage(text, text) -CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage(role_parent text, role_enfant text DEFAULT current_user) +CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( + role_parent text, + role_enfant text DEFAULT current_user + ) RETURNS boolean LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction détermine si un rôle est membre d'un autre ( - y compris indirectement) et hérite de ses droits. Elle est - équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') - en plus permissif - elle renvoie False quand l'un des rôles - n'existe pas plutôt que d'échouer. -ARGUMENTS : -- role_parent est le nom du rôle dont on souhaite savoir si l'autre -est membre ; -- (optionnel) role_enfant est le nom du rôle dont on souhaite savoir -s'il est membre de l'autre. Si non renseigné, la fonction testera -l'utilisateur courant. -SORTIE : True si la relation entre les rôles est vérifiée. False -si elle ne l'est pas ou si l'un des rôles n'existe pas. */ +/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. + + Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') + en plus permissif - elle renvoie False quand l'un des rôles + n'existe pas plutôt que d'échouer. + + Parameters + ---------- + role_parent : text + Nom du rôle dont on souhaite savoir si l'autre est membre. + role_enfant : text, optional + Nom du rôle dont on souhaite savoir s'il est membre de l'autre. + Si non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si la relation entre les rôles est vérifiée. False + si elle ne l'est pas ou si l'un des rôles n'existe pas. + +*/ BEGIN RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; @@ -6273,28 +6568,37 @@ COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le se -- Function: z_asgard.asgard_is_relation_owner(text, text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_relation_owner( - nom_schema text, - nom_relation text, - nom_role text DEFAULT current_user - ) + nom_schema text, + nom_relation text, + nom_role text DEFAULT current_user + ) RETURNS boolean LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction détermine si un rôle est membre du - propriétaire d'une table, vue ou autre relation. -ARGUMENTS : -- nom_schema est une chaîne de caractères correspondant au nom -du schéma contenant la relation ; -- nom_relation est une chaîne de caractères correspondant au nom -de la relation ; -- (optionnel) nom_role est le nom du rôle dont on veut vérifier -les permissions. Si non renseigné, la fonction testera -l'utilisateur courant. -Tous les arguments sont en écriture naturelle, sans les -guillemets des identifiants PostgreSQL. -SORTIE : True si le rôle est membre du propriétaire de la relation. -False sinon, incluant les cas où le rôle ou la relation n'existe -pas. */ +/* Détermine si un rôle est membre du propriétaire d'une table, vue ou autre relation. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant au nom du schéma dont + dépend la relation. + nom_relation : text + Chaîne de caractères correspondant au nom de la relation. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si non + renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du propriétaire de la relation. + False sinon, incluant les cas où le rôle ou la relation n'existe + pas. + +*/ DECLARE owner text ; BEGIN @@ -6318,31 +6622,39 @@ $_$; ALTER FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) IS 'ASGARD. Le rôle est-il membre du propriétaire de la relation considérée ?' ; +COMMENT ON FUNCTION z_asgard.asgard_is_relation_owner(text, text, text) IS 'ASGARD. Détermine si un rôle est membre du propriétaire d''une table, vue ou autre relation.' ; -- Function: z_asgard.asgard_is_producteur(text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_producteur( - schema_cible text, - nom_role text DEFAULT current_user - ) + schema_cible text, + nom_role text DEFAULT current_user + ) RETURNS boolean LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction détermine si le rôle considéré est membre - du rôle producteur d'un schéma donné. -ARGUMENTS : -- nom_schema est une chaîne de caractères correspondant à un -nom de schéma ; -- (optionnel) nom_role est le nom du rôle dont on veut vérifier -les permissions. Si non renseigné, la fonction testera -l'utilisateur courant. -Tous les arguments sont en écriture naturelle, sans les -guillemets des identifiants PostgreSQL. -SORTIE : True si le rôle est membre du rôle producteur du schéma. -False si le schéma n'existe pas ou si le rôle n'est pas membre de -son producteur. */ +/* Détermine si le rôle considéré est membre du rôle producteur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle producteur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son producteur. + +*/ DECLARE producteur text ; BEGIN @@ -6351,7 +6663,7 @@ BEGIN FROM z_asgard.gestion_schema_read_only WHERE gestion_schema_read_only.nom_schema = schema_cible ; - IF NOT FOUND + IF producteur IS NULL THEN RETURN False ; END IF ; @@ -6359,36 +6671,44 @@ BEGIN RETURN z_asgard.asgard_has_role_usage(producteur, nom_role) ; END -$_$; +$_$ ; ALTER FUNCTION z_asgard.asgard_is_producteur(text, text) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_is_producteur(text, text) IS 'ASGARD. Le rôle est-il membre du producteur du schéma considéré ?' ; +COMMENT ON FUNCTION z_asgard.asgard_is_producteur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle producteur d''un schéma donné.' ; -- Function: z_asgard.asgard_is_editeur(text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_editeur( - schema_cible text, - nom_role text DEFAULT current_user - ) + schema_cible text, + nom_role text DEFAULT current_user + ) RETURNS boolean LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction détermine si le rôle considéré est membre - du rôle éditeur d'un schéma donné. -ARGUMENTS : -- nom_schema est une chaîne de caractères correspondant à un -nom de schéma ; -- (optionnel) nom_role est le nom du rôle dont on veut vérifier -les permissions. Si non renseigné, la fonction testera -l'utilisateur courant. -Tous les arguments sont en écriture naturelle, sans les -guillemets des identifiants PostgreSQL. -SORTIE : True si le rôle est membre du rôle editeur du schéma. -False si le schéma n'existe pas ou si le rôle n'est pas membre de -son éditeur. */ +/* Détermine si le rôle considéré est membre du rôle éditeur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle éditeur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son éditeur. + +*/ DECLARE editeur text ; BEGIN @@ -6410,31 +6730,39 @@ $_$; ALTER FUNCTION z_asgard.asgard_is_editeur(text, text) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_is_editeur(text, text) IS 'ASGARD. Le rôle est-il membre du éditeur du schéma considéré ?' ; +COMMENT ON FUNCTION z_asgard.asgard_is_editeur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle éditeur d''un schéma donné.' ; -- Function: z_asgard.asgard_is_lecteur(text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_lecteur( - schema_cible text, - nom_role text DEFAULT current_user - ) + schema_cible text, + nom_role text DEFAULT current_user + ) RETURNS boolean LANGUAGE plpgsql AS $_$ -/* OBJET : Cette fonction détermine si le rôle considéré est membre - du rôle lecteur d'un schéma donné. -ARGUMENTS : -- nom_schema est une chaîne de caractères correspondant à un -nom de schéma ; -- (optionnel) nom_role est le nom du rôle dont on veut vérifier -les permissions. Si non renseigné, la fonction testera -l'utilisateur courant. -Tous les arguments sont en écriture naturelle, sans les -guillemets des identifiants PostgreSQL. -SORTIE : True si le rôle est membre du rôle lecteur du schéma. -False si le schéma n'existe pas ou si le rôle n'est pas membre de -son lecteur. */ +/* Détermine si le rôle considéré est membre du rôle lecteur d'un schéma donné. + + Tous les arguments sont en écriture naturelle, sans les + guillemets des identifiants PostgreSQL. + + Parameters + ---------- + nom_schema : text + Chaîne de caractères correspondant à un nom de schéma. + nom_role : text, optional + Nom du rôle dont on veut vérifier les permissions. Si + non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si le rôle est membre du rôle lecteur du schéma. + False si le schéma n'existe pas ou si le rôle n'est pas + membre de son lecteur. + +*/ DECLARE lecteur text ; BEGIN @@ -6456,7 +6784,7 @@ $_$; ALTER FUNCTION z_asgard.asgard_is_lecteur(text, text) OWNER TO g_admin_ext ; -COMMENT ON FUNCTION z_asgard.asgard_is_lecteur(text, text) IS 'ASGARD. Le rôle est-il membre du lecteur du schéma considéré ?' ; +COMMENT ON FUNCTION z_asgard.asgard_is_lecteur(text, text) IS 'ASGARD. Détermine si le rôle considéré est membre du rôle lecteur d''un schéma donné.' ; ------ 6.2 - FONCTION D'ADMINISTRATION DES PERMISSIONS SUR LAYER_STYLES ------ diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index 46eefeb..1573d51 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -61,7 +61,7 @@ EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t000() IS 'ASGARD recette. TEST : .' ; */ @@ -136,65 +136,48 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t001() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; + e_mssg text ; + e_detl text ; BEGIN - ------ création ------ CREATE SCHEMA c_bibliotheque ; - SELECT creation - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_bibliotheque' ; - - r := b ; + ASSERT (SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque'), 'échec assertion #1' ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_bibliotheque' ; - - r := r AND b ; + ASSERT 'c_bibliotheque' IN (SELECT nspname FROM pg_namespace), + 'échec assertion #2' ; ------ suppression ------ DROP SCHEMA c_bibliotheque ; - SELECT NOT creation - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_bibliotheque' ; - - r := r AND b ; - - SELECT count(*) = 0 - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_bibliotheque' ; + ASSERT (SELECT NOT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque'), 'échec assertion #3' ; - r := r AND b ; + ASSERT NOT 'c_bibliotheque' IN (SELECT nspname FROM pg_namespace), + 'échec assertion #4' ; ------ effacement ------ DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_bibliotheque' ; - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_bibliotheque' ; - - r := r AND b ; + ASSERT NOT 'c_bibliotheque' IN (SELECT nom_schema + FROM z_asgard.gestion_schema_usr), 'échec assertion #5' ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t001() IS 'ASGARD recette. TEST : création, suppression, effacement d''un schéma par commandes directes.' ; - -- FUNCTION: z_asgard_recette.t001b() CREATE OR REPLACE FUNCTION z_asgard_recette.t001b() @@ -202,61 +185,45 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t001b() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; + e_mssg text ; + e_detl text ; BEGIN - ------ création ------ CREATE SCHEMA "c_Bibliothèque" ; - SELECT creation - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_Bibliothèque' ; - - r := b ; + ASSERT (SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque'), 'échec assertion #1' ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; - - r := r AND b ; + ASSERT 'c_Bibliothèque' IN (SELECT nspname FROM pg_namespace), + 'échec assertion #2' ; ------ suppression ------ DROP SCHEMA "c_Bibliothèque" ; - SELECT NOT creation - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_Bibliothèque' ; - - r := r AND b ; - - SELECT count(*) = 0 - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; + ASSERT (SELECT NOT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque'), 'échec assertion #3' ; - r := r AND b ; + ASSERT NOT 'c_Bibliothèque' IN (SELECT nspname FROM pg_namespace), + 'échec assertion #4' ; ------ effacement ------ DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE nom_schema = 'c_Bibliothèque' ; - - r := r AND b ; + ASSERT NOT 'c_Bibliothèque' IN (SELECT nom_schema + FROM z_asgard.gestion_schema_usr), 'échec assertion #5' ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t001b() IS 'ASGARD recette. TEST : création, suppression, effacement d''un schéma par commandes directes.' ; @@ -1122,7 +1089,7 @@ BEGIN ------ révocation d''un privilège ------ REVOKE CREATE ON SCHEMA "c_Bibliothèque" FROM "Admin EXT" ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U/"Admin EXT"') INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; @@ -1512,8 +1479,8 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t015b() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; + e_mssg text ; + e_detl text ; BEGIN CREATE ROLE "Admin EXT" ; @@ -1528,53 +1495,38 @@ BEGIN CREATE TABLE "c_Bibliothèque"."Journal du mur_bis" (id serial PRIMARY KEY, jour date, entree text) ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; - - r := b ; - - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) - INTO STRICT b - FROM pg_catalog.pg_class - WHERE relname = 'Journal du mur' ; - - r := r AND b ; + ASSERT (SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || nspowner::regrole::text) + FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque'), 'échec assertion #1' ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) - INTO STRICT b - FROM pg_catalog.pg_class - WHERE relname = 'Journal du mur_id_seq' ; - - r := r AND b ; + ASSERT (SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || relowner::regrole::text) + FROM pg_catalog.pg_class WHERE relname = 'Journal du mur'), 'échec assertion #2' ; + + ASSERT (SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || relowner::regrole::text) + FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq'), 'échec assertion #3' ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) - INTO STRICT b - FROM pg_catalog.pg_class - WHERE relname = 'Journal du mur_bis' ; - - r := r AND b ; + ASSERT (SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || relowner::regrole::text) + FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis'), 'échec assertion #4' ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) - INTO STRICT b - FROM pg_catalog.pg_class - WHERE relname = 'Journal du mur_bis_id_seq' ; - - r := r AND b ; + ASSERT (SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || relowner::regrole::text) + FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis_id_seq'), 'échec assertion #5' ; DROP SCHEMA "c_Bibliothèque" CASCADE ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; DROP ROLE "Admin EXT" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t015b() IS 'ASGARD recette. TEST : désignation d''un éditeur.' ; @@ -1671,35 +1623,35 @@ BEGIN CREATE TABLE "c_Bibliothèque"."Journal du mur_bis" (id serial PRIMARY KEY, jour date, entree text) ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis_id_seq' ; @@ -1846,14 +1798,14 @@ BEGIN r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwad]{4}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -2002,21 +1954,21 @@ BEGIN SET lecteur = 'Admin EXT' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=U' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -2390,21 +2342,21 @@ BEGIN SET editeur = 'Admin EXT' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwa]{3}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwa]{3}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -2519,21 +2471,21 @@ BEGIN SET lecteur = 'Admin EXT' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rw]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rw]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -2647,35 +2599,35 @@ BEGIN CREATE TABLE "c_Bibliothèque"."Journal du mur_bis" (id serial PRIMARY KEY, jour date, entree text) ; - SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=U' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwad]{4}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwad]{4}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwad]{4}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwad]{4}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis_id_seq' ; @@ -2787,35 +2739,35 @@ BEGIN CREATE TABLE "c_Bibliothèque"."Journal du mur_bis" (id serial PRIMARY KEY, jour date, entree text) ; - SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=U' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=U' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_bis_id_seq' ; @@ -3184,21 +3136,21 @@ BEGIN SET editeur = 'Admin EXT' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwa]{3}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rwa]{3}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -3211,21 +3163,21 @@ BEGIN SET editeur = 'public' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwa]{3}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rwa]{3}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=r' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -3366,21 +3318,21 @@ BEGIN SET lecteur = 'Admin EXT' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('"Admin EXT"=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rw]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rw]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('"Admin EXT"=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -3392,21 +3344,21 @@ BEGIN SET lecteur = 'public' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=[UC]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(nspowner::regrole)) + SELECT array_to_string(nspacl, ',') ~ ('^(.*,)?=[UC]{2}' || '[/]' || nspowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rw]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rw]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur' ; r := r AND b ; - SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || z_asgard.asgard_role_trans_acl(relowner::regrole)) + SELECT array_to_string(relacl, ',') ~ ('^(.*,)?=[rU]{2}' || '[/]' || relowner::regrole::text) INTO STRICT b FROM pg_catalog.pg_class WHERE relname = 'Journal du mur_id_seq' ; @@ -5433,8 +5385,8 @@ BEGIN lecteur = 'rec_nouveau_lecteur' WHERE nom_schema = 'c_bibliotheque' ; - SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur'::regrole::text - AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur'::regrole::text + SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur' + AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur' AND nspowner = 'rec_nouveau_producteur'::regrole::oid INTO STRICT r FROM pg_catalog.pg_namespace @@ -5465,7 +5417,8 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t042b() LANGUAGE plpgsql AS $_$ DECLARE - r boolean ; + e_mssg text ; + e_detl text ; BEGIN CREATE SCHEMA "c_Bibliothèque" ; @@ -5476,12 +5429,15 @@ BEGIN lecteur = 'REC"Nouveau lecteur' WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC\Nouvel éditeur"'::regrole) - AND array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC""Nouveau lecteur"'::regrole) - AND nspowner = '"RECNouveauProducteur"'::regrole::oid - INTO STRICT r - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; + ASSERT has_schema_privilege('REC\Nouvel éditeur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #1' ; + + ASSERT has_schema_privilege('REC"Nouveau lecteur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #2' ; + + ASSERT '"RECNouveauProducteur"'::regrole::oid IN (SELECT nspowner + FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque'), + 'échec assertion #3' ; DROP SCHEMA "c_Bibliothèque" CASCADE ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; @@ -5490,13 +5446,18 @@ BEGIN DROP ROLE "REC\Nouvel éditeur" ; DROP ROLE "REC""Nouveau lecteur" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t042b() IS 'ASGARD recette. TEST : création de nouveaux rôles via un UPDATE dans la table de gestion.' ; @@ -5514,8 +5475,8 @@ BEGIN INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur, editeur, lecteur) VALUES ('c_bibliotheque', True, 'rec_nouveau_producteur', 'rec_nouvel_editeur', 'rec_nouveau_lecteur') ; - SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur'::regrole::text - AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur'::regrole::text + SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur' + AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur' AND nspowner = 'rec_nouveau_producteur'::regrole::oid INTO STRICT r FROM pg_catalog.pg_namespace @@ -5546,18 +5507,22 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t043b() LANGUAGE plpgsql AS $_$ DECLARE - r boolean ; + e_mssg text ; + e_detl text ; BEGIN - + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur, editeur, lecteur) VALUES ('c_Bibliothèque', True, 'RECNouveauProducteur', 'REC\Nouvel éditeur', 'REC"Nouveau lecteur') ; - SELECT array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC\Nouvel éditeur"'::regrole) - AND array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC""Nouveau lecteur"'::regrole) - AND nspowner = '"RECNouveauProducteur"'::regrole::oid - INTO STRICT r - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; + ASSERT has_schema_privilege('REC\Nouvel éditeur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #1' ; + + ASSERT has_schema_privilege('REC"Nouveau lecteur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #2' ; + + ASSERT '"RECNouveauProducteur"'::regrole::oid IN (SELECT nspowner + FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque'), + 'échec assertion #3' ; DROP SCHEMA "c_Bibliothèque" CASCADE ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; @@ -5566,13 +5531,18 @@ BEGIN DROP ROLE "REC\Nouvel éditeur" ; DROP ROLE "REC""Nouveau lecteur" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t043b() IS 'ASGARD recette. TEST : création de nouveaux rôles via un INSERT dans la table de gestion.' ; @@ -5595,8 +5565,8 @@ BEGIN SET creation = True WHERE nom_schema = 'c_bibliotheque' ; - SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur'::regrole::text - AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur'::regrole::text + SELECT array_to_string(nspacl, ',') ~ 'rec_nouvel_editeur' + AND array_to_string(nspacl, ',') ~ 'rec_nouveau_lecteur' AND nspowner = 'rec_nouveau_producteur'::regrole::oid INTO STRICT r FROM pg_catalog.pg_namespace @@ -5628,9 +5598,10 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t044b() LANGUAGE plpgsql AS $_$ DECLARE - r boolean ; + e_mssg text ; + e_detl text ; BEGIN - + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur, editeur, lecteur) VALUES ('c_Bibliothèque', False, 'RECNouveauProducteur', 'REC\Nouvel éditeur', 'REC"Nouveau lecteur') ; @@ -5638,12 +5609,15 @@ BEGIN SET creation = True WHERE nom_schema = 'c_Bibliothèque' ; - SELECT array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC\Nouvel éditeur"'::regrole) - AND array_to_string(nspacl, ',') ~ z_asgard.asgard_role_trans_acl('"REC""Nouveau lecteur"'::regrole) - AND nspowner = '"RECNouveauProducteur"'::regrole::oid - INTO STRICT r - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_Bibliothèque' ; + ASSERT has_schema_privilege('REC\Nouvel éditeur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #1' ; + + ASSERT has_schema_privilege('REC"Nouveau lecteur', 'c_Bibliothèque', 'USAGE'), + 'échec assertion #2' ; + + ASSERT '"RECNouveauProducteur"'::regrole::oid IN (SELECT nspowner + FROM pg_catalog.pg_namespace WHERE nspname = 'c_Bibliothèque'), + 'échec assertion #3' ; DROP SCHEMA "c_Bibliothèque" CASCADE ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; @@ -5652,13 +5626,18 @@ BEGIN DROP ROLE "REC\Nouvel éditeur" ; DROP ROLE "REC""Nouveau lecteur" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t044b() IS 'ASGARD recette. TEST : création de nouveaux rôles par bascule de creation.' ; @@ -6130,7 +6109,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) + WHERE array_to_string(defaclacl, ',') ~ 'g_asgard_rec1' AND defaclnamespace = quote_ident('c_bibliotheque')::regnamespace::oid ; r := r AND b ; @@ -6143,8 +6122,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec1[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46-3 > %', r::text ; @@ -6158,8 +6136,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec1[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46-4 > %', r::text ; @@ -6172,8 +6149,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec1[=]U[/]') ; r := r AND b ; RAISE NOTICE '46-5 > %', r::text ; @@ -6185,8 +6161,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_bibliotheque')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46-6 > %', r::text ; @@ -6198,8 +6173,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_bibliotheque')::regnamespace::oid AND defaclrole = quote_ident('g_admin_ext')::regrole::oid AND defaclobjtype = 'S' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][rwU]{3}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][rwU]{3}[/]') ; r := r AND b ; RAISE NOTICE '46-7 > %', r::text ; @@ -6211,8 +6185,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_bibliotheque')::regnamespace::oid AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'f' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=]X[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=]X[/]') ; r := r AND b ; RAISE NOTICE '46-8 > %', r::text ; @@ -6228,7 +6201,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) ; + WHERE array_to_string(defaclacl, ',') ~ 'g_asgard_rec1' ; r := r AND b ; RAISE NOTICE '46-9b > %', r::text ; @@ -6240,8 +6213,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46-10 > %', r::text ; @@ -6255,8 +6227,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46-11 > %', r::text ; @@ -6269,8 +6240,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=]U[/]') ; r := r AND b ; RAISE NOTICE '46-12 > %', r::text ; @@ -6286,7 +6256,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) + WHERE array_to_string(defaclacl, ',') ~ 'g_asgard_rec2' AND defaclnamespace = quote_ident('c_bibliotheque')::regnamespace::oid ; r := r AND b ; @@ -6299,8 +6269,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46-14 > %', r::text ; @@ -6314,8 +6283,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46-15 > %', r::text ; @@ -6328,8 +6296,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec2[=]U[/]') ; r := r AND b ; RAISE NOTICE '46-16 > %', r::text ; @@ -6345,7 +6312,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec2')::regrole) ; + WHERE array_to_string(defaclacl, ',') ~ 'g_asgard_rec2' ; r := r AND b ; RAISE NOTICE '46-18 > %', r::text ; @@ -6429,7 +6396,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD_REC1')::regrole) + WHERE array_to_string(defaclacl, ',') ~ 'g_ASGARD_REC1' AND defaclnamespace = quote_ident('c_Bibliothèque')::regnamespace::oid ; r := r AND b ; @@ -6442,8 +6409,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD_REC1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_ASGARD_REC1[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46b-3 > %', r::text ; @@ -6457,8 +6423,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD_REC1')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_ASGARD_REC1[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46b-4 > %', r::text ; @@ -6471,8 +6436,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD_REC1')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_ASGARD_REC1[=]U[/]') ; r := r AND b ; RAISE NOTICE '46b-5 > %', r::text ; @@ -6484,8 +6448,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Bibliothèque')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46b-6 > %', r::text ; @@ -6497,8 +6460,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Bibliothèque')::regnamespace::oid AND defaclrole = quote_ident('g_admin_ext')::regrole::oid AND defaclobjtype = 'S' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][rwU]{3}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][rwU]{3}[/]') ; r := r AND b ; RAISE NOTICE '46b-7 > %', r::text ; @@ -6510,8 +6472,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Bibliothèque')::regnamespace::oid AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'f' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=]X[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=]X[/]') ; r := r AND b ; RAISE NOTICE '46b-8 > %', r::text ; @@ -6527,7 +6488,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD_REC1')::regrole) ; + WHERE array_to_string(defaclacl, ',') ~ 'g_ASGARD_REC1' ; r := r AND b ; RAISE NOTICE '46b-9b > %', r::text ; @@ -6539,8 +6500,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46b-10 > %', r::text ; @@ -6554,8 +6514,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46b-11 > %', r::text ; @@ -6568,8 +6527,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=]U[/]') ; r := r AND b ; RAISE NOTICE '46b-12 > %', r::text ; @@ -6585,7 +6543,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) + WHERE array_to_string(defaclacl, ',') ~ '"g_ASGARD[[:space:]][*]REC2"' AND defaclnamespace = quote_ident('c_Bibliothèque')::regnamespace::oid ; r := r AND b ; @@ -6598,8 +6556,7 @@ BEGIN WHERE defaclnamespace = quote_ident('c_Librairie')::regnamespace::oid AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '46b-14 > %', r::text ; @@ -6613,8 +6570,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident('g_admin')::regrole::oid AND defaclobjtype = 'n' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=][UC]{2}[/]') ; r := r AND b ; RAISE NOTICE '46b-15 > %', r::text ; @@ -6627,8 +6583,7 @@ BEGIN WHERE defaclnamespace = 0 AND defaclrole = quote_ident(current_user)::regrole::oid AND defaclobjtype = 'T' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) - || '[=]U[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]][*]REC2"[=]U[/]') ; r := r AND b ; RAISE NOTICE '46b-16 > %', r::text ; @@ -6644,7 +6599,7 @@ BEGIN count(*) = 0 INTO STRICT b FROM pg_default_acl - WHERE array_to_string(defaclacl, ',') ~ z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD *REC2')::regrole) ; + WHERE array_to_string(defaclacl, ',') ~ '"g_ASGARD[[:space:]][*]REC2"' ; r := r AND b ; RAISE NOTICE '46b-18 > %', r::text ; @@ -7284,8 +7239,7 @@ BEGIN FROM pg_class WHERE relnamespace = quote_ident('c_librairie')::regnamespace::oid AND relname = 'journal_du_mur' - AND array_to_string(relacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(relacl, ',') ~ ('^(.*[,])?g_asgard_rec1[=][rwadDxt]{7}[/]') ; r := b ; RAISE NOTICE '49-1 > %', r::text ; @@ -7297,8 +7251,7 @@ BEGIN FROM pg_default_acl WHERE defaclnamespace = quote_ident('c_librairie')::regnamespace::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?g_asgard_rec1[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '49-2 > %', r::text ; @@ -7344,8 +7297,7 @@ BEGIN FROM pg_class WHERE relnamespace = quote_ident('c_Lib-rairie')::regnamespace::oid AND relname = 'Journal du mur !' - AND array_to_string(relacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD rec*1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(relacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]]rec[*]1"[=][rwadDxt]{7}[/]') ; r := b ; RAISE NOTICE '49b-1 > %', r::text ; @@ -7357,8 +7309,7 @@ BEGIN FROM pg_default_acl WHERE defaclnamespace = quote_ident('c_Lib-rairie')::regnamespace::oid AND defaclobjtype = 'r' - AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_ASGARD rec*1')::regrole) - || '[=][rwadDxt]{7}[/]') ; + AND array_to_string(defaclacl, ',') ~ ('^(.*[,])?"g_ASGARD[[:space:]]rec[*]1"[=][rwadDxt]{7}[/]') ; r := r AND b ; RAISE NOTICE '49b-2 > %', r::text ; @@ -7405,8 +7356,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_bibliotheque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_admin_ext')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?g_admin_ext[=][UC]{2}[/]') ; r := b ; RAISE NOTICE '50-1 > %', r::text ; @@ -7417,8 +7367,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_bibliotheque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_consult')::regrole) - || '[=]U[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?g_consult[=]U[/]') ; r := b ; RAISE NOTICE '50-2 > %', r::text ; @@ -7436,8 +7385,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_bibliotheque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_admin_ext')::regrole) - || '[=]U[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?g_admin_ext[=]U[/]') ; r := b ; RAISE NOTICE '50-3 > %', r::text ; @@ -7498,8 +7446,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_Bibliothèque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g ADMIN ext')::regrole) - || '[=][UC]{2}[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?"g ADMIN ext"[=][UC]{2}[/]') ; r := b ; RAISE NOTICE '50b-1 > %', r::text ; @@ -7510,8 +7457,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_Bibliothèque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g Consult !')::regrole) - || '[=]U[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?"g Consult !"[=]U[/]') ; r := b ; RAISE NOTICE '50b-2 > %', r::text ; @@ -7529,8 +7475,7 @@ BEGIN INTO STRICT b FROM pg_namespace WHERE nspname = 'c_Bibliothèque' - AND array_to_string(nspacl, ',') ~ ('^(.*[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g ADMIN ext')::regrole) - || '[=]U[/]') ; + AND array_to_string(nspacl, ',') ~ ('^(.*[,])?"g ADMIN ext"[=]U[/]') ; r := b ; RAISE NOTICE '50b-3 > %', r::text ; @@ -7934,12 +7879,12 @@ BEGIN REVOKE USAGE ON SCHEMA z_asgard_admin FROM g_admin_ext ; REVOKE UPDATE, DELETE, INSERT ON TABLE z_asgard_admin.gestion_schema FROM g_admin_ext ; - REVOKE USAGE ON SCHEMA z_asgard FROM g_consult ; - REVOKE SELECT ON TABLE z_asgard.gestion_schema_etr FROM g_consult ; - REVOKE SELECT ON TABLE z_asgard.gestion_schema_usr FROM g_consult ; - REVOKE SELECT ON TABLE z_asgard.asgardmenu_metadata FROM g_consult ; - REVOKE SELECT ON TABLE z_asgard.asgardmanager_metadata FROM g_consult ; - REVOKE SELECT ON TABLE z_asgard.gestion_schema_read_only FROM g_consult ; + REVOKE USAGE ON SCHEMA z_asgard FROM public ; + REVOKE SELECT ON TABLE z_asgard.gestion_schema_etr FROM public ; + REVOKE SELECT ON TABLE z_asgard.gestion_schema_usr FROM public ; + REVOKE SELECT ON TABLE z_asgard.asgardmenu_metadata FROM public ; + REVOKE SELECT ON TABLE z_asgard.asgardmanager_metadata FROM public ; + REVOKE SELECT ON TABLE z_asgard.gestion_schema_read_only FROM public ; -- #21 SELECT count(*) = 10 @@ -7959,12 +7904,12 @@ BEGIN ('z_asgard_admin', 'gestion_schema', 'table', 'INSERT', 'g_admin_ext'), ('z_asgard_admin', 'gestion_schema', 'table', 'UPDATE', 'g_admin_ext'), ('z_asgard_admin', 'gestion_schema', 'table', 'DELETE', 'g_admin_ext'), - ('z_asgard', 'z_asgard', 'schéma', 'USAGE', 'g_consult'), - ('z_asgard', 'gestion_schema_usr', 'vue', 'SELECT', 'g_consult'), - ('z_asgard', 'gestion_schema_etr', 'vue', 'SELECT', 'g_consult'), - ('z_asgard', 'asgardmenu_metadata', 'vue', 'SELECT', 'g_consult'), - ('z_asgard', 'asgardmanager_metadata', 'vue', 'SELECT', 'g_consult'), - ('z_asgard', 'gestion_schema_read_only', 'vue', 'SELECT', 'g_consult') + ('z_asgard', 'z_asgard', 'schéma', 'USAGE', 'public'), + ('z_asgard', 'gestion_schema_usr', 'vue', 'SELECT', 'public'), + ('z_asgard', 'gestion_schema_etr', 'vue', 'SELECT', 'public'), + ('z_asgard', 'asgardmenu_metadata', 'vue', 'SELECT', 'public'), + ('z_asgard', 'asgardmanager_metadata', 'vue', 'SELECT', 'public'), + ('z_asgard', 'gestion_schema_read_only', 'vue', 'SELECT', 'public') ) AS t (a_schema, a_objet, a_type, a_commande, a_role) ON typ_objet = a_type AND nom_schema = a_schema AND nom_objet = a_objet @@ -9492,8 +9437,6 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t055() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean := True ; variante record ; o_roles record ; c_roles record ; @@ -9502,8 +9445,8 @@ DECLARE t text ; e_mssg text ; e_detl text ; - acl_idi text ; - acl_ids text ; + acl_idi aclitem[] ; + acl_ids aclitem[] ; BEGIN CREATE ROLE g_asgard_rec ; @@ -9534,19 +9477,18 @@ BEGIN FOR variante IN ( SELECT * FROM unnest( - ARRAY[1, 2, 3, 4, 5, 6], -- numéro de variante - ARRAY[NULL, NULL, NULL, NULL, NULL, NULL], -- droits du producteur du schéma de départ - ARRAY['[rw]{2}', '[rwU]{3}', '[rw]{2}', '[rw]{2}', '[rw]{2}', '[rwU]{3}'], -- droits du producteur du schéma d'arrivée - ARRAY[NULL, NULL, NULL, '[rU]{2}', NULL, NULL], -- droits de l'éditeur du schéma de départ - ARRAY['[rU]{2}', '[rU]{2}', '[rU]{2}', NULL, '[rU]{2}', '[rU]{2}'], -- droits de l'éditeur du schéma d'arrivée - ARRAY[NULL, NULL, NULL, '[rU]{2}', NULL, NULL], -- droits du lecteur du schéma de départ - ARRAY['[rU]{2}', 'r', '[rU]{2}', NULL, 'r', 'r'], -- droits du lecteur du schéma d'arrivée - ARRAY['[rw]{2}', NULL, NULL, '[rw]{2}', NULL, '[rw]{2}'] -- droits de g_asgard_rec + ARRAY[1, 2, 3, 4, 5, 6], -- numéro de variante + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL], -- droits du producteur du schéma de départ + ARRAY['SELECT,UPDATE', 'SELECT,UPDATE,USAGE', 'SELECT,UPDATE', 'SELECT,UPDATE', 'SELECT,UPDATE', 'SELECT,UPDATE,USAGE'], -- droits du producteur du schéma d'arrivée + ARRAY[NULL, NULL, NULL, 'SELECT,USAGE', NULL, NULL], -- droits de l'éditeur du schéma de départ + ARRAY['SELECT,USAGE', 'SELECT,USAGE', 'SELECT,USAGE', NULL, 'SELECT,USAGE', 'SELECT,USAGE'], -- droits de l'éditeur du schéma d'arrivée + ARRAY[NULL, NULL, NULL, 'SELECT,USAGE', NULL, NULL], -- droits du lecteur du schéma de départ + ARRAY['SELECT,USAGE', 'SELECT', 'SELECT,USAGE', NULL, 'SELECT', 'SELECT'], -- droits du lecteur du schéma d'arrivée + ARRAY['SELECT,UPDATE', NULL, NULL, 'SELECT,UPDATE', NULL, 'SELECT,UPDATE'] -- droits de g_asgard_rec ) AS t (n, dpro_o, dpro_c, dedi_o, dedi_c, dlec_o, dlec_c, drec) ORDER BY n ) LOOP - RAISE NOTICE '-- variante % ----------------', variante.n::text ; PERFORM z_asgard.asgard_initialise_schema(o) ; SELECT producteur, editeur, lecteur @@ -9572,160 +9514,146 @@ BEGIN PERFORM z_asgard.asgard_deplace_obj(o, 'tours_de_garde', 'table', c, variante.n) ; - SELECT array_to_string(relacl, ',') + SELECT relacl INTO STRICT acl_idi FROM pg_class WHERE relnamespace::regnamespace::text = quote_ident(c) AND relname = 'journal_du_mur_idi_seq' ; - SELECT array_to_string(relacl, ',') + SELECT relacl INTO STRICT acl_ids FROM pg_class WHERE relnamespace::regnamespace::text = quote_ident(c) AND relname = 'tours_de_garde_ids_seq' ; -- serial - IF variante.dpro_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) || '[=]') - OR variante.dpro_o IS NOT NULL AND NOT acl_ids ~ ('^' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) - || '[=]' || variante.dpro_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dpro_o' ; - r := False ; - END IF ; - - IF variante.dpro_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) || '[=]') - OR variante.dpro_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) - || '[=]' || variante.dpro_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dpro_c' ; - r := False ; - END IF ; - - IF variante.dedi_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) || '[=]') - OR variante.dedi_o IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) - || '[=]' || variante.dedi_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dedi_o' ; - r := False ; - END IF ; - - IF variante.dedi_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) || '[=]') - OR variante.dedi_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) - || '[=]' || variante.dedi_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dedi_c' ; - r := False ; - END IF ; - - IF variante.dlec_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_o IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) - || '[=]' || variante.dlec_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dlec_o' ; - r := False ; - END IF ; - - IF variante.dlec_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) - || '[=]' || variante.dlec_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dlec_c' ; - r := False ; - END IF ; - - IF variante.drec IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec')::regrole) || '[=]') - OR variante.drec IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec')::regrole) - || '[=]' || variante.drec - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC drec' ; - r := False ; - END IF ; + ASSERT variante.dpro_o IS NULL AND NOT quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_o IS NOT NULL AND quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_o), + format('échec assertion #1 pour la variante %s', variante.n) ; + + ASSERT variante.dpro_c IS NULL AND NOT quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_c IS NOT NULL AND quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_c), + format('échec assertion #2 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_o IS NULL AND NOT quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_o IS NOT NULL AND quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_o), + format('échec assertion #3 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_c IS NULL AND NOT quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_c IS NOT NULL AND quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_c), + format('échec assertion #4 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_o IS NULL AND NOT quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_o IS NOT NULL AND quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_o), + format('échec assertion #5 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_c IS NULL AND NOT quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_c IS NOT NULL AND quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_c), + format('échec assertion #6 pour la variante %s', variante.n) ; + + ASSERT variante.drec IS NULL AND NOT quote_ident('g_asgard_rec')::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.drec IS NOT NULL AND quote_ident('g_asgard_rec')::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.drec), + format('échec assertion #6 pour la variante %s', variante.n) ; -- identity - IF variante.dpro_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) || '[=]') - OR variante.dpro_o IS NOT NULL AND NOT acl_idi ~ ('^' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) - || '[=]' || variante.dpro_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dpro_o' ; - r := False ; - END IF ; - - IF variante.dpro_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) || '[=]') - OR variante.dpro_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) - || '[=]' || variante.dpro_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dpro_c' ; - r := False ; - END IF ; - - IF variante.dedi_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) || '[=]') - OR variante.dedi_o IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) - || '[=]' || variante.dedi_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dedi_o' ; - r := False ; - END IF ; - - IF variante.dedi_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) || '[=]') - OR variante.dedi_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) - || '[=]' || variante.dedi_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dedi_c' ; - r := False ; - END IF ; - - IF variante.dlec_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_o IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) - || '[=]' || variante.dlec_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dlec_o' ; - r := False ; - END IF ; - - IF variante.dlec_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) - || '[=]' || variante.dlec_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dlec_c' ; - r := False ; - END IF ; - - IF variante.drec IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec')::regrole) || '[=]') - OR variante.drec IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard_rec')::regrole) - || '[=]' || variante.drec - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC drec' ; - r := False ; - END IF ; + ASSERT variante.dpro_o IS NULL AND NOT quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_o IS NOT NULL AND quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_o), + format('échec assertion #1 pour la variante %s', variante.n) ; + + ASSERT variante.dpro_c IS NULL AND NOT quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_c IS NOT NULL AND quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_c), + format('échec assertion #2 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_o IS NULL AND NOT quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_o IS NOT NULL AND quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_o), + format('échec assertion #3 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_c IS NULL AND NOT quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_c IS NOT NULL AND quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_c), + format('échec assertion #4 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_o IS NULL AND NOT quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_o IS NOT NULL AND quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_o), + format('échec assertion #5 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_c IS NULL AND NOT quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_c IS NOT NULL AND quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_c), + format('échec assertion #6 pour la variante %s', variante.n) ; + + ASSERT variante.drec IS NULL AND NOT quote_ident('g_asgard_rec')::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.drec IS NOT NULL AND quote_ident('g_asgard_rec')::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.drec), + format('échec assertion #6 pour la variante %s', variante.n) ; t := c ; c := o ; @@ -9745,9 +9673,9 @@ BEGIN DROP ROLE g_asgard_pro2 ; DROP ROLE g_asgard_rec ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; RAISE NOTICE '%', e_mssg @@ -9756,7 +9684,7 @@ EXCEPTION WHEN OTHERS THEN RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t055() IS 'ASGARD recette. TEST : (asgard_deplace_obj) gestion des séquences associées.' ; @@ -9768,8 +9696,6 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t055b() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean := True ; variante record ; o_roles record ; c_roles record ; @@ -9778,8 +9704,8 @@ DECLARE t text ; e_mssg text ; e_detl text ; - acl_idi text ; - acl_ids text ; + acl_idi aclitem[] ; + acl_ids aclitem[] ; BEGIN CREATE ROLE "g_asgard REC!!!" ; @@ -9810,14 +9736,14 @@ BEGIN FOR variante IN ( SELECT * FROM unnest( - ARRAY[1, 2, 3, 4, 5, 6], -- numéro de variante - ARRAY[NULL, NULL, NULL, NULL, NULL, NULL], -- droits du producteur du schéma de départ - ARRAY['[rw]{2}', '[rwU]{3}', '[rw]{2}', '[rw]{2}', '[rw]{2}', '[rwU]{3}'], -- droits du producteur du schéma d'arrivée - ARRAY[NULL, NULL, NULL, '[rU]{2}', NULL, NULL], -- droits de l'éditeur du schéma de départ - ARRAY['[rU]{2}', '[rU]{2}', '[rU]{2}', NULL, '[rU]{2}', '[rU]{2}'], -- droits de l'éditeur du schéma d'arrivée - ARRAY[NULL, NULL, NULL, '[rU]{2}', NULL, NULL], -- droits du lecteur du schéma de départ - ARRAY['[rU]{2}', 'r', '[rU]{2}', NULL, 'r', 'r'], -- droits du lecteur du schéma d'arrivée - ARRAY['[rw]{2}', NULL, NULL, '[rw]{2}', NULL, '[rw]{2}'] -- droits de "g_asgard REC!!!" + ARRAY[1, 2, 3, 4, 5, 6], -- numéro de variante + ARRAY[NULL, NULL, NULL, NULL, NULL, NULL], -- droits du producteur du schéma de départ + ARRAY['SELECT,UPDATE', 'SELECT,UPDATE,USAGE', 'SELECT,UPDATE', 'SELECT,UPDATE', 'SELECT,UPDATE', 'SELECT,UPDATE,USAGE'], -- droits du producteur du schéma d'arrivée + ARRAY[NULL, NULL, NULL, 'SELECT,USAGE', NULL, NULL], -- droits de l'éditeur du schéma de départ + ARRAY['SELECT,USAGE', 'SELECT,USAGE', 'SELECT,USAGE', NULL, 'SELECT,USAGE', 'SELECT,USAGE'], -- droits de l'éditeur du schéma d'arrivée + ARRAY[NULL, NULL, NULL, 'SELECT,USAGE', NULL, NULL], -- droits du lecteur du schéma de départ + ARRAY['SELECT,USAGE', 'SELECT', 'SELECT,USAGE', NULL, 'SELECT', 'SELECT'], -- droits du lecteur du schéma d'arrivée + ARRAY['SELECT,UPDATE', NULL, NULL, 'SELECT,UPDATE', NULL, 'SELECT,UPDATE'] -- droits de g_asgard_rec ) AS t (n, dpro_o, dpro_c, dedi_o, dedi_c, dlec_o, dlec_c, drec) ORDER BY n ) @@ -9848,160 +9774,146 @@ BEGIN PERFORM z_asgard.asgard_deplace_obj(o, 'tours-de-garde', 'table', c, variante.n) ; - SELECT array_to_string(relacl, ',') + SELECT relacl INTO STRICT acl_idi FROM pg_class WHERE relnamespace::regnamespace::text = quote_ident(c) AND relname = 'Journal du mur_i*di_seq' ; - SELECT array_to_string(relacl, ',') + SELECT relacl INTO STRICT acl_ids FROM pg_class WHERE relnamespace::regnamespace::text = quote_ident(c) AND relname = 'tours-de-garde_IDS_seq' ; -- serial - IF variante.dpro_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) || '[=]') - OR variante.dpro_o IS NOT NULL AND NOT acl_ids ~ ('^' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) - || '[=]' || variante.dpro_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dpro_o' ; - r := False ; - END IF ; - - IF variante.dpro_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) || '[=]') - OR variante.dpro_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) - || '[=]' || variante.dpro_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dpro_c' ; - r := False ; - END IF ; - - IF variante.dedi_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) || '[=]') - OR variante.dedi_o IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) - || '[=]' || variante.dedi_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dedi_o' ; - r := False ; - END IF ; - - IF variante.dedi_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) || '[=]') - OR variante.dedi_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) - || '[=]' || variante.dedi_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dedi_c' ; - r := False ; - END IF ; - - IF variante.dlec_o IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_o IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) - || '[=]' || variante.dlec_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dlec_o' ; - r := False ; - END IF ; - - IF variante.dlec_c IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_c IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) - || '[=]' || variante.dlec_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC dlec_c' ; - r := False ; - END IF ; - - IF variante.drec IS NULL AND acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard REC!!!')::regrole) || '[=]') - OR variante.drec IS NOT NULL AND NOT acl_ids ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard REC!!!')::regrole) - || '[=]' || variante.drec - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_ids = %', acl_ids ; - RAISE NOTICE 'ECHEC drec' ; - r := False ; - END IF ; + ASSERT variante.dpro_o IS NULL AND NOT quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_o IS NOT NULL AND quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_o), + format('échec assertion #1 pour la variante %s', variante.n) ; + + ASSERT variante.dpro_c IS NULL AND NOT quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_c IS NOT NULL AND quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_c), + format('échec assertion #2 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_o IS NULL AND NOT quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_o IS NOT NULL AND quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_o), + format('échec assertion #3 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_c IS NULL AND NOT quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_c IS NOT NULL AND quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_c), + format('échec assertion #4 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_o IS NULL AND NOT quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_o IS NOT NULL AND quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_o), + format('échec assertion #5 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_c IS NULL AND NOT quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_c IS NOT NULL AND quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_c), + format('échec assertion #6 pour la variante %s', variante.n) ; + + ASSERT variante.drec IS NULL AND NOT quote_ident('g_asgard REC!!!')::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable)) + OR variante.drec IS NOT NULL AND quote_ident('g_asgard REC!!!')::regrole + IN (SELECT grantee FROM aclexplode(acl_ids) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.drec), + format('échec assertion #6 pour la variante %s', variante.n) ; -- identity - IF variante.dpro_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) || '[=]') - OR variante.dpro_o IS NOT NULL AND NOT acl_idi ~ ('^' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.producteur)::regrole) - || '[=]' || variante.dpro_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dpro_o' ; - r := False ; - END IF ; - - IF variante.dpro_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) || '[=]') - OR variante.dpro_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole) - || '[=]' || variante.dpro_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dpro_c' ; - r := False ; - END IF ; - - IF variante.dedi_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) || '[=]') - OR variante.dedi_o IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.editeur)::regrole) - || '[=]' || variante.dedi_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dedi_o' ; - r := False ; - END IF ; - - IF variante.dedi_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) || '[=]') - OR variante.dedi_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.editeur)::regrole) - || '[=]' || variante.dedi_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dedi_c' ; - r := False ; - END IF ; - - IF variante.dlec_o IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_o IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(o_roles.lecteur)::regrole) - || '[=]' || variante.dlec_o - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dlec_o' ; - r := False ; - END IF ; - - IF variante.dlec_c IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) || '[=]') - OR variante.dlec_c IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.lecteur)::regrole) - || '[=]' || variante.dlec_c - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC dlec_c' ; - r := False ; - END IF ; - - IF variante.drec IS NULL AND acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard REC!!!')::regrole) || '[=]') - OR variante.drec IS NOT NULL AND NOT acl_idi ~ ('^(.+[,])?' || z_asgard.asgard_role_trans_acl(quote_ident('g_asgard REC!!!')::regrole) - || '[=]' || variante.drec - || '/' || z_asgard.asgard_role_trans_acl(quote_ident(c_roles.producteur)::regrole)) - THEN - RAISE NOTICE 'acl_idi = %', acl_idi ; - RAISE NOTICE 'ECHEC drec' ; - r := False ; - END IF ; + ASSERT variante.dpro_o IS NULL AND NOT quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_o IS NOT NULL AND quote_ident(o_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_o), + format('échec assertion #1 pour la variante %s', variante.n) ; + + ASSERT variante.dpro_c IS NULL AND NOT quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dpro_c IS NOT NULL AND quote_ident(c_roles.producteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dpro_c), + format('échec assertion #2 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_o IS NULL AND NOT quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_o IS NOT NULL AND quote_ident(o_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_o), + format('échec assertion #3 pour la variante %s', variante.n) ; + + ASSERT variante.dedi_c IS NULL AND NOT quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dedi_c IS NOT NULL AND quote_ident(c_roles.editeur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dedi_c), + format('échec assertion #4 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_o IS NULL AND NOT quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_o IS NOT NULL AND quote_ident(o_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_o), + format('échec assertion #5 pour la variante %s', variante.n) ; + + ASSERT variante.dlec_c IS NULL AND NOT quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.dlec_c IS NOT NULL AND quote_ident(c_roles.lecteur)::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.dlec_c), + format('échec assertion #6 pour la variante %s', variante.n) ; + + ASSERT variante.drec IS NULL AND NOT quote_ident('g_asgard REC!!!')::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable)) + OR variante.drec IS NOT NULL AND quote_ident('g_asgard REC!!!')::regrole + IN (SELECT grantee FROM aclexplode(acl_idi) AS acl (grantor, grantee, privilege, grantable) + WHERE grantor = quote_ident(c_roles.producteur)::regrole + GROUP BY grantee + HAVING string_agg(privilege, ',' ORDER BY privilege) = variante.drec), + format('échec assertion #6 pour la variante %s', variante.n) ; t := c ; c := o ; @@ -10021,9 +9933,9 @@ BEGIN DROP ROLE "g_asgard--pro2" ; DROP ROLE "g_asgard REC!!!" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; RAISE NOTICE '%', e_mssg @@ -10032,7 +9944,7 @@ EXCEPTION WHEN OTHERS THEN RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t055b() IS 'ASGARD recette. TEST : (asgard_deplace_obj) gestion des séquences associées.' ; @@ -10044,9 +9956,8 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t056() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; o_role oid ; + o_role_bis oid ; o_nsp oid ; e_mssg text ; e_detl text ; @@ -10055,6 +9966,7 @@ BEGIN CREATE ROLE g_asgard_rec ; CREATE ROLE g_asgard_rec_bis ; o_role = 'g_asgard_rec'::regrole::oid ; + o_role_bis = 'g_asgard_rec_bis'::regrole::oid ; CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_rec ; o_nsp = 'c_bibliotheque'::regnamespace::oid ; @@ -10069,24 +9981,18 @@ BEGIN CREATE COLLATION c_bibliotheque.biblicoll FROM pg_catalog.default ; END IF ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_collation - WHERE collnamespace = o_nsp AND collowner = o_role ; - - r := b ; - RAISE NOTICE '56-1 > %', r::text ; + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #1' ; -- #2 CREATE CONVERSION c_bibliotheque.biblicon FOR 'WIN' TO 'UTF8' FROM win_to_utf8 ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_conversion - WHERE connamespace = o_nsp AND conowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-2 > %', r::text ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #2' ; -- #3 CREATE FUNCTION c_bibliotheque.normalise(text) @@ -10095,108 +10001,145 @@ BEGIN LANGUAGE SQL ; CREATE OPERATOR c_bibliotheque.@ (PROCEDURE = c_bibliotheque.normalise(text), RIGHTARG = text) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_operator - WHERE oprnamespace = o_nsp AND oprowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-3 > %', r::text ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #3' ; -- #4 CREATE TEXT SEARCH CONFIGURATION c_bibliotheque.recherche_config (COPY = french) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_config - WHERE cfgnamespace = o_nsp AND cfgowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-4 > %', r::text ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #4' ; -- #5 CREATE TEXT SEARCH DICTIONARY c_bibliotheque.recherche_dico (TEMPLATE = snowball, LANGUAGE = french) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_dict - WHERE dictnamespace = o_nsp AND dictowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-5 > %', r::text ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #5' ; -- #6 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; + IF current_setting('server_version_num')::int >= 100000 + THEN + CREATE TABLE c_bibliotheque.dependant_columns (chiffre int, chiffre_txt text) ; + EXECUTE 'CREATE STATISTICS c_bibliotheque.stat_dependant_columns + (dependencies) ON chiffre, chiffre_txt + FROM c_bibliotheque.dependant_columns' ; + + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #6' ; + END IF ; + + -- #7 & 8 + -- avec création implicite de la famille d'opérateurs + CREATE OPERATOR CLASS c_bibliotheque.trans_opc + FOR TYPE int USING gist AS + OPERATOR 1 = ; - r := r AND b ; - RAISE NOTICE '56-6 > %', r::text ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #7' ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + ), 'échec assertion #8' ; + + -- #9 + -- création explicite de la famille d'opérateurs + CREATE OPERATOR FAMILY c_bibliotheque.trans_opf USING gist ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #9' ; + + -- #10 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #10' ; ------ ré-synchronisation auto en cas de modification ------ - -- #7 + -- #11 ALTER COLLATION c_bibliotheque.biblicoll OWNER TO g_asgard_rec_bis ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_collation - WHERE collnamespace = o_nsp AND collowner = o_role ; - - r := b ; - RAISE NOTICE '56-7 > %', r::text ; + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #11' ; - -- #8 + -- #12 ALTER CONVERSION c_bibliotheque.biblicon OWNER TO g_asgard_rec_bis ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_conversion - WHERE connamespace = o_nsp AND conowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-8 > %', r::text ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #12' ; - -- #9 + -- #13 ALTER OPERATOR c_bibliotheque.@ (NONE, text) OWNER TO g_asgard_rec_bis ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_operator - WHERE oprnamespace = o_nsp AND oprowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-9 > %', r::text ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #13' ; - -- #10 + -- #14 ALTER TEXT SEARCH CONFIGURATION c_bibliotheque.recherche_config OWNER TO g_asgard_rec_bis ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_config - WHERE cfgnamespace = o_nsp AND cfgowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-10 > %', r::text ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #14' ; - -- #11 + -- #15 ALTER TEXT SEARCH DICTIONARY c_bibliotheque.recherche_dico OWNER TO g_asgard_rec_bis ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_dict - WHERE dictnamespace = o_nsp AND dictowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56-11 > %', r::text ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #15' ; - -- #12 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-12 > %', r::text ; + -- #16 + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'ALTER STATISTICS c_bibliotheque.stat_dependant_columns + OWNER TO g_asgard_rec_bis ;' ; + + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #16' ; + END IF ; + + -- #17 + ALTER OPERATOR CLASS c_bibliotheque.trans_opc USING gist OWNER TO g_asgard_rec_bis ; + + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #17' ; + + -- #18 + ALTER OPERATOR FAMILY c_bibliotheque.trans_opf USING gist OWNER TO g_asgard_rec_bis ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #18' ; + + -- #19 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #19' ; ------ synchronisation avec asgard_initialise_schema ------ @@ -10207,51 +10150,190 @@ BEGIN ALTER OPERATOR c_bibliotheque.@ (NONE, text) OWNER TO g_asgard_rec_bis ; ALTER TEXT SEARCH CONFIGURATION c_bibliotheque.recherche_config OWNER TO g_asgard_rec_bis ; ALTER TEXT SEARCH DICTIONARY c_bibliotheque.recherche_dico OWNER TO g_asgard_rec_bis ; + ALTER OPERATOR CLASS c_bibliotheque.trans_opc USING gist OWNER TO g_asgard_rec_bis ; + ALTER OPERATOR FAMILY c_bibliotheque.trans_opf USING gist OWNER TO g_asgard_rec_bis ; + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'ALTER STATISTICS c_bibliotheque.stat_dependant_columns + OWNER TO g_asgard_rec_bis ;' ; + END IF ; ALTER EVENT TRIGGER asgard_on_alter_objet ENABLE ; - -- #13 - SELECT count(*) = 5 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-13 > %', r::text ; + -- #20 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 7 + + (current_setting('server_version_num')::int >= 100000)::int, + 'échec assertion #20' ; + ASSERT o_role_bis IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #20-1' ; + ASSERT o_role_bis IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #20-2' ; + ASSERT o_role_bis IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #20-3' ; + ASSERT o_role_bis IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #20-4' ; + ASSERT o_role_bis IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #20-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role_bis IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #20-6' ; + END IF ; + ASSERT o_role_bis IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #20-7' ; + ASSERT o_role_bis IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #20-8' ; + PERFORM z_asgard.asgard_initialise_schema('c_bibliotheque') ; - -- #14 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-14 > %', r::text ; + -- #21 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #21' ; + + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #21-1' ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #21-2' ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #21-3' ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #21-4' ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #21-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #21-6' ; + END IF ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #21-7' ; + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #21-8' ; ------ modification du propriétaire du schéma par un ALTER SCHEMA ------ ALTER SCHEMA c_bibliotheque OWNER TO g_asgard_rec_bis ; - -- #15 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-15 > %', r::text ; + -- #22 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #22' ; + + ASSERT o_role_bis IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #22-1' ; + ASSERT o_role_bis IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #22-2' ; + ASSERT o_role_bis IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #22-3' ; + ASSERT o_role_bis IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #22-4' ; + ASSERT o_role_bis IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #22-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role_bis IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #22-6' ; + END IF ; + ASSERT o_role_bis IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #22-7' ; + ASSERT o_role_bis IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #22-8' ; ------ modification du producteur du schéma par un UPDATE ------ UPDATE z_asgard.gestion_schema_usr SET producteur = 'g_asgard_rec' WHERE nom_schema = 'c_bibliotheque' ; - -- #16 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-16 > %', r::text ; - + -- #23 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #23' ; + + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #23-1' ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #23-2' ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #23-3' ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #23-4' ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #23-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #23-6' ; + END IF ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #23-7' ; + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans_opf' + ), 'échec assertion #23-8' ; DROP SCHEMA c_bibliotheque CASCADE ; DELETE FROM z_asgard.gestion_schema_usr ; @@ -10259,9 +10341,9 @@ BEGIN DROP ROLE g_asgard_rec ; DROP ROLE g_asgard_rec_bis ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; RAISE NOTICE '%', e_mssg @@ -10270,31 +10352,31 @@ EXCEPTION WHEN OTHERS THEN RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t056() IS 'ASGARD recette. TEST : synchronisation des propriétaires des objets sans ACL.' ; --- FUNCTION: z_asgard_recette.t056b() +-- Function: z_asgard_recette.t056b() CREATE OR REPLACE FUNCTION z_asgard_recette.t056b() RETURNS boolean LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; o_role oid ; + o_role_bis oid ; o_nsp oid ; e_mssg text ; e_detl text ; BEGIN - CREATE ROLE "g_asgard_REC" ; + CREATE ROLE """g_asgard_REC""" ; CREATE ROLE "g_asgard REC*" ; - o_role = '"g_asgard_REC"'::regrole::oid ; + o_role = '"""g_asgard_REC"""'::regrole::oid ; + o_role_bis = '"g_asgard REC*"'::regrole::oid ; - CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g_asgard_REC" ; + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION """g_asgard_REC""" ; o_nsp = '"c_Bibliothèque"'::regnamespace::oid ; ------ synchronisation à la création ------ @@ -10307,24 +10389,18 @@ BEGIN CREATE COLLATION "c_Bibliothèque"."BIBLIcoll" FROM pg_catalog.default ; END IF ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_collation - WHERE collnamespace = o_nsp AND collowner = o_role ; - - r := b ; - RAISE NOTICE '56b-1 > %', r::text ; + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #1' ; -- #2 CREATE CONVERSION "c_Bibliothèque"."BIBLI CO^N" FOR 'WIN' TO 'UTF8' FROM win_to_utf8 ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_conversion - WHERE connamespace = o_nsp AND conowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-2 > %', r::text ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #2' ; -- #3 CREATE FUNCTION "c_Bibliothèque".normalise(text) @@ -10333,108 +10409,145 @@ BEGIN LANGUAGE SQL ; CREATE OPERATOR "c_Bibliothèque".@ (PROCEDURE = "c_Bibliothèque".normalise(text), RIGHTARG = text) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_operator - WHERE oprnamespace = o_nsp AND oprowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-3 > %', r::text ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #3' ; -- #4 CREATE TEXT SEARCH CONFIGURATION "c_Bibliothèque"."? config" (COPY = french) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_config - WHERE cfgnamespace = o_nsp AND cfgowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-4 > %', r::text ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #4' ; -- #5 CREATE TEXT SEARCH DICTIONARY "c_Bibliothèque"."? dico" (TEMPLATE = snowball, LANGUAGE = french) ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_dict - WHERE dictnamespace = o_nsp AND dictowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-5 > %', r::text ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #5' ; -- #6 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; + IF current_setting('server_version_num')::int >= 100000 + THEN + CREATE TABLE "c_Bibliothèque".dependant_columns (chiffre int, chiffre_txt text) ; + EXECUTE 'CREATE STATISTICS "c_Bibliothèque"."Stat ""dependant_columns""" + (dependencies) ON chiffre, chiffre_txt + FROM "c_Bibliothèque".dependant_columns' ; + + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #6' ; + END IF ; + + -- #7 & 8 + -- avec création implicite de la famille d'opérateurs + CREATE OPERATOR CLASS "c_Bibliothèque"."trans=opc" + FOR TYPE int USING gist AS + OPERATOR 1 = ; - r := r AND b ; - RAISE NOTICE '56b-6 > %', r::text ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #7' ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + ), 'échec assertion #8' ; + + -- #9 + -- création explicite de la famille d'opérateurs + CREATE OPERATOR FAMILY "c_Bibliothèque"."trans=opf" USING gist ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #9' ; + + -- #10 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #10' ; ------ ré-synchronisation auto en cas de modification ------ - -- #7 + -- #11 ALTER COLLATION "c_Bibliothèque"."BIBLIcoll" OWNER TO "g_asgard REC*" ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_collation - WHERE collnamespace = o_nsp AND collowner = o_role ; - - r := b ; - RAISE NOTICE '56b-7 > %', r::text ; + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #11' ; - -- #8 + -- #12 ALTER CONVERSION "c_Bibliothèque"."BIBLI CO^N" OWNER TO "g_asgard REC*" ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_conversion - WHERE connamespace = o_nsp AND conowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-8 > %', r::text ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #12' ; - -- #9 + -- #13 ALTER OPERATOR "c_Bibliothèque".@ (NONE, text) OWNER TO "g_asgard REC*" ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_operator - WHERE oprnamespace = o_nsp AND oprowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-9 > %', r::text ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #13' ; - -- #10 + -- #14 ALTER TEXT SEARCH CONFIGURATION "c_Bibliothèque"."? config" OWNER TO "g_asgard REC*" ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_config - WHERE cfgnamespace = o_nsp AND cfgowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-10 > %', r::text ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #14' ; - -- #11 + -- #15 ALTER TEXT SEARCH DICTIONARY "c_Bibliothèque"."? dico" OWNER TO "g_asgard REC*" ; - SELECT count(*) = 1 - INTO STRICT b - FROM pg_ts_dict - WHERE dictnamespace = o_nsp AND dictowner = o_role ; - - r := r AND b ; - RAISE NOTICE '56b-11 > %', r::text ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #15' ; - -- #12 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56b-12 > %', r::text ; + -- #16 + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'ALTER STATISTICS "c_Bibliothèque"."Stat ""dependant_columns""" + OWNER TO "g_asgard REC*"' ; + + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #16' ; + END IF ; + + -- #17 + ALTER OPERATOR CLASS "c_Bibliothèque"."trans=opc" USING gist OWNER TO "g_asgard REC*" ; + + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #17' ; + + -- #18 + ALTER OPERATOR FAMILY "c_Bibliothèque"."trans=opf" USING gist OWNER TO "g_asgard REC*" ; + + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #18' ; + + -- #19 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #19' ; ------ synchronisation avec asgard_initialise_schema ------ @@ -10445,61 +10558,200 @@ BEGIN ALTER OPERATOR "c_Bibliothèque".@ (NONE, text) OWNER TO "g_asgard REC*" ; ALTER TEXT SEARCH CONFIGURATION "c_Bibliothèque"."? config" OWNER TO "g_asgard REC*" ; ALTER TEXT SEARCH DICTIONARY "c_Bibliothèque"."? dico" OWNER TO "g_asgard REC*" ; + ALTER OPERATOR CLASS "c_Bibliothèque"."trans=opc" USING gist OWNER TO "g_asgard REC*" ; + ALTER OPERATOR FAMILY "c_Bibliothèque"."trans=opf" USING gist OWNER TO "g_asgard REC*" ; + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'ALTER STATISTICS "c_Bibliothèque"."Stat ""dependant_columns""" + OWNER TO "g_asgard REC*"' ; + END IF ; ALTER EVENT TRIGGER asgard_on_alter_objet ENABLE ; - -- #13 - SELECT count(*) = 5 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56b-13 > %', r::text ; - + -- #20 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 7 + + (current_setting('server_version_num')::int >= 100000)::int, + 'échec assertion #20' ; + + ASSERT o_role_bis IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #20-1' ; + ASSERT o_role_bis IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #20-2' ; + ASSERT o_role_bis IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #20-3' ; + ASSERT o_role_bis IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #20-4' ; + ASSERT o_role_bis IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #20-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role_bis IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #20-6' ; + END IF ; + ASSERT o_role_bis IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #20-7' ; + ASSERT o_role_bis IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #20-8' ; + PERFORM z_asgard.asgard_initialise_schema('c_Bibliothèque') ; - -- #14 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56b-14 > %', r::text ; + -- #21 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #21' ; + + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #21-1' ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #21-2' ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #21-3' ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #21-4' ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #21-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #21-6' ; + END IF ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #21-7' ; + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #21-8' ; ------ modification du propriétaire du schéma par un ALTER SCHEMA ------ ALTER SCHEMA "c_Bibliothèque" OWNER TO "g_asgard REC*" ; - -- #15 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-15 > %', r::text ; + -- #22 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #22' ; + + ASSERT o_role_bis IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #22-1' ; + ASSERT o_role_bis IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #22-2' ; + ASSERT o_role_bis IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #22-3' ; + ASSERT o_role_bis IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #22-4' ; + ASSERT o_role_bis IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #22-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role_bis IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #22-6' ; + END IF ; + ASSERT o_role_bis IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #22-7' ; + ASSERT o_role_bis IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #22-8' ; ------ modification du producteur du schéma par un UPDATE ------ UPDATE z_asgard.gestion_schema_usr - SET producteur = 'g_asgard_REC' + SET producteur = '"g_asgard_REC"' WHERE nom_schema = 'c_Bibliothèque' ; - -- #16 - SELECT count(*) = 0 - INTO STRICT b - FROM z_asgard_admin.asgard_diagnostic() ; - - r := r AND b ; - RAISE NOTICE '56-16 > %', r::text ; + -- #23 + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion #23' ; + ASSERT o_role IN ( + SELECT collowner FROM pg_collation + WHERE collnamespace = o_nsp + ), 'échec assertion #23-1' ; + ASSERT o_role IN ( + SELECT conowner FROM pg_conversion + WHERE connamespace = o_nsp + ), 'échec assertion #23-2' ; + ASSERT o_role IN ( + SELECT oprowner FROM pg_operator + WHERE oprnamespace = o_nsp + ), 'échec assertion #23-3' ; + ASSERT o_role IN ( + SELECT cfgowner FROM pg_ts_config + WHERE cfgnamespace = o_nsp + ), 'échec assertion #23-4' ; + ASSERT o_role IN ( + SELECT dictowner FROM pg_ts_dict + WHERE dictnamespace = o_nsp + ), 'échec assertion #23-5' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT o_role IN ( + SELECT stxowner FROM pg_statistic_ext + WHERE stxnamespace = o_nsp + ), 'échec assertion #23-6' ; + END IF ; + ASSERT o_role IN ( + SELECT opcowner FROM pg_opclass + WHERE opcnamespace = o_nsp + ), 'échec assertion #23-7' ; + ASSERT o_role IN ( + SELECT opfowner FROM pg_opfamily + WHERE opfnamespace = o_nsp + AND opfname = 'trans=opf' + ), 'échec assertion #23-8' ; DROP SCHEMA "c_Bibliothèque" CASCADE ; DELETE FROM z_asgard.gestion_schema_usr ; - DROP ROLE "g_asgard_REC" ; + DROP ROLE """g_asgard_REC""" ; DROP ROLE "g_asgard REC*" ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; RAISE NOTICE '%', e_mssg @@ -10508,7 +10760,7 @@ EXCEPTION WHEN OTHERS THEN RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t056b() IS 'ASGARD recette. TEST : synchronisation des propriétaires des objets sans ACL.' ; @@ -16659,3 +16911,325 @@ END $_$; COMMENT ON FUNCTION z_asgard_recette.t090b() IS 'ASGARD recette. TEST : Préservation du référencement des schémas lors des montées de version.' ; + + +-- FUNCTION: z_asgard_recette.t091() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t091() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + table_oid oid ; + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE """Producteur A""" ; + CREATE SCHEMA "Ma ""Bibliothèque""" AUTHORIZATION """Producteur A""" ; + + table_oid := '"Ma ""Bibliothèque"""'::regnamespace::oid ; + + ASSERT 'Ma "Bibliothèque"' IN ( + SELECT nom_schema + FROM z_asgard.gestion_schema_usr + WHERE producteur = '"Producteur A"' + ), 'échec assertion #1' ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = '"Producteur B"', + editeur = '"Editeur"', + lecteur = '"Lecteur"' + WHERE nom_schema = 'Ma "Bibliothèque"' ; + + CREATE TABLE "Ma ""Bibliothèque""".table_test () ; + + ASSERT z_asgard.asgard_is_relation_owner('Ma "Bibliothèque"', 'table_test', + '"Producteur B"'), 'échec assertion #2' ; + ASSERT has_table_privilege('"Editeur"', '"Ma ""Bibliothèque""".table_test'::regclass, + 'INSERT'), 'échec assertion #3' ; + ASSERT has_table_privilege('"Lecteur"', '"Ma ""Bibliothèque""".table_test'::regclass, + 'SELECT'), 'échec assertion #4' ; + + ALTER SCHEMA "Ma ""Bibliothèque""" OWNER TO """Producteur A""" ; + + ASSERT z_asgard.asgard_is_relation_owner('Ma "Bibliothèque"', 'table_test', + '"Producteur A"'), 'échec assertion #5' ; + + ASSERT 'Ma "Bibliothèque"' IN ( + SELECT nom_schema + FROM z_asgard.gestion_schema_usr + WHERE producteur = '"Producteur A"' + ), 'échec assertion #6' ; + + ALTER SCHEMA "Ma ""Bibliothèque""" RENAME TO "C'est MA ""Bibliothèque""" ; + + ASSERT 'C''est MA "Bibliothèque"' IN ( + SELECT nom_schema + FROM z_asgard.gestion_schema_usr + WHERE producteur = '"Producteur A"' + ), 'échec assertion #7' ; + + ASSERT table_oid = '"C''est MA ""Bibliothèque"""'::regnamespace::oid, + 'échec assertion #8' ; + + DROP SCHEMA "C'est MA ""Bibliothèque""" CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE """Producteur A""" ; + DROP ROLE """Producteur B""" ; + DROP ROLE """Editeur""" ; + DROP ROLE """Lecteur""" ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t091() IS 'ASGARD recette. TEST : Guillemets dans les identifiants de schémas et rôles.' ; + + +-- FUNCTION: z_asgard_recette.t092() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t092() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA c_bibliotheque ; + CREATE SCHEMA c_librairie ; + + CREATE TYPE c_bibliotheque.intervalle AS (d int, f int) ; + + CREATE FUNCTION c_bibliotheque.cherche_intervalle_sfunc(c_bibliotheque.intervalle, int) + RETURNS c_bibliotheque.intervalle + AS $$ SELECT LEAST($1.d, $2), GREATEST($1.f, $2) $$ + LANGUAGE SQL ; + CREATE FUNCTION c_librairie.cherche_intervalle_sfunc(c_bibliotheque.intervalle, int) + RETURNS c_bibliotheque.intervalle + AS $$ SELECT LEAST($1.d, $2), GREATEST($1.f, $2) $$ + LANGUAGE SQL ; + -- arguments différents + CREATE FUNCTION c_librairie.cherche_intervalle_sfunc_bis(c_bibliotheque.intervalle, int, int) + RETURNS c_bibliotheque.intervalle + AS $$ SELECT LEAST($1.d, $2, $3), GREATEST($1.f, $2, $3) $$ + LANGUAGE SQL ; + + CREATE SEQUENCE c_bibliotheque.compteur ; + CREATE SEQUENCE c_librairie.compteur ; + -- séquence témoin : ne sera pas déplacée, donc ne devrait pas + -- empêcher le déplacement de la table + + CREATE TABLE c_bibliotheque.journal_du_mur ( + idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + ids serial, + id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), + jour date, entree text, auteur text + ) ; + CREATE TABLE c_librairie.journal_du_mur_bis ( + idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + ids serial, + id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), + jour date, entree text, auteur text + ) ; + + CREATE INDEX journal_du_mur_auteur_idx ON c_bibliotheque.journal_du_mur + USING btree (auteur) ; + CREATE INDEX journal_du_mur_auteur_idx ON c_librairie.journal_du_mur_bis + USING btree (auteur) ; + + -- fonction + BEGIN + -- avec espaces et diminutifs dans la liste des types + -- d'arguments de la fonction, ce qui est supposé passer + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'cherche_intervalle_sfunc(c_bibliotheque.intervalle, int)', + 'function', 'c_librairie') ; + ASSERT False, 'échec assertion 1-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + RAISE NOTICE '%', e_mssg ; + ASSERT e_mssg ~ '^FDO8[.]', 'échec assertion 1-b' ; + END ; + + ALTER FUNCTION c_bibliotheque.cherche_intervalle_sfunc(c_bibliotheque.intervalle, int) + RENAME TO cherche_intervalle_sfunc_bis ; + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'cherche_intervalle_sfunc_bis(c_bibliotheque.intervalle, int)', + 'function', 'c_librairie') ; + + -- index libre + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 2-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO10[.].*journal_du_mur_auteur_idx', 'échec assertion 2-b' ; + END ; + + ALTER INDEX c_librairie.journal_du_mur_auteur_idx + RENAME TO journal_du_mur_auteur_bis_idx ; + + -- index de contrainte + ALTER INDEX c_librairie.journal_du_mur_bis_pkey + RENAME TO journal_du_mur_pkey ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 3-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO9[.].*journal_du_mur_pkey', 'échec assertion 3-b' ; + END ; + + ALTER INDEX c_librairie.journal_du_mur_pkey + RENAME TO journal_du_mur_bis_pkey ; + + -- séquence serial + ALTER INDEX c_librairie.journal_du_mur_bis_ids_seq + RENAME TO journal_du_mur_ids_seq ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 4-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*journal_du_mur_ids_seq', 'échec assertion 4-b' ; + END ; + + ALTER INDEX c_librairie.journal_du_mur_ids_seq + RENAME TO journal_du_mur_bis_ids_seq ; + + -- séquence identity + ALTER INDEX c_librairie.journal_du_mur_bis_idi_seq + RENAME TO journal_du_mur_idi_seq ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 5-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*journal_du_mur_idi_seq', 'échec assertion 5-b' ; + END ; + + ALTER INDEX c_librairie.journal_du_mur_idi_seq + RENAME TO journal_du_mur_bis_idi_seq ; + + -- table + ALTER TABLE c_librairie.journal_du_mur_bis + RENAME TO journal_du_mur ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 6-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO8[.]', 'échec assertion 6-b' ; + END ; + + ALTER TABLE c_librairie.journal_du_mur + RENAME TO journal_du_mur_bis ; + + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-a' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-b' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur_auteur_idx' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-c' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur_idi_seq' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-d' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur_ids_seq' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-e' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur_pkey' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-f' ; + + DROP SCHEMA c_bibliotheque CASCADE ; + DROP SCHEMA c_librairie CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t092() IS 'ASGARD recette. TEST : (asgard_deplace_obj) Quand l''objet existe déjà dans le schéma cible.' ; + + +-- FUNCTION: z_asgard_recette.t093() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t093() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE g_asgard_owner ; + CREATE SCHEMA e_blabla AUTHORIZATION g_asgard_owner ; + CREATE TABLE e_blabla.some_table () ; + ASSERT (SELECT creation FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'e_blabla'), + 'échec assertion 1' ; + + DROP OWNED BY g_asgard_owner CASCADE ; + ASSERT NOT (SELECT creation FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'e_blabla'), + 'échec assertion 2' ; + + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE g_asgard_owner ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t093() IS 'ASGARD recette. TEST : Répercution des commandes DROP OWNED sur la table de gestion.' ; + From 0cf34d9a9c9256177005cdbee7d82351fc504e19 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 18 May 2022 20:21:46 +0200 Subject: [PATCH 08/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout des tests t092b (déclinaison du t092 avec noms non normalisés) et t093 (capacité d'action des producteurs et administrateurs délégués). --- recette/asgard_recette.sql | 319 +++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index 1573d51..d124303 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -17194,6 +17194,204 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t092() IS 'ASGARD recette. TEST : (asgard_deplace_obj) Quand l''objet existe déjà dans le schéma cible.' ; +-- FUNCTION: z_asgard_recette.t092b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t092b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA "c_Bibliothèque" ; + CREATE SCHEMA "c_LIB $rairie" ; + + CREATE TYPE "c_Bibliothèque".intervalle AS (d int, f int) ; + + CREATE FUNCTION "c_Bibliothèque"."CHERCHE intervalle_sfunc"("c_Bibliothèque".intervalle, int) + RETURNS "c_Bibliothèque".intervalle + AS $$ SELECT LEAST($1.d, $2), GREATEST($1.f, $2) $$ + LANGUAGE SQL ; + CREATE FUNCTION "c_LIB $rairie"."CHERCHE intervalle_sfunc"("c_Bibliothèque".intervalle, int) + RETURNS "c_Bibliothèque".intervalle + AS $$ SELECT LEAST($1.d, $2), GREATEST($1.f, $2) $$ + LANGUAGE SQL ; + -- arguments différents + CREATE FUNCTION "c_LIB $rairie"."CHERCHE intervalle_sfunc B!S"("c_Bibliothèque".intervalle, int, int) + RETURNS "c_Bibliothèque".intervalle + AS $$ SELECT LEAST($1.d, $2, $3), GREATEST($1.f, $2, $3) $$ + LANGUAGE SQL ; + + CREATE SEQUENCE "c_Bibliothèque"."""compteur""" ; + CREATE SEQUENCE "c_LIB $rairie"."""compteur""" ; + -- séquence témoin : ne sera pas déplacée, donc ne devrait pas + -- empêcher le déplacement de la table + + CREATE TABLE "c_Bibliothèque"."JournalDuMur" ( + "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + "ID$" serial, + id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), + jour date, entree text, auteur text + ) ; + CREATE TABLE "c_LIB $rairie"."JournalDuMur B!s" ( + "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + "ID$" serial, + id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), + jour date, entree text, auteur text + ) ; + + CREATE INDEX "JournalDuMur_auteur_idx" ON "c_Bibliothèque"."JournalDuMur" + USING btree (auteur) ; + CREATE INDEX "JournalDuMur_auteur_idx" ON "c_LIB $rairie"."JournalDuMur B!s" + USING btree (auteur) ; + + -- fonction + BEGIN + -- avec espaces et diminutifs dans la liste des types + -- d'arguments de la fonction, ce qui est supposé passer + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + '"CHERCHE intervalle_sfunc"("c_Bibliothèque".intervalle, int)', + 'function', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 1-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + RAISE NOTICE '%', e_mssg ; + ASSERT e_mssg ~ '^FDO8[.]', 'échec assertion 1-b' ; + END ; + + ALTER FUNCTION "c_Bibliothèque"."CHERCHE intervalle_sfunc"("c_Bibliothèque".intervalle, int) + RENAME TO "CHERCHE intervalle_sfunc B!S" ; + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + '"CHERCHE intervalle_sfunc B!S"("c_Bibliothèque".intervalle, int)', + 'function', 'c_LIB $rairie') ; + + -- index libre + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 2-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO10[.].*JournalDuMur_auteur_idx', 'échec assertion 2-b' ; + END ; + + ALTER INDEX "c_LIB $rairie"."JournalDuMur_auteur_idx" + RENAME TO "JournalDuMur_auteur_bis_idx" ; + + -- index de contrainte + ALTER INDEX "c_LIB $rairie"."JournalDuMur B!s_pkey" + RENAME TO "JournalDuMur_pkey" ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 3-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO9[.].*JournalDuMur_pkey', 'échec assertion 3-b' ; + END ; + + ALTER INDEX "c_LIB $rairie"."JournalDuMur_pkey" + RENAME TO "JournalDuMur B!s_pkey" ; + + -- séquence serial + ALTER INDEX "c_LIB $rairie"."JournalDuMur B!s_ID$_seq" + RENAME TO "JournalDuMur_ID$_seq" ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 4-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*JournalDuMur_ID[$]_seq', 'échec assertion 4-b' ; + END ; + + ALTER INDEX "c_LIB $rairie"."JournalDuMur_ID$_seq" + RENAME TO "JournalDuMur B!s_ID$_seq" ; + + -- séquence identity + ALTER INDEX "c_LIB $rairie"."JournalDuMur B!s_IDI_seq" + RENAME TO "JournalDuMur_IDI_seq" ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 5-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*JournalDuMur_IDI_seq', 'échec assertion 5-b' ; + END ; + + ALTER INDEX "c_LIB $rairie"."JournalDuMur_IDI_seq" + RENAME TO "JournalDuMur B!s_IDI_seq" ; + + -- table + ALTER TABLE "c_LIB $rairie"."JournalDuMur B!s" + RENAME TO "JournalDuMur" ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 6-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO8[.]', 'échec assertion 6-b' ; + END ; + + ALTER TABLE "c_LIB $rairie"."JournalDuMur" + RENAME TO "JournalDuMur B!s" ; + + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-a' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-b' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur_auteur_idx' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-c' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur_IDI_seq' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-d' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur_ID$_seq' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-e' ; + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur_pkey' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-f' ; + + DROP SCHEMA "c_Bibliothèque" CASCADE ; + DROP SCHEMA "c_LIB $rairie" CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t092b() IS 'ASGARD recette. TEST : (asgard_deplace_obj) Quand l''objet existe déjà dans le schéma cible.' ; + + -- FUNCTION: z_asgard_recette.t093() CREATE OR REPLACE FUNCTION z_asgard_recette.t093() @@ -17233,3 +17431,124 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t093() IS 'ASGARD recette. TEST : Répercution des commandes DROP OWNED sur la table de gestion.' ; + +-- FUNCTION: z_asgard_recette.t094() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t094() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE g_asgard_admin_delegue ; + CREATE ROLE g_asgard_producteur ; + CREATE ROLE g_asgard_connexion ; + GRANT g_asgard_admin_delegue TO g_asgard_connexion WITH ADMIN OPTION ; + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_producteur ; + + EXECUTE format('GRANT CREATE ON DATABASE %I TO %I', current_database(), 'g_asgard_admin_delegue') ; + + -- tentative de création de schéma via la table de gestion + -- par un rôle qui n'est pas habilité à créer des schémas + BEGIN + SET ROLE g_asgard_producteur ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_librairie', True, 'g_asgard_producteur') ; + ASSERT False, 'échec assertion 1-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^TB1[.]', 'échec assertion 1-b' ; + END ; + + -- tentative d'ajout d'un schéma inactif dans la table de gestion + -- par un rôle qui n'est pas habilité à créer des schémas + BEGIN + SET ROLE g_asgard_producteur ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_librairie', False, 'g_asgard_producteur') ; + ASSERT False, 'échec assertion 2-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^TB1[.]', 'échec assertion 2-b' ; + END ; + + ------ Manipulations par un rôle habilité à créer des schémas ------ + SET ROLE g_asgard_connexion ; + + -- création d'un schéma par un rôle qui en a le droit + -- - commande directe : + CREATE SCHEMA x_secret AUTHORIZATION g_asgard_admin_delegue ; + -- - via la table de gestion : + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_librairy', True, 'g_asgard_admin_delegue') ; + -- modification par commande directe : + ALTER SCHEMA c_librairy RENAME TO c_librairie ; + -- modification par la table de gestion : + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Ma jolie librairie' + WHERE nom_schema = 'c_librairie' ; + ASSERT FOUND, 'échec assertion 3' ; + -- suppression : + DROP SCHEMA c_librairie ; + DELETE FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_librairie' ; + ASSERT FOUND, 'échec assertion 4' ; + + ------ Manipulations par un rôle non habilité ------ + SET ROLE g_asgard_producteur ; + + -- vérification qu'un rôle ne voit pas les schémas dont + -- il n'est pas producteur dans gestion_schema_usr + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'x_secret') = 0, 'échec assertion 5' ; + + -- ... et ça ne fait évidemment rien quand il tente de modifier + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'blabla' + WHERE nom_schema = 'x_secret' ; + ASSERT NOT FOUND, 'échec assertion 6' ; + + -- ... mais dans gestion_schema_read_only, il voit le schéma + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_read_only + WHERE nom_schema = 'x_secret') = 1, 'échec assertion 7' ; + + ------ Manipulations par le producteur du schéma ------ + -- via la table de gestion + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Ma grande bibliothèque' + WHERE nom_schema = 'c_bibliotheque' ; + ASSERT FOUND, 'échec assertion 8' ; + + -- pas de test de modification du schéma par + -- commande directe ou indirecte - les ALTER SCHEMA requièrent + -- le privilège CREATE sur la base + + -- création d'un objet dans le schéma + CREATE TABLE c_bibliotheque.journal_du_mur (jour date PRIMARY KEY, entree text) ; + + RESET ROLE ; + DROP SCHEMA c_bibliotheque CASCADE ; + DROP SCHEMA x_secret ; + DELETE FROM z_asgard.gestion_schema_usr ; + EXECUTE format('REVOKE CREATE ON DATABASE %I FROM %I', current_database(), 'g_asgard_admin_delegue') ; + DROP ROLE g_asgard_admin_delegue ; + DROP ROLE g_asgard_producteur ; + DROP ROLE g_asgard_connexion ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t094() IS 'ASGARD recette. TEST : Capacité d''action des producteurs et administrateurs délégués.' ; From cdff19ee18f293e1b51228735296becb80aa6404 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 19 May 2022 19:37:12 +0200 Subject: [PATCH 09/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Réécriture formelle de deux tests. --- recette/asgard_recette.sql | 64 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index d124303..471fcc7 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -1032,37 +1032,36 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t011() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; + e_mssg text ; + e_detl text ; BEGIN CREATE SCHEMA c_bibliotheque AUTHORIZATION g_admin_ext ; ------ révocation d''un privilège ------ - REVOKE CREATE ON SCHEMA c_bibliotheque FROM g_admin_ext ; + REVOKE CREATE ON SCHEMA c_bibliotheque FROM g_admin_ext ; - SELECT nspacl::text ~ ('g_admin_ext=U' || '[/]' || nspowner::regrole::text) - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_bibliotheque' ; - - r := b ; + ASSERT (SELECT nspacl::text ~ ('g_admin_ext=U' || '[/]' || nspowner::regrole::text) + FROM pg_catalog.pg_namespace WHERE nspname = 'c_bibliotheque'), + 'échec assertion #1' ; ALTER SCHEMA c_bibliotheque OWNER TO g_admin ; - SELECT nspacl::text ~ ('g_admin=U' || '[/]' || nspowner::regrole::text) - INTO STRICT b - FROM pg_catalog.pg_namespace - WHERE nspname = 'c_bibliotheque' ; - - r := r AND b ; + ASSERT (SELECT nspacl::text ~ ('g_admin=U' || '[/]' || nspowner::regrole::text) + FROM pg_catalog.pg_namespace WHERE nspname = 'c_bibliotheque'), + 'échec assertion #2' ; DROP SCHEMA c_bibliotheque ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_bibliotheque' ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END @@ -4926,30 +4925,22 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t039() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; + e_mssg text ; + e_detl text ; s record ; BEGIN ------ hors z_asgard_admin ------ PERFORM z_asgard_admin.asgard_initialisation_gestion_schema(ARRAY['z_asgard_admin']) ; - SELECT count(*) = 1 - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE NOT nom_schema = 'z_asgard_recette' ; - - r := b ; + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_usr + WHERE NOT nom_schema = 'z_asgard_recette') = 1, 'échec assertion #1' ; ------ le reste ------ PERFORM z_asgard_admin.asgard_initialisation_gestion_schema() ; - SELECT count(*) = 2 - INTO STRICT b - FROM z_asgard.gestion_schema_usr - WHERE NOT nom_schema = 'z_asgard_recette' ; - - r := r AND b ; + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_usr + WHERE NOT nom_schema = 'z_asgard_recette') = 2, 'échec assertion #1' ; FOR s IN (SELECT * FROM z_asgard.gestion_schema_usr) LOOP @@ -4957,13 +4948,18 @@ BEGIN END LOOP ; DELETE FROM z_asgard.gestion_schema_usr ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END -$_$; +$_$ ; COMMENT ON FUNCTION z_asgard_recette.t039() IS 'ASGARD recette. TEST : initialisation de la table de gestion (référencement des schémas existants).' ; From 217884adfcb14cdef81e12ec12b8861bb7473819 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 19 May 2022 19:43:15 +0200 Subject: [PATCH 10/32] Update asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (en cours) Amélioration de la méthode d'attribution à g_admin de la permission sur les rôles producteurs : plus simple et devient utilisable même quand le rôle courant ni CREATEROLE ni ADMIN OPTION sur le producteur. Pour cette première phase : création d'un nouveau déclencheur sur gestion_schema, asgard_visibilite_admin_after, qui appelle une fonction SECURITY DEFINER. La recette passe toujours, mais asgard_on_modify_gestion_schema_after() n'a pas encore été débarrassée de ses commandes redondantes. --- asgard--1.4.0.sql | 136 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index 40d5c16..d82c0b2 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -4986,7 +4986,8 @@ COMMENT ON FUNCTION z_asgard.asgard_expend_privileges(text) IS 'ASGARD. Fonction ------ 5 - TRIGGERS SUR GESTION_SCHEMA ------ --------------------------------------------- /* 5.1 - TRIGGER BEFORE - 5.2 - TRIGGER AFTER */ + 5.2 - TRIGGER AFTER + 5.3 - TRIGGER DE GESTION DES PERMISSIONS DE G_ADMIN */ ------ 5.1 - TRIGGER BEFORE ------ @@ -6508,6 +6509,139 @@ CREATE TRIGGER asgard_on_modify_gestion_schema_after COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui répercute physiquement les modifications de la table de gestion.' ; +------ 5.3 - TRIGGER DE GESTION DES PERMISSIONS DE G_ADMIN ------ + +-- Function: z_asgard_admin.asgard_visibilite_admin_after() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_visibilite_admin_after() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + AS $BODY$ +/* Fonction exécutée par le déclencheur asgard_visibilite_admin_after sur +z_asgard_admin.gestion_schema, qui assure que g_admin soit toujours membre +des rôles producteurs des schémas référencés, hors super-utilisateurs. + + Notes + ----- + Il s'agit d'une fonction SECURITY DEFINER, qui s'exécute avec les + droits de g_admin, profitant notamment de son attribut CREATEROLE. + + Ne pas renommer le déclencheur asgard_visibilite_admin_after à la légère. + Il est essentiel que cette fonction soit lancée alors que la table + de gestion est déjà à jour, soit après l'exécution de la fonction + asgard_on_modify_gestion_schema_after(). Pour mémoire, l'ordre + d'activation des déclencheurs est déterminé par un tri alphabétique + de leurs noms. + + Raises + ------ + trigger_protocol_violated + TVA1. Lorsque la fonction est utilisée dans un contexte autre que + celui prévu par Asgard. + invalid_grant_operation + TVA2. Lorsque le rôle producteur du schéma auquel se rapporte + l'enregistrement en cours d'édition est membre de g_admin, directement + ou non (permissions circulaires). + +*/ +DECLARE + e_mssg text ; + e_hint text ; + e_detl text ; + e_errcode text ; + e_schema text ; +BEGIN + + ------ CONTRÔLES PREALABLES ------ + -- comme asgard_visibilite_admin_after() est une fonction SECURITY DEFINER, + -- on s'assure qu'elle est bien appelée dans le seul contexte autorisé, + -- à savoir par un trigger asgard_visibilite_admin_after sur une table + -- z_asgard_admin.gestion_schema. + IF NOT TG_TABLE_NAME = 'gestion_schema' OR NOT TG_TABLE_SCHEMA = 'z_asgard_admin' + OR NOT TG_NAME = 'asgard_visibilite_admin_after' + THEN + RAISE EXCEPTION 'TVA1. Opération interdite. La fonction asgard_visibilite_admin_after() ne peut être appelée que par le déclencheur asgard_visibilite_admin_after défini sur la table z_asgard_admin.gestion_schema.' + USING ERRCODE = 'trigger_protocol_violated' ; + END IF ; + + ------- GESTION DES PERMISSIONS DE G_ADMIN ------ + + -- on écarte les schémas non actifs et le cas où le + -- producteur est g_admin + IF NOT NEW.creation OR NEW.producteur = 'g_admin' + THEN + RETURN NULL ; + END IF ; + + -- pas de contrôle sur le fait que le producteur a changé, car il n'est + -- pas plus mal de confirmer les permissions de g_admin, au cas où elles + -- auraient été supprimées entre temps, ce qui ne sera jamais une bonne + -- chose + + -- on ne considère pas le cas où le producteur est un super-utilisateur + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + RETURN NULL ; + END IF ; + + -- ni le cas où g_admin est déjà directement membre du producteur + IF 'g_admin'::regrole IN ( + SELECT member FROM pg_catalog.pg_auth_members + WHERE roleid = quote_ident(NEW.producteur)::regrole + ) + -- NB: on n'utilise pas NEW.oid_producteur, car c'est + -- asgard_on_modify_gestion_schema_after qui arrête définitivement + -- sa valeur. + THEN + RETURN NULL ; + END IF ; + + -- si le producteur est membres de g_admin, on retourne une erreur + IF pg_has_role(NEW.producteur, 'g_admin', 'MEMBER') + THEN + RAISE EXCEPTION 'TVA2. Opération interdite. Les rôles producteurs/propriétaires de schémas non super-utilisateurs ne peuvent pas être membres de g_admin, y compris par héritage.' + USING HINT = format('Pourquoi ne pas désigner directement de g_admin comme producteur du schéma %I ?', NEW.nom_schema), + SCHEMA = NEW.nom_schema, + ERRCODE = 'invalid_grant_operation' ; + END IF ; + + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + + RETURN NULL ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL, + e_errcode = RETURNED_SQLSTATE, + e_schema = SCHEMA_NAME ; + RAISE EXCEPTION 'TVA0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint, + SCHEMA = e_schema, + ERRCODE = e_errcode ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_visibilite_admin_after() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_visibilite_admin_after() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_visibilite_admin_after sur z_asgard_admin.gestion_schema, qui assure que g_admin soit toujours membre des rôles producteurs des schémas référencés, hors super-utilisateurs.' ; + +-- Trigger: asgard_visibilite_admin_after + +CREATE TRIGGER asgard_visibilite_admin_after + AFTER INSERT OR UPDATE + ON z_asgard_admin.gestion_schema + FOR EACH ROW + EXECUTE PROCEDURE z_asgard_admin.asgard_visibilite_admin_after() ; + +COMMENT ON TRIGGER asgard_visibilite_admin_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui assure que g_admin soit toujours membre des rôles producteurs des schémas référencés, hors super-utilisateurs.' ; + + -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ----------------------------------------------------------- From c4b2eca40097127efa74e921ba5a283d51f9dff3 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 19 May 2022 19:43:22 +0200 Subject: [PATCH 11/32] Update asgard--1.3.2--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (en cours) Amélioration de la méthode d'attribution à g_admin de la permission sur les rôles producteurs : plus simple et devient utilisable même quand le rôle courant ni CREATEROLE ni ADMIN OPTION sur le producteur. Pour cette première phase : création d'un nouveau déclencheur sur gestion_schema, asgard_visibilite_admin_after, qui appelle une fonction SECURITY DEFINER. La recette passe toujours, mais asgard_on_modify_gestion_schema_after() n'a pas encore été débarrassée de ses commandes redondantes. --- asgard--1.3.2--1.4.0.sql | 140 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index a617aa6..7dc88e0 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -70,7 +70,9 @@ -- -- schémas contenant les objets : z_asgard et z_asgard_admin. -- --- objets créés par le script : néant. +-- objets créés par le script : +-- - Function: z_asgard_admin.asgard_visibilite_admin_after() +-- - Trigger: asgard_visibilite_admin_after -- -- objets modifiés par le script (parfois seulement leur descriptif) : -- - Schema: z_asgard @@ -3148,7 +3150,8 @@ COMMENT ON FUNCTION z_asgard_admin.asgard_diagnostic(text[]) IS 'ASGARD. Pour to --------------------------------------------- /* 5.1 - TRIGGER BEFORE - 5.2 - TRIGGER AFTER */ + 5.2 - TRIGGER AFTER + 5.3 - TRIGGER DE GESTION DES PERMISSIONS DE G_ADMIN */ ------ 5.1 - TRIGGER BEFORE ------ @@ -4657,6 +4660,139 @@ COMMENT ON FUNCTION z_asgard_admin.asgard_on_modify_gestion_schema_after() IS 'A COMMENT ON TRIGGER asgard_on_modify_gestion_schema_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui répercute physiquement les modifications de la table de gestion.' ; +------ 5.3 - TRIGGER DE GESTION DES PERMISSIONS DE G_ADMIN ------ + +-- Function: z_asgard_admin.asgard_visibilite_admin_after() + +CREATE OR REPLACE FUNCTION z_asgard_admin.asgard_visibilite_admin_after() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + AS $BODY$ +/* Fonction exécutée par le déclencheur asgard_visibilite_admin_after sur +z_asgard_admin.gestion_schema, qui assure que g_admin soit toujours membre +des rôles producteurs des schémas référencés, hors super-utilisateurs. + + Notes + ----- + Il s'agit d'une fonction SECURITY DEFINER, qui s'exécute avec les + droits de g_admin, profitant notamment de son attribut CREATEROLE. + + Ne pas renommer le déclencheur asgard_visibilite_admin_after à la légère. + Il est essentiel que cette fonction soit lancée alors que la table + de gestion est déjà à jour, soit après l'exécution de la fonction + asgard_on_modify_gestion_schema_after(). Pour mémoire, l'ordre + d'activation des déclencheurs est déterminé par un tri alphabétique + de leurs noms. + + Raises + ------ + trigger_protocol_violated + TVA1. Lorsque la fonction est utilisée dans un contexte autre que + celui prévu par Asgard. + invalid_grant_operation + TVA2. Lorsque le rôle producteur du schéma auquel se rapporte + l'enregistrement en cours d'édition est membre de g_admin, directement + ou non (permissions circulaires). + +*/ +DECLARE + e_mssg text ; + e_hint text ; + e_detl text ; + e_errcode text ; + e_schema text ; +BEGIN + + ------ CONTRÔLES PREALABLES ------ + -- comme asgard_visibilite_admin_after() est une fonction SECURITY DEFINER, + -- on s'assure qu'elle est bien appelée dans le seul contexte autorisé, + -- à savoir par un trigger asgard_visibilite_admin_after sur une table + -- z_asgard_admin.gestion_schema. + IF NOT TG_TABLE_NAME = 'gestion_schema' OR NOT TG_TABLE_SCHEMA = 'z_asgard_admin' + OR NOT TG_NAME = 'asgard_visibilite_admin_after' + THEN + RAISE EXCEPTION 'TVA1. Opération interdite. La fonction asgard_visibilite_admin_after() ne peut être appelée que par le déclencheur asgard_visibilite_admin_after défini sur la table z_asgard_admin.gestion_schema.' + USING ERRCODE = 'trigger_protocol_violated' ; + END IF ; + + ------- GESTION DES PERMISSIONS DE G_ADMIN ------ + + -- on écarte les schémas non actifs et le cas où le + -- producteur est g_admin + IF NOT NEW.creation OR NEW.producteur = 'g_admin' + THEN + RETURN NULL ; + END IF ; + + -- pas de contrôle sur le fait que le producteur a changé, car il n'est + -- pas plus mal de confirmer les permissions de g_admin, au cas où elles + -- auraient été supprimées entre temps, ce qui ne sera jamais une bonne + -- chose + + -- on ne considère pas le cas où le producteur est un super-utilisateur + IF NEW.producteur IN (SELECT rolname FROM pg_catalog.pg_roles WHERE rolsuper) + THEN + RETURN NULL ; + END IF ; + + -- ni le cas où g_admin est déjà directement membre du producteur + IF 'g_admin'::regrole IN ( + SELECT member FROM pg_catalog.pg_auth_members + WHERE roleid = quote_ident(NEW.producteur)::regrole + ) + -- NB: on n'utilise pas NEW.oid_producteur, car c'est + -- asgard_on_modify_gestion_schema_after qui arrête définitivement + -- sa valeur. + THEN + RETURN NULL ; + END IF ; + + -- si le producteur est membres de g_admin, on retourne une erreur + IF pg_has_role(NEW.producteur, 'g_admin', 'MEMBER') + THEN + RAISE EXCEPTION 'TVA2. Opération interdite. Les rôles producteurs/propriétaires de schémas non super-utilisateurs ne peuvent pas être membres de g_admin, y compris par héritage.' + USING HINT = format('Pourquoi ne pas désigner directement de g_admin comme producteur du schéma %I ?', NEW.nom_schema), + SCHEMA = NEW.nom_schema, + ERRCODE = 'invalid_grant_operation' ; + END IF ; + + EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; + RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; + + RETURN NULL ; + +EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_hint = PG_EXCEPTION_HINT, + e_detl = PG_EXCEPTION_DETAIL, + e_errcode = RETURNED_SQLSTATE, + e_schema = SCHEMA_NAME ; + RAISE EXCEPTION 'TVA0 > %', e_mssg + USING DETAIL = e_detl, + HINT = e_hint, + SCHEMA = e_schema, + ERRCODE = e_errcode ; + +END +$BODY$ ; + +ALTER FUNCTION z_asgard_admin.asgard_visibilite_admin_after() + OWNER TO g_admin ; + +COMMENT ON FUNCTION z_asgard_admin.asgard_visibilite_admin_after() IS 'ASGARD. Fonction exécutée par le déclencheur asgard_visibilite_admin_after sur z_asgard_admin.gestion_schema, qui assure que g_admin soit toujours membre des rôles producteurs des schémas référencés, hors super-utilisateurs.' ; + +-- Trigger: asgard_visibilite_admin_after + +CREATE TRIGGER asgard_visibilite_admin_after + AFTER INSERT OR UPDATE + ON z_asgard_admin.gestion_schema + FOR EACH ROW + EXECUTE PROCEDURE z_asgard_admin.asgard_visibilite_admin_after() ; + +COMMENT ON TRIGGER asgard_visibilite_admin_after ON z_asgard_admin.gestion_schema IS 'ASGARD. Déclencheur qui assure que g_admin soit toujours membre des rôles producteurs des schémas référencés, hors super-utilisateurs.' ; + + -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 0bdd2001c683debe910942befcac38aeac7354fb Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:12:50 +0200 Subject: [PATCH 12/32] Update asgard--1.3.2--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite de l'amélioration de la méthode d'attribution à `g_admin` de la permission sur les rôles producteurs : - Suppression des commandes obsolètes dans `asgard_on_modify_gestion_schema_after()`. - Ajustements dans `asgard_on_modify_gestion_schema_before()` et `asgard_visibilite_admin_after()`, avec l'ajout d'une modalité `'END'` pour le champ `ctrl` qui permet de signaler la fin des traitements découlant de la modification de la table de gestion, inhibe la ré-exécution de `asgard_on_modify_gestion_schema_before()` et `asgard_on_modify_gestion_schema_after()`, et autorise l'exécution de `asgard_visibilite_admin_after()`. - Modification en conséquence de la contrainte sur `gestion_schema` qui fixe les valeurs autorisées pour `ctrl`. Correction d'une coquille dans la définition des vues `gestion_schema_usr` et `gestion_schema_etr`, qui affectait la création de schémas via la table de gestion, avec un producteur dont le nom n'est pas normalisé, et par un utilisateur non membre de `g_admin`. --- asgard--1.3.2--1.4.0.sql | 131 +++++++++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 40 deletions(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index 7dc88e0..4e8ca18 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -76,6 +76,7 @@ -- -- objets modifiés par le script (parfois seulement leur descriptif) : -- - Schema: z_asgard +-- - Table: z_asgard_admin.gestion_schema -- - View: z_asgard.gestion_schema_usr -- - View: z_asgard.gestion_schema_etr -- - View: z_asgard.asgardmenu_metadata @@ -129,6 +130,7 @@ ---------------------------------------- /* 2.1 - CREATION DES SCHEMAS + 2.2 - TABLE GESTION_SCHEMA 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA 2.6 - VUE POUR ASGARDMENU 2.7 - VUE POUR ASGARDMANAGER @@ -147,11 +149,46 @@ UPDATE z_asgard_admin.gestion_schema REVOKE USAGE ON SCHEMA z_asgard FROM g_consult ; GRANT USAGE ON SCHEMA z_asgard TO public ; +------ 2.2 - TABLE GESTION_SCHEMA ------ + +-- Table: z_asgard_admin.gestion_schema + +ALTER TABLE z_asgard_admin.gestion_schema + DROP CONSTRAINT gestion_schema_ctrl_check ; + +ALTER TABLE z_asgard_admin.gestion_schema + ADD CONSTRAINT gestion_schema_ctrl_check + CHECK (ctrl IS NULL OR array_length(ctrl, 1) >= 2 + AND ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'MANUEL', 'EXIT', 'END')) ; ------ 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA ------ -- View: z_asgard.gestion_schema_usr +CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( + SELECT + gestion_schema.nom_schema, + gestion_schema.bloc, + gestion_schema.nomenclature, + gestion_schema.niv1, + gestion_schema.niv1_abr, + gestion_schema.niv2, + gestion_schema.niv2_abr, + gestion_schema.creation, + gestion_schema.producteur, + gestion_schema.editeur, + gestion_schema.lecteur + FROM z_asgard_admin.gestion_schema + WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR + CASE + WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL + THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) + WHEN gestion_schema.creation + THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) + ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + END +) ; + REVOKE SELECT ON TABLE z_asgard.gestion_schema_usr FROM g_consult ; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO public ; @@ -160,6 +197,30 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_usr TO pub REVOKE SELECT ON TABLE z_asgard.gestion_schema_etr FROM g_consult ; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE z_asgard.gestion_schema_etr TO public ; +CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( + SELECT + gestion_schema.bloc, + gestion_schema.nom_schema, + gestion_schema.oid_schema, + gestion_schema.creation, + gestion_schema.producteur, + gestion_schema.oid_producteur, + gestion_schema.editeur, + gestion_schema.oid_editeur, + gestion_schema.lecteur, + gestion_schema.oid_lecteur, + gestion_schema.ctrl + FROM z_asgard_admin.gestion_schema + WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR + CASE + WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL + THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) + WHEN gestion_schema.creation + THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) + ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + END +) ; + ------ View: z_asgard.asgardmenu_metadata ------ REVOKE SELECT ON TABLE z_asgard.asgardmenu_metadata FROM g_consult ; @@ -3255,7 +3316,7 @@ BEGIN IF NEW.ctrl[2] IS NULL OR NOT array_length(NEW.ctrl, 1) >= 2 OR NEW.ctrl[1] IS NULL - OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT') + OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT', 'END') OR NOT NEW.ctrl[2] = 'x7-A;#rzo' -- ctrl NULL ou invalide THEN @@ -3307,7 +3368,7 @@ BEGIN END IF ; ------ REQUETES AUTO A IGNORER ------ - -- les remontées du trigger AFTER (SELF) + -- les remontées du trigger AFTER (SELF ou END) -- sont exclues, car les contraintes ont déjà -- été validées (et pose problèmes avec les -- contrôles d'OID sur les UPDATE, car ceux-ci @@ -3315,7 +3376,7 @@ BEGIN -- les requêtes EXIT de même, car c'est un -- pré-requis à la suppression qui ne fait -- que modifier le champ ctrl - IF NEW.ctrl[1] IN ('SELF', 'EXIT') + IF NEW.ctrl[1] IN ('SELF', 'EXIT', 'END') THEN -- aucune action RETURN NEW ; @@ -3917,7 +3978,7 @@ DECLARE BEGIN ------ REQUETES AUTO A IGNORER ------ - -- les remontées du trigger lui-même (SELF), + -- les remontées du trigger lui-même (SELF ou END), -- ainsi que des event triggers sur les -- suppressions de schémas (DROP), n'appellent -- aucune action, elles sont donc exclues dès @@ -3932,7 +3993,7 @@ BEGIN -- des opérations sur les droits plus lourdes -- qui ne permettent pas de les exclure en -- amont - IF NEW.ctrl[1] IN ('SELF', 'DROP') + IF NEW.ctrl[1] IN ('SELF', 'END', 'DROP') THEN -- aucune action RETURN NULL ; @@ -3995,7 +4056,13 @@ BEGIN -- à des changements de noms IF NEW.ctrl[1] = 'RENAME' THEN - -- aucune action + -- on signale la fin du traitement, ce qui + -- va notamment permettre l'exécution de + -- asgard_visibilite_admin_after + UPDATE z_asgard.gestion_schema_etr + SET ctrl = ARRAY['END', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema ; + RETURN NULL ; END IF ; @@ -4121,40 +4188,6 @@ BEGIN END IF ; EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; - - -- permission de g_admin sur le producteur, s'il y a encore lieu - -- à noter que, dans le cas où le producteur n'a pas été modifié, g_admin - -- devrait déjà avoir une permission sur NEW.producteur, sauf à ce qu'elle - -- lui ait été retirée manuellement entre temps. Les requêtes suivantes - -- génèreraient alors une erreur même dans le cas où la modification ne - -- porte que sur les rôles lecteur/éditeur - ce qui peut-être perçu comme - -- discutable. - IF NOT pg_has_role('g_admin', NEW.producteur, 'USAGE') AND NOT b_superuser - THEN - IF createur IS NOT NULL - THEN - EXECUTE format('SET ROLE %I', createur) ; - EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; - RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE format('SET ROLE %I', utilisateur) ; - ELSE - SELECT grantee INTO administrateur - FROM information_schema.applicable_roles - WHERE is_grantable = 'YES' AND role_name = NEW.producteur ; - IF FOUND - THEN - EXECUTE format('SET ROLE %I', administrateur) ; - EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; - RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE format('SET ROLE %I', utilisateur) ; - ELSE - RAISE EXCEPTION 'TA6. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur - USING DETAIL = format('GRANT %I TO g_admin', NEW.producteur), - HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', - NEW.producteur) ; - END IF ; - END IF ; - END IF ; END IF ; ------ PREPARATION DE L'EDITEUR ------ @@ -4635,6 +4668,14 @@ BEGIN EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; + + -- on signale la fin du traitement, ce qui + -- va notamment permettre l'exécution de + -- asgard_visibilite_admin_after + UPDATE z_asgard.gestion_schema_etr + SET ctrl = ARRAY['END', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema + AND (ctrl[1] IS NULL OR NOT ctrl[1] = 'EXIT') ; RETURN NULL ; @@ -4716,6 +4757,16 @@ BEGIN USING ERRCODE = 'trigger_protocol_violated' ; END IF ; + ------ REQUETES AUTO A IGNORER ------ + -- ce trigger ne doit être déclenché qu'une fois, après la + -- fin de l'exécution de asgard_modify_gestion_schema_after, + -- soit quand ctrl indique END + IF NEW.ctrl[1] IS NULL OR NOT NEW.ctrl[1] = 'END' + THEN + -- aucune action + RETURN NULL ; + END IF ; + ------- GESTION DES PERMISSIONS DE G_ADMIN ------ -- on écarte les schémas non actifs et le cas où le From d6509e821e8b7552c5723537a231149aa0498da1 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:12:58 +0200 Subject: [PATCH 13/32] Update asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite de l'amélioration de la méthode d'attribution à `g_admin` de la permission sur les rôles producteurs : - Suppression des commandes obsolètes dans `asgard_on_modify_gestion_schema_after()`. - Ajustements dans `asgard_on_modify_gestion_schema_before()` et `asgard_visibilite_admin_after()`, avec l'ajout d'une modalité `'END'` pour le champ `ctrl` qui permet de signaler la fin des traitements découlant de la modification de la table de gestion, inhibe la ré-exécution de `asgard_on_modify_gestion_schema_before()` et `asgard_on_modify_gestion_schema_after()`, et autorise l'exécution de `asgard_visibilite_admin_after()`. - Modification en conséquence de la contrainte sur `gestion_schema` qui fixe les valeurs autorisées pour `ctrl`. Correction d'une coquille dans la définition des vues `gestion_schema_usr` et `gestion_schema_etr`, qui affectait la création de schémas via la table de gestion, avec un producteur dont le nom n'est pas normalisé, et par un utilisateur non membre de `g_admin`. --- asgard--1.4.0.sql | 76 ++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index d82c0b2..5ca7217 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -292,7 +292,7 @@ CREATE TABLE z_asgard_admin.gestion_schema CONSTRAINT gestion_schema_oid_roles_check CHECK ((oid_lecteur IS NULL OR NOT oid_lecteur = oid_producteur) AND (oid_editeur IS NULL OR NOT oid_editeur = oid_producteur) AND (oid_lecteur IS NULL OR oid_editeur IS NULL OR NOT oid_lecteur = oid_editeur)), - CONSTRAINT gestion_schema_ctrl_check CHECK (ctrl IS NULL OR array_length(ctrl, 1) >= 2 AND ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'MANUEL', 'EXIT')) + CONSTRAINT gestion_schema_ctrl_check CHECK (ctrl IS NULL OR array_length(ctrl, 1) >= 2 AND ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'MANUEL', 'EXIT', 'END')) ) WITH ( OIDS = FALSE @@ -361,7 +361,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR CASE WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL - THEN pg_has_role(quote_ident(gestion_schema.producteur::text)::name, 'USAGE'::text) + THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name @@ -415,7 +415,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( WHERE pg_has_role('g_admin'::text, 'USAGE'::text) OR CASE WHEN gestion_schema.creation AND gestion_schema.oid_producteur IS NULL - THEN pg_has_role(quote_ident(gestion_schema.producteur::text)::name, 'USAGE'::text) + THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name @@ -5091,7 +5091,7 @@ BEGIN IF NEW.ctrl[2] IS NULL OR NOT array_length(NEW.ctrl, 1) >= 2 OR NEW.ctrl[1] IS NULL - OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT') + OR NOT NEW.ctrl[1] IN ('CREATE', 'RENAME', 'OWNER', 'DROP', 'SELF', 'EXIT', 'END') OR NOT NEW.ctrl[2] = 'x7-A;#rzo' -- ctrl NULL ou invalide THEN @@ -5143,7 +5143,7 @@ BEGIN END IF ; ------ REQUETES AUTO A IGNORER ------ - -- les remontées du trigger AFTER (SELF) + -- les remontées du trigger AFTER (SELF ou END) -- sont exclues, car les contraintes ont déjà -- été validées (et pose problèmes avec les -- contrôles d'OID sur les UPDATE, car ceux-ci @@ -5151,7 +5151,7 @@ BEGIN -- les requêtes EXIT de même, car c'est un -- pré-requis à la suppression qui ne fait -- que modifier le champ ctrl - IF NEW.ctrl[1] IN ('SELF', 'EXIT') + IF NEW.ctrl[1] IN ('SELF', 'EXIT', 'END') THEN -- aucune action RETURN NEW ; @@ -5760,7 +5760,7 @@ DECLARE BEGIN ------ REQUETES AUTO A IGNORER ------ - -- les remontées du trigger lui-même (SELF), + -- les remontées du trigger lui-même (SELF ou END), -- ainsi que des event triggers sur les -- suppressions de schémas (DROP), n'appellent -- aucune action, elles sont donc exclues dès @@ -5775,7 +5775,7 @@ BEGIN -- des opérations sur les droits plus lourdes -- qui ne permettent pas de les exclure en -- amont - IF NEW.ctrl[1] IN ('SELF', 'DROP') + IF NEW.ctrl[1] IN ('SELF', 'END', 'DROP') THEN -- aucune action RETURN NULL ; @@ -5838,7 +5838,13 @@ BEGIN -- à des changements de noms IF NEW.ctrl[1] = 'RENAME' THEN - -- aucune action + -- on signale la fin du traitement, ce qui + -- va notamment permettre l'exécution de + -- asgard_visibilite_admin_after + UPDATE z_asgard.gestion_schema_etr + SET ctrl = ARRAY['END', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema ; + RETURN NULL ; END IF ; @@ -5964,40 +5970,6 @@ BEGIN END IF ; EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; - - -- permission de g_admin sur le producteur, s'il y a encore lieu - -- à noter que, dans le cas où le producteur n'a pas été modifié, g_admin - -- devrait déjà avoir une permission sur NEW.producteur, sauf à ce qu'elle - -- lui ait été retirée manuellement entre temps. Les requêtes suivantes - -- génèreraient alors une erreur même dans le cas où la modification ne - -- porte que sur les rôles lecteur/éditeur - ce qui peut-être perçu comme - -- discutable. - IF NOT pg_has_role('g_admin', NEW.producteur, 'USAGE') AND NOT b_superuser - THEN - IF createur IS NOT NULL - THEN - EXECUTE format('SET ROLE %I', createur) ; - EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; - RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE format('SET ROLE %I', utilisateur) ; - ELSE - SELECT grantee INTO administrateur - FROM information_schema.applicable_roles - WHERE is_grantable = 'YES' AND role_name = NEW.producteur ; - IF FOUND - THEN - EXECUTE format('SET ROLE %I', administrateur) ; - EXECUTE format('GRANT %I TO g_admin', NEW.producteur) ; - RAISE NOTICE '... Permission accordée à g_admin sur le rôle %.', NEW.producteur ; - EXECUTE format('SET ROLE %I', utilisateur) ; - ELSE - RAISE EXCEPTION 'TA6. Opération interdite. Permissions insuffisantes pour le rôle %.', NEW.producteur - USING DETAIL = format('GRANT %I TO g_admin', NEW.producteur), - HINT = format('Votre rôle doit être membre de %s avec admin option ou disposer de l''attribut CREATEROLE pour réaliser cette opération.', - NEW.producteur) ; - END IF ; - END IF ; - END IF ; END IF ; ------ PREPARATION DE L'EDITEUR ------ @@ -6478,6 +6450,14 @@ BEGIN EXECUTE format('SET ROLE %I', utilisateur) ; END IF ; + + -- on signale la fin du traitement, ce qui + -- va notamment permettre l'exécution de + -- asgard_visibilite_admin_after + UPDATE z_asgard.gestion_schema_etr + SET ctrl = ARRAY['END', 'x7-A;#rzo'] + WHERE nom_schema = NEW.nom_schema + AND (ctrl[1] IS NULL OR NOT ctrl[1] = 'EXIT') ; RETURN NULL ; @@ -6565,6 +6545,16 @@ BEGIN USING ERRCODE = 'trigger_protocol_violated' ; END IF ; + ------ REQUETES AUTO A IGNORER ------ + -- ce trigger ne doit être déclenché qu'une fois, après la + -- fin de l'exécution de asgard_modify_gestion_schema_after, + -- soit quand ctrl indique END + IF NEW.ctrl[1] IS NULL OR NOT NEW.ctrl[1] = 'END' + THEN + -- aucune action + RETURN NULL ; + END IF ; + ------- GESTION DES PERMISSIONS DE G_ADMIN ------ -- on écarte les schémas non actifs et le cas où le From 3d8b76521ff1b5ad9fcc37a1c72aaf63edbc5233 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:16:16 +0200 Subject: [PATCH 14/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout d'une variante non normalisée `t094b()` pour le test `t094()`. Modification du test `t094()` pour tenir compte du fait qu'un rôle qui crée un schéma avec un producteur non membre de `g_admin` n'a plus besoin d'avoir `ADMIN OPTION` sur ce rôle (il doit toujours en être membre). Nettoyage formel du test `t045()`. --- recette/asgard_recette.sql | 177 +++++++++++++++++++++++++++++++------ 1 file changed, 151 insertions(+), 26 deletions(-) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index 471fcc7..ed607ac 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -5645,8 +5645,6 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t045() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; e_mssg text ; e_detl text ; BEGIN @@ -5661,13 +5659,13 @@ BEGIN SET nom_schema = 'c_librairie' WHERE nom_schema = 'c_bibliotheque' ; - RETURN False ; + ASSERT False, 'échec assertion #1-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := (e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False) ; + ASSERT e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False, 'échec assertion #1-b' ; END ; ------ modification du champ producteur ------ @@ -5676,13 +5674,13 @@ BEGIN SET producteur = 'g_admin' WHERE nom_schema = 'c_bibliotheque' ; - RETURN False ; + ASSERT False, 'échec assertion #2-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False) ; + ASSERT e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False, 'échec assertion #2-b' ; END ; ------ modification du champ éditeur ------ @@ -5691,13 +5689,13 @@ BEGIN SET editeur = 'g_admin' WHERE nom_schema = 'c_bibliotheque' ; - RETURN False ; + ASSERT False, 'échec assertion #3-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False) ; + ASSERT e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False, 'échec assertion #3-b' ; END ; ------ modification du champ lecteur ------ @@ -5706,13 +5704,13 @@ BEGIN SET lecteur = 'g_admin' WHERE nom_schema = 'c_bibliotheque' ; - RETURN False ; + ASSERT False, 'échec assertion #4-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False) ; + ASSERT e_mssg ~ 'TB20[.]' OR e_detl ~ 'TB20[.]' OR False, 'échec assertion #4-b' ; END ; ------ mise à la corbeille ------ @@ -5726,13 +5724,13 @@ BEGIN SET creation = False WHERE nom_schema = 'c_bibliotheque' ; - RETURN False ; + ASSERT False, 'échec assertion #5-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB23[.]' OR e_detl ~ 'TB23[.]' OR False) ; + ASSERT e_mssg ~ 'TB23[.]' OR e_detl ~ 'TB23[.]' OR False, 'échec assertion #5-b' ; END ; ------ restauration du schéma ------ @@ -5747,14 +5745,14 @@ BEGIN BEGIN PERFORM z_asgard.asgard_initialise_schema('c_bibliotheque') ; - RETURN False ; + ASSERT False, 'échec assertion #6-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB25[.]' OR e_detl ~ 'TB25[.]' - OR e_mssg ~ 'FIS3[.]' OR e_detl ~ 'FIS3[.]' OR False) ; + ASSERT e_mssg ~ 'TB25[.]' OR e_detl ~ 'TB25[.]' + OR e_mssg ~ 'FIS3[.]' OR e_detl ~ 'FIS3[.]' OR False, 'échec assertion #6-b' ; END ; ------ référencement du schéma (par un INSERT) ------ @@ -5762,13 +5760,13 @@ BEGIN INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation) VALUES ('c_bibliotheque', 'postgres', True) ; - RETURN False ; + ASSERT False, 'échec assertion #7-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB25[.]' OR e_detl ~ 'TB25[.]' OR False) ; + ASSERT e_mssg ~ 'TB25[.]' OR e_detl ~ 'TB25[.]' OR False, 'échec assertion #7-b' ; END ; ------ création d'un schéma par INSERT ------ @@ -5778,13 +5776,13 @@ BEGIN DROP SCHEMA IF EXISTS c_librairie ; - RETURN False ; + ASSERT False, 'échec assertion #8-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB22[.]' OR e_detl ~ 'TB22[.]' OR False) ; + ASSERT e_mssg ~ 'TB22[.]' OR e_detl ~ 'TB22[.]' OR False, 'échec assertion #8-b' ; END ; ------ création d'un schéma par bascule de creation ------ @@ -5798,13 +5796,13 @@ BEGIN DROP SCHEMA IF EXISTS c_librairie ; - RETURN False ; + ASSERT False, 'échec assertion #9-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB21[.]' OR e_detl ~ 'TB21[.]' OR False) ; + ASSERT e_mssg ~ 'TB21[.]' OR e_detl ~ 'TB21[.]' OR False, 'échec assertion #9-b' ; END ; ------ attributation à postgres d'un schéma existant ------ @@ -5816,13 +5814,13 @@ BEGIN SET producteur = 'postgres' WHERE nom_schema = 'c_archives' ; - RETURN False ; + ASSERT False, 'échec assertion #10-a' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := r AND (e_mssg ~ 'TB24[.]' OR e_detl ~ 'TB24[.]' OR False) ; + ASSERT e_mssg ~ 'TB24[.]' OR e_detl ~ 'TB24[.]' OR False, 'échec assertion #10-b' ; END ; RESET ROLE ; @@ -5830,9 +5828,14 @@ BEGIN DROP SCHEMA c_archives ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema IN ('c_bibliotheque', 'c_librairie', 'c_archives') ; - RETURN coalesce(r, False) ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END @@ -17442,7 +17445,7 @@ BEGIN CREATE ROLE g_asgard_admin_delegue ; CREATE ROLE g_asgard_producteur ; CREATE ROLE g_asgard_connexion ; - GRANT g_asgard_admin_delegue TO g_asgard_connexion WITH ADMIN OPTION ; + GRANT g_asgard_admin_delegue TO g_asgard_connexion ; CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_producteur ; EXECUTE format('GRANT CREATE ON DATABASE %I TO %I', current_database(), 'g_asgard_admin_delegue') ; @@ -17548,3 +17551,125 @@ END $_$ ; COMMENT ON FUNCTION z_asgard_recette.t094() IS 'ASGARD recette. TEST : Capacité d''action des producteurs et administrateurs délégués.' ; + +-- FUNCTION: z_asgard_recette.t094b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t094b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE "g_asgard_ADMIN_délégué" ; + CREATE ROLE "g_asgard_PROducteur" ; + CREATE ROLE "g_asgard.connexion" ; + GRANT "g_asgard_ADMIN_délégué" TO "g_asgard.connexion" ; + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g_asgard_PROducteur" ; + + EXECUTE format('GRANT CREATE ON DATABASE %I TO %I', current_database(), 'g_asgard_ADMIN_délégué') ; + + -- tentative de création de schéma via la table de gestion + -- par un rôle qui n'est pas habilité à créer des schémas + BEGIN + SET ROLE "g_asgard_PROducteur" ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_"Librairie"', True, 'g_asgard_PROducteur') ; + ASSERT False, 'échec assertion 1-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^TB1[.]', 'échec assertion 1-b' ; + END ; + + -- tentative d'ajout d'un schéma inactif dans la table de gestion + -- par un rôle qui n'est pas habilité à créer des schémas + BEGIN + SET ROLE "g_asgard_PROducteur" ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_"Librairie"', False, 'g_asgard_PROducteur') ; + ASSERT False, 'échec assertion 2-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^TB1[.]', 'échec assertion 2-b' ; + END ; + + ------ Manipulations par un rôle habilité à créer des schémas ------ + SET ROLE "g_asgard.connexion" ; + + -- création d'un schéma par un rôle qui en a le droit + -- - commande directe : + CREATE SCHEMA "X=secret" AUTHORIZATION "g_asgard_ADMIN_délégué" ; + -- - via la table de gestion : + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_$Librairy$', True, 'g_asgard_ADMIN_délégué') ; + -- modification par commande directe : + ALTER SCHEMA "c_$Librairy$" RENAME TO "c_""Librairie""" ; + -- modification par la table de gestion : + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Ma jolie librairie' + WHERE nom_schema = 'c_"Librairie"' ; + ASSERT FOUND, 'échec assertion 3' ; + -- suppression : + DROP SCHEMA "c_""Librairie""" ; + DELETE FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_"Librairie"' ; + ASSERT FOUND, 'échec assertion 4' ; + + ------ Manipulations par un rôle non habilité ------ + SET ROLE "g_asgard_PROducteur" ; + + -- vérification qu'un rôle ne voit pas les schémas dont + -- il n'est pas producteur dans gestion_schema_usr + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'X=secret') = 0, 'échec assertion 5' ; + + -- ... et ça ne fait évidemment rien quand il tente de modifier + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'blabla' + WHERE nom_schema = 'X=secret' ; + ASSERT NOT FOUND, 'échec assertion 6' ; + + -- ... mais dans gestion_schema_read_only, il voit le schéma + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_read_only + WHERE nom_schema = 'X=secret') = 1, 'échec assertion 7' ; + + ------ Manipulations par le producteur du schéma ------ + -- via la table de gestion + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Ma grande bibliothèque' + WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT FOUND, 'échec assertion 8' ; + + -- pas de test de modification du schéma par + -- commande directe ou indirecte - les ALTER SCHEMA requièrent + -- le privilège CREATE sur la base + + -- création d'un objet dans le schéma + CREATE TABLE "c_Bibliothèque"."journal du mur" (jour date PRIMARY KEY, entree text) ; + + RESET ROLE ; + DROP SCHEMA "c_Bibliothèque" CASCADE ; + DROP SCHEMA "X=secret" ; + DELETE FROM z_asgard.gestion_schema_usr ; + EXECUTE format('REVOKE CREATE ON DATABASE %I FROM %I', current_database(), 'g_asgard_ADMIN_délégué') ; + DROP ROLE "g_asgard_ADMIN_délégué" ; + DROP ROLE "g_asgard_PROducteur" ; + DROP ROLE "g_asgard.connexion" ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t094b() IS 'ASGARD recette. TEST : Capacité d''action des producteurs et administrateurs délégués.' ; + From 959eaf0631c3a5c57b9e0d57e1d92d49ec355351 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:11:59 +0200 Subject: [PATCH 15/32] Update .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout des répertoires `__pycache__`, puisqu'il y a maintenant un petit module python pour accélérer l'exécution de la recette. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4afc38b..30e4af3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __save__/ +__pycache__/ From 5cc2f8e8400414b7b025aa25c34e2462f2bbd073 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:15:00 +0200 Subject: [PATCH 16/32] Update asgard--1.3.2--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise à jour des noms des ministères + service du numérique devenu direction du numérique. Le déclencheur `asgard_on_alter_objet` est désormais bien activé par les commandes `ALTER OPERATOR CLASS` et `ALTER OPERATOR FAMILY` sous PostgreSQL 9.5. --- asgard--1.3.2--1.4.0.sql | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index 4e8ca18..1c1f389 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -5,10 +5,10 @@ -- > Script de mise à jour depuis la version 1.3.2. -- -- Copyright République Française, 2020-2022. --- Secrétariat général du Ministère de la transition écologique, du --- Ministère de la cohésion des territoires et des relations avec les --- collectivités territoriales et du Ministère de la Mer. --- Service du numérique. +-- Secrétariat général du Ministère de la Transition écologique et +-- de la Cohésion des territoires, du Ministère de la Transition +-- énergétique et du Secrétariat d'Etat à la Mer. +-- Direction du numérique. -- -- contributrice pour cette version : Leslie Lemaire (SNUM/UNI/DRC). -- @@ -955,7 +955,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE') + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; ELSIF current_setting('server_version_num')::int < 110000 THEN @@ -965,8 +966,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', - 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY') + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY', 'ALTER STATISTICS') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; ELSE -- + ALTER PROCEDURE, ALTER ROUTINE @@ -975,8 +976,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', - 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY', 'ALTER PROCEDURE', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY', 'ALTER STATISTICS', 'ALTER PROCEDURE', 'ALTER ROUTINE') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; END IF ; From 71d2523bf5c6e2493e6751e93f07911e53741ec4 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:15:15 +0200 Subject: [PATCH 17/32] Update asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise à jour des noms des ministères + service du numérique devenu direction du numérique. Le déclencheur `asgard_on_alter_objet` est désormais bien activé par les commandes `ALTER OPERATOR CLASS` et `ALTER OPERATOR FAMILY` sous PostgreSQL 9.5. --- asgard--1.4.0.sql | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index 5ca7217..ff3cd52 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -4,10 +4,10 @@ -- ASGARD - Système de gestion des droits pour PostgreSQL, version 1.4.0 -- -- Copyright République Française, 2020-2022. --- Secrétariat général du Ministère de la transition écologique, du --- Ministère de la cohésion des territoires et des relations avec les --- collectivités territoriales et du Ministère de la Mer. --- Service du numérique. +-- Secrétariat général du Ministère de la Transition écologique et +-- de la Cohésion des territoires, du Ministère de la Transition +-- énergétique et du Secrétariat d'Etat à la Mer. +-- Direction du numérique. -- -- contributeurs : Leslie Lemaire (SNUM/UNI/DRC) et Alain Ferraton -- (SNUM/MSP/DS/GSG). @@ -1299,7 +1299,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE') + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; ELSIF current_setting('server_version_num')::int < 110000 THEN @@ -1309,8 +1310,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', - 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY') + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY', 'ALTER STATISTICS') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; ELSE -- + ALTER PROCEDURE, ALTER ROUTINE @@ -1319,8 +1320,8 @@ BEGIN 'ALTER MATERIALIZED VIEW', 'ALTER SEQUENCE', 'ALTER FOREIGN TABLE', 'ALTER FUNCTION', 'ALTER OPERATOR', 'ALTER AGGREGATE', 'ALTER COLLATION', 'ALTER CONVERSION', 'ALTER DOMAIN', 'ALTER TEXT SEARCH CONFIGURATION', - 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER STATISTICS', - 'ALTER OPERATOR CLASS', 'ALTER OPERATOR FAMILY', 'ALTER PROCEDURE', + 'ALTER TEXT SEARCH DICTIONARY', 'ALTER TYPE', 'ALTER OPERATOR CLASS', + 'ALTER OPERATOR FAMILY', 'ALTER STATISTICS', 'ALTER PROCEDURE', 'ALTER ROUTINE') EXECUTE PROCEDURE z_asgard_admin.asgard_on_alter_objet() ; END IF ; From 967d941f9b5ff641fa02e74fe5e13ecbd3cb481c Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:18:25 +0200 Subject: [PATCH 18/32] Add diagrams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les schémas de la documentation d'Asgard sont désormais publiés sur le Git. --- docs/diagrams/fonctions_asgard.png | Bin 0 -> 97376 bytes docs/diagrams/fonctions_asgard.svg | 1452 +++++++++++++++++++++ docs/diagrams/mecanique_asgard.png | Bin 0 -> 117860 bytes docs/diagrams/mecanique_asgard.svg | 1449 ++++++++++++++++++++ docs/diagrams/principe_role_de_groupe.png | Bin 0 -> 13449 bytes docs/diagrams/principe_role_de_groupe.svg | 410 ++++++ docs/diagrams/profils_asgard.png | Bin 0 -> 52423 bytes docs/diagrams/profils_asgard.svg | 933 +++++++++++++ docs/diagrams/roles_asgard.png | Bin 0 -> 75724 bytes docs/diagrams/roles_asgard.svg | 695 ++++++++++ docs/diagrams/tables_et_vues_asgard.png | Bin 0 -> 60619 bytes docs/diagrams/tables_et_vues_asgard.svg | 687 ++++++++++ 12 files changed, 5626 insertions(+) create mode 100644 docs/diagrams/fonctions_asgard.png create mode 100644 docs/diagrams/fonctions_asgard.svg create mode 100644 docs/diagrams/mecanique_asgard.png create mode 100644 docs/diagrams/mecanique_asgard.svg create mode 100644 docs/diagrams/principe_role_de_groupe.png create mode 100644 docs/diagrams/principe_role_de_groupe.svg create mode 100644 docs/diagrams/profils_asgard.png create mode 100644 docs/diagrams/profils_asgard.svg create mode 100644 docs/diagrams/roles_asgard.png create mode 100644 docs/diagrams/roles_asgard.svg create mode 100644 docs/diagrams/tables_et_vues_asgard.png create mode 100644 docs/diagrams/tables_et_vues_asgard.svg diff --git a/docs/diagrams/fonctions_asgard.png b/docs/diagrams/fonctions_asgard.png new file mode 100644 index 0000000000000000000000000000000000000000..1333cdb6c4af2b38c7fb377f6c2e6044fb790e79 GIT binary patch literal 97376 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�hV&Z&CU{Q$&TLgX150=9G^dkxLXF>jV_lk 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 FzX0hxgR1}l literal 0 HcmV?d00001 diff --git a/docs/diagrams/fonctions_asgard.svg b/docs/diagrams/fonctions_asgard.svg new file mode 100644 index 0000000..55b8f75 --- /dev/null +++ b/docs/diagrams/fonctions_asgard.svg @@ -0,0 +1,1452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + fonctionsd'ASGARD + + + + + + + + z_asgard + accessible à tous les utilisateurs(membres de g_consult) + asgard_deplace_obj + change un objet de schéma avec unegestion des droits adpatée + fonctions utilitaires à l'usagede l'ADL, des administrateursdélégués et des producteursdes schémas + asgard_initialise_obj + réinitialise les droits sur un objet(efface les modifications manuelles) + asgard_initialise_schema + réinitialise les droits sur un schéma et soncontenu (efface les modifications manuelles) + asgard_nettoyage_roles + met à jour la table de gestion après lerenommage ou la suppression d'un rôle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fonctions supplémentaires + • appelées par les fonctions déclencheursd'ASGARD ou les fonctions utilitaires• ne sont pas pensées pour un usage manuel,mais ce n'est pas interdit + + + + + + + + + + + + z_asgard_admin + + + + + + réservé à l'ADL (membres de g_admin) + asgard_all_login_grant_role + donne permission sur un rôle à tousles rôles de connexion du serveur + fonctions utilitaires àl'usage de l'ADL + asgard_import_nomenclature + pré-référence les schémas de la nomenclaturethématique nationale dans la table de gestion + asgard_initialisation_gestion_schema + référence les schémas pré-existants de la basedans la table de gestion + asgard_reaffecte_role + supprime tous les droits d'un rôle sur les objetsde la base ou les réassigne à un autre rôle + asgard_sortie_gestion_schema + déréférence un schéma de la table de gestion + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fonctions déclencheurs + • implémentent les mécanismes d'ASGARD :contrôle des informations saisies dans la tablede gestion, interception des commandes,application automatique des droits... + + + + + + + + + + + + + + asgard_initialise_all_schemas + réinitialise les droits sur tous les schémasréférencés (efface les modifications manuelles) + + + + + + + + + asgard_diagnostic + examine tous les schémas référencés et liste lesécarts aux droits standards prévus par ASGARD + + + + + + + + + + diff --git a/docs/diagrams/mecanique_asgard.png b/docs/diagrams/mecanique_asgard.png new file mode 100644 index 0000000000000000000000000000000000000000..6615f63f781cd0cdffef2da08216f79975db0cda GIT binary patch literal 117860 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

CY2VqTn!05h>)p&mN?xUFoW~^M*}pcHv_JT) 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 L{G|S|)9e2S%(Ov; literal 0 HcmV?d00001 diff --git a/docs/diagrams/mecanique_asgard.svg b/docs/diagrams/mecanique_asgard.svg new file mode 100644 index 0000000..0bf6c6f --- /dev/null +++ b/docs/diagrams/mecanique_asgard.svg @@ -0,0 +1,1449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + mécanique interned'ASGARD + + + saisie manuelledans la vueutilisateur + commandemanuelle demodificationde schéma + commandemanuelle desuppressionde schéma + commandemanuelle decréation d'objet + commande manuelle demodification d'objet + commandemanuelle decréation deschéma + + + + + + + + + + + + + contrôlede validité + répercutionautomatiquedes modifications + + déclencheurasgard_on_modify_gestion_schema_before + + + + + + + + commandes decréation, modification,suppression de schémaset d'attribution de droitsselon les principesd'ASGARD + + déclencheur asgard_on_modify_ gestion_schema_after + + + + + + + + commandes d'attributionde droits selon les principesd'ASGARD + + déclencheur sur évènementasgard_on_alter_objet + + + + + + + + + traduction + + déclencheur sur évènementasgard_on_create_schema + + + + + + + + + commandes d'attributionde droits selon lesprincipes d'ASGARD + + déclencheur sur évènementasgard_on_create_objet + + + + + + + + + traduction + + déclencheur surévènementasgard_on_drop_schema + + + + + + + + + + traduction + + déclencheur sur évènementasgard_on_alter_schema + + + + + + + + + BASE DE DONNEES + ASGARD + + tablez_asgard_admin.gestion_schema + + + + + + + + + + + vue modifiablez_asgard.gestion_schema_usr + + + + + + + + vue modifiablez_asgard.gestion_schema_etr + + + + + + + + attribution à g_admin dela permission sur le rôleproducteur du schéma + + déclencheur asgard_visibilite_admin_after + + + + + + + + diff --git a/docs/diagrams/principe_role_de_groupe.png b/docs/diagrams/principe_role_de_groupe.png new file mode 100644 index 0000000000000000000000000000000000000000..53fd56059d234737f7e2f7f26edf73e80950e226 GIT binary patch literal 13449 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 hyR9~L&YVjq| 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 Mq@`FdZyojj0A`WpC;$Ke literal 0 HcmV?d00001 diff --git a/docs/diagrams/principe_role_de_groupe.svg b/docs/diagrams/principe_role_de_groupe.svg new file mode 100644 index 0000000..42bfa72 --- /dev/null +++ b/docs/diagrams/principe_role_de_groupe.svg @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + rôle deconnexion + + + + + + + + + + + + + + + + + + rôle degroupe + + + appartientà + + a des droitssur + + + objets : + schémastablesvuesfonctionsetc. + + + + + diff --git a/docs/diagrams/profils_asgard.png b/docs/diagrams/profils_asgard.png new file mode 100644 index 0000000000000000000000000000000000000000..7990d5dc5cc8a1954b8c248ff77799cc9746f24d GIT binary patch literal 52423 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 RcQFGHc)I$ztaD0e0s!o1LQwz! literal 0 HcmV?d00001 diff --git a/docs/diagrams/profils_asgard.svg b/docs/diagrams/profils_asgard.svg new file mode 100644 index 0000000..7f70b18 --- /dev/null +++ b/docs/diagrams/profils_asgard.svg @@ -0,0 +1,933 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + 1 + 3 + .. + + + + + + + + + + + + + + + + + + 1 rôle producteur + 0 ou 1 rôle éditeur + • est propriétaire du schémaet tout ce qu'il contient• peut créer de nouveauxobjets dans le schéma• peut changer la définitiondes objets et les supprimer• peut conférer à d'autresrôles des droits sur les objets• peut manipuler sansrestriction les données destables et vues• a plus largement tous lesdroits sur tous les objets duschéma + + + + + + + 1 + 3 + .. + + + + + + + + + + + + + + + + + + etc. + obligatoire, rôle de groupe + optionnel + + + + + + • accède au schéma• peut modifier les donnéesdes tables et des vues, ajouteret supprimer des enregistrements• peut utiliser les séquences + + + + + + + + + + 1 + 3 + .. + + + + + + + + 0 ou 1 rôle lecteur + optionnel + • accède au schéma• peut lire les donnéesdes tables et des vues• peut lire la valeur couranted'une séquence + + + + + + + + + + + + + + + + + + 1 schéma,3 profils de droits + + + + + + + + + + + + + + diff --git a/docs/diagrams/roles_asgard.png b/docs/diagrams/roles_asgard.png new file mode 100644 index 0000000000000000000000000000000000000000..30092c427db185ebee90c80b8f08cb6b7f0002e8 GIT binary patch literal 75724 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + g_admin + g_consult + • à attribuer aux administrateurs duserveur et à personne d'autre• peut réaliser toutes les actions quine requièrent pas un super-utilisateur• de nombreuses fonctionnalitésd'ASGARD ne sont accessibles qu'àses membres• ASGARD le rend automatiquementmembre de tous les rôles de groupeutilisés comme producteurs des schémas + rôle de groupe, créé par ASGARD + rôle de groupe, créé par ASGARD + g_admin_ext + rôle de groupe, créé par ASGARD + • rôle technique + rôlesd'ASGARD + + consult.default + rôle de connexion, créé par ASGARD + • rôle de connexion partagé pour unaccès en lecture seule aux donnéespubliques• peut être supprimé au gré du service + • à désigner comme lecteur pour tousles schémas contenant des donnéespubliques• à attribuer à tous les rôles de connexion + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NE PAS MODIFIER LES DROITS DU RÔLESUR LES OBJETS D'ASGARD + NE PAS LUI CONFÉRER DE DROITS D'ÉDITION + NE PAS MODIFIER LES DROITS DU RÔLESUR LES OBJETS D'ASGARDNE PAS CONFÉRER À D'AUTRES RÔLESQUE G_ADMIN + NE PAS LUI CONFÉRER D'AUTRES DROITSQUE L'APPARTENANCE À G_CONSULT + + + diff --git a/docs/diagrams/tables_et_vues_asgard.png b/docs/diagrams/tables_et_vues_asgard.png new file mode 100644 index 0000000000000000000000000000000000000000..f744dfb7aba84b36810e87934bce6ba3978c601f GIT binary patch literal 60619 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 literal 0 HcmV?d00001 diff --git a/docs/diagrams/tables_et_vues_asgard.svg b/docs/diagrams/tables_et_vues_asgard.svg new file mode 100644 index 0000000..bad9b48 --- /dev/null +++ b/docs/diagrams/tables_et_vues_asgard.svg @@ -0,0 +1,687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + z_asgard_admin + + + + + + réservé à l'ADL (membres de g_admin) + + + + + + tables et vuesd'ASGARD + + z_asgard + accessible à tous les utilisateurs + + gestion_schema_usr + VUE UTILISATEUR• outil de gestion des schémas et des droits(vue modifiable)• à l'usage de l'ADL, des administrateursdélégués et des producteurs des schémas• l'utilisateur ne voit que les schémas qu'ilest habilité à gérer + + + + + + + + + + + + + + + gestion_schema + • table de stockage des données d'ASGARD• alimente les vues de z_asgardNE PAS UTILISER DIRECTEMENT + + gestion_schema_etr + • vue technique, sans intérêt pour lesutilisateurs + + + + + + + + asgardmenu_metadata + • vue destinée au plugin QGIS AsgardMenu• répertorie tous les schémas référencés enspécifiant les droits de l'utilisateur courantsur chacun d'eux + + + + + + + + asgardmanager_metadata + • vue destinée au plugin QGIS AsgardManager• répertorie tous les schémas référencés + + + + + + + + gestion_schema_read_only + • en lecture seule• mêmes informations que dansgestion_schema_usr, mais l'utilisateur voittous les schémas référencés + + + + + + + + + From 5c6068c2bb8149b566061138c9268ec9a08ef6d4 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:22:43 +0200 Subject: [PATCH 19/32] Create __init__.py --- recette/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 recette/__init__.py diff --git a/recette/__init__.py b/recette/__init__.py new file mode 100644 index 0000000..e69de29 From b38d83ee0975bdbdab9800dcad095a85d5706532 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:25:53 +0200 Subject: [PATCH 20/32] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise à jour des noms des ministères + service du numérique devenu direction du numérique. Licence pour la documentation. Ajustement du paragraphe sur la compatibilité. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 717bc44..7bccb94 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ASGARD a été conçue et développée dans le cadre du groupe de travail PostGI ## Compatibilité -PostgreSQL 9.5 ou supérieur. Toutes les versions d'ASGARD sont testées sur les versions de PostgreSQL officiellement supportées au moment de leur déploiement, ainsi que la version 9.5. La version 1.3.2 d'ASGARD est par exemple la première dont la compatibilité avec PostgreSQL 14 a été évaluée. +PostgreSQL 9.5 et PostgreSQL 10 ou supérieur. Toutes les versions d'ASGARD sont testées sur les versions de PostgreSQL officiellement supportées au moment de leur déploiement, ainsi que la version 9.5. La version 1.4.0 d'ASGARD est par exemple la première dont la compatibilité avec PostgreSQL 15 a été évaluée. ## Installation et utilisation @@ -21,13 +21,15 @@ L'extension PostgreSQL ASGARD est un logiciel libre publié sous licence CeCILL- Cf. [LICENCE.txt](https://github.com/MTES-MCT/asgard-postgresql/blob/master/LICENCE.txt). +La documentation de la suite ASGARD, incluant ses illustrations (répertoire `docs`), est publiée sous [licence ouverte Etalab 2.0](https://spdx.org/licenses/etalab-2.0). + ## Crédits © République Française, 2020-2022. ### Éditeur -Service du numérique du ministère de la transition écologique, du ministère de la cohésion des territoires et des relations avec les collectivités territoriales et du ministère de la mer. +Direction du numérique du Ministère de la transition écologique et de la cohésion des territoires, du Ministère de la transition énergétique et du Secrétariat d'Etat à la mer. ### Contributeurs From 3b6dad7e4b841d6ab0a418975cf661f564bdbe28 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:33:03 +0200 Subject: [PATCH 21/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise à jour des noms des ministères + service du numérique devenu direction du numérique. Correction sur les tests `t092` et `t092b` pour qu'ils ne tentent pas de créer des séquences `GENERATED ALWAYS AS IDENTITY` sous PostgreSQL 9.5. Ajout du test `t095`, qui confirme qu'un utilisateur non habilité ne peut réaliser aucune action via la table de gestion en dépit de ses droits d'édition sur `z_asgard.gestion_schema_usr`. --- recette/asgard_recette.sql | 237 +++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 64 deletions(-) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index ed607ac..c787d0c 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -4,10 +4,10 @@ -- > Script de recette. -- -- Copyright République Française, 2020-2022. --- Secrétariat général du Ministère de la transition écologique, du --- Ministère de la cohésion des territoires et des relations avec les --- collectivités territoriales et du Ministère de la Mer. --- Service du numérique. +-- Secrétariat général du Ministère de la Transition écologique et +-- de la Cohésion des territoires, du Ministère de la Transition +-- énergétique et du Secrétariat d'Etat à la Mer. +-- Direction du numérique. -- -- contributeurs pour la recette : Leslie Lemaire (SNUM/UNI/DRC). -- @@ -17030,18 +17030,32 @@ BEGIN -- séquence témoin : ne sera pas déplacée, donc ne devrait pas -- empêcher le déplacement de la table - CREATE TABLE c_bibliotheque.journal_du_mur ( - idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - ids serial, - id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), - jour date, entree text, auteur text - ) ; - CREATE TABLE c_librairie.journal_du_mur_bis ( - idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - ids serial, - id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), - jour date, entree text, auteur text - ) ; + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'CREATE TABLE c_bibliotheque.journal_du_mur ( + idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + ids serial, + id int DEFAULT nextval(''c_bibliotheque.compteur''::regclass), + jour date, entree text, auteur text + )' ; + EXECUTE 'CREATE TABLE c_librairie.journal_du_mur_bis ( + idi int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + ids serial, + id int DEFAULT nextval(''c_bibliotheque.compteur''::regclass), + jour date, entree text, auteur text + )' ; + ELSE + CREATE TABLE c_bibliotheque.journal_du_mur ( + ids serial PRIMARY KEY, + id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), + jour date, entree text, auteur text + ) ; + CREATE TABLE c_librairie.journal_du_mur_bis ( + ids serial PRIMARY KEY, + id int DEFAULT nextval('c_bibliotheque.compteur'::regclass), + jour date, entree text, auteur text + ) ; + END IF ; CREATE INDEX journal_du_mur_auteur_idx ON c_bibliotheque.journal_du_mur USING btree (auteur) ; @@ -17114,20 +17128,23 @@ BEGIN RENAME TO journal_du_mur_bis_ids_seq ; -- séquence identity - ALTER INDEX c_librairie.journal_du_mur_bis_idi_seq - RENAME TO journal_du_mur_idi_seq ; - - BEGIN - PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', - 'journal_du_mur', 'table', 'c_librairie') ; - ASSERT False, 'échec assertion 5-a' ; - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; - ASSERT e_mssg ~ '^FDO11[.].*journal_du_mur_idi_seq', 'échec assertion 5-b' ; - END ; - - ALTER INDEX c_librairie.journal_du_mur_idi_seq - RENAME TO journal_du_mur_bis_idi_seq ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ALTER INDEX c_librairie.journal_du_mur_bis_idi_seq + RENAME TO journal_du_mur_idi_seq ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_bibliotheque', + 'journal_du_mur', 'table', 'c_librairie') ; + ASSERT False, 'échec assertion 5-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*journal_du_mur_idi_seq', 'échec assertion 5-b' ; + END ; + + ALTER INDEX c_librairie.journal_du_mur_idi_seq + RENAME TO journal_du_mur_bis_idi_seq ; + END IF ; -- table ALTER TABLE c_librairie.journal_du_mur_bis @@ -17160,10 +17177,13 @@ BEGIN WHERE relname = 'journal_du_mur_auteur_idx' AND relnamespace = 'c_librairie'::regnamespace), 'échec assertion 7-c' ; - ASSERT EXISTS (SELECT relname FROM pg_class - WHERE relname = 'journal_du_mur_idi_seq' - AND relnamespace = 'c_librairie'::regnamespace), - 'échec assertion 7-d' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'journal_du_mur_idi_seq' + AND relnamespace = 'c_librairie'::regnamespace), + 'échec assertion 7-d' ; + END IF ; ASSERT EXISTS (SELECT relname FROM pg_class WHERE relname = 'journal_du_mur_ids_seq' AND relnamespace = 'c_librairie'::regnamespace), @@ -17228,18 +17248,32 @@ BEGIN -- séquence témoin : ne sera pas déplacée, donc ne devrait pas -- empêcher le déplacement de la table - CREATE TABLE "c_Bibliothèque"."JournalDuMur" ( - "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - "ID$" serial, - id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), - jour date, entree text, auteur text - ) ; - CREATE TABLE "c_LIB $rairie"."JournalDuMur B!s" ( - "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - "ID$" serial, - id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), - jour date, entree text, auteur text - ) ; + IF current_setting('server_version_num')::int >= 100000 + THEN + EXECUTE 'CREATE TABLE "c_Bibliothèque"."JournalDuMur" ( + "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + "ID$" serial, + id int DEFAULT nextval(''"c_Bibliothèque"."""compteur"""''::regclass), + jour date, entree text, auteur text + )' ; + EXECUTE 'CREATE TABLE "c_LIB $rairie"."JournalDuMur B!s" ( + "IDI" int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + "ID$" serial, + id int DEFAULT nextval(''"c_Bibliothèque"."""compteur"""''::regclass), + jour date, entree text, auteur text + )' ; + ELSE + CREATE TABLE "c_Bibliothèque"."JournalDuMur" ( + "ID$" serial PRIMARY KEY, + id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), + jour date, entree text, auteur text + ) ; + CREATE TABLE "c_LIB $rairie"."JournalDuMur B!s" ( + "ID$" serial PRIMARY KEY, + id int DEFAULT nextval('"c_Bibliothèque"."""compteur"""'::regclass), + jour date, entree text, auteur text + ) ; + END IF ; CREATE INDEX "JournalDuMur_auteur_idx" ON "c_Bibliothèque"."JournalDuMur" USING btree (auteur) ; @@ -17312,20 +17346,23 @@ BEGIN RENAME TO "JournalDuMur B!s_ID$_seq" ; -- séquence identity - ALTER INDEX "c_LIB $rairie"."JournalDuMur B!s_IDI_seq" - RENAME TO "JournalDuMur_IDI_seq" ; - - BEGIN - PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', - 'JournalDuMur', 'table', 'c_LIB $rairie') ; - ASSERT False, 'échec assertion 5-a' ; - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; - ASSERT e_mssg ~ '^FDO11[.].*JournalDuMur_IDI_seq', 'échec assertion 5-b' ; - END ; - - ALTER INDEX "c_LIB $rairie"."JournalDuMur_IDI_seq" - RENAME TO "JournalDuMur B!s_IDI_seq" ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ALTER INDEX "c_LIB $rairie"."JournalDuMur B!s_IDI_seq" + RENAME TO "JournalDuMur_IDI_seq" ; + + BEGIN + PERFORM z_asgard.asgard_deplace_obj('c_Bibliothèque', + 'JournalDuMur', 'table', 'c_LIB $rairie') ; + ASSERT False, 'échec assertion 5-a' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^FDO11[.].*JournalDuMur_IDI_seq', 'échec assertion 5-b' ; + END ; + + ALTER INDEX "c_LIB $rairie"."JournalDuMur_IDI_seq" + RENAME TO "JournalDuMur B!s_IDI_seq" ; + END IF ; -- table ALTER TABLE "c_LIB $rairie"."JournalDuMur B!s" @@ -17358,10 +17395,13 @@ BEGIN WHERE relname = 'JournalDuMur_auteur_idx' AND relnamespace = '"c_LIB $rairie"'::regnamespace), 'échec assertion 7-c' ; - ASSERT EXISTS (SELECT relname FROM pg_class - WHERE relname = 'JournalDuMur_IDI_seq' - AND relnamespace = '"c_LIB $rairie"'::regnamespace), - 'échec assertion 7-d' ; + IF current_setting('server_version_num')::int >= 100000 + THEN + ASSERT EXISTS (SELECT relname FROM pg_class + WHERE relname = 'JournalDuMur_IDI_seq' + AND relnamespace = '"c_LIB $rairie"'::regnamespace), + 'échec assertion 7-d' ; + END IF ; ASSERT EXISTS (SELECT relname FROM pg_class WHERE relname = 'JournalDuMur_ID$_seq' AND relnamespace = '"c_LIB $rairie"'::regnamespace), @@ -17673,3 +17713,72 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t094b() IS 'ASGARD recette. TEST : Capacité d''action des producteurs et administrateurs délégués.' ; + +-- FUNCTION: z_asgard_recette.t095() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t095() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE mister_asgard_x ; + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_admin ; + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Bibliothèque' ; + + SET ROLE mister_asgard_x ; + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_read_only) = 1, + 'échec assertion 1' ; + ASSERT ( + SELECT niv1 FROM z_asgard.gestion_schema_read_only + WHERE nom_schema = 'c_bibliotheque' + ) = 'Bibliothèque', + 'échec assertion 2' ; + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_usr) = 0, + 'échec assertion 3' ; + + UPDATE z_asgard.gestion_schema_usr + SET niv1 = 'Archives' ; + ASSERT ( + SELECT niv1 FROM z_asgard.gestion_schema_read_only + WHERE nom_schema = 'c_bibliotheque' + ) = 'Bibliothèque', + 'échec assertion 4' ; + + DELETE FROM z_asgard.gestion_schema_usr ; + ASSERT (SELECT count(*) FROM z_asgard.gestion_schema_read_only) = 1, + 'échec assertion 5' ; + + BEGIN + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation) + VALUES ('c_librairie', False) ; + ASSERT False, 'échec assertion 6' ; + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT ; + ASSERT e_mssg ~ '^TB1[.]', 'échec assertion 1-b' ; + END ; + + RESET ROLE ; + DROP SCHEMA c_bibliotheque CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE mister_asgard_x ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t095() IS 'ASGARD recette. TEST : Un utilisateur lambda ne peut rien faire avec la table de gestion.' ; + From be360919e7fbafc7d7f7c66fd9c3d1d82600a8c1 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:34:38 +0200 Subject: [PATCH 22/32] Create runner.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module pour accélérer l'exécution de la recette sur un ensemble de bases (à date, une par version de PostgreSQL prise en charge). --- recette/runner.py | 452 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 recette/runner.py diff --git a/recette/runner.py b/recette/runner.py new file mode 100644 index 0000000..5644487 --- /dev/null +++ b/recette/runner.py @@ -0,0 +1,452 @@ +"""Exécution multi-serveurs de la recette. + +Ce module permet de lancer la recette d'Asgard sur un +ensemble de bases pré-définies, pour différentes versions de +PostgreSQL. Les bases et leur accès sont configurés via le +dictionnaire :py:data:`PG_DATABASES` ci-dessus. + +Sous Windows, il est nécessaire de lancer les commandes en +tant qu'administrateur pour permettre la mise à jour des fichiers +de l'extension dans les répertoires dédiés des serveurs. À défaut +il faudra inhiber la mise à jour automatique, soit dans la +configuration des bases, sans en exécutant la fonction +:py:fun:`run` avec un paramètre ``do_not_copy`` valant ``True``. + +Exécution de la recette complète, pour toutes les versions +de PostgreSQL : + + >>> run() + +Exécution de la recette pour une version donnée, ici +PostgreSQL 15 : + + >>> run('15') + +... ou plusieurs versions : + + >>> run(['14', '15']) + +En pratique, les opérations suivantes sont réalisées : + +* copie des fichiers d'Asgard dans le répertoire des + extensions du serveur, ie tous les fichiers .sql et + .control à la racine du dépôt. +* activation des extensions nécessaires à l'exécution des + tests sur la base cible. +* désactivation/réactivation d'Asgard dans la version + considérée. +* création des fonctions de recette à partir de la + dernière version du fichier ``recette/asgard_recette.sql``, + dans un schéma ``asgard_recette``. +* exécution de la recette. + +Pour chaque base testée, les erreurs rencontrées (ie les +fonctions qui ont échoué) sont affichées dans la console. + +""" + +import psycopg2 +from psycopg2 import sql +from pathlib import Path +from contextlib import contextmanager + +from recette import __path__ as recette_path + +ASGARD_PATH = Path(recette_path[0]).parent + +PG_DATABASES = [ + { + 'pg_version': '9.5', + 'port': '5431' + }, + { + 'pg_version': '10', + 'port': '5432' + }, + { + 'pg_version': '11', + 'port': '5433' + }, + { + 'pg_version': '12', + 'port': '5434' + }, + { + 'pg_version': '13', + 'port': '5435' + }, + { + 'pg_version': '14', + 'port': '5436' + }, + { + 'pg_version': '15', + 'port': '5437' + } +] +"""Bases de tests. + +``pg_version`` est le numéro de la version de PostgreSQL. + +``host`` est l'adresse de l'hôte, par défaut ``'localhost'``. + +``port`` est le port d'accès, par défaut ``'5432'``. + +``dbname`` est le nom de la base de données, par défaut ``asgard_rec``. + +``user`` est le nom du rôle de connexion super-utilisateur, par +défaut ``'postgres'``. + +``password`` est le mot de passe de connexion. S'il n'est pas écrit +en clair dans cette liste (ce qui est a priori déconseillé), il sera +demandé dynamiquement d'abord un mot de passe général pour toutes les +connexions, puis, à défaut, un mot de passe par connexion. + +``extension_dir`` est le chemin du répertoire où placer les fichiers +de l'extension pour que celle-ci puisse être activée sur la base. Si +non fourni, il est déduit du numéro de version, avec les règles +de nommage par défaut sous Windows. + +``do_not_copy`` est un booléen. S'il est présent et vaut ``True``, +les fichiers de l'extension ne seront pas mis à jour d'après la +source. L'opération devra avoir été réalisée manuellement avant +l'exécution de la recette. + +""" + +class PgConnectionInfo(): + """Chaîne de connexion PostgreSQL. + + Parameters + ---------- + pg_version : str, optional + Numéro de la version de PostgreSQL. + host : str, default 'localhost' + Adresse de l'hôte. + port : str, default '5432' + Port d'accès. + dbname : str, default 'asgard_rec' + Nom de la base de données de recette. + user : str, default 'postgres' + Rôle de connexion à utiliser. Il doit s'agir d'un + super-utilisateur. + password : str, optional + Mot de passe de connexion. + extension_dir : str, optional + Chemin du répertoire où placer les fichiers + de l'extension pour que celle-ci puisse être activée + sur la base. Si non fourni, il est déduit du numéro + de version, avec les règles de nommage par défaut sous + Windows. + do_not_copy : bool, optional + Si ``True``, les fichiers de l'extension ne seront pas + mis à jour d'après la source. L'opération devra avoir été + réalisée manuellement avant l'exécution de la recette. + + Attributes + ---------- + pg_version : str + Numéro de la version de PostgreSQL. + host : str + Adresse de l'hôte. + port : str + Port d'accès. + dbname : str + Nom de la base de données de recette. + user : str + Rôle de connexion. + password : str + Mot de passe de connexion. + extension_dir : Path + Chemin du répertoire où placer les fichiers + de l'extension pour que celle-ci puisse être activée + sur la base. + + """ + PASSWORD = None + DATABASES = [] + + def __init__( + self, pg_version, host='localhost', port='5432', dbname='asgard_rec', + user='postgres', password=None, extension_dir=None, do_not_copy=False + ): + self.pg_version = pg_version + self.host = host + self.port = port + self.dbname = dbname + self.user = user + self.password = password + self.extension_dir = None + if not do_not_copy: + self.extension_dir = Path( + extension_dir or f'C:\\Program Files\\PostgreSQL\\{pg_version}\\share\\extension' + ) + + def __str__(self): + return f'host={self.host} port={self.port} dbname={self.dbname} user={self.user} password={self.password}' + + @property + def pretty(self): + return f'< PostgreSQL {self.pg_version} - {self.host}:{self.port} {self.dbname} >' + + @classmethod + def password(cls, password=None): + """Définit un mot de passe par défaut pour toutes les connexions. + + Il est possible de passer cette étape en ne saisissant simplement + aucune valeur avant de valider. + + """ + if not password: + password = input('Mot de passe par défaut : ') + if not password: + return + cls.PASSWORD = password + + @classmethod + def add( + cls, pg_version='', host='localhost', port='5432', dbname='asgard_rec', + user='postgres', password=None + ): + """Génère et mémorise une nouvelle connexion. + + Si aucun mot de passe n'est fourni en argument et qu'il + n'y avait pas non plus de mot de passe par défaut, il + est redemandé dynamiquement. + + Parameters + ---------- + pg_version : str, optional + Numéro de la version de PostgreSQL. + host : str, default 'localhost' + Adresse de l'hôte. + port : str, default '5432' + Port d'accès. + dbname : str, default 'asgard_rec' + Nom de la base de données de recette. + user : str, default 'postgres' + Rôle de connexion. Il doit s'agir d'un + super-utilisateur. + password : str, optional + Mot de passe de connexion. + + """ + password = password or cls.PASSWORD + while not password: + password = input( + f'Mot de passe pour le rôle "{user}" sur ' + f'{host}:{port} ({pg_version}) ? ' + ) + cls.DATABASES.append( + PgConnectionInfo( + pg_version, host, port, dbname, user, + password + ) + ) + + @classmethod + def remove(cls, pg_connection_info): + """Supprime une connexion de la liste des connexions mémorisées. + + La fonction n'a aucun effet si la connexion n'était + pas référencée. + + Parameters + ---------- + pg_connection_info : PgConnectionInfo + La connexion à supprimer. + + """ + if pg_connection_info and pg_connection_info in cls.DATABASES: + cls.DATABASES.remove(pg_connection_info) + + @classmethod + def build(cls): + """Mémorise les connexions pré-définies dans PG_DATABASES.""" + if not cls.PASSWORD: + cls.password() + for connection_dict in PG_DATABASES: + cls.add(**connection_dict) + + @classmethod + def databases(cls): + """Générateur sur les connexions mémorisées.""" + if not cls.DATABASES: + cls.build() + for pg_connection_info in cls.DATABASES: + yield pg_connection_info + + +def pg_test_functions(filepath=None): + """Importe les commandes de création des fonctions PostgreSQL qui servent à la recette. + + Parameters + ---------- + filepath : str or Path, optional + Chemin complet du fichier source. Si non + fourni, la fonction ira chercher le fichier + ``recette/asgard_recette.sql``. + + Returns + ------- + str + + """ + pfile = Path(filepath) if filepath else Path(recette_path[0]) / 'asgard_recette.sql' + + if not pfile.exists(): + raise FileNotFoundError("Can't find file {}.".format(pfile)) + + if not pfile.is_file(): + raise TypeError("{} is not a file.".format(pfile)) + + return pfile.read_text(encoding='UTF-8') + +def copy_file(filepath, target_dir): + """Copie un fichier ou plusieurs fichiers. + + Parameters + ---------- + filepath : str or Path or list(str or Path), optional + Chemin complet du fichier source. Il est possible + de fournir une liste de chemins. + target_dir : str or Path, optional + Chemin complet du répertoire de destination. + + """ + if isinstance(filepath, list): + for file in filepath: + copy_file(file, target_dir) + + pfile = Path(filepath) + + if not pfile.exists(): + raise FileNotFoundError("Can't find file {}.".format(pfile)) + + if not pfile.is_file(): + raise TypeError("{} is not a file.".format(pfile)) + + pdir = Path(target_dir) + + if not pdir.exists(): + raise FileNotFoundError("Can't find directory {}.".format(pdir)) + + if not pdir.is_dir(): + raise TypeError("{} is not a directory.".format(pdir)) + + content = pfile.read_text(encoding='UTF-8') + + target_pfile = pdir / pfile.name + target_pfile.write_text(content, encoding='UTF-8') + +@contextmanager +def pg_connection( + pg_connection_info, pg_test_functions, + extension_name, extension_version=None, + extension_requirements=None, test_requirements=None +): + conn = psycopg2.connect(str(pg_connection_info)) + with conn: + with conn.cursor() as cur: + for extension in test_requirements or []: + cur.execute( + sql.SQL( + ''' + CREATE EXTENSION IF NOT EXISTS {} ; + ''' + ).format(sql.Identifier(extension)) + ) + for extension in extension_requirements or []: + cur.execute( + sql.SQL( + ''' + CREATE EXTENSION IF NOT EXISTS {} ; + ''' + ).format(sql.Identifier(extension)) + ) + if extension_version: + cur.execute( + sql.SQL( + ''' + DROP EXTENSION IF EXISTS {extension} ; + CREATE EXTENSION {extension} VERSION %s ; + ''' + ).format(extension=sql.Identifier(extension_name)), + (extension_version,) + ) + else: + cur.execute( + sql.SQL( + ''' + DROP EXTENSION IF EXISTS {extension} ; + CREATE EXTENSION {extension} ; + ''' + ).format(extension=sql.Identifier(extension_name)) + ) + cur.execute(pg_test_functions) + try: + yield conn + + finally: + conn.close() + +def run(pg_versions=None, extension_version=None, do_not_copy=False): + """Exécute la recette. + + Parameters + ---------- + pg_versions : str or list(str), optional + Numéros des versions pour lesquelles la recette doit être + lancée. Si non spécifié, la recette est lancée sur toutes + les bases de test listées par :py:data:`PG_DATABASES`. + extension_version : str, optional + Le numéro de la version de l'extension à tester. Si non + spécifié, c'est la version par défaut du fichier + ``asgard.control`` qui est considérée. + do_not_copy : bool, default False + Si ``True``, les fichiers de l'extension ne seront pas + mis à jour avant exécution de la recette. + + """ + if isinstance(pg_versions, str): + pg_versions = [pg_versions] + + kwargs = { + 'pg_test_functions': pg_test_functions(), + 'extension_name': 'asgard', + 'test_requirements': ['postgres_fdw'] + } + if extension_version: + kwargs['extension_version'] = extension_version + + for pg_connection_info in PgConnectionInfo.databases(): + if pg_versions and not pg_connection_info.pg_version in pg_versions: + continue + print('\n--------------') + print(pg_connection_info.pretty) + + # mise à jour des fichiers de l'extension + if not do_not_copy and pg_connection_info.extension_dir: + for file in ASGARD_PATH.iterdir(): + if file.is_file() and file.suffix in ('.control', '.sql'): + copy_file(file, pg_connection_info.extension_dir) + + # exécution de la recette + with pg_connection(pg_connection_info, **kwargs) as conn: + with conn: + with conn.cursor() as cur: + cur.execute( + ''' + SELECT * FROM z_asgard_recette.execute_recette() ; + ''' + ) + failures = cur.fetchall() + if failures: + print('... {} erreurs'.format(len(failures))) + for failure in failures: + print(f'{failure[0]}: {failure[1]}') + else: + print('... aucune erreur') + +if __name__ == '__main__': + run() \ No newline at end of file From 2417a76f4e28bd29d5ed3b1273c5e9d8f5574e07 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:08:35 +0200 Subject: [PATCH 23/32] Update asgard--1.3.2--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Résolution de l'issue #7 : modification de la fonction `z_asgard_admin.asgard_on_drop_schema()` pour qu'elle réinitialise le bloc en plus de basculer `creation` sur `false`. --- asgard--1.3.2--1.4.0.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index 1c1f389..8def241 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -485,8 +485,13 @@ BEGIN WHERE object_type = 'schema' LOOP ------ ENREGISTREMENT DE LA SUPPRESSION ------ + -- avec réinitialisation du bloc, pour les schémas + -- qui avait été mis à la corbeille UPDATE z_asgard.gestion_schema_etr - SET (creation, oid_schema, ctrl) = (False, NULL, ARRAY['DROP', 'x7-A;#rzo']) + SET creation = false, + oid_schema = NULL, + ctrl = ARRAY['DROP', 'x7-A;#rzo'], + bloc = substring(nom_schema, '^([a-z])_') WHERE quote_ident(nom_schema) = obj.object_identity RETURNING nom_schema INTO objname ; IF FOUND THEN From a316f99c9d75a8f14640f7f94861c1af645d7226 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:08:41 +0200 Subject: [PATCH 24/32] Update asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Résolution de l'issue #7 : modification de la fonction `z_asgard_admin.asgard_on_drop_schema()` pour qu'elle réinitialise le bloc en plus de basculer `creation` sur `false`. --- asgard--1.4.0.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index ff3cd52..0ed588f 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -833,8 +833,13 @@ BEGIN WHERE object_type = 'schema' LOOP ------ ENREGISTREMENT DE LA SUPPRESSION ------ + -- avec réinitialisation du bloc, pour les schémas + -- qui avait été mis à la corbeille UPDATE z_asgard.gestion_schema_etr - SET (creation, oid_schema, ctrl) = (False, NULL, ARRAY['DROP', 'x7-A;#rzo']) + SET creation = false, + oid_schema = NULL, + ctrl = ARRAY['DROP', 'x7-A;#rzo'], + bloc = substring(nom_schema, '^([a-z])_') WHERE quote_ident(nom_schema) = obj.object_identity RETURNING nom_schema INTO objname ; IF FOUND THEN From 63fbf6c8a690aed29725fad63d937792e2ec9e5a Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:11:39 +0200 Subject: [PATCH 25/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout des tests 96 et 97, qui confirment qu'un schéma supprimé après avoir été mis à la corbeille est recréé avec un bloc correspondant à son préfixe (96) ou sans bloc s'il n'avait pas de préfixe (97), et pas dans la corbeille / avec le bloc d. Issue #7 --- recette/asgard_recette.sql | 367 +++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index c787d0c..d7f5ccc 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -17782,3 +17782,370 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t095() IS 'ASGARD recette. TEST : Un utilisateur lambda ne peut rien faire avec la table de gestion.' ; + +-- FUNCTION: z_asgard_recette.t096() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t096() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA c_bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'c', 'échec assertion 1-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'd', 'échec assertion 2-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 2-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'c_bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'c', 'échec assertion 3-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = True WHERE nom_schema = 'c_bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'c', 'échec assertion 4-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_bibliotheque' ; + + DROP SCHEMA c_bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'c', 'échec assertion 5-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 5-b' ; + + CREATE SCHEMA c_bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ) = 'c', 'échec assertion 6-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_bibliotheque' + ), 'échec assertion 6-b' ; + + DROP SCHEMA c_bibliotheque CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t096() IS 'ASGARD recette. TEST : Un schéma mis à la corbeille et supprimé n''est pas recréé dans la corbeille (schéma avec préfixe).' ; + + +-- FUNCTION: z_asgard_recette.t096b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t096b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA "c_Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'c', 'échec assertion 1-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'd', 'échec assertion 2-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 2-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'c', 'échec assertion 3-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = True WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'c', 'échec assertion 4-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_Bibliothèque' ; + + DROP SCHEMA "c_Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'c', 'échec assertion 5-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 5-b' ; + + CREATE SCHEMA "c_Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ) = 'c', 'échec assertion 6-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_Bibliothèque' + ), 'échec assertion 6-b' ; + + DROP SCHEMA "c_Bibliothèque" CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t096b() IS 'ASGARD recette. TEST : Un schéma mis à la corbeille et supprimé n''est pas recréé dans la corbeille (schéma avec préfixe).' ; + + +-- FUNCTION: z_asgard_recette.t097() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t097() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) IS NULL, 'échec assertion 1-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) = 'd', 'échec assertion 2-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 2-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) IS NULL, 'échec assertion 3-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = True WHERE nom_schema = 'bibliotheque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) IS NULL, 'échec assertion 4-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'bibliotheque' ; + + DROP SCHEMA bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) IS NULL, 'échec assertion 5-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 5-b' ; + + CREATE SCHEMA bibliotheque ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ) IS NULL, 'échec assertion 6-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'bibliotheque' + ), 'échec assertion 6-b' ; + + DROP SCHEMA bibliotheque CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t097() IS 'ASGARD recette. TEST : Un schéma mis à la corbeille et supprimé n''est pas recréé dans la corbeille (schéma sans préfixe).' ; + + +-- FUNCTION: z_asgard_recette.t097b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t097b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE SCHEMA "Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) IS NULL, 'échec assertion 1-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) = 'd', 'échec assertion 2-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 2-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) IS NULL, 'échec assertion 3-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr SET creation = True WHERE nom_schema = 'Bibliothèque' ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) IS NULL, 'échec assertion 4-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'Bibliothèque' ; + + DROP SCHEMA "Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) IS NULL, 'échec assertion 5-a' ; + ASSERT NOT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 5-b' ; + + CREATE SCHEMA "Bibliothèque" ; + ASSERT ( + SELECT bloc FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ) IS NULL, 'échec assertion 6-a' ; + ASSERT ( + SELECT creation FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'Bibliothèque' + ), 'échec assertion 6-b' ; + + DROP SCHEMA "Bibliothèque" CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t097b() IS 'ASGARD recette. TEST : Un schéma mis à la corbeille et supprimé n''est pas recréé dans la corbeille (schéma sans préfixe).' ; From 605ae6b610b1e46721c5b197cf307cf025420da9 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Thu, 20 Oct 2022 18:10:34 +0200 Subject: [PATCH 26/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout du test 98, test de non régression assurant qu'un rôle producteur peut mettre son schéma à la corbeille et le supprimer par le moyen qu'il souhaite, sans avoir besoin d'aucun autre droit. --- recette/asgard_recette.sql | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index d7f5ccc..e1f206a 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -18149,3 +18149,90 @@ END $_$ ; COMMENT ON FUNCTION z_asgard_recette.t097b() IS 'ASGARD recette. TEST : Un schéma mis à la corbeille et supprimé n''est pas recréé dans la corbeille (schéma sans préfixe).' ; + + +-- FUNCTION: z_asgard_recette.t098() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t098() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE g_asgard_producteur ; + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_producteur ; + + SET ROLE g_asgard_producteur ; + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_bibliotheque' ; + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'c_bibliotheque' ; + + RESET ROLE ; + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_producteur ; + + SET ROLE g_asgard_producteur ; + DROP SCHEMA c_bibliotheque ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RESET ROLE ; + DROP ROLE g_asgard_producteur ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t098() IS 'ASGARD recette. TEST : Un producteur peut mettre à la corbeille et supprimer son schéma.' ; + + +-- FUNCTION: z_asgard_recette.t098b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t098b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE "g ASGARD producteur" ; + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g ASGARD producteur" ; + + SET ROLE "g ASGARD producteur" ; + UPDATE z_asgard.gestion_schema_usr SET bloc = 'd' WHERE nom_schema = 'c_Bibliothèque' ; + UPDATE z_asgard.gestion_schema_usr SET creation = False WHERE nom_schema = 'c_Bibliothèque' ; + + RESET ROLE ; + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g ASGARD producteur" ; + + SET ROLE "g ASGARD producteur" ; + DROP SCHEMA "c_Bibliothèque" ; + DELETE FROM z_asgard.gestion_schema_usr ; + + RESET ROLE ; + DROP ROLE "g ASGARD producteur" ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t098b() IS 'ASGARD recette. TEST : Un producteur peut mettre à la corbeille et supprimer son schéma.' ; + From e6190c7869745f876718145d47efc94391d1b174 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Sun, 23 Oct 2022 18:43:30 +0200 Subject: [PATCH 27/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Petites corrections sur les tests 34 et 35 pour éviter la persistance du privilège CREATE sur la base pour g_admin_ext. Avec le test 34, c'est maintenant le super-utilisateur qui avait conféré le privilège qui le retire et plus g_admin. Avec le test 35, c'est g_admin qui confère et retire le privilège. Le test 35b est modifié dans le même esprit que le 35 (+ modernisation formelle) : c'est g_admin qui crée le rôle, lui donne le privilège et détruit le rôle. --- recette/asgard_recette.sql | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index e1f206a..cff88ec 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -4201,8 +4201,8 @@ BEGIN DROP SCHEMA c_librairie ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_librairie' ; - EXECUTE 'REVOKE CREATE ON DATABASE ' || quote_ident(current_database()) || ' FROM g_admin_ext' ; RESET ROLE ; + EXECUTE 'REVOKE CREATE ON DATABASE ' || quote_ident(current_database()) || ' FROM g_admin_ext' ; RETURN r ; @@ -4428,6 +4428,7 @@ DECLARE e_detl text ; BEGIN + SET ROLE g_admin ; EXECUTE 'GRANT CREATE ON DATABASE ' || quote_ident(current_database()) || ' TO g_admin_ext' ; ------ avec g_admin_ext ------ @@ -4478,12 +4479,11 @@ CREATE OR REPLACE FUNCTION z_asgard_recette.t035b() LANGUAGE plpgsql AS $_$ DECLARE - b boolean ; - r boolean ; e_mssg text ; e_detl text ; BEGIN + SET ROLE g_admin ; CREATE ROLE "Admin EXT" ; GRANT g_admin_ext TO "Admin EXT" ; @@ -4496,13 +4496,13 @@ BEGIN INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation, nomenclature) VALUES ('c_Bibliothèque', 'Admin EXT', True, True) ; - r := False ; + ASSERT False, 'échec assertion 1' ; EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, e_detl = PG_EXCEPTION_DETAIL ; - r := e_mssg ~ 'TB19[.]' OR e_detl ~ 'TB19[.]' OR False ; + ASSERT e_mssg ~ 'TB19[.]' OR e_detl ~ 'TB19[.]' OR False, 'échec assertion 2' ; END ; ------ avec g_admin ------ @@ -4510,22 +4510,24 @@ BEGIN INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur, creation, nomenclature) VALUES ('c_Bibliothèque', 'Admin EXT', True, True) ; - - + UPDATE z_asgard.gestion_schema_usr SET nomenclature = False WHERE nom_schema = 'c_Bibliothèque' ; DROP SCHEMA "c_Bibliothèque" ; DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; - - RESET ROLE ; EXECUTE 'REVOKE CREATE ON DATABASE ' || quote_ident(current_database()) || ' FROM "Admin EXT"' ; - DROP ROLE "Admin EXT" ; + RESET ROLE ; - RETURN r ; + RETURN True ; -EXCEPTION WHEN OTHERS THEN +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + RETURN False ; END From 86c2b8249fe3adc118d86642d929f4b134fc73d0 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 25 Oct 2022 01:52:58 +0200 Subject: [PATCH 28/32] Update asgard--1.3.2--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modification des vues `gestion_schema_usr` et `gestion_schema_etr` pour qu'elles permettent aux membres du rôle producteur d'un schéma inactif d'agir sur celui-ci, dès lors que ce rôle existe. La commande de création de la fonction `asgard_has_role_usage` est déplacée en conséquence. --- asgard--1.3.2--1.4.0.sql | 98 ++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/asgard--1.3.2--1.4.0.sql b/asgard--1.3.2--1.4.0.sql index 8def241..7c429af 100644 --- a/asgard--1.3.2--1.4.0.sql +++ b/asgard--1.3.2--1.4.0.sql @@ -163,6 +163,53 @@ ALTER TABLE z_asgard_admin.gestion_schema ------ 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA ------ +-- Function: z_asgard.asgard_has_role_usage(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( + role_parent text, + role_enfant text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. + + Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') + en plus permissif - elle renvoie False quand l'un des rôles + n'existe pas plutôt que d'échouer. + + Parameters + ---------- + role_parent : text + Nom du rôle dont on souhaite savoir si l'autre est membre. + role_enfant : text, optional + Nom du rôle dont on souhaite savoir s'il est membre de l'autre. + Si non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si la relation entre les rôles est vérifiée. False + si elle ne l'est pas ou si l'un des rôles n'existe pas. + +*/ +BEGIN + + RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; + +EXCEPTION WHEN undefined_object +THEN + RETURN False ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; + + -- View: z_asgard.gestion_schema_usr CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( @@ -185,7 +232,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) - ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + ELSE z_asgard.asgard_has_role_usage(gestion_schema.producteur) END ) ; @@ -217,7 +264,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) - ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + ELSE z_asgard.asgard_has_role_usage(gestion_schema.producteur) END ) ; @@ -4861,53 +4908,6 @@ COMMENT ON TRIGGER asgard_visibilite_admin_after ON z_asgard_admin.gestion_schem ------ 6.1 - PETITES FONCTIONS UTILITAIRES ------ --- Function: z_asgard.asgard_has_role_usage(text, text) - -CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( - role_parent text, - role_enfant text DEFAULT current_user - ) - RETURNS boolean - LANGUAGE plpgsql - AS $_$ -/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. - - Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') - en plus permissif - elle renvoie False quand l'un des rôles - n'existe pas plutôt que d'échouer. - - Parameters - ---------- - role_parent : text - Nom du rôle dont on souhaite savoir si l'autre est membre. - role_enfant : text, optional - Nom du rôle dont on souhaite savoir s'il est membre de l'autre. - Si non renseigné, la fonction testera l'utilisateur courant. - - Returns - ------- - boolean - True si la relation entre les rôles est vérifiée. False - si elle ne l'est pas ou si l'un des rôles n'existe pas. - -*/ -BEGIN - - RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; - -EXCEPTION WHEN undefined_object -THEN - RETURN False ; - -END -$_$; - -ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) - OWNER TO g_admin_ext ; - -COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; - - -- Function: z_asgard.asgard_is_relation_owner(text, text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_relation_owner( From efb4777505601352fce47b0e8983d1347720bc12 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 25 Oct 2022 01:53:07 +0200 Subject: [PATCH 29/32] Update asgard--1.4.0.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modification des vues `gestion_schema_usr` et `gestion_schema_etr` pour qu'elles permettent aux membres du rôle producteur d'un schéma inactif d'agir sur celui-ci, dès lors que ce rôle existe. La commande de création de la fonction `asgard_has_role_usage` est déplacée en conséquence. --- asgard--1.4.0.sql | 98 +++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/asgard--1.4.0.sql b/asgard--1.4.0.sql index 0ed588f..c85abb3 100644 --- a/asgard--1.4.0.sql +++ b/asgard--1.4.0.sql @@ -342,6 +342,53 @@ SELECT pg_extension_config_dump('z_asgard_admin.gestion_schema'::regclass, '') ; ------ 2.4 - VUES D'ALIMENTATION DE GESTION_SCHEMA ------ +-- Function: z_asgard.asgard_has_role_usage(text, text) + +CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( + role_parent text, + role_enfant text DEFAULT current_user + ) + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. + + Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') + en plus permissif - elle renvoie False quand l'un des rôles + n'existe pas plutôt que d'échouer. + + Parameters + ---------- + role_parent : text + Nom du rôle dont on souhaite savoir si l'autre est membre. + role_enfant : text, optional + Nom du rôle dont on souhaite savoir s'il est membre de l'autre. + Si non renseigné, la fonction testera l'utilisateur courant. + + Returns + ------- + boolean + True si la relation entre les rôles est vérifiée. False + si elle ne l'est pas ou si l'un des rôles n'existe pas. + +*/ +BEGIN + + RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; + +EXCEPTION WHEN undefined_object +THEN + RETURN False ; + +END +$_$; + +ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) + OWNER TO g_admin_ext ; + +COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; + + -- View: z_asgard.gestion_schema_usr CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( @@ -364,7 +411,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_usr AS ( THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) - ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + ELSE z_asgard.asgard_has_role_usage(gestion_schema.producteur) END ) ; @@ -418,7 +465,7 @@ CREATE OR REPLACE VIEW z_asgard.gestion_schema_etr AS ( THEN pg_has_role(gestion_schema.producteur::text, 'USAGE'::text) WHEN gestion_schema.creation THEN pg_has_role(gestion_schema.oid_producteur, 'USAGE'::text) - ELSE has_database_privilege(current_database()::text, 'CREATE'::text) OR CURRENT_USER = gestion_schema.producteur::name + ELSE z_asgard.asgard_has_role_usage(gestion_schema.producteur) END ) ; @@ -6648,53 +6695,6 @@ COMMENT ON TRIGGER asgard_visibilite_admin_after ON z_asgard_admin.gestion_schem ------ 6.1 - PETITES FONCTIONS UTILITAIRES ------ --- Function: z_asgard.asgard_has_role_usage(text, text) - -CREATE OR REPLACE FUNCTION z_asgard.asgard_has_role_usage( - role_parent text, - role_enfant text DEFAULT current_user - ) - RETURNS boolean - LANGUAGE plpgsql - AS $_$ -/* Détermine si un rôle est membre d'un autre (y compris indirectement) et hérite de ses droits. - - Cette fonction est équivalente à pg_has_role(role_enfant, role_parent, 'USAGE') - en plus permissif - elle renvoie False quand l'un des rôles - n'existe pas plutôt que d'échouer. - - Parameters - ---------- - role_parent : text - Nom du rôle dont on souhaite savoir si l'autre est membre. - role_enfant : text, optional - Nom du rôle dont on souhaite savoir s'il est membre de l'autre. - Si non renseigné, la fonction testera l'utilisateur courant. - - Returns - ------- - boolean - True si la relation entre les rôles est vérifiée. False - si elle ne l'est pas ou si l'un des rôles n'existe pas. - -*/ -BEGIN - - RETURN pg_has_role(role_enfant, role_parent, 'USAGE') ; - -EXCEPTION WHEN undefined_object -THEN - RETURN False ; - -END -$_$; - -ALTER FUNCTION z_asgard.asgard_has_role_usage(text, text) - OWNER TO g_admin_ext ; - -COMMENT ON FUNCTION z_asgard.asgard_has_role_usage(text, text) IS 'ASGARD. Le second rôle est-il membre du premier (avec héritage de ses droits) ?' ; - - -- Function: z_asgard.asgard_is_relation_owner(text, text, text) CREATE OR REPLACE FUNCTION z_asgard.asgard_is_relation_owner( From b7baae102097ff68712265abaf268be199c4b774 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 25 Oct 2022 01:53:51 +0200 Subject: [PATCH 30/32] Update asgard_recette.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout des tests 99 à 101. --- recette/asgard_recette.sql | 412 +++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index cff88ec..3338262 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -18238,3 +18238,415 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t098b() IS 'ASGARD recette. TEST : Un producteur peut mettre à la corbeille et supprimer son schéma.' ; + +-- FUNCTION: z_asgard_recette.t099() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t099() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + SET ROLE g_admin ; + CREATE ROLE g_asgard_producteur ; + CREATE ROLE g_asgard_ghost ; + + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_bibliotheque', true, 'g_asgard_producteur') ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_librairie', true, 'g_asgard_producteur') ; + + ASSERT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 1-a' ; + ASSERT 'c_librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 1-b' ; + + DROP SCHEMA c_bibliotheque ; + + SET ROLE g_asgard_producteur ; + DROP SCHEMA c_librairie ; + + SET ROLE g_asgard_ghost ; + DELETE FROM z_asgard.gestion_schema_usr ; + + SET ROLE g_asgard_producteur ; + ASSERT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 2-a' ; + ASSERT 'c_librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 2-b' ; + + DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_bibliotheque' ; + ASSERT NOT 'c_bibliotheque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 3' ; + + SET ROLE g_admin ; + DELETE FROM z_asgard.gestion_schema_usr ; + ASSERT NOT 'c_librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 4' ; + + DROP ROLE g_asgard_producteur ; + DROP ROLE g_asgard_ghost ; + RESET ROLE ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t099() IS 'ASGARD recette. TEST : Il faut être membre de g_admin ou du producteur du schéma pour l''effacer de la table de gestion.' ; + + +-- FUNCTION: z_asgard_recette.t099b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t099b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + SET ROLE g_admin ; + CREATE ROLE "g_asgard producteur" ; + CREATE ROLE "g_asgard_GHOST" ; + + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c_Bibliothèque', true, 'g_asgard producteur') ; + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, creation, producteur) + VALUES ('c $Librairie', true, 'g_asgard producteur') ; + + ASSERT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 1-a' ; + ASSERT 'c $Librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 1-b' ; + + DROP SCHEMA "c_Bibliothèque" ; + + SET ROLE "g_asgard producteur" ; + DROP SCHEMA "c $Librairie" ; + + SET ROLE "g_asgard_GHOST" ; + DELETE FROM z_asgard.gestion_schema_usr ; + + SET ROLE "g_asgard producteur" ; + ASSERT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 2-a' ; + ASSERT 'c $Librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 2-b' ; + + DELETE FROM z_asgard.gestion_schema_usr WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT NOT 'c_Bibliothèque' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 3' ; + + SET ROLE g_admin ; + DELETE FROM z_asgard.gestion_schema_usr ; + ASSERT NOT 'c $Librairie' IN (SELECT nom_schema FROM z_asgard.gestion_schema_usr), + 'échec assertion 4' ; + + DROP ROLE "g_asgard producteur" ; + DROP ROLE "g_asgard_GHOST" ; + RESET ROLE ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t099b() IS 'ASGARD recette. TEST : Il faut être membre de g_admin ou du producteur du schéma pour l''effacer de la table de gestion.' ; + + +-- FUNCTION: z_asgard_recette.t100() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t100() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + SET ROLE g_admin ; + CREATE ROLE g_asgard_producteur ; + CREATE ROLE g_asgard_ghost ; + + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur) + VALUES ('c_bibliotheque', 'g_asgard_producteur') ; + + ASSERT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE not creation + ), 'échec assertion 1' ; + + SET ROLE g_asgard_ghost ; + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c_librairie' ; + + SET ROLE g_asgard_producteur ; + ASSERT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE not creation + ), 'échec assertion 2' ; + + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c_librairie' + WHERE nom_schema = 'c_bibliotheque' ; + ASSERT NOT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3-a' ; + ASSERT 'c_librairie' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET lecteur = 'g_consult', + editeur = 'n_existe_pas' + WHERE nom_schema = 'c_librairie' ; + ASSERT 'g_consult' IN ( + SELECT lecteur FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_librairie' + ), 'échec assertion 4-a' ; + ASSERT 'n_existe_pas' IN ( + SELECT editeur FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c_librairie' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = 'g_admin' + WHERE nom_schema = 'c_librairie' ; + ASSERT NOT 'c_librairie' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 5' ; + + SET ROLE g_admin ; + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c_bibliotheque' + WHERE nom_schema = 'c_librairie' ; + ASSERT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 6' ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE g_asgard_producteur ; + DROP ROLE g_asgard_ghost ; + + RESET ROLE ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t100() IS 'ASGARD recette. TEST : Il faut être membre de g_admin ou du producteur d''un schéma inactif pour modifier ses informations dans la table de gestion.' ; + + +-- FUNCTION: z_asgard_recette.t100b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t100b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + SET ROLE g_admin ; + CREATE ROLE "g_asgard producteur" ; + CREATE ROLE "g_asgard_GHOST" ; + + INSERT INTO z_asgard.gestion_schema_usr (nom_schema, producteur) + VALUES ('c_Bibliothèque', 'g_asgard producteur') ; + + ASSERT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE not creation + ), 'échec assertion 1' ; + + SET ROLE "g_asgard_GHOST" ; + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c $librairie' ; + + SET ROLE "g_asgard producteur" ; + ASSERT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr WHERE not creation + ), 'échec assertion 2' ; + + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c $librairie' + WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT NOT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3-a' ; + ASSERT 'c $librairie' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET lecteur = 'g_consult', + editeur = 'N''existe pas' + WHERE nom_schema = 'c $librairie' ; + ASSERT 'g_consult' IN ( + SELECT lecteur FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c $librairie' + ), 'échec assertion 4-a' ; + ASSERT 'N''existe pas' IN ( + SELECT editeur FROM z_asgard.gestion_schema_usr + WHERE nom_schema = 'c $librairie' + ), 'échec assertion 4-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = 'g_admin' + WHERE nom_schema = 'c $librairie' ; + ASSERT NOT 'c $librairie' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 5' ; + + SET ROLE g_admin ; + UPDATE z_asgard.gestion_schema_usr + SET nom_schema = 'c_Bibliothèque' + WHERE nom_schema = 'c $librairie' ; + ASSERT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 6' ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE "g_asgard producteur" ; + DROP ROLE "g_asgard_GHOST" ; + + RESET ROLE ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t100b() IS 'ASGARD recette. TEST : Il faut être membre de g_admin ou du producteur d''un schéma inactif pour modifier ses informations dans la table de gestion.' ; + + +-- FUNCTION: z_asgard_recette.t101() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t101() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE g_asgard_prod_edi ; + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_prod_edi ; + ASSERT has_schema_privilege('g_asgard_prod_edi', 'c_bibliotheque', 'USAGE'), + 'échec assertion 1-a' ; + ASSERT has_schema_privilege('g_asgard_prod_edi', 'c_bibliotheque', 'CREATE'), + 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = 'g_admin', + editeur = 'g_asgard_prod_edi' + WHERE nom_schema = 'c_bibliotheque' ; + ASSERT has_schema_privilege('g_asgard_prod_edi', 'c_bibliotheque', 'USAGE'), + 'échec assertion 2-a' ; + ASSERT NOT has_schema_privilege('g_asgard_prod_edi', 'c_bibliotheque', 'CREATE'), + 'échec assertion 2-b' ; + + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion 3' ; + + DROP SCHEMA c_bibliotheque ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE g_asgard_prod_edi ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t101() IS 'ASGARD recette. TEST : Quand un producteur devient éditeur.' ; + + +-- FUNCTION: z_asgard_recette.t101b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t101b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE "g_asgard_PROD#edi" ; + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g_asgard_PROD#edi" ; + ASSERT has_schema_privilege('g_asgard_PROD#edi', 'c_Bibliothèque', 'USAGE'), + 'échec assertion 1-a' ; + ASSERT has_schema_privilege('g_asgard_PROD#edi', 'c_Bibliothèque', 'CREATE'), + 'échec assertion 1-b' ; + + UPDATE z_asgard.gestion_schema_usr + SET producteur = 'g_admin', + editeur = 'g_asgard_PROD#edi' + WHERE nom_schema = 'c_Bibliothèque' ; + ASSERT has_schema_privilege('g_asgard_PROD#edi', 'c_Bibliothèque', 'USAGE'), + 'échec assertion 2-a' ; + ASSERT NOT has_schema_privilege('g_asgard_PROD#edi', 'c_Bibliothèque', 'CREATE'), + 'échec assertion 2-b' ; + + ASSERT (SELECT count(*) FROM z_asgard_admin.asgard_diagnostic()) = 0, + 'échec assertion 3' ; + + DROP SCHEMA "c_Bibliothèque" ; + DELETE FROM z_asgard.gestion_schema_usr ; + DROP ROLE "g_asgard_PROD#edi" ; + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t101b() IS 'ASGARD recette. TEST : Quand un producteur devient éditeur.' ; + From f38dd189bf0b2e2856a8c1c8d50763a35249bc09 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:36:14 +0200 Subject: [PATCH 31/32] Update asgard_recette.sql Ajout de la fonction `count_tests()` qui renvoie le nombre total de tests disponibles. Ajout du test 102. --- recette/asgard_recette.sql | 142 +++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/recette/asgard_recette.sql b/recette/asgard_recette.sql index 3338262..cda4a6b 100644 --- a/recette/asgard_recette.sql +++ b/recette/asgard_recette.sql @@ -126,6 +126,16 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.execute_recette() IS 'ASGARD recette. Exécution de la recette.' ; +-- FUNCTION: z_asgard_recette.count_tests() + +CREATE OR REPLACE FUNCTION z_asgard_recette.count_tests() + RETURNS int + LANGUAGE SQL + AS ' + SELECT count(*)::int FROM pg_catalog.pg_proc + WHERE pronamespace = ''z_asgard_recette''::regnamespace::oid + AND proname ~ ''^t[0-9]+b*$'' + ' ; ------ 9.02 - Bibliothèque de tests ------ @@ -18650,3 +18660,135 @@ $_$ ; COMMENT ON FUNCTION z_asgard_recette.t101b() IS 'ASGARD recette. TEST : Quand un producteur devient éditeur.' ; + +-- FUNCTION: z_asgard_recette.t102() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t102() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE g_asgard_delegue ; + CREATE ROLE g_asgard_other ; + GRANT g_asgard_other TO g_asgard_delegue ; + EXECUTE format( + 'GRANT CREATE ON DATABASE %I TO g_asgard_delegue', + current_database() + ) ; + + CREATE SCHEMA c_bibliotheque AUTHORIZATION g_asgard_delegue ; + PERFORM z_asgard_admin.asgard_sortie_gestion_schema('c_bibliotheque') ; + ASSERT NOT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 1' ; + + CREATE TABLE c_bibliotheque.journal_du_mur (jour date PRIMARY KEY, entree text) ; + ALTER TABLE c_bibliotheque.journal_du_mur OWNER TO g_asgard_other ; + ASSERT z_asgard.asgard_is_relation_owner( + 'c_bibliotheque', 'journal_du_mur', 'g_asgard_other' + ), 'échec assertion 2' ; + + SET ROLE g_asgard_delegue ; + PERFORM z_asgard.asgard_initialise_schema('c_bibliotheque') ; + ASSERT 'c_bibliotheque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3' ; + ASSERT z_asgard.asgard_is_relation_owner( + 'c_bibliotheque', 'journal_du_mur', 'g_asgard_delegue' + ), 'échec assertion 4' ; + + DROP SCHEMA c_bibliotheque CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + RESET ROLE ; + EXECUTE format( + 'REVOKE CREATE ON DATABASE %I FROM g_asgard_delegue', + current_database() + ) ; + DROP ROLE g_asgard_delegue ; + DROP ROLE g_asgard_other ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t102() IS 'ASGARD recette. TEST : Référencement d''un schéma par un administrateur délégué.' ; + + +-- FUNCTION: z_asgard_recette.t102b() + +CREATE OR REPLACE FUNCTION z_asgard_recette.t102b() + RETURNS boolean + LANGUAGE plpgsql + AS $_$ +DECLARE + e_mssg text ; + e_detl text ; +BEGIN + + CREATE ROLE "g_asgard_Délégué" ; + CREATE ROLE "g_asgard OTHER" ; + GRANT "g_asgard OTHER" TO "g_asgard_Délégué" ; + EXECUTE format( + 'GRANT CREATE ON DATABASE %I TO "g_asgard_Délégué"', + current_database() + ) ; + + CREATE SCHEMA "c_Bibliothèque" AUTHORIZATION "g_asgard_Délégué" ; + PERFORM z_asgard_admin.asgard_sortie_gestion_schema('c_Bibliothèque') ; + ASSERT NOT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 1' ; + + CREATE TABLE "c_Bibliothèque"."journal du mur" (jour date PRIMARY KEY, entree text) ; + ALTER TABLE "c_Bibliothèque"."journal du mur" OWNER TO "g_asgard OTHER" ; + ASSERT z_asgard.asgard_is_relation_owner( + 'c_Bibliothèque', 'journal du mur', 'g_asgard OTHER' + ), 'échec assertion 2' ; + + SET ROLE "g_asgard_Délégué" ; + PERFORM z_asgard.asgard_initialise_schema('c_Bibliothèque') ; + ASSERT 'c_Bibliothèque' IN ( + SELECT nom_schema FROM z_asgard.gestion_schema_usr + ), 'échec assertion 3' ; + ASSERT z_asgard.asgard_is_relation_owner( + 'c_Bibliothèque', 'journal du mur', 'g_asgard_Délégué' + ), 'échec assertion 4' ; + + DROP SCHEMA "c_Bibliothèque" CASCADE ; + DELETE FROM z_asgard.gestion_schema_usr ; + RESET ROLE ; + EXECUTE format( + 'REVOKE CREATE ON DATABASE %I FROM "g_asgard_Délégué"', + current_database() + ) ; + DROP ROLE "g_asgard_Délégué" ; + DROP ROLE "g_asgard OTHER" ; + + RETURN True ; + +EXCEPTION WHEN OTHERS OR ASSERT_FAILURE THEN + GET STACKED DIAGNOSTICS e_mssg = MESSAGE_TEXT, + e_detl = PG_EXCEPTION_DETAIL ; + RAISE NOTICE '%', e_mssg + USING DETAIL = e_detl ; + + RETURN False ; + +END +$_$ ; + +COMMENT ON FUNCTION z_asgard_recette.t102b() IS 'ASGARD recette. TEST : Référencement d''un schéma par un administrateur délégué.' ; + From b9cb0f27eddf216c09091d9eed63267af5361b91 Mon Sep 17 00:00:00 2001 From: alhyss <55992003+alhyss@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:37:15 +0200 Subject: [PATCH 32/32] Update runner.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modification du message renvoyé à l'issue des tests pour qu'il affiche le nombre de tests réalisés. Le schéma `z_asgard_recette` est supprimé s'il existe avant recréation des tests. --- recette/runner.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/recette/runner.py b/recette/runner.py index 5644487..69b323d 100644 --- a/recette/runner.py +++ b/recette/runner.py @@ -380,6 +380,7 @@ def pg_connection( ''' DROP EXTENSION IF EXISTS {extension} ; CREATE EXTENSION {extension} ; + DROP SCHEMA IF EXISTS z_asgard_recette CASCADE ; ''' ).format(extension=sql.Identifier(extension_name)) ) @@ -435,6 +436,12 @@ def run(pg_versions=None, extension_version=None, do_not_copy=False): with pg_connection(pg_connection_info, **kwargs) as conn: with conn: with conn.cursor() as cur: + cur.execute( + ''' + SELECT z_asgard_recette.count_tests() ; + ''' + ) + nb_tests = cur.fetchone()[0] cur.execute( ''' SELECT * FROM z_asgard_recette.execute_recette() ; @@ -442,11 +449,11 @@ def run(pg_versions=None, extension_version=None, do_not_copy=False): ) failures = cur.fetchall() if failures: - print('... {} erreurs'.format(len(failures))) + print('... {} tests, {} erreurs'.format(nb_tests, len(failures))) for failure in failures: print(f'{failure[0]}: {failure[1]}') else: - print('... aucune erreur') + print(f'... {nb_tests} tests, aucune erreur') if __name__ == '__main__': run() \ No newline at end of file