- Migration 18: 6 tables + 3 colonnes permission_type + indexes - PermissionManager: heritage page->collection->workspace, least privilege, groups, cache 60s - API /api/v2: pages/collections/properties/groups/users/audit (401/403/404/400) - Guards board.py + collections.py (404/403, admin/owner bypass) - Tests 21/21 (inherit/restricted/private, grant, revoke, batch, group, audit) - Docs + ROADMAP + CHANGELOG + VERSION 6.1.0
519 lines
19 KiB
Markdown
519 lines
19 KiB
Markdown
# 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
|
|
<div class="page-permissions-panel" x-show="showPermissions" x-transition>
|
|
<h3>Permissions de la page</h3>
|
|
|
|
<div class="perm-section">
|
|
<h4>Accès direct</h4>
|
|
<table>
|
|
<tr>
|
|
<th>Utilisateur/Groupe</th>
|
|
<th>Rôle</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
<template x-for="perm in pagePermissions">
|
|
<tr>
|
|
<td x-text="perm.name"></td>
|
|
<td>
|
|
<select x-model="perm.role" @change="updatePermission(perm)">
|
|
<option value="viewer">Viewer</option>
|
|
<option value="commenter">Commenter</option>
|
|
<option value="editor">Editor</option>
|
|
<option value="owner">Owner</option>
|
|
</select>
|
|
</td>
|
|
<td><button @click="revokePermission(perm.id)">✕</button></td>
|
|
</tr>
|
|
</template>
|
|
</table>
|
|
<button @click="addPermission()">+ Ajouter un accès</button>
|
|
</div>
|
|
|
|
<div class="perm-section">
|
|
<h4>Groupes</h4>
|
|
<template x-for="group in groups">
|
|
<div class="perm-group-row">
|
|
<span x-text="group.name"></span>
|
|
<select x-model="group.role" @change="updateGroupPermission(group)">
|
|
<option value="viewer">Viewer</option>
|
|
<option value="editor">Editor</option>
|
|
<option value="owner">Owner</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
<button @click="createGroup()">+ Nouveau groupe</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 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)
|