# V6.0.0 — Permissions Granulaires : Page-level & Property-level Access Control > **Statut** : Conception détaillée — v6.0.0 > **Date** : 2026-09-15 > **Route** : `feat/v6-permissions` → `develop` → `main` > **Dépendances** : v4.0.0 Accounts & Integrations + v2.0 Multi-utilisateurs (workspaces, workspace_members) --- ## 1. Vision & objectifs Étendre le modèle de permissions existant (workspace-level avec rôles owner/admin/editor/commenter/viewer) vers des permissions **plus fines** : - **Page-level** : contrôler qui peut voir, éditer, commenter une page spécifique - **Property-level** : contrôler quels utilisateurs peuvent voir ou modifier des propriétés spécifiques - **Collection-level** : permissions héritables ou individuelles sur une collection/database ### Objectifs | Critère | Cible | |---------|-------| | Héritage | Page-level → Collection-level → Workspace-level | | Performance | Vérification de permission < 10 ms (cache) | | Compatibilité | Tous les systèmes existants continuent de fonctionner | | UI | Indicateurs clairs des permissions (icônes, masquage) | | Audit | Log complet de tous les changements de permissions | --- ## 2. Modèle de permissions étendu ### 2.1 Hiérarchie des permissions ``` Workspace (owner/admin/editor/commenter/viewer) └── Collection (hérite du workspace + override possible) └── Page (hérite de la collection + override possible) └── Block (hérite de la page + override possible) └── Property (hérite de la page/collection + override possible) ``` ### 2.2 Matrice des permissions | Permission | workspace | collection | page | property | |------------|-----------|------------|------|----------| | **View** | ✓ viewer+ | ✓ viewer+ | ✓ viewer+ | ✓ viewer+ | | **Create** | ✓ editor+ | ✓ editor+ | ✓ editor+ | N/A | | **Edit** | ✓ editor+ | ✓ editor+ | ✓ editor+ | ✓ editor+ | | **Comment** | ✓ commenter+ | ✓ commenter+ | ✓ commenter+ | N/A | | **Delete** | ✓ admin+ | ✓ admin+ | ✓ admin+ | ✓ admin+ | | **Manage permissions** | ✓ admin+ | ✓ admin+ | ✓ admin+ | ✓ admin+ | | **Share** | ✓ editor+ | ✓ editor+ | ✓ editor+ | N/A | | **Lock** | ✓ admin+ | ✓ admin+ | ✓ admin+ | N/A | ### 2.3 Rôles par ressource ```python # Rôles disponibles par ressource (au-delà du workspace) RESOURCE_ROLES = { "page": { "viewer": "Peut lire la page", "commenter": "Peut lire + commenter", "editor": "Peut lire + éditer", "owner": "Peut tout faire + gérer les permissions", }, "property": { "viewer": "Peut lire la propriété", "editor": "Peut modifier la propriété", }, } ``` --- ## 3. Tables de base de données ### 3.1 Nouveau table `page_permissions` ```sql CREATE TABLE page_permissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, page_id INTEGER NOT NULL REFERENCES collection_pages(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, -- Soit user_id, soit group_id, soit role (pour rôle global) group_id INTEGER REFERENCES user_groups(id) ON DELETE CASCADE, role TEXT NOT NULL, -- 'viewer', 'commenter', 'editor', 'owner' grant_type TEXT NOT NULL DEFAULT 'explicit', -- 'explicit' (direct user), 'group', 'workspace_role' inherited BOOLEAN NOT NULL DEFAULT 0, -- True = hérité de la collection, pas de permission explicite granted_by INTEGER REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(page_id, user_id, group_id) -- Une permission par utilisateur/groupe ); CREATE INDEX idx_pp_page ON page_permissions(page_id, role); CREATE INDEX idx_pp_user ON page_permissions(user_id); ``` ### 3.2 Nouveau table `collection_permissions` ```sql CREATE TABLE collection_permissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, group_id INTEGER REFERENCES user_groups(id) ON DELETE CASCADE, role TEXT NOT NULL, grant_type TEXT NOT NULL DEFAULT 'explicit', inherited BOOLEAN NOT NULL DEFAULT 0, granted_by INTEGER REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(collection_id, user_id, group_id) ); CREATE INDEX idx_cp_collection ON collection_permissions(collection_id, role); ``` ### 3.3 Nouveau table `property_permissions` ```sql CREATE TABLE property_permissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE, property_id INTEGER NOT NULL REFERENCES collection_properties(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, group_id INTEGER REFERENCES user_groups(id) ON DELETE CASCADE, role TEXT NOT NULL, -- 'viewer', 'editor' grant_type TEXT NOT NULL DEFAULT 'explicit', inherited BOOLEAN NOT NULL DEFAULT 0, granted_by INTEGER REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(collection_id, property_id, user_id, group_id) ); ``` ### 3.4 Nouveau table `user_groups` ```sql CREATE TABLE user_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT DEFAULT '', created_by INTEGER REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(workspace_id, name) ); CREATE TABLE group_members ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(group_id, user_id) ); ``` ### 3.5 Extension des tables existantes ```sql -- Ajout de colonnes de permission sur les tables principales ALTER TABLE collection_pages ADD COLUMN permission_type TEXT DEFAULT 'inherit'; -- 'inherit' (default) | 'restricted' | 'private' ALTER TABLE collections ADD COLUMN permission_type TEXT DEFAULT 'inherit'; -- 'inherit' | 'restricted' | 'private' ``` ### 3.6 Table d'audit ```sql CREATE TABLE permission_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type TEXT NOT NULL, -- 'page', 'collection', 'property', 'group' resource_id INTEGER NOT NULL, action TEXT NOT NULL, -- 'grant', 'revoke', 'inherit', 'inherit_override' target_user_id INTEGER, target_group_id INTEGER, old_role TEXT, new_role TEXT, performed_by INTEGER REFERENCES users(id), ip_address TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_paudit_resource ON permission_audit_log(resource_type, resource_id, created_at); ``` --- ## 4. Endpoints API ### 4.1 Routeur `app/routers/permissions.py` ``` GET /api/v2/pages/{id}/permissions — Lister les permissions d'une page POST /api/v2/pages/{id}/permissions — Accorder une permission DELETE /api/v2/pages/{id}/permissions/{id} — Révoquer une permission POST /api/v2/pages/{id}/permissions/batch — Batch de permissions GET /api/v2/collections/{id}/permissions — Lister les permissions d'une collection POST /api/v2/collections/{id}/permissions — Accorder une permission DELETE /api/v2/collections/{id}/permissions/{id} — Révoquer GET /api/v2/properties/{id}/permissions — Lister les permissions d'une propriété POST /api/v2/properties/{id}/permissions — Accorder DELETE /api/v2/properties/{id}/permissions/{id} — Révoquer GET /api/v2/groups — Lister les groupes du workspace POST /api/v2/groups — Créer un groupe PUT /api/v2/groups/{id} — Mettre à jour un groupe DELETE /api/v2/groups/{id} — Supprimer un groupe POST /api/v2/groups/{id}/members — Ajouter un membre au groupe DELETE /api/v2/groups/{id}/members/{uid} — Retirer un membre GET /api/v2/audit/permissions — Historique des changements de permissions ``` ### 4.2 Service `app/services/permission_manager.py` — Extension Le `PermissionManager` existant est étendu : ```python class PermissionManager: """Granular permission resolution — héritage workspace → collection → page → property.""" # ── Méthodes existantes (conservées) ── def role_in_workspace(self, workspace_id: int) -> str: ... def can_read(self, workspace_id: int) -> bool: ... def can_write(self, workspace_id: int) -> bool: ... # ── Méthodes nouvelles — Page-level ── def get_page_permission(self, page_id: int, user_id: int) -> str: """Retourne le rôle effectif pour une page ('viewer', 'commenter', 'editor', 'owner', None). Résout l'héritage : page → collection → workspace.""" def can_view_page(self, page_id: int, user_id: int) -> bool: """Peut voir la page ?""" def can_edit_page(self, page_id: int, user_id: int) -> bool: """Peut éditer la page ?""" def can_comment_page(self, page_id: int, user_id: int) -> bool: """Peut commenter la page ?""" def can_manage_page_permissions(self, page_id: int, user_id: int) -> bool: """Peut gérer les permissions de la page ?""" # ── Méthodes nouvelles — Collection-level ── def get_collection_permission(self, collection_id: int, user_id: int) -> str: """Rôle effectif pour une collection.""" def can_view_collection(self, collection_id: int, user_id: int) -> bool: ... def can_edit_collection(self, collection_id: int, user_id: int) -> bool: ... # ── Méthodes nouvelles — Property-level ── def can_view_property(self, collection_id: int, property_id: int, user_id: int) -> bool: """Peut voir cette propriété ?""" def can_edit_property(self, collection_id: int, property_id: int, user_id: int) -> bool: """Peut éditer cette propriété ?""" def get_visible_properties(self, collection_id: int, user_id: int) -> list[int]: """Retourne la liste des IDs de propriétés visibles pour l'utilisateur.""" # ── Méthodes nouvelles — Groups ── def create_group(self, workspace_id: int, name: str, created_by: int) -> int: ... def add_user_to_group(self, group_id: int, user_id: int, added_by: int) -> None: ... def get_groups_for_workspace(self, workspace_id: int) -> list[dict]: ... # ── Méthodes nouvelles — Audit ── def log_permission_change(self, resource_type: str, resource_id: int, action: str, target_user_id: int | None, target_group_id: int | None, old_role: str | None, new_role: str | None, performed_by: int) -> None: ... # ── Méthodes nouvelles — Résolution rapide ── def _resolve_page_effective_role(self, page_id: int, user_id: int) -> str: """Résout le rôle effectif en parcourant la chaîne d'héritage. Cache le résultat pendant 60s.""" ``` --- ## 5. Logique de résolution des permissions ### 5.1 Algorithme de résolution ```python def resolve_effective_permission(resource_type: str, resource_id: int, user_id: int) -> str: """ Résout la permission effective en parcourant la chaîne d'héritage. Règle : la permission la plus restrictive prévaut (principe du moindre privilège). """ if resource_type == "page": # 1. Vérifier page_permissions directe direct = query_page_permission(page_id, user_id) if direct and not direct.inherited: return direct.role # 2. Remonter à la collection collection_id = get_page_collection(page_id) coll_perm = query_collection_permission(collection_id, user_id) if coll_perm and not coll_perm.inherited: return coll_perm.role # 3. Remonter au workspace ws_id = get_collection_workspace(collection_id) return get_workspace_role(ws_id, user_id) elif resource_type == "property": # Même logique : property → collection → workspace ... # Fallback return "viewer" # Tout utilisateur authentifié a au moins le rôle viewer ``` ### 5.2 Principe du moindre privilège - Si un utilisateur a `editor` au niveau workspace mais `viewer` sur une page spécifique → il est **viewer** sur cette page - Si un utilisateur a `viewer` au niveau workspace et aucune permission explicite sur la page → il est **viewer** sur cette page - Les permissions explicites de page **écrasent** toujours l'héritage ### 5.3 Cache de résolution ```python # Cache LRU pour les résolutions de permissions (60s TTL) from functools import lru_cache class PermissionResolver: @lru_cache(maxsize=1024) def cached_resolve(self, page_id: int, user_id: int, timestamp: int) -> str: # Le timestamp force l'invalidation du cache toutes les 60s return self.resolve_effective_permission(...) ``` --- ## 6. Interface utilisateur ### 6.1 Page editor — Bouton de permissions Dans `page_editor.html`, un nouveau bouton **"Permissions"** (icône 🔒) dans le menu `...` : ```html

Permissions de la page

Accès direct

Utilisateur/Groupe Rôle Actions

Groupes

``` ### 6.2 Collection-level permissions Dans `collections.py` router et `_database_table_scripts.html` : - Nouveau bouton **"Permissions"** dans le menu de la collection - Panneau latéral avec la même logique que les pages - Indication visuelle : icône 🔒 sur les pages en "restricted" ### 6.3 Property-level — Masquage dans les vues ```javascript // Dans la vue table/kanban/gallery : // Les propriétés restreintes sont masquées ou verrouillées if (!canViewProperty(propertyId, currentUser)) { // Masquer la colonne entière // OU afficher une placeholder "🔒 Restricted" } ``` ### 6.4 Indicateurs visuels | Situation | Indicateur | |-----------|-----------| | Page restreinte (permission différente du workspace) | Icône 🔒 dans le sidebar à côté du titre | | Propriété masquée | Colonne invisible ou placeholder 🔒 | | Utilisateur sans accès | Page affiche "Vous n'avez pas accès à cette page" | | Propriété en lecture seule | Champ désactivé avec tooltip "Permissions insuffisantes" | --- ## 7. Endpoints existants — Modifications nécessaires ### 7.1 Protection dans les routeurs existants Les routeurs suivants doivent intégrer les vérifications de permissions granulaires : ```python # board.py — pages @router.get("/board/api/pages/{id}") async def get_page(id: int, request: Request): user = get_current_user(request) if not permission_manager.can_view_page(id, user["id"]): raise HTTPException(404, "Page not found") # 404 et non 403 pour la sécurité ... # collections.py — pages de collection @router.post("/db/{id}/pages/api") async def create_page(id: int, ...): user = get_current_user(request) if not permission_manager.can_edit_collection(id, user["id"]): raise HTTPException(403, "Insufficient permissions") ... ``` ### 7.2 Propriétés visibles dans les vues ```python # Dans les vues (table, board, calendar, etc.) : def get_visible_collection_data(collection_id, user_id): """Retourne seulement les propriétés visibles pour l'utilisateur.""" visible_props = permission_manager.get_visible_properties(collection_id, user_id) # Filtrer la réponse API et le rendu HTML ``` --- ## 8. Tests | Test | Description | Outil | |------|-------------|-------| | Page permission grant | Accorder viewer à un utilisateur → il peut voir | Unit test | | Page permission revoke | Révoquer → 404 sur la page | Unit test | | Inheritance | Workspace editor + page viewer → viewer sur la page | Unit test | | Property hide | Propriété restreinte → colonne masquée | Integration test | | Group permissions | Groupe editor → tous les membres editor | Unit test | | Batch permissions | 10+ permissions en un POST | Unit test | | Audit log | Chaque changement de permission logué | Unit test | | Performance | 1000 utilisateurs, résolution < 10ms | Benchmark | | Conflict | Deux admins modifient les mêmes permissions → dernière écriture gagne | Integration test | | Public page | Page partagée publiquement ignore les restrictions | Unit test | --- ## 9. Checklist d'implémentation 1. **Migrations DB** — `page_permissions`, `collection_permissions`, `property_permissions`, `user_groups`, `group_members`, `permission_audit_log`, colonnes `permission_type` 2. **`app/services/permission_manager.py`** — extension complète (résolution héritage, cache, groupes) 3. **`app/routers/permissions.py`** — endpoints CRUD permissions 4. **Protection dans les routeurs existants** (`board.py`, `collections.py`, `api.py`, `workspace.py`) 5. **UI — Page permissions panel** (extension `page_editor.html`) 6. **UI — Collection permissions panel** (extension `collections.py`) 7. **UI — Property visibility in views** (`_database_table_scripts.html`) 8. **UI — Groups management** (settings page) 9. **Audit log** — tous les changements de permissions 10. **Tests** — tous les scénarios de permissions 11. **Documentation** — `/help` section permissions granulaires 12. **Performance** — cache LRU + index DB optimaux --- ## 10. Performance | Opération | Sans cache | Avec cache (LRU 60s) | |-----------|-----------|---------------------| | Résolution permission page | ~15 ms (3 requêtes DB) | ~0.5 ms | | Liste des propriétés visibles | ~20 ms | ~1 ms | | Vérification page dans sidebar | ~10 ms | ~0.3 ms | | Batch permissions (10 items) | ~50 ms | ~5 ms | --- ## 11. Références - `app/services/permission_manager.py` — PermissionManager existant (base) - `app/routers/board.py` — Protection de pages à étendre - `app/routers/collections.py` — Protection de collections à étendre - `app/routers/workspace.py` — Gestion des membres existants - `app/db.py` — Schéma de base de données - `docs/API_GUIDE_V6.md` — Référence API v2 (endpoints permissions) - `ROADMAP.md` — v6.0.0 Granular permissions item - [Notion API Permissions](https://developers.notion.com/docs/permissions) - [Google ACL Model](https://cloud.google.com/iam/docs/overview)