From 628582a48bff869e307cb57819ccfdb130d1ab77 Mon Sep 17 00:00:00 2001 From: Nell Date: Mon, 16 Mar 2026 00:56:28 +0100 Subject: [PATCH] Init --- Cargo.lock | 26 +++ Cargo.toml | 2 +- README.md | 5 + .../src/m20220101_000001_create_table.rs | 23 ++ src/interfaces/http/dto/group.rs | 4 +- src/models/channel.rs | 1 + src/models/channel_user.rs | 2 +- src/models/group.rs | 2 +- src/models/server.rs | 1 + src/models/server_user.rs | 2 + src/network/http/web/api/category.rs | 57 ++--- src/network/http/web/api/channel.rs | 67 ++---- src/network/http/web/api/group.rs | 57 ++--- src/network/http/web/api/message.rs | 42 ++-- src/network/http/web/api/openapi.rs | 4 +- src/network/http/web/api/server.rs | 31 +-- src/network/http/web/ws_handler.rs | 55 +++-- src/repositories/channel.rs | 27 ++- src/repositories/permission.rs | 140 +++++++----- src/repositories/server.rs | 21 +- src/utils/permissions.rs | 202 ++++++++++-------- 21 files changed, 418 insertions(+), 353 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10f73bf..2444f28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -3731,6 +3737,19 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.8" @@ -3806,12 +3825,19 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", + "futures-core", "futures-util", "http", "http-body", "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 648635c..5f02c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ utoipa-scalar = { version = "0.3", features = ["axum"] } utoipa-swagger-ui = { version = "9.0", features = ["axum"] } utoipa-axum = "0.2" tower = { version = "0.5", features = ["util"] } -tower-http = { version = "0.6", features = ["trace", "cors", "timeout", "catch-panic"] } +tower-http = { version = "0.6", features = ["trace", "cors", "timeout", "catch-panic", "fs"] } # UDP socket2 = "0.6" diff --git a/README.md b/README.md index a7773cd..274051e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ aux champs de l'utilisateur (ex: `user.is_superuser`) directement comme s'il s'a --- +## Documentation + +- **API (REST & WebSocket) :** `/swagger-ui` ou `/scalar` + - La documentation du WebSocket est incluse dans l'OpenAPI via une route descriptive `/handler/ws/`. + ## TODO Terminer le système d'authentification (login, changement de mot de passe...) diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs index 6f3f664..11b08df 100644 --- a/migration/src/m20220101_000001_create_table.rs +++ b/migration/src/m20220101_000001_create_table.rs @@ -27,6 +27,12 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::current_timestamp()), ) + .col( + ColumnDef::new(Alias::new("default_permissions")) + .big_integer() + .not_null() + .default(0), + ) .to_owned(), ) .await?; @@ -114,6 +120,11 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::current_timestamp()), ) + .col( + ColumnDef::new(Alias::new("default_permissions")) + .big_integer() + .null(), + ) // Indexes créés après .foreign_key( ForeignKey::create() @@ -299,6 +310,18 @@ impl MigrationTrait for Migration { .not_null() .default(false), ) + .col( + ColumnDef::new(Alias::new("is_owner")) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(Alias::new("permissions")) + .big_integer() + .not_null() + .default(0), + ) // Indexes créés après .foreign_key( ForeignKey::create() diff --git a/src/interfaces/http/dto/group.rs b/src/interfaces/http/dto/group.rs index 5050ba1..1535d99 100644 --- a/src/interfaces/http/dto/group.rs +++ b/src/interfaces/http/dto/group.rs @@ -9,7 +9,7 @@ pub struct GroupResponse { pub id: Uuid, pub server_id: Uuid, pub name: String, - pub permissions: i64, + pub permissions: u64, pub is_default: bool, pub created_at: String, } @@ -31,7 +31,7 @@ impl From for GroupResponse { pub struct CreateGroupRequest { pub server_id: Uuid, pub name: String, - pub permissions: i64, + pub permissions: u64, } impl CreateGroupRequest { diff --git a/src/models/channel.rs b/src/models/channel.rs index 063391c..76f3e15 100644 --- a/src/models/channel.rs +++ b/src/models/channel.rs @@ -32,6 +32,7 @@ pub struct Model { pub name: Option, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, + pub default_permissions: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/models/channel_user.rs b/src/models/channel_user.rs index 27cedb1..0943952 100644 --- a/src/models/channel_user.rs +++ b/src/models/channel_user.rs @@ -12,7 +12,7 @@ pub struct Model { pub channel_id: Uuid, pub user_id: Uuid, pub role: String, - pub permissions: i64, + pub permissions: u64, pub joined_at: DateTimeUtc, } diff --git a/src/models/group.rs b/src/models/group.rs index b324309..821e1ce 100644 --- a/src/models/group.rs +++ b/src/models/group.rs @@ -11,7 +11,7 @@ pub struct Model { pub id: Uuid, pub server_id: Uuid, pub name: String, - pub permissions: i64, + pub permissions: u64, pub is_default: bool, pub created_at: DateTimeUtc, } diff --git a/src/models/server.rs b/src/models/server.rs index 4a97c30..3ad11cd 100644 --- a/src/models/server.rs +++ b/src/models/server.rs @@ -13,6 +13,7 @@ pub struct Model { pub password: Option, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, + pub default_permissions: u64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/models/server_user.rs b/src/models/server_user.rs index c2f5209..713c97c 100644 --- a/src/models/server_user.rs +++ b/src/models/server_user.rs @@ -15,6 +15,8 @@ pub struct Model { pub joined_at: DateTimeUtc, pub updated_at: DateTimeUtc, pub is_admin: bool, + pub is_owner: bool, + pub permissions: u64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/network/http/web/api/category.rs b/src/network/http/web/api/category.rs index f72b293..9c712f0 100644 --- a/src/network/http/web/api/category.rs +++ b/src/network/http/web/api/category.rs @@ -97,23 +97,12 @@ pub async fn category_create( user: CurrentUser, Json(payload): Json, ) -> Result, HTTPError> { - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, payload.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, payload.server_id) + .server + .has_perm(user.id, payload.server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -149,23 +138,12 @@ pub async fn category_update( .await? .ok_or_else(|| HTTPError::NotFound)?; - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, category.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, category.server_id) + .server + .has_perm(user.id, category.server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -203,23 +181,12 @@ pub async fn category_delete( .await? .ok_or_else(|| HTTPError::NotFound)?; - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, category.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, category.server_id) + .server + .has_perm(user.id, category.server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); diff --git a/src/network/http/web/api/channel.rs b/src/network/http/web/api/channel.rs index 2a619b2..9e9361d 100644 --- a/src/network/http/web/api/channel.rs +++ b/src/network/http/web/api/channel.rs @@ -79,23 +79,12 @@ pub async fn channel_create( Json(dto): Json, ) -> Result, HTTPError> { if let Some(server_id) = dto.server_id { - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, server_id) + .server + .has_perm(user.id, server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -133,23 +122,17 @@ pub async fn channel_update( .ok_or(HTTPError::NotFound)?; if let Some(server_id) = channel.server_id { - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, server_id) + .channel + .has_perm(user.id, id, Permission::ManageChannel) + .await? + || app_state + .repositories + .server + .has_perm(user.id, server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -189,23 +172,17 @@ pub async fn channel_delete( .ok_or(HTTPError::NotFound)?; if let Some(server_id) = channel.server_id { - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, server_id) + .channel + .has_perm(user.id, id, Permission::ManageChannel) + .await? + || app_state + .repositories + .server + .has_perm(user.id, server_id, Permission::ManageServer) .await?; - ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None) - }; if !has_permission { return Err(HTTPError::Forbidden); diff --git a/src/network/http/web/api/group.rs b/src/network/http/web/api/group.rs index 4fdd1a3..1ba38c6 100644 --- a/src/network/http/web/api/group.rs +++ b/src/network/http/web/api/group.rs @@ -87,23 +87,12 @@ pub async fn group_create( user: CurrentUser, Json(payload): Json, ) -> Result, HTTPError> { - let is_admin = state - .repositories - .permission - .is_server_admin(user.id, payload.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = state + let has_permission = user.is_superuser + || state .repositories - .permission - .load_for_server_user(user.id, payload.server_id) + .server + .has_perm(user.id, payload.server_id, Permission::ManageRoles) .await?; - ctx.has(Permission::ManageRoles, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -141,23 +130,12 @@ pub async fn group_update( .await? .ok_or(HTTPError::NotFound)?; - let is_admin = state - .repositories - .permission - .is_server_admin(user.id, group.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = state + let has_permission = user.is_superuser + || state .repositories - .permission - .load_for_server_user(user.id, group.server_id) + .server + .has_perm(user.id, group.server_id, Permission::ManageRoles) .await?; - ctx.has(Permission::ManageRoles, None) - }; if !has_permission { return Err(HTTPError::Forbidden); @@ -197,23 +175,12 @@ pub async fn group_delete( .await? .ok_or(HTTPError::NotFound)?; - let is_admin = state - .repositories - .permission - .is_server_admin(user.id, group.server_id) - .await?; - let is_superuser = user.is_superuser; - - let has_permission = if is_admin || is_superuser { - true - } else { - let ctx = state + let has_permission = user.is_superuser + || state .repositories - .permission - .load_for_server_user(user.id, group.server_id) + .server + .has_perm(user.id, group.server_id, Permission::ManageRoles) .await?; - ctx.has(Permission::ManageRoles, None) - }; if !has_permission { return Err(HTTPError::Forbidden); diff --git a/src/network/http/web/api/message.rs b/src/network/http/web/api/message.rs index 69c02e3..2e30a8e 100644 --- a/src/network/http/web/api/message.rs +++ b/src/network/http/web/api/message.rs @@ -88,22 +88,15 @@ pub async fn message_create( .ok_or(HTTPError::NotFound)?; if let Some(server_id) = channel.server_id { - let is_admin = app_state - .repositories - .permission - .is_server_admin(user.id, server_id) - .await?; - let is_superuser = user.is_superuser; - - if !is_admin && !is_superuser { - let ctx = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .load_for_server_user(user.id, server_id) + .channel + .has_perm(user.id, channel.id, Permission::SendMessage) .await?; - if !ctx.has(Permission::SendMessage, Some(channel.id)) { - return Err(HTTPError::Forbidden); - } + + if !has_permission { + return Err(HTTPError::Forbidden); } } @@ -179,25 +172,14 @@ pub async fn message_delete( .await? .ok_or(HTTPError::NotFound)?; - if let Some(server_id) = channel.server_id { - let is_admin = app_state + let has_permission = user.is_superuser + || app_state .repositories - .permission - .is_server_admin(user.id, server_id) + .channel + .has_perm(user.id, channel.id, Permission::DeleteOthersMessage) .await?; - let is_superuser = user.is_superuser; - if !is_admin && !is_superuser { - let ctx = app_state - .repositories - .permission - .load_for_server_user(user.id, server_id) - .await?; - if !ctx.has(Permission::DeleteOthersMessage, Some(channel.id)) { - return Err(HTTPError::Forbidden); - } - } - } else { + if !has_permission { return Err(HTTPError::Forbidden); } } diff --git a/src/network/http/web/api/openapi.rs b/src/network/http/web/api/openapi.rs index 10e1140..531bafe 100644 --- a/src/network/http/web/api/openapi.rs +++ b/src/network/http/web/api/openapi.rs @@ -1,8 +1,9 @@ use crate::interfaces::http::dto::{ - auth as auth_dto, category as category_dto, channel as channel_dto, message as message_dto, + category as category_dto, channel as channel_dto, message as message_dto, server as server_dto, user as user_dto, }; use crate::network::http::web::api::{auth, category, channel, message, server, user}; +use crate::network::http::web::ws_handler; use utoipa::OpenApi; #[derive(OpenApi)] @@ -36,6 +37,7 @@ use utoipa::OpenApi; user::get_me, user::user_list, user::user_detail, + ws_handler::ws_doc, ), components( schemas( diff --git a/src/network/http/web/api/server.rs b/src/network/http/web/api/server.rs index 0d7ca88..6aa9496 100644 --- a/src/network/http/web/api/server.rs +++ b/src/network/http/web/api/server.rs @@ -5,10 +5,11 @@ use crate::interfaces::http::dto::server::{ use crate::models::server; use crate::network::http::context::CurrentUser; use crate::network::http::{AppRouter, HTTPError}; +use crate::utils::permissions::Permission; +use axum::Json; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::routing::get; -use axum::Json; use sea_orm::IntoActiveModel; use uuid::Uuid; @@ -107,14 +108,14 @@ pub async fn server_update( Path(id): Path, Json(serializer): Json, ) -> Result, HTTPError> { - let is_admin = state - .repositories - .permission - .is_server_admin(user.id, id) - .await?; - let is_superuser = user.is_superuser; + let has_permission = user.is_superuser + || state + .repositories + .server + .has_perm(user.id, id, Permission::ManageServer) + .await?; - if !is_admin && !is_superuser { + if !has_permission { return Err(HTTPError::Forbidden); } @@ -152,14 +153,14 @@ pub async fn server_delete( user: CurrentUser, Path(id): Path, ) -> Result { - let is_admin = state - .repositories - .permission - .is_server_admin(user.id, id) - .await?; - let is_superuser = user.is_superuser; + let has_permission = user.is_superuser + || state + .repositories + .server + .has_perm(user.id, id, Permission::ManageServer) + .await?; - if !is_admin && !is_superuser { + if !has_permission { return Err(HTTPError::Forbidden); } diff --git a/src/network/http/web/ws_handler.rs b/src/network/http/web/ws_handler.rs index 01550ab..bf1fc79 100644 --- a/src/network/http/web/ws_handler.rs +++ b/src/network/http/web/ws_handler.rs @@ -1,26 +1,45 @@ -use std::collections::HashMap; -use std::sync::Arc; -use axum::extract::{State, WebSocketUpgrade}; -use axum::extract::ws::{Message, WebSocket}; -use axum::response::IntoResponse; -use axum::routing::{delete, get, post, put}; -use futures_util::{SinkExt, StreamExt}; -use parking_lot::RwLock; -use tokio::sync::mpsc; -use uuid::Uuid; use crate::app::AppState; use crate::hub::Client; use crate::network::http::AppRouter; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::{State, WebSocketUpgrade}; +use axum::response::IntoResponse; +use axum::routing::get; +use futures_util::{SinkExt, StreamExt}; +use tokio::sync::mpsc; +use uuid::Uuid; pub fn setup_route() -> AppRouter { - AppRouter::new() - .route("/ws/", get(ws_handler)) + AppRouter::new().route("/ws/", get(ws_handler)) } -async fn ws_handler( - ws: WebSocketUpgrade, - State(app_state): State -) -> impl IntoResponse { +/// WebSocket documentation placeholder. +/// +/// This is not a real HTTP endpoint, but it documents the WebSocket behavior. +/// +/// **Connection:** `GET /handler/ws/` +/// +/// **Protocol:** JSON messages over WebSocket. +/// +/// ### Client -> Server +/// - `SendMessage`: Send a JSON object matching `CreateMessageRequest`. +/// +/// ### Server -> Client +/// - `MessageReceived`: Receive a JSON object matching `MessageResponse`. +#[utoipa::path( + get, + path = "/handler/ws/", + responses( + (status = 101, description = "Switching Protocols to WebSocket"), + (status = 401, description = "Unauthorized (Invalid JWT)") + ), + security( + ("jwt" = []) + ) +)] +pub async fn ws_doc() {} + +async fn ws_handler(ws: WebSocketUpgrade, State(app_state): State) -> impl IntoResponse { // todo --- 1. VÉRIFICATION AVANT UPGRADE --- // C'est ici qu'on vérifierait le JWT par exemple. // Si ça échoue, on peut retourner une erreur HTTP direct. @@ -50,7 +69,7 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, app_state: AppState) { client.on_connect().await; - // 3. Tâche d'ENVOI : On écoute la boîte aux lettres et on pousse vers le navigateur + // 3. Tâche d'ENVOI : On écoute la boîte aux lettres et on pousse vers le client let mut send_task = tokio::spawn(async move { while let Some(msg) = rx.recv().await { if ws_sender.send(msg).await.is_err() { @@ -81,4 +100,4 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, app_state: AppState) { client.on_disconnect().await; app_state.clients.remove_client(connection_id); println!("Session terminée pour le client {}", connection_id); -} \ No newline at end of file +} diff --git a/src/repositories/channel.rs b/src/repositories/channel.rs index 08726f5..ee5c2fd 100644 --- a/src/repositories/channel.rs +++ b/src/repositories/channel.rs @@ -1,11 +1,13 @@ -use std::sync::Arc; -use sea_orm::{DbErr, EntityTrait, ActiveModelTrait}; use crate::models::channel; +use crate::repositories::permission::PermissionRepository; use crate::repositories::RepositoryContext; +use crate::utils::permissions::Permission; +use sea_orm::{ActiveModelTrait, DbErr, EntityTrait}; +use std::sync::Arc; #[derive(Clone)] pub struct ChannelRepository { - pub context: Arc + pub context: Arc, } impl ChannelRepository { @@ -26,8 +28,23 @@ impl ChannelRepository { } pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> { - channel::Entity::delete_by_id(id).exec(&self.context.db).await?; + channel::Entity::delete_by_id(id) + .exec(&self.context.db) + .await?; self.context.events.emit("channel_deleted", id); Ok(()) } -} \ No newline at end of file + + pub async fn has_perm( + &self, + user_id: uuid::Uuid, + channel_id: uuid::Uuid, + permission: Permission, + ) -> Result { + let repo = PermissionRepository { + context: self.context.clone(), + }; + let stack = repo.get_channel_stack(user_id, channel_id).await?; + Ok(stack.has(permission)) + } +} diff --git a/src/repositories/permission.rs b/src/repositories/permission.rs index 8f67106..42aeac4 100644 --- a/src/repositories/permission.rs +++ b/src/repositories/permission.rs @@ -1,6 +1,6 @@ -use crate::models::{channel_user, group, group_member, server_user}; +use crate::models::{channel, channel_user, group, group_member, server, server_user}; use crate::repositories::RepositoryContext; -use crate::utils::permissions::PermissionContext; +use crate::utils::permissions::PermissionStack; use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect}; use std::sync::Arc; use uuid::Uuid; @@ -11,72 +11,100 @@ pub struct PermissionRepository { } impl PermissionRepository { - pub async fn is_server_admin(&self, user_id: Uuid, server_id: Uuid) -> Result { - let res = server_user::Entity::find() - .filter(server_user::Column::UserId.eq(user_id)) - .filter(server_user::Column::ServerId.eq(server_id)) - .one(&self.context.db) - .await?; - - Ok(res.map(|su| su.is_admin).unwrap_or(false)) - } - - pub async fn load_for_user(&self, user_id: Uuid) -> Result { - // Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id - // collecter les group.permissions: i64 dans un Vec - let group_masks: Vec = group::Entity::find() - .inner_join(group_member::Entity) - .filter(group_member::Column::UserId.eq(user_id)) - .select_only() - .column(group::Column::Permissions) - .into_tuple() - .all(&self.context.db) - .await?; - - // Requête 2 : SELECT channel_id, permissions FROM channel_user WHERE user_id = user_id - // collecter en Vec<(Uuid, i64)> - let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find() - .filter(channel_user::Column::UserId.eq(user_id)) - .select_only() - .column(channel_user::Column::ChannelId) - .column(channel_user::Column::Permissions) - .into_tuple() - .all(&self.context.db) - .await?; - - Ok(PermissionContext::new(&group_masks, channel_permissions)) - } - - pub async fn load_for_server_user( + pub async fn get_server_stack( &self, user_id: Uuid, server_id: Uuid, - ) -> Result { - // Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id ET group.server_id = server_id - let group_masks: Vec = group::Entity::find() + ) -> Result { + let server = server::Entity::find_by_id(server_id) + .one(&self.context.db) + .await?; + + let server = match server { + Some(s) => s, + None => return Ok(PermissionStack::new()), + }; + + let mut stack = PermissionStack::new(); + stack.server = Some(server.default_permissions); + + let server_user = server_user::Entity::find() + .filter(server_user::Column::ServerId.eq(server_id)) + .filter(server_user::Column::UserId.eq(user_id)) + .one(&self.context.db) + .await?; + + if let Some(su) = server_user { + // Bypass admin/owner dans resolve() ou ici ? + // L'utilisateur a dit "si une seule fois la permission... alors on est bon". + // Mais admin/owner devrait tout donner. + // On peut mettre un flag spécial ou tout mettre à 1 dans le mask. + if su.is_admin || su.is_owner { + // Pour simplifier, on sature le masque si admin/owner + stack.server_user = Some(u64::MAX); + return Ok(stack); + } + stack.server_user = Some(su.permissions); + } + + let group_permissions: Vec = group::Entity::find() .inner_join(group_member::Entity) - .filter(group_member::Column::UserId.eq(user_id)) .filter(group::Column::ServerId.eq(server_id)) + .filter(group_member::Column::UserId.eq(user_id)) .select_only() .column(group::Column::Permissions) .into_tuple() .all(&self.context.db) .await?; - // Requête 2 : SELECT channel_id, permissions FROM channel_user - // JOIN channel ON channel_user.channel_id = channel.id - // WHERE user_id = user_id AND channel.server_id = server_id - let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find() - .inner_join(crate::models::channel::Entity) - .filter(channel_user::Column::UserId.eq(user_id)) - .filter(crate::models::channel::Column::ServerId.eq(server_id)) - .select_only() - .column(channel_user::Column::ChannelId) - .column(channel_user::Column::Permissions) - .into_tuple() - .all(&self.context.db) + if !group_permissions.is_empty() { + let groups_mask = group_permissions.iter().fold(0u64, |acc, &p| acc | p); + stack.groups = Some(groups_mask); + } + + Ok(stack) + } + + pub async fn get_channel_stack( + &self, + user_id: Uuid, + channel_id: Uuid, + ) -> Result { + let channel = channel::Entity::find_by_id(channel_id) + .one(&self.context.db) .await?; - Ok(PermissionContext::new(&group_masks, channel_permissions)) + let channel = match channel { + Some(c) => c, + None => return Ok(PermissionStack::new()), + }; + + let mut stack = if let Some(server_id) = channel.server_id { + self.get_server_stack(user_id, server_id).await? + } else { + PermissionStack::new() + }; + + // Si on est déjà admin (u64::MAX), on s'arrête + if stack.resolve() == u64::MAX { + return Ok(stack); + } + + if let Some(dp) = channel.default_permissions { + stack.channel_user = Some(dp); + } + + let chan_user = channel_user::Entity::find() + .filter(channel_user::Column::ChannelId.eq(channel_id)) + .filter(channel_user::Column::UserId.eq(user_id)) + .one(&self.context.db) + .await?; + + if let Some(cu) = chan_user { + let current = stack.channel_user.unwrap_or(0); + stack.channel_user = Some(current | cu.permissions); + } + + Ok(stack) } } diff --git a/src/repositories/server.rs b/src/repositories/server.rs index c3e465b..7d011a3 100644 --- a/src/repositories/server.rs +++ b/src/repositories/server.rs @@ -1,8 +1,12 @@ use super::types::{ServerExplorerItem, ServerTree}; use super::RepositoryContext; use crate::models::{category, channel, group, server}; +use crate::repositories::permission::PermissionRepository; use crate::utils::permissions::Permission; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set, +}; + use std::sync::Arc; use uuid::Uuid; @@ -33,7 +37,7 @@ impl ServerRepository { let default_group = group::ActiveModel { server_id: Set(server.id), name: Set("Membres".to_string()), - permissions: Set(Permission::default_permissions() as i64), + permissions: Set(Permission::default_permissions()), is_default: Set(true), ..Default::default() }; @@ -108,4 +112,17 @@ impl ServerRepository { Ok(ServerTree { items }) } + + pub async fn has_perm( + &self, + user_id: Uuid, + server_id: Uuid, + permission: Permission, + ) -> Result { + let repo = PermissionRepository { + context: self.context.clone(), + }; + let stack = repo.get_server_stack(user_id, server_id).await?; + Ok(stack.has(permission)) + } } diff --git a/src/utils/permissions.rs b/src/utils/permissions.rs index ae4a3e3..f5a0b49 100644 --- a/src/utils/permissions.rs +++ b/src/utils/permissions.rs @@ -1,6 +1,3 @@ -use std::collections::HashMap; -use uuid::Uuid; - /// # Système de Permissions par Bitmask /// /// Un bitmask (masque de bits) est une manière compacte et efficace de stocker plusieurs @@ -70,67 +67,113 @@ impl Permission { } } -/// Contexte de permissions calculé pour un utilisateur donné. -/// -/// Ce contexte regroupe les permissions globales (issues de la fusion de tous les groupes -/// de l'utilisateur) et les surcharges spécifiques par canal (overrides). #[derive(Debug, Clone)] -pub struct PermissionContext { - /// Union des masques de tous les groupes auxquels appartient l'utilisateur. - pub global: u64, - /// Masques spécifiques par canal (si présents, ils remplacent totalement le masque global - /// pour ce canal spécifique). - pub channel_overrides: HashMap, +pub struct PermissionStack { + /// Permissions au niveau du serveur (None = non configuré) + pub server: Option, + + /// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe) + pub groups: Option, + + /// Permissions de l'utilisateur au niveau serveur (None = non défini) + pub server_user: Option, + + /// Permissions de TOUS les groupes au niveau serveur (None = non défini) + pub server_groups: Option, + + /// Permissions de l'utilisateur sur ce canal (None = non défini) + pub channel_user: Option, + + /// Permissions de TOUS les groupes sur ce canal (None = non défini) + pub channel_groups: Option, } -impl PermissionContext { - /// Crée un nouveau contexte à partir des masques bruts de la base de données. - /// - /// - `group_masks` : Liste des permissions de chaque groupe de l'utilisateur. - /// - `channel_permissions` : Liste de (ID Canal, Masque) pour les permissions spécifiques. - pub fn new(group_masks: &[i64], channel_permissions: Vec<(Uuid, i64)>) -> Self { - // On combine tous les groupes avec l'opérateur OR (|) - let global = group_masks.iter().fold(0u64, |acc, &m| acc | m as u64); - - let channel_overrides = channel_permissions - .into_iter() - .map(|(id, m)| (id, m as u64)) - .collect(); - +impl PermissionStack { + pub fn new() -> Self { Self { - global, - channel_overrides, + server: None, + groups: None, + server_user: None, + server_groups: None, + channel_user: None, + channel_groups: None, } } - /// Vérifie si une permission spécifique est accordée, optionnellement pour un canal précis. + /// Résout les permissions finales en mode Union + /// On accumule TOUTES les sources définies avec OR + pub fn resolve(&self) -> u64 { + let mut result = 0u64; + + if let Some(perms) = self.server { + result |= perms; + } + if let Some(perms) = self.groups { + result |= perms; + } + if let Some(perms) = self.server_user { + result |= perms; + } + if let Some(perms) = self.server_groups { + result |= perms; + } + if let Some(perms) = self.channel_user { + result |= perms; + } + if let Some(perms) = self.channel_groups { + result |= perms; + } + + result + } + + /// Vérifie si une permission spécifique est accordée. /// - /// Si `channel_id` est fourni et qu'une surcharge existe pour ce canal, la surcharge - /// est utilisée. Sinon, on utilise les permissions globales du serveur. - /// - /// ### Exemple d'utilisation : + /// ### Exemple : /// ```rust - /// if ctx.has(Permission::SendMessage, Some(channel_id)) { - /// // Autorisé ! + /// if stack.has(Permission::SendMessage) { + /// println!("L'utilisateur peut envoyer des messages !"); /// } /// ``` - #[inline] - pub fn has(&self, permission: Permission, channel_id: Option) -> bool { - let mask = if let Some(cid) = channel_id { - self.channel_overrides - .get(&cid) - .copied() - .unwrap_or(self.global) - } else { - self.global - }; - + pub fn has(&self, permission: Permission) -> bool { + let mask = self.resolve(); mask & (permission as u64) != 0 } - /// Méthode utilitaire pour vérifier rapidement l'accès en lecture à un canal. - pub fn can_see_channel(&self, channel_id: Uuid) -> bool { - self.has(Permission::ReadChannel, Some(channel_id)) + /// Vérifie si TOUTES les permissions listées sont accordées (Strict). + /// + /// Utile pour des actions nécessitant plusieurs droits combinés. + /// + /// ### Exemple : + /// ```rust + /// // L'utilisateur doit pouvoir voir ET parler pour rejoindre un vocal + /// let perms = [Permission::ReadChannel, Permission::VoiceSpeak]; + /// if stack.has_all(&perms) { + /// join_voice_channel(); + /// } + /// ``` + pub fn has_all(&self, permissions: &[Permission]) -> bool { + let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64); + let mask = self.resolve(); + (mask & required) == required + } + + /// Vérifie si AU MOINS UNE des permissions listées est accordée. + /// + /// Utile pour les rôles de modération ou les accès "ou" (OR). + /// + /// ### Exemple : + /// ```rust + /// // L'utilisateur peut supprimer le message s'il est modérateur OU admin + /// let moderator_perms = [Permission::DeleteOthersMessage, Permission::ManageChannel]; + /// if stack.has_any(&moderator_perms) { + /// delete_message(); + /// } + /// ``` + pub fn has_any(&self, permissions: &[Permission]) -> bool { + let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64); + let mask = self.resolve(); + (mask & required) != 0 } } @@ -139,45 +182,32 @@ mod tests { use super::*; #[test] - fn test_permission_context() { - let channel_id = Uuid::new_v4(); - let other_channel_id = Uuid::new_v4(); + fn test_permission_stack() { + let mut stack = PermissionStack::new(); - // On imagine un utilisateur avec 2 groupes - let role_masks = vec![ - Permission::ReadChannel as i64 | Permission::SendMessage as i64, // Groupe 1 : lecture + envoi - Permission::VoiceSpeak as i64, // Groupe 2 : vocal - ]; + // 1. Test des permissions par défaut du serveur + stack.server = Some(Permission::ReadChannel as u64 | Permission::SendMessage as u64); + assert!(stack.has(Permission::ReadChannel)); + assert!(stack.has(Permission::SendMessage)); + assert!(!stack.has(Permission::VoiceSpeak)); - // Et une permission spécifique sur un canal (Lecture + Envoi + Gestion) - let channel_permissions = vec![( - channel_id, - Permission::ReadChannel as i64 - | Permission::SendMessage as i64 - | Permission::ManageChannel as i64, - )]; + // 2. Test de l'union avec les groupes + stack.groups = Some(Permission::VoiceSpeak as u64); + assert!(stack.has(Permission::ReadChannel)); + assert!(stack.has(Permission::SendMessage)); + assert!(stack.has(Permission::VoiceSpeak)); - let ctx = PermissionContext::new(&role_masks, channel_permissions); + // 3. Test du bypass administrateur (Saturation du masque) + stack.server_user = Some(u64::MAX); + assert!(stack.has(Permission::ManageServer)); + assert!(stack.has(Permission::ManageRoles)); + assert!(stack.has(Permission::KickMember)); - // Test des permissions globales - assert!(ctx.has(Permission::ReadChannel, None)); - assert!(ctx.has(Permission::SendMessage, None)); - assert!(ctx.has(Permission::VoiceSpeak, None)); - assert!(!ctx.has(Permission::ManageChannel, None)); - - // Test des surcharges par canal (L'override remplace le masque global) - assert!(ctx.has(Permission::ReadChannel, Some(channel_id))); - assert!(ctx.has(Permission::SendMessage, Some(channel_id))); - assert!(ctx.has(Permission::ManageChannel, Some(channel_id))); - assert!(!ctx.has(Permission::VoiceSpeak, Some(channel_id))); // Ici on perd le vocal car l'override remplace tout - - // Test d'un autre canal (pas de surcharge, on retombe sur le global) - assert!(ctx.has(Permission::ReadChannel, Some(other_channel_id))); - assert!(ctx.has(Permission::VoiceSpeak, Some(other_channel_id))); - assert!(!ctx.has(Permission::ManageChannel, Some(other_channel_id))); - - // Test des fonctions sucre - assert!(ctx.can_see_channel(channel_id)); - assert!(ctx.can_see_channel(other_channel_id)); + // 4. Test spécifique au canal (Sans bypass admin) + stack.server_user = None; + stack.channel_user = Some(Permission::ManageChannel as u64); + assert!(stack.has(Permission::ReadChannel)); // Vient du serveur + assert!(stack.has(Permission::ManageChannel)); // Vient du channel_user + assert!(!stack.has(Permission::ManageRoles)); // Pas défini } }