Créer le rôle applicatif à chaque démarrage, pas seulement au premier
Les migrations échouaient sur « Authentication failed for planflow_app ». Le script qui crée ce rôle était monté dans /docker-entrypoint-initdb.d, lequel ne s'exécute qu'à la **toute première** initialisation du volume : une pile dont le volume existait déjà — créé par une tentative antérieure — n'a jamais vu passer le rôle. Je l'avais noté dans le README au lieu de le traiter. Un service `db-init` rejoue désormais le script à chaque démarrage, et le script est rendu rejouable : il crée le rôle ou aligne son mot de passe, transfère la propriété de la base, du schéma et des objets déjà présents. Les identifiants du superutilisateur restent dans ce service ; les confier au conteneur applicatif lui donnerait de quoi contourner la row-level security, ce que tout ce montage cherche à empêcher. Éprouvé sur une base créée par le superutilisateur, comme celle du client : rôle posé en NOSUPERUSER NOBYPASSRLS, propriétaire de la base et des tables préexistantes, capable de se connecter et d'exécuter du DDL ; les 22 migrations s'appliquent ; la RLS le filtre — deux comptes visibles par le superutilisateur, zéro par lui hors périmètre. Rejoué une seconde fois sans effet de bord. Non vérifiable ici : l'authentification par mot de passe, mon cluster de développement étant en mode `trust`. Corrigé au passage une interférence entre tests : celui qui retire « Gérer les rôles » aux rôles semés, pour atteindre le refus de verrouillage, s'exécutait en parallèle de ceux qui s'appuient sur cette capacité et les faisait échouer par intermittence. Le fichier passe en série. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
3 files changed
+75
-5
No files matched your search
@@ -39,6 +39,36 @@ services:
|
||||
expose:
|
||||
- '5432'
|
||||
|
||||
# Pose le rôle applicatif à **chaque** démarrage de la pile.
|
||||
#
|
||||
# `/docker-entrypoint-initdb.d` ne s'exécute qu'à la toute première
|
||||
# initialisation du volume : un volume créé par une tentative antérieure
|
||||
# n'aurait jamais vu passer ce rôle, et l'application échouerait à
|
||||
# s'authentifier sans que rien n'explique pourquoi. Le script est rejouable,
|
||||
# ce service le rejoue.
|
||||
#
|
||||
# Les identifiants du superutilisateur restent ici : les confier au conteneur
|
||||
# applicatif lui donnerait de quoi contourner la row-level security, ce que
|
||||
# tout ce montage cherche précisément à empêcher.
|
||||
db-init:
|
||||
image: postgres:16-alpine
|
||||
restart: 'no'
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGHOST: db
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD:-planflow-interne}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-planflow}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-planflow}
|
||||
APP_DB_USER: ${APP_DB_USER:-planflow_app}
|
||||
APP_DB_PASSWORD: ${APP_DB_PASSWORD:-planflow-app-interne}
|
||||
volumes:
|
||||
- ./docker/init-app-role.sh:/init-app-role.sh:ro
|
||||
entrypoint: ['sh', '/init-app-role.sh']
|
||||
networks:
|
||||
- interne
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
@@ -46,6 +76,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
db-init:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# Le compte applicatif, **pas** le superutilisateur d'amorçage : un
|
||||
|
||||
+37
-5
@@ -14,25 +14,57 @@ set -eu
|
||||
# ni superutilisateur ni BYPASSRLS. C'est précisément pourquoi les politiques
|
||||
# sont déclarées en FORCE : elles s'appliquent aussi au propriétaire.
|
||||
#
|
||||
# Ce script ne s'exécute qu'à la **première** initialisation du volume. Pour une
|
||||
# installation déjà en place, jouer le même SQL à la main (voir README).
|
||||
# Ce script est **rejouable**, et il le doit : `/docker-entrypoint-initdb.d` ne
|
||||
# s'exécute qu'à la toute première initialisation du volume. Une installation
|
||||
# déjà en place — un volume créé par une tentative antérieure, par exemple —
|
||||
# n'aurait jamais vu passer ce rôle, et l'application échouerait à se connecter
|
||||
# sans que rien n'explique pourquoi. Il est donc aussi joué à chaque démarrage
|
||||
# de la pile, par le service `db-init`.
|
||||
|
||||
APP_ROLE="${APP_DB_USER:-planflow_app}"
|
||||
APP_PASSWORD="${APP_DB_PASSWORD:-planflow-app-interne}"
|
||||
DB_NAME="${POSTGRES_DB:-planflow}"
|
||||
DB_USER="${POSTGRES_USER:-planflow}"
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<SQL
|
||||
# Sans PGHOST, psql passe par la socket locale — le cas quand le script est
|
||||
# joué par l'image postgres à l'initialisation. Avec, il passe par le réseau —
|
||||
# le cas du service qui le rejoue à chaque démarrage.
|
||||
psql -v ON_ERROR_STOP=1 --username "$DB_USER" --dbname "$DB_NAME" <<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${APP_ROLE}') THEN
|
||||
CREATE ROLE ${APP_ROLE} LOGIN PASSWORD '${APP_PASSWORD}'
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
|
||||
ELSE
|
||||
-- Le mot de passe peut avoir changé dans la configuration de la pile ;
|
||||
-- laisser l'ancien produirait un refus d'authentification illisible.
|
||||
ALTER ROLE ${APP_ROLE} WITH LOGIN PASSWORD '${APP_PASSWORD}'
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
|
||||
ALTER DATABASE ${POSTGRES_DB} OWNER TO ${APP_ROLE};
|
||||
ALTER DATABASE ${DB_NAME} OWNER TO ${APP_ROLE};
|
||||
ALTER SCHEMA public OWNER TO ${APP_ROLE};
|
||||
GRANT ALL ON SCHEMA public TO ${APP_ROLE};
|
||||
|
||||
-- Objets déjà créés par un compte d'amorçage : sans ce transfert, le rôle
|
||||
-- applicatif ne pourrait ni migrer ni lire ce qui existe déjà.
|
||||
DO \$\$
|
||||
DECLARE
|
||||
statement text;
|
||||
BEGIN
|
||||
FOR statement IN
|
||||
SELECT format('ALTER TABLE %I.%I OWNER TO ${APP_ROLE}', schemaname, tablename)
|
||||
FROM pg_tables WHERE schemaname = 'public'
|
||||
UNION ALL
|
||||
SELECT format('ALTER SEQUENCE %I.%I OWNER TO ${APP_ROLE}', sequence_schema, sequence_name)
|
||||
FROM information_schema.sequences WHERE sequence_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE statement;
|
||||
END LOOP;
|
||||
END
|
||||
\$\$;
|
||||
SQL
|
||||
|
||||
echo "Rôle applicatif ${APP_ROLE} prêt — NOSUPERUSER NOBYPASSRLS, propriétaire de ${POSTGRES_DB}."
|
||||
echo "Rôle applicatif ${APP_ROLE} prêt — NOSUPERUSER NOBYPASSRLS, propriétaire de ${DB_NAME}."
|
||||
@@ -9,7 +9,13 @@ import { slugifyRoleKey } from '../../src/domain/access/role-editing';
|
||||
* changement de code. » Le vérifier suppose d'aller jusqu'au bout : créer le
|
||||
* rôle, l'attribuer, et constater qu'un écran s'ouvre ou se ferme en
|
||||
* conséquence. Un test qui s'arrêterait à « la case est cochée » ne dirait rien.
|
||||
*
|
||||
* En série, et il le faut : l'un de ces tests retire momentanément « Gérer les
|
||||
* rôles » aux rôles semés pour atteindre le refus de verrouillage. Joué en
|
||||
* parallèle des autres, qui s'appuient sur cette même capacité, il les fait
|
||||
* échouer par intermittence — et la cause n'est visible nulle part.
|
||||
*/
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const OWNER_PASSWORD = 'planflow-demo-2026';
|
||||
|
||||
|
||||
Reference in new issue
Block a user