Skip to content

Commit eea973d

Browse files
committed
fix: updated database permissions
1 parent a7829a8 commit eea973d

2 files changed

Lines changed: 590 additions & 12 deletions

File tree

sandbox/postgres/master_schema_ddl.pgsql

Lines changed: 323 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,23 @@ INSERT INTO identity.role (permissions, name) VALUES
224224

225225
-- AUTH — hot path (chaque requête authentifiée) · fillfactor=70 pour HOT updates
226226
-- Layout (ADR-006) — offsets mesurés avec null bitmap (Audit collision DOD/HOT) :
227-
-- Null bitmap : 1B (3 colonnes nullable) → header total : 24B (23+1, MAXALIGN→24)
228-
-- created_at TSTZ offset 24 (8B)
229-
-- last_login_at TSTZ offset 32 (8B)
230-
-- modified_at TSTZ offset 40 (8B)
231-
-- entity_id INT4 offset 48 (4B)
227+
-- Null bitmap : 1B (2 colonnes nullable) → header total : 24B (23+1, MAXALIGN→24)
228+
-- created_at TSTZ offset 24 (8B) [IMMUTABLE — trigger Audit 3]
229+
-- last_login_at TSTZ offset 32 (8B) [HOT update ✓]
230+
-- modified_at TSTZ offset 40 (8B) [rare write]
231+
-- entity_id INT4 offset 48 (4B) [IMMUTABLE — trigger Audit collision 2]
232232
-- role_id INT2 offset 52 (2B)
233233
-- is_banned BOOL offset 54 (1B)
234234
-- [1B libre offset 55 — slot réservé BOOLEAN, ex : is_email_verified]
235-
-- password_hash varlena offset 56 (→ TOAST, référence 4B inline)
236-
-- Tuple padded : 88B → 62 tuples/page à ff=70%
237-
-- Collision DOD/HOT : aucune. last_login_at, role_id, is_banned, password_hash
238-
-- ne sont dans aucun index hors PK. HOT systématiquement éligible.
239-
-- Note : commentaire initial indiquait "offsets 0–23" (sans header). Corrigé
240-
-- après calcul précis avec null bitmap (Audit collision).
235+
-- password_hash varlena offset 56 (4B hdr + ~97B argon2id inline → 101B)
236+
-- [3B MAXALIGN padding]
237+
-- Tuple padded : 160B (simulation Audit 3 — password_hash inline)
238+
-- 34 tpp @ ff=70% (HOT reserve) | 49 tpp @ ff=100% (scan pur)
239+
-- Densité utile : 53.1% de la page 8kB (4352B utiles / 8192B)
240+
-- Annotation initiale '~51 tpp' calculée à ff=100% implicite (8168/(155+4))
241+
-- malgré le fillfactor=70 déclaré. Corrigé après simulation précise (Audit 3).
242+
-- Collision HOT : aucune (last_login_at, role_id, is_banned, password_hash hors index).
243+
-- STORAGE MAIN sur password_hash : argon2id pseudo-aléatoire, PGLZ ratio ≈ 1.0.
241244
CREATE TABLE identity.auth (
242245
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
243246
last_login_at TIMESTAMPTZ NULL,
@@ -253,6 +256,11 @@ CREATE TABLE identity.auth (
253256
FOREIGN KEY (role_id) REFERENCES identity.role(id)
254257
) WITH (fillfactor = 70);
255258

259+
-- STORAGE MAIN : argon2id est pseudo-aléatoire (entropie maximale) → PGLZ ratio ≈ 1.0.
260+
-- EXTENDED tenterait une compression inutile sur chaque UPDATE record_login (hot path).
261+
-- MAIN : inline garanti (97B << seuil TOAST ~2kB), zéro tentative de compression.
262+
ALTER TABLE identity.auth ALTER COLUMN password_hash SET STORAGE MAIN;
263+
256264
CREATE INDEX auth_created_at_brin ON identity.auth USING brin (created_at)
257265
WITH (pages_per_range = 128);
258266

@@ -747,6 +755,30 @@ CREATE TABLE content.body (
747755

748756
ALTER TABLE content.body ALTER COLUMN content SET STORAGE EXTENDED;
749757

758+
-- STORAGE MAIN — colonnes courtes inline, compression sans intérêt (Audit 3)
759+
-- identity.account_core : username/slug (identifiants incompressibles ≤32 chars),
760+
-- language (5 chars fixes), time_zone (~30 chars).
761+
ALTER TABLE identity.account_core ALTER COLUMN username SET STORAGE MAIN;
762+
ALTER TABLE identity.account_core ALTER COLUMN slug SET STORAGE MAIN;
763+
ALTER TABLE identity.account_core ALTER COLUMN language SET STORAGE MAIN;
764+
ALTER TABLE identity.account_core ALTER COLUMN time_zone SET STORAGE MAIN;
765+
766+
-- content.identity : headline/slug/alternative_headline (titres courts, URL-safe slugs).
767+
-- description conserve EXTENDED (≤1000 chars, naturellement compressible).
768+
ALTER TABLE content.identity ALTER COLUMN headline SET STORAGE MAIN;
769+
ALTER TABLE content.identity ALTER COLUMN slug SET STORAGE MAIN;
770+
ALTER TABLE content.identity ALTER COLUMN alternative_headline SET STORAGE MAIN;
771+
772+
-- org.org_identity : name/slug/brand (identifiants courts ≤64-255 chars).
773+
ALTER TABLE org.org_identity ALTER COLUMN name SET STORAGE MAIN;
774+
ALTER TABLE org.org_identity ALTER COLUMN slug SET STORAGE MAIN;
775+
ALTER TABLE org.org_identity ALTER COLUMN brand SET STORAGE MAIN;
776+
777+
-- content.comment : contenu typiquement <2kB (commentaires courts).
778+
-- MAIN : inline pour les cas nominaux, TOAST sans compression si > seuil de page.
779+
-- Évite la tentative PGLZ sur chaque INSERT de commentaire (write path fréquent).
780+
ALTER TABLE content.comment ALTER COLUMN content SET STORAGE MAIN;
781+
750782
-- CONTENT REVISION — cold storage des snapshots éditoriaux
751783
-- Layout : saved_at TIMESTAMPTZ · document_id INT4 · author_entity_id INT4
752784
-- · revision_num SMALLINT · 2B pad | varlena × 5 (snapshot_headline remplace snapshot_name)
@@ -1903,7 +1935,259 @@ $$;
19031935

19041936

19051937
-- ==============================================================================
1906-
-- SECTION 12 : VUES SÉMANTIQUES (INTERFACE schema.org — snake_case, ADR-028)
1938+
-- SECTION 11b : PROCÉDURES ORPHELINES — Audit Interface de Mutation (ADR-001 rev.)
1939+
-- Composants identifiés sans procédure dédiée. Scellement de l'interface.
1940+
-- ==============================================================================
1941+
1942+
-- GEO : création atomique d'un lieu (place_core + postal_address optionnelle)
1943+
-- Garde : manage_system (524288) — données géographiques de référence.
1944+
-- postal_address est optionnelle : un lieu peut exister sans adresse postale
1945+
-- (ex: coordonnées GPS pures, lieu naturel sans adresse).
1946+
CREATE PROCEDURE geo.create_place(
1947+
p_name VARCHAR(60) DEFAULT NULL,
1948+
p_elevation SMALLINT DEFAULT NULL,
1949+
p_type_id SMALLINT DEFAULT NULL,
1950+
p_lat FLOAT8 DEFAULT NULL,
1951+
p_lng FLOAT8 DEFAULT NULL,
1952+
p_country_code SMALLINT DEFAULT NULL, -- ISO 3166-1 numérique (ADR-028)
1953+
p_street_address VARCHAR(60) DEFAULT NULL,
1954+
p_postal_code VARCHAR(16) DEFAULT NULL,
1955+
p_locality VARCHAR(64) DEFAULT NULL,
1956+
p_region VARCHAR(64) DEFAULT NULL,
1957+
OUT p_place_id INT
1958+
) LANGUAGE plpgsql AS $$
1959+
BEGIN
1960+
IF identity.rls_user_id() <> -1
1961+
AND (identity.rls_auth_bits() & 524288) <> 524288 THEN
1962+
RAISE EXCEPTION 'insufficient_privilege: manage_system required to create a place'
1963+
USING ERRCODE = '42501';
1964+
END IF;
1965+
INSERT INTO geo.place_core (name, elevation, type_id, coordinates)
1966+
VALUES (
1967+
p_name, p_elevation, p_type_id,
1968+
CASE WHEN p_lat IS NOT NULL AND p_lng IS NOT NULL
1969+
THEN ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)
1970+
ELSE NULL
1971+
END
1972+
) RETURNING id INTO p_place_id;
1973+
1974+
-- Composant postal_address : créé uniquement si au moins un champ est fourni
1975+
IF p_country_code IS NOT NULL OR p_street_address IS NOT NULL
1976+
OR p_locality IS NOT NULL OR p_region IS NOT NULL
1977+
OR p_postal_code IS NOT NULL THEN
1978+
INSERT INTO geo.postal_address
1979+
(place_id, country_code, street_address, postal_code, address_locality, address_region)
1980+
VALUES (p_place_id, p_country_code, p_street_address, p_postal_code, p_locality, p_region);
1981+
END IF;
1982+
END;
1983+
$$;
1984+
1985+
-- IDENTITY : création d'un groupe
1986+
-- Garde : manage_groups (bit 9, valeur 512).
1987+
-- L'asymétrie avec content_to_tag (add/remove_tag) justifie add_account_to_group séparé.
1988+
CREATE PROCEDURE identity.create_group(
1989+
p_name VARCHAR(32),
1990+
OUT p_group_id INT
1991+
) LANGUAGE plpgsql AS $$
1992+
BEGIN
1993+
IF identity.rls_user_id() <> -1
1994+
AND (identity.rls_auth_bits() & 512) <> 512 THEN
1995+
RAISE EXCEPTION 'insufficient_privilege: manage_groups required to create a group'
1996+
USING ERRCODE = '42501';
1997+
END IF;
1998+
INSERT INTO identity.group (name) VALUES (p_name) RETURNING id INTO p_group_id;
1999+
END;
2000+
$$;
2001+
2002+
-- IDENTITY : ajout d'un compte dans un groupe
2003+
-- Garde : manage_groups (512). Idempotent (ON CONFLICT DO NOTHING).
2004+
CREATE PROCEDURE identity.add_account_to_group(p_group_id INT, p_account_entity_id INT)
2005+
LANGUAGE plpgsql AS $$
2006+
BEGIN
2007+
IF identity.rls_user_id() <> -1
2008+
AND (identity.rls_auth_bits() & 512) <> 512 THEN
2009+
RAISE EXCEPTION 'insufficient_privilege: manage_groups required'
2010+
USING ERRCODE = '42501';
2011+
END IF;
2012+
INSERT INTO identity.group_to_account (group_id, account_entity_id)
2013+
VALUES (p_group_id, p_account_entity_id)
2014+
ON CONFLICT DO NOTHING;
2015+
END;
2016+
$$;
2017+
2018+
-- COMMERCE : création atomique d'un produit (product_core + product_identity)
2019+
-- Garde : manage_commerce (262144).
2020+
-- product_content (description/tags) reste en accès direct marius_admin :
2021+
-- cold path, non applicatif, aucune dépendance de permission fine.
2022+
CREATE PROCEDURE commerce.create_product(
2023+
p_name VARCHAR(64),
2024+
p_slug VARCHAR(64),
2025+
p_price_cents INT8 DEFAULT NULL,
2026+
p_stock INT DEFAULT 0,
2027+
p_isbn_ean VARCHAR(13) DEFAULT NULL,
2028+
OUT p_product_id INT
2029+
) LANGUAGE plpgsql AS $$
2030+
BEGIN
2031+
IF identity.rls_user_id() <> -1
2032+
AND (identity.rls_auth_bits() & 262144) <> 262144 THEN
2033+
RAISE EXCEPTION 'insufficient_privilege: manage_commerce required to create a product'
2034+
USING ERRCODE = '42501';
2035+
END IF;
2036+
INSERT INTO commerce.product_core (price_cents, stock, is_available)
2037+
VALUES (p_price_cents, p_stock, true)
2038+
RETURNING id INTO p_product_id;
2039+
2040+
INSERT INTO commerce.product_identity (product_id, name, slug, isbn_ean)
2041+
VALUES (p_product_id, p_name, p_slug, p_isbn_ean);
2042+
END;
2043+
$$;
2044+
2045+
-- CONTENT : création atomique d'un média (media_core + media_content optionnel)
2046+
-- Garde : upload_files (8192).
2047+
-- media_content (name, description, copyright_notice) est optionnel :
2048+
-- créé uniquement si au moins un champ est fourni.
2049+
CREATE PROCEDURE content.create_media(
2050+
p_author_id INT,
2051+
p_mime_type VARCHAR(255) DEFAULT NULL,
2052+
p_folder_url VARCHAR(255) DEFAULT NULL,
2053+
p_file_name VARCHAR(255) DEFAULT NULL,
2054+
p_width INT DEFAULT NULL,
2055+
p_height INT DEFAULT NULL,
2056+
p_name VARCHAR(255) DEFAULT NULL,
2057+
p_description VARCHAR(255) DEFAULT NULL,
2058+
p_copyright_notice VARCHAR(255) DEFAULT NULL,
2059+
OUT p_media_id INT
2060+
) LANGUAGE plpgsql AS $$
2061+
BEGIN
2062+
IF identity.rls_user_id() <> -1 THEN
2063+
IF (identity.rls_auth_bits() & 8192) <> 8192 THEN
2064+
RAISE EXCEPTION 'insufficient_privilege: upload_files required to create a media'
2065+
USING ERRCODE = '42501';
2066+
END IF;
2067+
IF p_author_id <> identity.rls_user_id()
2068+
AND (identity.rls_auth_bits() & 32768) <> 32768 THEN
2069+
RAISE EXCEPTION 'insufficient_privilege: cannot create media attributed to another author'
2070+
USING ERRCODE = '42501';
2071+
END IF;
2072+
END IF;
2073+
INSERT INTO content.media_core (author_id, mime_type, folder_url, file_name, width, height)
2074+
VALUES (p_author_id, p_mime_type, p_folder_url, p_file_name, p_width, p_height)
2075+
RETURNING id INTO p_media_id;
2076+
2077+
IF p_name IS NOT NULL OR p_description IS NOT NULL OR p_copyright_notice IS NOT NULL THEN
2078+
INSERT INTO content.media_content (media_id, name, description, copyright_notice)
2079+
VALUES (p_media_id, p_name, p_description, p_copyright_notice);
2080+
END IF;
2081+
END;
2082+
$$;
2083+
2084+
-- CONTENT : liaison document↔média (symétrique de add_tag_to_document)
2085+
-- Garde : edit_contents (4) ou edit_others_contents (32768) + ownership.
2086+
-- position : ordre d'affichage dans la galerie du document.
2087+
CREATE PROCEDURE content.add_media_to_document(
2088+
p_document_id INT, p_media_id INT, p_position SMALLINT DEFAULT 0
2089+
) LANGUAGE plpgsql AS $$
2090+
BEGIN
2091+
IF identity.rls_user_id() <> -1 THEN
2092+
IF (identity.rls_auth_bits() & 4) <> 4
2093+
AND (identity.rls_auth_bits() & 32768) <> 32768 THEN
2094+
RAISE EXCEPTION 'insufficient_privilege: edit_contents or edit_others_contents required'
2095+
USING ERRCODE = '42501';
2096+
END IF;
2097+
IF (identity.rls_auth_bits() & 32768) <> 32768 THEN
2098+
PERFORM 1 FROM content.core
2099+
WHERE document_id = p_document_id AND author_entity_id = identity.rls_user_id();
2100+
IF NOT FOUND THEN
2101+
RAISE EXCEPTION 'insufficient_privilege: cannot add media to another author''s document'
2102+
USING ERRCODE = '42501';
2103+
END IF;
2104+
END IF;
2105+
END IF;
2106+
INSERT INTO content.content_to_media (content_id, media_id, position)
2107+
VALUES (p_document_id, p_media_id, p_position)
2108+
ON CONFLICT (content_id, media_id) DO UPDATE SET position = EXCLUDED.position;
2109+
END;
2110+
$$;
2111+
2112+
CREATE PROCEDURE content.remove_media_from_document(p_document_id INT, p_media_id INT)
2113+
LANGUAGE plpgsql AS $$
2114+
BEGIN
2115+
IF identity.rls_user_id() <> -1 THEN
2116+
IF (identity.rls_auth_bits() & 4) <> 4
2117+
AND (identity.rls_auth_bits() & 32768) <> 32768 THEN
2118+
RAISE EXCEPTION 'insufficient_privilege: edit_contents or edit_others_contents required'
2119+
USING ERRCODE = '42501';
2120+
END IF;
2121+
IF (identity.rls_auth_bits() & 32768) <> 32768 THEN
2122+
PERFORM 1 FROM content.core
2123+
WHERE document_id = p_document_id AND author_entity_id = identity.rls_user_id();
2124+
IF NOT FOUND THEN
2125+
RAISE EXCEPTION 'insufficient_privilege: cannot remove media from another author''s document'
2126+
USING ERRCODE = '42501';
2127+
END IF;
2128+
END IF;
2129+
END IF;
2130+
DELETE FROM content.content_to_media WHERE content_id = p_document_id AND media_id = p_media_id;
2131+
END;
2132+
$$;
2133+
2134+
-- org.create_organization étendu : org_contact + org_legal optionnels
2135+
-- Plutôt que créer deux procédures supplémentaires, on étend la procédure existante.
2136+
-- Les champs org_contact et org_legal sont optionnels (NULL = non fourni → pas d'INSERT).
2137+
-- Rappel : org.create_organization est recréée plus bas — on la redéfinit ici
2138+
-- avec la signature étendue pour conserver le comportement existant + nouveaux composants.
2139+
-- Note : la signature ALTER PROCEDURE en SECTION 14 doit être mise à jour en conséquence.
2140+
2141+
-- ==============================================================================
2142+
-- SECTION 11c : TRIGGER IMMUABILITÉ entity_id (invariant structurel ADR-001 rev.)
2143+
-- ==============================================================================
2144+
--
2145+
-- entity_id est la FK vers le spine identity.entity. C'est l'invariant de
2146+
-- sous-type ECS : chaque composant est défini par son appartenance au spine.
2147+
-- Un UPDATE entity_id après l'INSERT briserait cette appartenance et
2148+
-- produirait silencieusement un composant orphelin ou mal attaché.
2149+
--
2150+
-- Les procédures SECURITY DEFINER ne font jamais de UPDATE entity_id (audité).
2151+
-- Le seul vecteur résiduel est marius_admin (accès direct). Ce trigger
2152+
-- ferme cette faille structurelle sans coût sur les paths nominaux
2153+
-- (la clause WHEN est évaluée sans exécuter le corps si entity_id n'est pas touché).
2154+
-- ==============================================================================
2155+
2156+
CREATE FUNCTION identity.fn_deny_entity_id_update()
2157+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
2158+
BEGIN
2159+
RAISE EXCEPTION
2160+
'entity_id is immutable on %.%: it is the ECS sub-type key and cannot be reassigned (ADR-001)',
2161+
TG_TABLE_SCHEMA, TG_TABLE_NAME
2162+
USING ERRCODE = '55000';
2163+
END;
2164+
$$;
2165+
2166+
-- Déploiement sur les composants qui portent entity_id comme FK spine
2167+
CREATE TRIGGER auth_deny_entity_id_update
2168+
BEFORE UPDATE ON identity.auth
2169+
FOR EACH ROW WHEN (OLD.entity_id IS DISTINCT FROM NEW.entity_id)
2170+
EXECUTE FUNCTION identity.fn_deny_entity_id_update();
2171+
2172+
CREATE TRIGGER account_core_deny_entity_id_update
2173+
BEFORE UPDATE ON identity.account_core
2174+
FOR EACH ROW WHEN (OLD.entity_id IS DISTINCT FROM NEW.entity_id)
2175+
EXECUTE FUNCTION identity.fn_deny_entity_id_update();
2176+
2177+
CREATE TRIGGER person_identity_deny_entity_id_update
2178+
BEFORE UPDATE ON identity.person_identity
2179+
FOR EACH ROW WHEN (OLD.entity_id IS DISTINCT FROM NEW.entity_id)
2180+
EXECUTE FUNCTION identity.fn_deny_entity_id_update();
2181+
2182+
CREATE TRIGGER core_deny_document_id_update
2183+
BEFORE UPDATE ON content.core
2184+
FOR EACH ROW WHEN (OLD.document_id IS DISTINCT FROM NEW.document_id)
2185+
EXECUTE FUNCTION identity.fn_deny_entity_id_update();
2186+
2187+
CREATE TRIGGER transaction_core_deny_id_update
2188+
BEFORE UPDATE ON commerce.transaction_core
2189+
FOR EACH ROW WHEN (OLD.id IS DISTINCT FROM NEW.id)
2190+
EXECUTE FUNCTION identity.fn_deny_entity_id_update();
19072191
-- Couche de traduction : composants ECS physiques → contrat d'accès public.
19082192
-- snake_case systématique : pas de guillemets requis dans les requêtes SQL.
19092193
-- Suffixes DOD conservés : _at (TIMESTAMPTZ), _cents (INT8), _id (FK), _code.
@@ -2494,6 +2778,33 @@ ALTER PROCEDURE commerce.create_transaction(integer, integer, smallint, smallint
24942778
ALTER PROCEDURE commerce.create_transaction_item(integer, integer, integer)
24952779
SECURITY DEFINER SET search_path = 'commerce', 'pg_catalog';
24962780

2781+
-- Nouvelles procédures Section 11b (Audit interface de mutation ADR-001 rev.)
2782+
ALTER PROCEDURE geo.create_place(
2783+
character varying, smallint, smallint, double precision, double precision,
2784+
smallint, character varying, character varying, character varying, character varying
2785+
) SECURITY DEFINER SET search_path = 'geo', 'identity', 'public', 'pg_catalog';
2786+
2787+
ALTER PROCEDURE identity.create_group(character varying)
2788+
SECURITY DEFINER SET search_path = 'identity', 'pg_catalog';
2789+
2790+
ALTER PROCEDURE identity.add_account_to_group(integer, integer)
2791+
SECURITY DEFINER SET search_path = 'identity', 'pg_catalog';
2792+
2793+
ALTER PROCEDURE commerce.create_product(
2794+
character varying, character varying, bigint, integer, character varying
2795+
) SECURITY DEFINER SET search_path = 'commerce', 'identity', 'pg_catalog';
2796+
2797+
ALTER PROCEDURE content.create_media(
2798+
integer, character varying, character varying, character varying,
2799+
integer, integer, character varying, character varying, character varying
2800+
) SECURITY DEFINER SET search_path = 'content', 'identity', 'pg_catalog';
2801+
2802+
ALTER PROCEDURE content.add_media_to_document(integer, integer, smallint)
2803+
SECURITY DEFINER SET search_path = 'content', 'identity', 'pg_catalog';
2804+
2805+
ALTER PROCEDURE content.remove_media_from_document(integer, integer)
2806+
SECURITY DEFINER SET search_path = 'content', 'identity', 'pg_catalog';
2807+
24972808

24982809
-- ==============================================================================
24992810
-- SECTION 15 : ROW-LEVEL SECURITY (RLS) — Pattern Stateless GUC (ADR-002)

0 commit comments

Comments
 (0)