From 931e4d740b5d9ab93286e619bb2bbd02f750c277 Mon Sep 17 00:00:00 2001 From: Spark Date: Sat, 7 Nov 2026 04:29:23 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Flowdeck=207.85.5=20=E2=80=94=20inscription?= =?UTF-8?q?=20locale=20ferm=C3=A9e=20par=20d=C3=A9faut=20(fusion=20#146)?= =?UTF-8?q?=20+=20=C3=A9chec=20de=20connexion=20nommant=20la=20voie=20Obsi?= =?UTF-8?q?Gate=20SSO=20(fusion=20#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 ++ VERSION | 2 +- app/config.py | 7 ++ app/routers/admin.py | 24 ++++ app/routers/auth.py | 98 +++++++++++++++- app/routers/dashboard/workspace.py | 20 +++- app/services/reglages_acces.py | 113 ++++++++++++++++++ app/templates/landing.html | 4 +- tests/conftest.py | 8 ++ tests/test_reglages_acces.py | 177 +++++++++++++++++++++++++++++ 10 files changed, 451 insertions(+), 9 deletions(-) create mode 100644 app/services/reglages_acces.py create mode 100644 tests/test_reglages_acces.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b0834..cacc8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v7.85.5 (2026-11-06) — Inscription locale fermée par défaut + échec de connexion qui nomme la voie SSO (fusion #146, #144) + +- **#146 — l'inscription publique locale n'est plus ouverte par défaut** : nouveau réglage d'administration « inscription locale ouverte » (`app/services/reglages_acces.py`, patron `llm_config`/`sso_config` : ligne unique en base qui prime sur la variable `INSCRIPTION_LOCALE_OUVERTE`, défaut **fermée** — décision de Bruno : les comptes naissent dans le Hub Dracodev). Réversible à tout moment par un administrateur via `GET`/`PUT /api/admin/reglages-acces`. +- **Quand elle est fermée** : `POST /auth/register` répond 403 et `GET /auth/register` affiche une page de refus propre en français — les comptes se créent dans le Hub Dracodev, avec le lien du Hub dérivé de l'hôte (`draco_urls.hub_url()`, comme les autres liens de la suite) ; l'onglet Register disparaît de l'écran de connexion (avis nommant le Hub à la place) et les boutons « Get started » de la page d'accueil cèdent la place au lien Hub. **La connexion des comptes existants, locaux comme draco, n'est jamais affectée.** +- **#144 (part Flowdeck)** : l'échec du formulaire de connexion locale (« Invalid credentials », 401) suggère désormais la voie centrale : « Si votre compte a été créé dans le Hub Dracodev, connectez-vous avec le bouton « Login with ObsiGate (SSO) »… » — le bouton existant, dont le libellé est repris tel quel. Aucune logique d'authentification ne change. +- **Tests** : `tests/test_reglages_acces.py` (nouveau, 12 tests) — fermée par défaut (défaut du code vérifié), POST refusé 403 + message + aucun compte créé, page de refus, onglet Register absent, ouverte = inscription d'aujourd'hui, connexion des comptes existants (local et draco) inchangée ouvert/fermé, échec local nommant ObsiGate (SSO), aller-retour de l'API d'administration et refus aux anonymes. Le harnais de test ouvre explicitement l'inscription pour les suites historiques qui créent leurs comptes par `/auth/register` (conftest, commenté). + ## v7.85.4 (2026-11-06) — Barre commune v0.6.0 re-vendangée : entrée « Administration » (fusion #142) - **Re-vendange verbatim** de la barre commons v0.6.0 (PR dracodev-commons #14) : `static/js/vendor/draco-barre.js` + `static/css/draco-barre.css`. Le menu compte gagne l'entrée **Administration** → `/admin` (le Hub redirige vers `/admin/utilisateurs`), à **visibilité conditionnelle** (affirmée par l'hôte via `cfg.admin` / `DRACO_BAR.setAdmin`, jamais déduite par la barre). diff --git a/VERSION b/VERSION index b2c144f..4876892 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.85.4 +7.85.5 diff --git a/app/config.py b/app/config.py index bce6539..9662447 100644 --- a/app/config.py +++ b/app/config.py @@ -46,6 +46,13 @@ class Settings(BaseSettings): log_level: str = "INFO" default_lang: str = "fr" + # Réglages d'accès (fusion #146) — défauts repris par + # `app/services/reglages_acces.py` ; la ligne enregistrée en base + # par un administrateur prime sur ces valeurs. + # Inscription publique locale : fermée par défaut (décision de + # Bruno — les comptes naissent dans le Hub Dracodev). + inscription_locale_ouverte: bool = False + # Rate limiting rate_limit_enabled: bool = True rate_limit_requests: int = 60 # per minute diff --git a/app/routers/admin.py b/app/routers/admin.py index 24ab861..ed88c44 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -146,6 +146,30 @@ def delete_user(user_id: int, _admin=Depends(admin_required)): return {"status": "ok"} +# ── Réglages d'accès (fusion #146) ── +@router.get("/reglages-acces") +def get_reglages_acces(_admin=Depends(admin_required)): + """Réglages d'accès effectifs de l'instance (administrateurs).""" + from app.services import reglages_acces + return reglages_acces.get_reglages_acces() + + +@router.put("/reglages-acces") +def put_reglages_acces(_admin=Depends(admin_required), body: dict = Body(default={})): + """Modifie les réglages d'accès — réversible à tout moment. + + Corps accepté : ``{"inscription_locale_ouverte": true|false}``. + Fermée (défaut) : aucun compte local ne se crée sur l'instance, + les comptes naissent dans le Hub Dracodev. La connexion des + comptes existants n'est jamais affectée. + """ + from app.services import reglages_acces + if "inscription_locale_ouverte" in body: + return reglages_acces.set_reglages_acces( + inscription_locale_ouverte=bool(body["inscription_locale_ouverte"])) + return reglages_acces.get_reglages_acces() + + # ── Stats ── @router.get("/stats") def user_stats(_admin=Depends(admin_required)): diff --git a/app/routers/auth.py b/app/routers/auth.py index 335a67b..44df0a8 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -51,6 +51,76 @@ def _sso_login_url() -> str: f"?return_to={quote(retour, safe='')}") +# Échec de connexion locale (fusion #144) : le message suggère la voie +# centrale — un compte créé dans le Hub Dracodev n'a pas de mot de +# passe local, son entrée est le bouton « Login with ObsiGate (SSO) » +# du même écran (libellé repris tel quel). +MESSAGE_ECHEC_CONNEXION_LOCALE = ( + "Identifiants invalides. Si votre compte a été créé dans le Hub " + "Dracodev, connectez-vous avec le bouton « Login with ObsiGate " + "(SSO) » de cet écran : les comptes de la suite n'ont pas de mot " + "de passe local." +) + + +def _page_inscription_fermee() -> str: + """Page de refus propre du parcours d'inscription fermé (#146).""" + from app.services import draco_urls, reglages_acces + + hub = draco_urls.hub_url() + message = reglages_acces.message_inscription_fermee() + return f""" + + + + +FlowDeck — Inscription fermée + + + +
+

Inscription fermée

+

{message}

+

Ouvrir le Hub Dracodev · + Retour à la connexion

+
+ +""" + + +def _rendre_page_connexion(html: str) -> str: + """Rend la page de connexion en tenant compte du réglage #146. + + Inscription fermée : l'onglet Register disparaît et un avis nomme + le Hub Dracodev (lien dérivé de l'hôte, comme les autres liens de + la suite). Inscription ouverte : page intégralement inchangée. + """ + from app.services import draco_urls, reglages_acces + + if reglages_acces.inscription_locale_ouverte(): + return _with_nonce(html) + html = html.replace( + """""", + "", + ) + avis = ( + '
' + "Pas encore de compte ? Les comptes se créent dans le " + f'' + "Hub Dracodev, puis se connectent ici avec le bouton " + "« Login with ObsiGate (SSO) ».
" + ) + html = html.replace('
str: """A20 : injecte le nonce CSP au moment du rendu. @@ -180,7 +250,15 @@ async function handleLogin(e){e.preventDefault();const email=document.getElement @router.get("/register") def register_page(request: Request): - """Show the registration page (local login page with register tab active).""" + """Show the registration page (local login page with register tab active). + + Inscription fermée (#146, défaut) : refus propre — page en + français nommant le Hub Dracodev, là où les comptes se créent. + """ + from app.services import reglages_acces + + if not reglages_acces.inscription_locale_ouverte(): + return HTMLResponse(_page_inscription_fermee(), status_code=403) return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML.replace( 'class="tab active" onclick="switchTab(\'login\')"', 'class="tab" onclick="switchTab(\'login\')"' @@ -205,7 +283,7 @@ def login(request: Request, provider: str = Query("gitea")): # Local login page (POST handled by /auth/local-login) from fastapi.responses import HTMLResponse if provider == "local": - return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML), status_code=200) + return HTMLResponse(_rendre_page_connexion(LOCAL_LOGIN_HTML), status_code=200) # OAuth flow — check if provider is configured from app.auth.providers import get_provider @@ -259,6 +337,18 @@ def register(request: Request, body: dict = Body(default={})): from fastapi.responses import JSONResponse return JSONResponse({"error": "Password must be at least 6 characters"}, status_code=400) + # Inscription locale fermée (fusion #146, défaut) : les comptes + # naissent dans le Hub Dracodev — aucun compte local ne se crée + # ici. La connexion des comptes existants n'est pas touchée. + from app.services import draco_urls, reglages_acces + if not reglages_acces.inscription_locale_ouverte(): + from fastapi.responses import JSONResponse + return JSONResponse( + {"error": reglages_acces.message_inscription_fermee(), + "hub_url": draco_urls.hub_url()}, + status_code=403, + ) + # SSO-only instance (v6.7.0): local registration is refused — accounts are # auto-provisioned by the IdP instead (admins still come from Settings). from app.services.sso_provisioning import is_sso_only @@ -313,7 +403,7 @@ def local_login(request: Request, body: dict = Body(default={})): with get_conn() as conn: user = conn.execute("SELECT * FROM users WHERE login=?", (email,)).fetchone() if not user: - return JSONResponse({"error": "Invalid credentials"}, status_code=401) + return JSONResponse({"error": MESSAGE_ECHEC_CONNEXION_LOCALE}, status_code=401) ud = dict(user) if not ud.get("is_active"): @@ -332,7 +422,7 @@ def local_login(request: Request, body: dict = Body(default={})): (attempts, lock, ud["id"]), ) conn.commit() - return JSONResponse({"error": "Invalid credentials"}, status_code=401) + return JSONResponse({"error": MESSAGE_ECHEC_CONNEXION_LOCALE}, status_code=401) # Successful local login — SSO-only instances keep a way in for admins # only (every other account must use the IdP, design §7.1). diff --git a/app/routers/dashboard/workspace.py b/app/routers/dashboard/workspace.py index 5f3e956..012d80f 100644 --- a/app/routers/dashboard/workspace.py +++ b/app/routers/dashboard/workspace.py @@ -27,6 +27,22 @@ router = APIRouter(tags=["dashboard"]) + +def _contexte_accueil() -> dict: + """Contexte de la page d'accueil publique (fusion #146). + + Inscription fermée (défaut) : les boutons « Get started » de + l'accueil cèdent la place à un lien vers le Hub Dracodev, là où + les comptes se créent (URL dérivée de l'hôte, comme les autres + liens de la suite). + """ + from app.services import draco_urls, reglages_acces + + return { + "inscription_ouverte": reglages_acces.inscription_locale_ouverte(), + "hub_url": draco_urls.hub_url(), + } + @router.get("/", response_class=HTMLResponse) async def dashboard( request: Request, @@ -46,13 +62,13 @@ async def dashboard( from app.templating import ENV env = ENV template = env.get_template("landing.html") - return template.render() + return template.render(**_contexte_accueil()) except Exception: logger.exception("dashboard") from app.templating import ENV env = ENV template = env.get_template("landing.html") - return template.render() + return template.render(**_contexte_accueil()) # ── Authenticated ── user_id = user.get("id", 1) diff --git a/app/services/reglages_acces.py b/app/services/reglages_acces.py new file mode 100644 index 0000000..680efbb --- /dev/null +++ b/app/services/reglages_acces.py @@ -0,0 +1,113 @@ +"""Réglages d'accès de l'instance — inscription locale (fusion #146). + +Patron des réglages existants (``llm_config``, ``sso_config``) : une +table à ligne unique (``id = 1``) créée paresseusement, qui prime sur +les valeurs de ``app.config.settings`` (variables d'environnement / +``.env``), elles-mêmes aux défauts du code. + +Réglage « inscription locale ouverte » : + +- **fermée par défaut** (décision de Bruno, programme « compte + partout ») : les comptes naissent dans le Hub Dracodev et entrent + dans Flowdeck par la voie ObsiGate (SSO) / le provisionnement JIT ; + le parcours public d'inscription locale est refusé proprement. +- réversible à tout moment par un administrateur de l'instance + (``GET``/``PUT /api/admin/reglages-acces``) ou en posant + ``INSCRIPTION_LOCALE_OUVERTE=true`` avant tout enregistrement en + base. + +Ce réglage ne touche **jamais** la connexion des comptes existants, +locaux comme draco : il ne garde que la création de comptes locaux. +""" +from __future__ import annotations + +import logging + +from app.config import settings + +logger = logging.getLogger(__name__) + +__all__ = [ + "get_reglages_acces", + "set_reglages_acces", + "inscription_locale_ouverte", + "message_inscription_fermee", +] + + +def _assurer_table(conn) -> None: + """Crée la table des réglages d'accès si elle n'existe pas encore.""" + conn.execute(""" + CREATE TABLE IF NOT EXISTS reglages_acces ( + id INTEGER PRIMARY KEY CHECK (id = 1), + inscription_locale_ouverte INTEGER, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + +def get_reglages_acces() -> dict: + """Réglages d'accès effectifs — la ligne en base prime sur la config.""" + reglages = { + # Défaut du code et de la config : inscription fermée. + "inscription_locale_ouverte": bool( + getattr(settings, "inscription_locale_ouverte", False)), + } + try: + from app.db import get_conn + + with get_conn() as conn: + _assurer_table(conn) + row = conn.execute( + "SELECT inscription_locale_ouverte FROM reglages_acces" + " WHERE id=1" + ).fetchone() + except Exception: # noqa: BLE001 — base pas encore prête → config seule + return reglages + if row is not None and row["inscription_locale_ouverte"] is not None: + reglages["inscription_locale_ouverte"] = bool( + row["inscription_locale_ouverte"]) + return reglages + + +def set_reglages_acces(*, inscription_locale_ouverte: bool | None = None) -> dict: + """Enregistre les réglages d'accès (ligne unique) et les renvoie.""" + from app.db import get_conn + + actuels = get_reglages_acces() + if inscription_locale_ouverte is not None: + actuels["inscription_locale_ouverte"] = bool(inscription_locale_ouverte) + with get_conn() as conn: + _assurer_table(conn) + conn.execute( + "INSERT INTO reglages_acces (id, inscription_locale_ouverte," + " updated_at) VALUES (1, ?, CURRENT_TIMESTAMP)" + " ON CONFLICT(id) DO UPDATE SET" + " inscription_locale_ouverte=excluded.inscription_locale_ouverte," + " updated_at=CURRENT_TIMESTAMP", + (1 if actuels["inscription_locale_ouverte"] else 0,), + ) + conn.commit() + return actuels + + +def inscription_locale_ouverte() -> bool: + """Vrai si l'inscription publique locale est ouverte sur l'instance.""" + return bool(get_reglages_acces()["inscription_locale_ouverte"]) + + +def message_inscription_fermee() -> str: + """Message affiché quand l'inscription locale est fermée (#146). + + Nomme le Hub Dracodev — là où les comptes se créent — avec son URL + dérivée de l'hôte comme les autres liens de la suite + (``draco_urls.hub_url()``, jamais un nom figé). + """ + from app.services import draco_urls + + return ( + "L'inscription locale est fermée sur cette instance : " + "les comptes se créent dans le Hub Dracodev " + f"({draco_urls.hub_url()}), puis se connectent ici avec le " + "bouton « Login with ObsiGate (SSO) »." + ) diff --git a/app/templates/landing.html b/app/templates/landing.html index 453e36d..a735611 100644 --- a/app/templates/landing.html +++ b/app/templates/landing.html @@ -179,7 +179,7 @@
Log in - Get started free + {% if inscription_ouverte %}Get started free{% else %}Créer un compte dans le Hub Dracodev{% endif %}
@@ -191,7 +191,7 @@ All on your own server.

diff --git a/tests/conftest.py b/tests/conftest.py index c395dc1..b290014 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -127,6 +127,14 @@ def client(): from app.main import app init_db() + # Fusion #146 : l'inscription locale est fermée par défaut en + # production (les comptes naissent au Hub). L'environnement de + # test l'ouvre explicitement pour les suites historiques qui + # créent leurs comptes par /auth/register ; les tests du réglage + # (tests/test_reglages_acces.py) la ferment eux-mêmes. + from app.services import reglages_acces + reglages_acces.set_reglages_acces(inscription_locale_ouverte=True) + yield login_test_client(TestClient(app)) # Cleanup diff --git a/tests/test_reglages_acces.py b/tests/test_reglages_acces.py new file mode 100644 index 0000000..1e78658 --- /dev/null +++ b/tests/test_reglages_acces.py @@ -0,0 +1,177 @@ +"""FlowDeck — réglages d'accès : inscription locale (fusion #146, #144). + +Contrat de la phase 1 du programme « compte partout » : + +- inscription locale **fermée par défaut** (décision de Bruno) : + le parcours public d'inscription est refusé proprement et nomme + le Hub Dracodev, là où les comptes se créent ; +- réglage **réversible par un administrateur** (API + ``/api/admin/reglages-acces``) : ouverte, l'inscription redevient + exactement celle d'aujourd'hui ; +- la connexion des comptes existants (locaux et draco) n'est + **jamais** affectée par le réglage ; +- #144 : l'échec du formulaire de connexion locale suggère la voie + ObsiGate (SSO). + +Le harnais de test ouvre l'inscription pour les suites historiques +(voir conftest) ; chaque test pose donc explicitement l'état qu'il +vérifie via le service des réglages. +""" +from __future__ import annotations + +from conftest import anon_csrf + +from app.services import reglages_acces + + +def _fermer(): + reglages_acces.set_reglages_acces(inscription_locale_ouverte=False) + + +def _ouvrir(): + reglages_acces.set_reglages_acces(inscription_locale_ouverte=True) + + +def _creer_compte_local(login: str, mot_de_passe: str, admin: int = 0) -> None: + from app.db import get_conn + from app.password_utils import hash_password + + with get_conn() as conn: + conn.execute( + "INSERT OR REPLACE INTO users (login, full_name, email," + " password_hash, is_admin) VALUES (?, ?, ?, ?, ?)", + (login, login, f"{login}@test.dev", + hash_password(mot_de_passe), admin), + ) + conn.commit() + + +# ── Réglage fermé : inscription impossible + message ──────────────── + +def test_inscription_fermee_par_defaut_dans_le_code(): + """Le défaut du code (hors toute ligne en base) est « fermée ».""" + from app.config import settings + + assert settings.inscription_locale_ouverte is False + + +def test_inscription_fermee_post_refuse_avec_message_hub(client): + _fermer() + r = client.post("/auth/register", json={ + "email": "visiteur@test.dev", "password": "secret123", + "name": "Visiteur"}) + assert r.status_code == 403 + erreur = r.json()["error"] + assert "Hub Dracodev" in erreur + # Lien dérivé vers le Hub fourni à l'écran comme aux API. + assert r.json()["hub_url"] + # Aucun compte n'a été créé. + from app.db import get_conn + with get_conn() as conn: + assert conn.execute( + "SELECT COUNT(*) FROM users WHERE login='visiteur@test.dev'" + ).fetchone()[0] == 0 + + +def test_inscription_fermee_page_refus_propre(client): + _fermer() + r = anon_csrf(client).get("/auth/register", follow_redirects=False) + assert r.status_code == 403 + assert "Hub Dracodev" in r.text + assert "les comptes se créent" in r.text + + +def test_inscription_fermee_page_connexion_sans_onglet_register(client): + _fermer() + r = anon_csrf(client).get("/auth/login?provider=local") + assert r.status_code == 200 + assert 'id="tab-register"' not in r.text + # L'avis nomme le Hub et la voie SSO reste visible. + assert "Hub Dracodev" in r.text + assert "Login with ObsiGate (SSO)" in r.text + + +# ── Réglage ouvert : inscription comme aujourd'hui ────────────────── + +def test_inscription_ouverte_post_comme_avant(client): + _ouvrir() + r = client.post("/auth/register", json={ + "email": "nouveau@test.dev", "password": "secret123", + "name": "Nouveau"}) + assert r.status_code == 200 + assert r.json()["status"] == "ok" + + +def test_inscription_ouverte_page_register_affichee(client): + _ouvrir() + r = anon_csrf(client).get("/auth/register", follow_redirects=False) + assert r.status_code == 200 + assert 'id="tab-register"' in r.text + + +# ── Connexion des comptes existants : jamais affectée ─────────────── + +def test_connexion_compte_local_existant_inscription_fermee(client): + _creer_compte_local("ancien", "secret123") + _fermer() + r = anon_csrf(client).post("/auth/local-login", json={ + "email": "ancien", "password": "secret123"}) + assert r.status_code == 200 + assert r.json()["status"] == "ok" + + +def test_connexion_compte_draco_sans_mot_de_passe_inchangee(client): + """Un compte draco (sans mot de passe local) reste refusé en local + de la même façon, réglage ouvert ou fermé : le réglage ne change + rien à l'authentification.""" + from app.db import get_conn + with get_conn() as conn: + conn.execute( + "INSERT OR REPLACE INTO users (login, full_name, email," + " password_hash, auth_method) VALUES" + " ('draco-42', 'Dan Test', 'dan@test.dev', NULL, 'draco')") + conn.commit() + for ouvrir in (True, False): + reglages_acces.set_reglages_acces( + inscription_locale_ouverte=ouvrir) + r = anon_csrf(client).post("/auth/local-login", json={ + "email": "draco-42", "password": "nimporte"}) + assert r.status_code == 401 + + +# ── #144 : l'échec local suggère la voie ObsiGate (SSO) ───────────── + +def test_echec_connexion_locale_suggere_sso(client): + _creer_compte_local("quelquun", "secret123") + r = anon_csrf(client).post("/auth/local-login", json={ + "email": "quelquun", "password": "mauvais-mot-de-passe"}) + assert r.status_code == 401 + assert "ObsiGate (SSO)" in r.json()["error"] + assert "Hub Dracodev" in r.json()["error"] + + +def test_echec_connexion_compte_inconnu_suggere_sso(client): + r = anon_csrf(client).post("/auth/local-login", json={ + "email": "inconnu@test.dev", "password": "nimporte"}) + assert r.status_code == 401 + assert "ObsiGate (SSO)" in r.json()["error"] + + +# ── Administration du réglage ──────────────────────────────────────── + +def test_api_admin_reglages_acces_aller_retour(client): + """Le client de test est administrateur : lecture + écriture.""" + _fermer() + r = client.get("/api/admin/reglages-acces") + assert r.status_code == 200 + assert r.json()["inscription_locale_ouverte"] is False + r = client.put("/api/admin/reglages-acces", + json={"inscription_locale_ouverte": True}) + assert r.status_code == 200 + assert r.json()["inscription_locale_ouverte"] is True + assert reglages_acces.inscription_locale_ouverte() is True + + +def test_api_admin_reglages_acces_refusee_aux_anonymes(client): + r = anon_csrf(client).get("/api/admin/reglages-acces") + assert r.status_code in (401, 403) From 31fe0c181ad3cb162224b30887b9d5abbecc605a Mon Sep 17 00:00:00 2001 From: Spark Date: Sat, 7 Nov 2026 04:52:49 +0000 Subject: [PATCH 2/2] tests: ouvrir explicitement l'inscription dans les fixtures locales de test_app et test_v66_agent_api (fusion #146) --- CHANGELOG.md | 2 +- tests/test_app.py | 6 ++++++ tests/test_v66_agent_api.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cacc8c1..6d03851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ - **#146 — l'inscription publique locale n'est plus ouverte par défaut** : nouveau réglage d'administration « inscription locale ouverte » (`app/services/reglages_acces.py`, patron `llm_config`/`sso_config` : ligne unique en base qui prime sur la variable `INSCRIPTION_LOCALE_OUVERTE`, défaut **fermée** — décision de Bruno : les comptes naissent dans le Hub Dracodev). Réversible à tout moment par un administrateur via `GET`/`PUT /api/admin/reglages-acces`. - **Quand elle est fermée** : `POST /auth/register` répond 403 et `GET /auth/register` affiche une page de refus propre en français — les comptes se créent dans le Hub Dracodev, avec le lien du Hub dérivé de l'hôte (`draco_urls.hub_url()`, comme les autres liens de la suite) ; l'onglet Register disparaît de l'écran de connexion (avis nommant le Hub à la place) et les boutons « Get started » de la page d'accueil cèdent la place au lien Hub. **La connexion des comptes existants, locaux comme draco, n'est jamais affectée.** - **#144 (part Flowdeck)** : l'échec du formulaire de connexion locale (« Invalid credentials », 401) suggère désormais la voie centrale : « Si votre compte a été créé dans le Hub Dracodev, connectez-vous avec le bouton « Login with ObsiGate (SSO) »… » — le bouton existant, dont le libellé est repris tel quel. Aucune logique d'authentification ne change. -- **Tests** : `tests/test_reglages_acces.py` (nouveau, 12 tests) — fermée par défaut (défaut du code vérifié), POST refusé 403 + message + aucun compte créé, page de refus, onglet Register absent, ouverte = inscription d'aujourd'hui, connexion des comptes existants (local et draco) inchangée ouvert/fermé, échec local nommant ObsiGate (SSO), aller-retour de l'API d'administration et refus aux anonymes. Le harnais de test ouvre explicitement l'inscription pour les suites historiques qui créent leurs comptes par `/auth/register` (conftest, commenté). +- **Tests** : `tests/test_reglages_acces.py` (nouveau, 12 tests) — fermée par défaut (défaut du code vérifié), POST refusé 403 + message + aucun compte créé, page de refus, onglet Register absent, ouverte = inscription d'aujourd'hui, connexion des comptes existants (local et draco) inchangée ouvert/fermé, échec local nommant ObsiGate (SSO), aller-retour de l'API d'administration et refus aux anonymes. Le harnais de test ouvre explicitement l'inscription pour les suites historiques qui créent leurs comptes par `/auth/register` (conftest et les fixtures locales des suites historiques, commenté). ## v7.85.4 (2026-11-06) — Barre commune v0.6.0 re-vendangée : entrée « Administration » (fusion #142) diff --git a/tests/test_app.py b/tests/test_app.py index a5b4025..4cf8ea1 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -31,6 +31,12 @@ def client(): from app.main import app init_db() + # Fusion #146 : inscription fermée par défaut en production — + # cette suite historique crée ses comptes par /auth/register, + # elle ouvre donc le réglage explicitement (comme conftest). + from app.services import reglages_acces + reglages_acces.set_reglages_acces(inscription_locale_ouverte=True) + # A default user id=1 so tests that reference the implicit user (favorites, # workspace ownership) satisfy foreign keys without relying on leaked state. from app.db import get_conn diff --git a/tests/test_v66_agent_api.py b/tests/test_v66_agent_api.py index f7f11d8..75e0e33 100644 --- a/tests/test_v66_agent_api.py +++ b/tests/test_v66_agent_api.py @@ -42,6 +42,11 @@ def client(): settings.agent_run_timeout_seconds = 30 init_db() + # Fusion #146 : inscription fermée par défaut en production — + # _token() crée ses comptes par /auth/register, la suite ouvre + # donc le réglage explicitement (comme conftest). + from app.services import reglages_acces + reglages_acces.set_reglages_acces(inscription_locale_ouverte=True) with get_conn() as conn: conn.execute( "INSERT OR IGNORE INTO users (login, full_name, email, password_hash, is_admin) "