329 lines
8.8 KiB
Markdown
329 lines
8.8 KiB
Markdown
|
|
# Système de Permissions du Chat Server
|
||
|
|
|
||
|
|
## Vue d'ensemble
|
||
|
|
|
||
|
|
Le système de permissions du chat server Veza fournit un contrôle d'accès granulaire pour les conversations, avec support des rôles (admin, moderator, member) et vérifications centralisées.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Module `security/permission.rs`
|
||
|
|
|
||
|
|
Le module `PermissionService` centralise toutes les vérifications de permissions :
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub struct PermissionService {
|
||
|
|
pool: PgPool,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fonctions principales
|
||
|
|
|
||
|
|
#### `user_in_conversation(user_id, conversation_id) -> Result<bool>`
|
||
|
|
|
||
|
|
Vérifie si un utilisateur est membre d'une conversation.
|
||
|
|
|
||
|
|
**Retourne** : `true` si membre, `false` sinon.
|
||
|
|
|
||
|
|
#### `user_role_in_conversation(user_id, conversation_id) -> Result<Role>`
|
||
|
|
|
||
|
|
Récupère le rôle d'un utilisateur dans une conversation spécifique.
|
||
|
|
|
||
|
|
**Retourne** : Le rôle (`Admin`, `Moderator`, `User`, `SuperAdmin`) ou une erreur si non membre.
|
||
|
|
|
||
|
|
#### `user_global_role(user_id) -> Result<Role>`
|
||
|
|
|
||
|
|
Récupère le rôle global d'un utilisateur depuis la table `users`.
|
||
|
|
|
||
|
|
**Retourne** : Le rôle global, ou `User` par défaut.
|
||
|
|
|
||
|
|
#### `can_send_message(user_id, conversation_id) -> Result<()>`
|
||
|
|
|
||
|
|
Vérifie si un utilisateur peut envoyer un message dans une conversation.
|
||
|
|
|
||
|
|
**Règles** :
|
||
|
|
- Les membres peuvent envoyer des messages
|
||
|
|
- Les admins globaux peuvent envoyer des messages même sans être membres
|
||
|
|
- Les non-membres (non-admin) sont refusés
|
||
|
|
|
||
|
|
#### `can_read_conversation(user_id, conversation_id) -> Result<()>`
|
||
|
|
|
||
|
|
Vérifie si un utilisateur peut lire une conversation.
|
||
|
|
|
||
|
|
**Règles** :
|
||
|
|
- Les membres peuvent lire
|
||
|
|
- Les admins globaux peuvent lire même sans être membres
|
||
|
|
- Les non-membres (non-admin) sont refusés
|
||
|
|
|
||
|
|
#### `can_mark_read(user_id, conversation_id) -> Result<()>`
|
||
|
|
|
||
|
|
Vérifie si un utilisateur peut marquer un message comme lu.
|
||
|
|
|
||
|
|
**Règles** : Identiques à `can_read_conversation`.
|
||
|
|
|
||
|
|
#### `can_join_conversation(user_id, conversation_id) -> Result<()>`
|
||
|
|
|
||
|
|
Vérifie si un utilisateur peut rejoindre une conversation.
|
||
|
|
|
||
|
|
**Règles** :
|
||
|
|
- Les conversations publiques peuvent être rejointes par tous
|
||
|
|
- Les conversations privées nécessitent d'être membre ou admin global
|
||
|
|
|
||
|
|
## Rôles et Permissions
|
||
|
|
|
||
|
|
### Rôles disponibles
|
||
|
|
|
||
|
|
| Rôle | Description |
|
||
|
|
|------|-------------|
|
||
|
|
| `User` | Utilisateur standard |
|
||
|
|
| `Moderator` | Modérateur avec permissions étendues |
|
||
|
|
| `Admin` | Administrateur avec tous les pouvoirs |
|
||
|
|
| `SuperAdmin` | Super administrateur |
|
||
|
|
|
||
|
|
### Matrice des permissions
|
||
|
|
|
||
|
|
| Action | User | Moderator | Admin | SuperAdmin |
|
||
|
|
|--------|------|-----------|-------|------------|
|
||
|
|
| Envoyer message (membre) | ✅ | ✅ | ✅ | ✅ |
|
||
|
|
| Envoyer message (non-membre) | ❌ | ❌ | ✅ | ✅ |
|
||
|
|
| Lire conversation (membre) | ✅ | ✅ | ✅ | ✅ |
|
||
|
|
| Lire conversation (non-membre) | ❌ | ❌ | ✅ | ✅ |
|
||
|
|
| Marquer comme lu | ✅ | ✅ | ✅ | ✅ |
|
||
|
|
| Rejoindre conversation publique | ✅ | ✅ | ✅ | ✅ |
|
||
|
|
| Rejoindre conversation privée | ❌* | ❌* | ✅ | ✅ |
|
||
|
|
|
||
|
|
\* Nécessite d'être membre de la conversation
|
||
|
|
|
||
|
|
## Intégration dans les Handlers
|
||
|
|
|
||
|
|
### WebSocket Handler (`websocket/handler.rs`)
|
||
|
|
|
||
|
|
Tous les handlers WebSocket vérifient les permissions avant d'exécuter les actions :
|
||
|
|
|
||
|
|
#### `SendMessage`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Vérifier les permissions avant d'envoyer le message
|
||
|
|
state
|
||
|
|
.permission_service
|
||
|
|
.can_send_message(sender_uuid, conversation_id)
|
||
|
|
.await?;
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `JoinConversation`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Vérifier les permissions avant de rejoindre
|
||
|
|
state
|
||
|
|
.permission_service
|
||
|
|
.can_join_conversation(user_uuid, conversation_id)
|
||
|
|
.await?;
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `MarkAsRead`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Vérifier les permissions pour marquer comme lu
|
||
|
|
state
|
||
|
|
.permission_service
|
||
|
|
.can_mark_read(user_uuid, conversation_id)
|
||
|
|
.await?;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Message Handler (`message_handler.rs`)
|
||
|
|
|
||
|
|
Les handlers de messages vérifient également les permissions :
|
||
|
|
|
||
|
|
#### `handle_room_message`
|
||
|
|
|
||
|
|
Vérifie `can_send_message` avant d'envoyer un message dans un salon.
|
||
|
|
|
||
|
|
#### `handle_direct_message`
|
||
|
|
|
||
|
|
Vérifie `can_send_message` avant d'envoyer un message direct.
|
||
|
|
|
||
|
|
#### `handle_room_history`
|
||
|
|
|
||
|
|
Vérifie `can_read_conversation` via `can_read_room_history`.
|
||
|
|
|
||
|
|
#### `handle_dm_history`
|
||
|
|
|
||
|
|
Vérifie `can_read_conversation` via `can_read_dm_conversation`.
|
||
|
|
|
||
|
|
## Schéma de Base de Données
|
||
|
|
|
||
|
|
### Table `conversation_members`
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE conversation_members (
|
||
|
|
conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
|
||
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||
|
|
joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
|
|
PRIMARY KEY (conversation_id, user_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Colonne `role`** : Peut être `'user'`, `'moderator'`, `'admin'`, ou `'superadmin'`.
|
||
|
|
|
||
|
|
### Table `users`
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE users (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||
|
|
role VARCHAR(20) DEFAULT 'user', -- Rôle global
|
||
|
|
...
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Colonne `role`** : Rôle global de l'utilisateur dans le système.
|
||
|
|
|
||
|
|
## Gestion des Erreurs
|
||
|
|
|
||
|
|
### Types d'erreurs
|
||
|
|
|
||
|
|
#### `PermissionError::NotMember`
|
||
|
|
|
||
|
|
L'utilisateur n'est pas membre de la conversation.
|
||
|
|
|
||
|
|
**Code HTTP** : 403 Forbidden
|
||
|
|
|
||
|
|
#### `PermissionError::InsufficientPermissions`
|
||
|
|
|
||
|
|
L'utilisateur n'a pas les permissions suffisantes pour l'action.
|
||
|
|
|
||
|
|
**Code HTTP** : 403 Forbidden
|
||
|
|
|
||
|
|
#### `PermissionError::InvalidRole`
|
||
|
|
|
||
|
|
Le rôle spécifié est invalide.
|
||
|
|
|
||
|
|
**Code HTTP** : 500 Internal Server Error
|
||
|
|
|
||
|
|
### Logging
|
||
|
|
|
||
|
|
Toutes les violations de permissions sont loggées avec `tracing::warn!` :
|
||
|
|
|
||
|
|
```rust
|
||
|
|
warn!(
|
||
|
|
user_id = %user_id,
|
||
|
|
conversation_id = %conversation_id,
|
||
|
|
error = %e,
|
||
|
|
"Permission refusée pour l'envoi de message"
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Messages WebSocket d'Erreur
|
||
|
|
|
||
|
|
Lorsqu'une permission est refusée, le client reçoit un message d'erreur :
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "error",
|
||
|
|
"message": "Permission refusée: Utilisateur non membre de la conversation",
|
||
|
|
"code": "permission_denied"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## JWT Manager
|
||
|
|
|
||
|
|
Le `JwtManager` a été mis à jour pour récupérer les informations utilisateur depuis la base de données lors du refresh token :
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Récupérer username et role depuis la DB
|
||
|
|
let user_info: Option<(String, Option<String>)> = sqlx::query_as(
|
||
|
|
r#"
|
||
|
|
SELECT username, role FROM users
|
||
|
|
WHERE id = $1
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.bind(user_uuid)
|
||
|
|
.fetch_optional(pool)
|
||
|
|
.await?;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fallback** : Si l'utilisateur n'est pas trouvé ou si le pool DB n'est pas disponible, utilise `"user"` / `"user"` par défaut (avec warning).
|
||
|
|
|
||
|
|
## Tests
|
||
|
|
|
||
|
|
Les tests sont disponibles dans `tests/test_permissions.rs` :
|
||
|
|
|
||
|
|
- `test_can_send_message_non_member` : Vérifie qu'un non-membre ne peut pas envoyer
|
||
|
|
- `test_can_send_message_member` : Vérifie qu'un membre peut envoyer
|
||
|
|
- `test_can_send_message_admin_global` : Vérifie qu'un admin global peut envoyer sans être membre
|
||
|
|
- `test_can_read_conversation_non_member` : Vérifie qu'un non-membre ne peut pas lire
|
||
|
|
- `test_can_read_conversation_member` : Vérifie qu'un membre peut lire
|
||
|
|
- `test_user_in_conversation` : Vérifie la fonction `user_in_conversation`
|
||
|
|
- `test_user_role_in_conversation` : Vérifie la fonction `user_role_in_conversation`
|
||
|
|
- `test_integration_send_message_with_permissions` : Test d'intégration complet
|
||
|
|
|
||
|
|
**Note** : Les tests nécessitent une base de données de test et sont marqués avec `#[ignore]`.
|
||
|
|
|
||
|
|
## Exemples d'utilisation
|
||
|
|
|
||
|
|
### Vérifier les permissions avant d'envoyer un message
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use chat_server::security::permission::PermissionService;
|
||
|
|
|
||
|
|
let permission_service = PermissionService::new(pool);
|
||
|
|
|
||
|
|
// Vérifier avant d'envoyer
|
||
|
|
permission_service
|
||
|
|
.can_send_message(user_id, conversation_id)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
// Envoyer le message...
|
||
|
|
```
|
||
|
|
|
||
|
|
### Vérifier les permissions avant de lire
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Vérifier avant de lire
|
||
|
|
permission_service
|
||
|
|
.can_read_conversation(user_id, conversation_id)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
// Récupérer les messages...
|
||
|
|
```
|
||
|
|
|
||
|
|
### Récupérer le rôle d'un utilisateur
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Rôle dans une conversation spécifique
|
||
|
|
let role = permission_service
|
||
|
|
.user_role_in_conversation(user_id, conversation_id)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
// Rôle global
|
||
|
|
let global_role = permission_service
|
||
|
|
.user_global_role(user_id)
|
||
|
|
.await?;
|
||
|
|
```
|
||
|
|
|
||
|
|
## Sécurité
|
||
|
|
|
||
|
|
### Bonnes pratiques
|
||
|
|
|
||
|
|
1. **Toujours vérifier les permissions** avant d'exécuter une action
|
||
|
|
2. **Logger les violations** pour audit et monitoring
|
||
|
|
3. **Ne jamais faire confiance au client** : toutes les vérifications sont côté serveur
|
||
|
|
4. **Utiliser le service centralisé** : ne pas dupliquer la logique de vérification
|
||
|
|
5. **Gérer les erreurs gracieusement** : envoyer des messages d'erreur clairs au client
|
||
|
|
|
||
|
|
### Points d'attention
|
||
|
|
|
||
|
|
- Les admins globaux peuvent contourner certaines restrictions (par design)
|
||
|
|
- Les conversations privées nécessitent une vérification explicite d'appartenance
|
||
|
|
- Le rôle dans `conversation_members` peut différer du rôle global dans `users`
|
||
|
|
|
||
|
|
## Évolution future
|
||
|
|
|
||
|
|
- Support de permissions custom par conversation
|
||
|
|
- Permissions granulaires (edit, delete, pin, etc.)
|
||
|
|
- Système de rôles hiérarchiques
|
||
|
|
- Permissions temporaires (time-based)
|
||
|
|
- Audit trail des changements de permissions
|
||
|
|
|