diff --git a/CHANGELOG.md b/CHANGELOG.md index f93fcc55c0a..ca17319c421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) · Versioning ### Added -- **api+web/sso-zelian** — SSO OIDC via le **serveur OAuth 2.1 de Supabase Auth** (feature Enterprise), réimplémenté en CE **clean-room à partir du provider `gitea`** (jamais de copie plane-ee) — **zéro migration**. Backend : `ZelianOAuthProvider` (`provider/oauth/zelian.py`) avec **PKCE S256** + **`client_secret_basic`** au token endpoint et mapping userinfo Supabase (`sub`→provider_id, `name`→prénom/nom, `picture`→avatar) ; endpoints app + space (`views/{app,space}/zelian.py`, `generate_pkce_pair()` — `code_verifier`/`state` en session) ; 4 routes (`/auth/zelian/[callback/]`, `/auth/spaces/zelian/[callback/]`) ; codes d'erreur `ZELIAN_NOT_CONFIGURED=5113` / `ZELIAN_OAUTH_PROVIDER_ERROR=5124` ; `is_zelian_enabled` exposé sur `/api/instances/` (piloté par `IS_ZELIAN_ENABLED`). Frontend : bouton « Continue with Zelian » (web + space) via les **seams d'extension** (`hooks/oauth/extended.tsx`, `TExtendedLoginMediums="zelian"`, `EXTENDED_LOGIN_MEDIUM_LABELS`) — **zéro modif de fichier core** ; type `is_zelian_enabled`, logo. Config par env (`ZELIAN_AUTH_BASE_URL`/`CLIENT_ID`/`CLIENT_SECRET`). Vérifié **offline** : 9 tests unitaires (PKCE S256, URL authorize, Basic auth + `code_verifier`, mapping userinfo), résolution des 4 routes, `/api/instances/` expose le flag, ruff clean, turbo `check:types` web+space 12/12. ⚠️ **Validation E2E bloquée** tant que la config Supabase (dashboard OAuth Server + client + page `/oauth/consent` externe) n'est pas fournie — cf. `PLAN-SSO-SUPABASE-PLANE.md` §1-3. Hors scope v1 : auto-redirect « sans clic » (§6, touche un fichier core → follow-up). +- **api+web/sso-zelian** — SSO OIDC via le **serveur OAuth 2.1 de Supabase Auth** (feature Enterprise), réimplémenté en CE **clean-room à partir du provider `gitea`** (jamais de copie plane-ee) — **zéro migration**. Backend : `ZelianOAuthProvider` (`provider/oauth/zelian.py`) avec **PKCE S256** + **`client_secret_basic`** au token endpoint et mapping userinfo Supabase (`sub`→provider_id, `name`→prénom/nom, `picture`→avatar) ; endpoints app + space (`views/{app,space}/zelian.py`, `generate_pkce_pair()` — `code_verifier`/`state` en session) ; 4 routes (`/auth/zelian/[callback/]`, `/auth/spaces/zelian/[callback/]`) ; codes d'erreur `ZELIAN_NOT_CONFIGURED=5113` / `ZELIAN_OAUTH_PROVIDER_ERROR=5910` ; `is_zelian_enabled` exposé sur `/api/instances/` (piloté par `IS_ZELIAN_ENABLED`). Frontend : bouton « Continue with Zelian » (web + space) via les **seams d'extension** (`hooks/oauth/extended.tsx`, `TExtendedLoginMediums="zelian"`, `EXTENDED_LOGIN_MEDIUM_LABELS`) — **zéro modif de fichier core** ; type `is_zelian_enabled`, logo. Config par env (`ZELIAN_AUTH_BASE_URL`/`CLIENT_ID`/`CLIENT_SECRET`). Vérifié **offline** : 9 tests unitaires (PKCE S256, URL authorize, Basic auth + `code_verifier`, mapping userinfo), résolution des 4 routes, `/api/instances/` expose le flag, ruff clean, turbo `check:types` web+space 12/12. ⚠️ **Validation E2E bloquée** tant que la config Supabase (dashboard OAuth Server + client + page `/oauth/consent` externe) n'est pas fournie — cf. `PLAN-SSO-SUPABASE-PLANE.md` §1-3. Hors scope v1 : auto-redirect « sans clic » (§6, touche un fichier core → follow-up). - **api+web/views-access** — Vues privées/publiques (feature Pro) fonctionnelles en CE — **zéro migration** : le modèle (`IssueView.access` 0=Private/1=Public, défaut Public), le queryset de visibilité (`Q(owned_by)|Q(access=1)`, listes + détail, projet + workspace) et les gardes owner-only existaient déjà ; le POST/PATCH droppait simplement `access` (read_only). Backend : `access` retiré des `read_only_fields` de `IssueViewSerializer` (partagé par les 2 viewsets) — `is_locked` reste non inscriptible ; valeurs hors {0,1} → 400 (ChoiceField dérivé du modèle). Web : stubs CE remplis — sélecteur Public/Private du formulaire de vue (`access-controller.tsx`, réutilise `AccessField` + `VIEW_ACCESS_SPECIFIERS` upstream) et facette de filtre « Access » (`filters/access-filter.tsx`). **Revue adversariale passée** — 3 fuites de vue privée fermées (surfaces devenues significatives dès que `access` est inscriptible) : VA-01 une vue publiée puis passée en privé restait servie via l'anchor public (`space` `AllowAny`) → publication d'une vue privée refusée (400) + dépublication automatique (DeployBoard supprimé) au passage en privé ; VA-02 la recherche globale (`filter_views`) fuitait nom/id des vues privées d'autrui → filtre `Q(access=1)|Q(owned_by=user)` ; VA-03 on pouvait mettre en favori la vue privée d'un autre → vérification d'accès avant création du favori (404 sinon). Deux correctifs UI de la même revue : COR-va-1 le retrait du chip « Private » (valeur 0, falsy) effaçait tout le filtre `view_type` (garde `value === null` au lieu de `!value`) ; COR-va-2 les libellés Private/Public de la facette de filtre passés en clés i18n (`common.access.*` présentes dans les 19 locales) au lieu de littéraux anglais. Vérifié : **16 tests pytest Docker** (persistance access=0, toggle owner, refus non-owner 400/403, access invalide 400, vue privée cachée aux autres / visible au owner — projet ET workspace, is_locked non inscriptible, non-publication + dépublication auto, recherche + favoris scopés), E2E navigateur (création « Vue privee test » → `access:0` persisté, facette rendue), `makemigrations --check` clean, tsc EXIT=0. Hors scope : tier « Shared » (table de partage inexistante), verrouillage de vue, layouts additionnels workspace. - **api+web/milestones** — Milestones / jalons projet (feature payante) alignés sur le **SDK/MCP officiel** (source de vérité : plane-mcp-server 0.2.19). Modèles greenfield `Milestone(ProjectBaseModel)` (`name`, `description`, `target_date`, `external_source/id`, `sort_order`) table `milestones` et `MilestoneIssue` (FK issue+milestone, `UniqueConstraint(issue, milestone) WHERE deleted_at IS NULL`, calque `CycleIssue`) table `milestone_issues` + toggle `Project.is_milestone_enabled` (défaut False) — **migration `0128_milestones`** (dép 0127). **Couche v1 token/MCP** : CRUD `/api/v1/.../milestones/[{id}/]` + `GET/POST/DELETE .../work-items/` — **`title` exposé via `source="name"`** (le SDK Pydantic l'exige), listes cursor-paginées enveloppe `BasePaginator`, POST work-items `{issues:[uuid]}` AJOUTE (validation UUID + même-projet 400, déjà-liés ignorés), **DELETE avec body** RETIRE (204), dedup import externe 409, écritures gatées 400 si toggle off, scoping ProjectMember. **Couche app interne** pour le web (mêmes règles, retrait unitaire `/milestone-issues/:issue_id/`, rejet explicite des ids étrangers — parité v1). **Web** : type `TMilestone`, `milestone.service.ts`, store MobX scopé projet, page `/:ws/projects/:pid/milestones` + entrée nav gatée, toggle « Milestones » (Project Settings → Features), section liste (modal create/edit RHF, delete + confirmation, attach/detach via `ExistingIssuesListModal`, compteurs X/Y). i18n : 6 clés plates + bloc `disabled_project.empty_state.milestone` × **19 locales** (`sync:check` OK). **Revue adversariale passée** (2 audits sécurité + correction) : sécurité solide (isolation cross-projet/workspace, permissions, gate vérifiés) — 1 durcissement SEC-ms-1 (`MilestoneWriteSerializer` app : `created_by`/`updated_by`/`deleted_at`/`sort_order` passés en `read_only_fields` contre le mass-assignment via PATCH) ; corrections COR-ms-1 (`is_milestone_enabled` ajouté à la projection `.values()` de la liste projets → l'entrée nav sidebar apparaît sans ouvrir le projet), COR-ms-2 (work items du jalon exposés inline via `IssueProjectLiteSerializer` → nom + identifiant `PROJ-N` au lieu de l'UUID brut), COR-ms-3 (liens fetchés à l'ouverture du modal d'attache → items déjà attachés cachés), COR-ms-4 (en-tête de modal + tooltips i18n ×19), COR-ms-5 (lien sidebar ouvert aux guests, parité backend/page). Vérifié : **19 tests pytest Docker** (contrat v1 title/enveloppe/409/gate/work-items ; app CRUD/guest/scoping/compteurs/expansion issue_detail/anti-mass-assignment), `makemigrations --check` clean, turbo `check:types` 11/11, **chaîne MCP validée EN DIRECT sur l'instance locale** (`create_milestone` → `title` ✓, `manage_milestone_work_items` ✓, `list_milestone_work_items` ✓, `update_milestone` ✓, `list_milestones` avec compteurs ✓), UI vérifiée navigateur (page, nav gatée, jalon déplié → work item « DEMO-1 Aider Anthony »). - **api+web/active-cycles-workspace** — Active Cycles workspace (feature Pro) : page `/:ws/active-cycles` réelle remplaçant le paywall CE. Backend : nouvel endpoint interne `GET /api/workspaces/:slug/active-cycles/` (`WorkspaceActiveCyclesEndpoint`) — cycles actifs (start ≤ now ≤ end, non archivés) des projets où le demandeur est membre actif, avec compteurs de progression (total/completed/started/unstarted/backlog/cancelled), dates en timezone projet, pagination cursor — **zéro migration** (agrégation lecture seule, permission `WorkspaceEntityPermission` + scoping ProjectMember). Web : `ce/components/active-cycles/root.tsx` rempli (grille de cartes cycle : nom, projet, plage de dates, barre de progression + %, compteurs ; loading/erreur/empty state), badge « Pro » neutralisé (header + sidebar). Service front dormant `cycleService.workspaceActiveCycles` réutilisé. i18n : clés existantes uniquement (compteurs des cartes inclus : `workspace_projects.state.*`, `common.pending`, `common.load_more` × 19 locales). Revue adversariale passée (2 auditeurs + double vérif, 6 findings corrigés) : exclusion des projets `cycle_view=False` (COR-4), % de complétion hors cancelled + compteur cancelled affiché — cohérent avec la barre (COR-1), libellés compteurs i18n (COR-2), erreur réseau → état d'erreur et non empty state (COR-3), pagination réelle « Load more » via `useSWRInfinite` (COR-6). Vérifié : **16 tests pytest Docker** (`test_workspace_active_cycles_app.py` : fenêtre active, scoping membre/guest/inactif/étranger, exclusions archivé/draft/soft-delete/cycle_view, compteurs anti-inflation de jointures, pagination/cursor malformé), ruff/py_compile clean, `makemigrations --check` = No changes detected, **E2E live** (cycle actif seedé → `status: CURRENT`, compteurs mis à jour après ajout d'un work item via MCP local), `check:types` EXIT=0, vérif visuelle navigateur. diff --git a/docs/specs/api/sso-zelian/VERSIONNING.md b/docs/specs/api/sso-zelian/VERSIONNING.md index 9d75afc2ac0..63296145db2 100644 --- a/docs/specs/api/sso-zelian/VERSIONNING.md +++ b/docs/specs/api/sso-zelian/VERSIONNING.md @@ -1,8 +1,9 @@ # VERSIONNING — api/sso-zelian -| Version | Date | Type | Description | Fichiers touchés | -| ------- | ---------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 0.1.0 | 2026-07-12 | feat | SSO Zelian via serveur OAuth 2.1 Supabase, clean-room depuis `gitea` + PKCE S256 + `client_secret_basic`. Backend (provider + views app/space + routes + erreurs + `is_zelian_enabled`) + frontend (seams web/space, types, label, logo). Zéro migration. Validé offline (9 tests unitaires, routes, config, typecheck 12/12) ; E2E bloqué sur config Supabase. | apps/api/plane/authentication/{provider/oauth/zelian.py, views/app/zelian.py, views/space/zelian.py, adapter/error.py, adapter/oauth.py, views/**init**.py, urls.py}, apps/api/plane/license/api/views/instance.py, apps/api/.env.example, apps/api/plane/tests/unit/test_zelian_oauth_provider.py, packages/types/src/instance/{auth-ee.ts, base.ts}, packages/constants/src/auth/extended.ts, apps/web/core/hooks/oauth/extended.tsx, apps/space/hooks/oauth/extended.tsx, apps/{web,space}/app/assets/logos/zelian-logo.svg | -| 0.1.1 | 2026-07-13 | docs | Validation E2E locale réussie : flux Sign in with Zelian → authorize PKCE S256 → mire auto-consent → callback → User + Account(provider='zelian') créés en base (dev@zelian.local). Découverte et documentation du comportement d'auto-consent Supabase (`getAuthorizationDetails` renvoie `redirect_url` directement si client first-party) ; fix corrigé dans le repo mire `@zelian/auth` (hors arbre plane). Aucun fichier source plane modifié ; `apps/api/.env` pointé sur projet Supabase dev `jgwcadcexcsifxbbvpzv` (non versionné, gitignored). | docs/specs/api/sso-zelian/spec-technique.md (mise à jour auto-consent + statut E2E) | +| Version | Date | Type | Description | Fichiers touchés | +| ------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 0.1.0 | 2026-07-12 | feat | SSO Zelian via serveur OAuth 2.1 Supabase, clean-room depuis `gitea` + PKCE S256 + `client_secret_basic`. Backend (provider + views app/space + routes + erreurs + `is_zelian_enabled`) + frontend (seams web/space, types, label, logo). Zéro migration. Validé offline (9 tests unitaires, routes, config, typecheck 12/12) ; E2E bloqué sur config Supabase. | apps/api/plane/authentication/{provider/oauth/zelian.py, views/app/zelian.py, views/space/zelian.py, adapter/error.py, adapter/oauth.py, views/**init**.py, urls.py}, apps/api/plane/license/api/views/instance.py, apps/api/.env.example, apps/api/plane/tests/unit/test_zelian_oauth_provider.py, packages/types/src/instance/{auth-ee.ts, base.ts}, packages/constants/src/auth/extended.ts, apps/web/core/hooks/oauth/extended.tsx, apps/space/hooks/oauth/extended.tsx, apps/{web,space}/app/assets/logos/zelian-logo.svg | +| 0.1.1 | 2026-07-13 | docs | Validation E2E locale réussie : flux Sign in with Zelian → authorize PKCE S256 → mire auto-consent → callback → User + Account(provider='zelian') créés en base (dev@zelian.local). Découverte et documentation du comportement d'auto-consent Supabase (`getAuthorizationDetails` renvoie `redirect_url` directement si client first-party) ; fix corrigé dans le repo mire `@zelian/auth` (hors arbre plane). Aucun fichier source plane modifié ; `apps/api/.env` pointé sur projet Supabase dev `jgwcadcexcsifxbbvpzv` (non versionné, gitignored). | docs/specs/api/sso-zelian/spec-technique.md (mise à jour auto-consent + statut E2E) | +| 0.1.2 | 2026-07-18 | refactor | Renumérotation des codes d'erreur Zelian pour écarter les collisions avec l'upstream. `ZELIAN_OAUTH_PROVIDER_ERROR` déplacé de `5124` vers `5910` ; le bloc `59xx` est désormais réservé au fork. Le code `5124` ainsi libéré est adopté par l'upstream comme `OAUTH_PROVIDER_UNVERIFIED_EMAIL`. Renumérotation livrée avec le fix sécurité ATO (`d891e50061`, 2026-07-18) ; documentation resynchronisée le 2026-07-21 (spec-technique + CHANGELOG). | `apps/api/plane/authentication/adapter/error.py` (code de vérité, livré en `d891e50061`), `docs/specs/api/sso-zelian/spec-technique.md`, `CHANGELOG.md` | > Table mise à jour par @update-writer-after-implement après chaque implémentation. diff --git a/docs/specs/api/sso-zelian/spec-technique.md b/docs/specs/api/sso-zelian/spec-technique.md index 5c57c6e7ca8..5c6c0b03c44 100644 --- a/docs/specs/api/sso-zelian/spec-technique.md +++ b/docs/specs/api/sso-zelian/spec-technique.md @@ -3,8 +3,8 @@ | Champ | Valeur | | ------- | -------------------------------------------- | | Module | api/sso-zelian | -| Version | 0.1.1 | -| Date | 2026-07-13 | +| Version | 0.1.2 | +| Date | 2026-07-18 | | Statut | IMPLÉMENTÉ et validé E2E (local, 2026-07-13) | --- @@ -27,17 +27,17 @@ Réplication **clean-room** du provider OAuth `gitea` (host configurable) + 2 aj ## Fichiers modifiés -| Fichier | Modification | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `apps/api/plane/authentication/adapter/error.py` | `ZELIAN_NOT_CONFIGURED=5113`, `ZELIAN_OAUTH_PROVIDER_ERROR=5124` | -| `apps/api/plane/authentication/adapter/oauth.py` | branche `zelian` dans `authentication_error_code()` | -| `apps/api/plane/authentication/views/__init__.py` | exports app + space | -| `apps/api/plane/authentication/urls.py` | 4 routes : `zelian/`, `zelian/callback/`, `spaces/zelian/`, `spaces/zelian/callback/` | -| `apps/api/plane/license/api/views/instance.py` | `IS_ZELIAN_ENABLED` → `data["is_zelian_enabled"]` | -| `apps/api/.env.example` | doc des vars `IS_ZELIAN_ENABLED` / `ZELIAN_AUTH_BASE_URL` / `ZELIAN_CLIENT_ID` / `ZELIAN_CLIENT_SECRET` | -| `packages/types/src/instance/auth-ee.ts` | `TExtendedLoginMediums = "zelian"` | -| `packages/types/src/instance/base.ts` | `is_zelian_enabled: boolean` sur `IInstanceConfig` | -| `packages/constants/src/auth/extended.ts` | `EXTENDED_LOGIN_MEDIUM_LABELS.zelian = "Zelian"` | +| Fichier | Modification | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apps/api/plane/authentication/adapter/error.py` | `ZELIAN_NOT_CONFIGURED=5113`, `ZELIAN_OAUTH_PROVIDER_ERROR=5910` — codes du fork dans le bloc `59xx` réservé ; `5124` est désormais possédé par l'upstream (`OAUTH_PROVIDER_UNVERIFIED_EMAIL`) | +| `apps/api/plane/authentication/adapter/oauth.py` | branche `zelian` dans `authentication_error_code()` | +| `apps/api/plane/authentication/views/__init__.py` | exports app + space | +| `apps/api/plane/authentication/urls.py` | 4 routes : `zelian/`, `zelian/callback/`, `spaces/zelian/`, `spaces/zelian/callback/` | +| `apps/api/plane/license/api/views/instance.py` | `IS_ZELIAN_ENABLED` → `data["is_zelian_enabled"]` | +| `apps/api/.env.example` | doc des vars `IS_ZELIAN_ENABLED` / `ZELIAN_AUTH_BASE_URL` / `ZELIAN_CLIENT_ID` / `ZELIAN_CLIENT_SECRET` | +| `packages/types/src/instance/auth-ee.ts` | `TExtendedLoginMediums = "zelian"` | +| `packages/types/src/instance/base.ts` | `is_zelian_enabled: boolean` sur `IInstanceConfig` | +| `packages/constants/src/auth/extended.ts` | `EXTENDED_LOGIN_MEDIUM_LABELS.zelian = "Zelian"` | ## Contrat OAuth (endpoints Supabase)