This commit is contained in:
2026-03-16 00:56:28 +01:00
parent 50e1d4c25f
commit 628582a48b
21 changed files with 418 additions and 353 deletions

26
Cargo.lock generated
View File

@@ -1580,6 +1580,12 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -3731,6 +3737,19 @@ dependencies = [
"tungstenite", "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]] [[package]]
name = "toml" name = "toml"
version = "0.9.8" version = "0.9.8"
@@ -3806,12 +3825,19 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",

View File

@@ -44,7 +44,7 @@ utoipa-scalar = { version = "0.3", features = ["axum"] }
utoipa-swagger-ui = { version = "9.0", features = ["axum"] } utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
utoipa-axum = "0.2" utoipa-axum = "0.2"
tower = { version = "0.5", features = ["util"] } 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 # UDP
socket2 = "0.6" socket2 = "0.6"

View File

@@ -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 ## TODO
Terminer le système d'authentification (login, changement de mot de passe...) Terminer le système d'authentification (login, changement de mot de passe...)

View File

@@ -27,6 +27,12 @@ impl MigrationTrait for Migration {
.not_null() .not_null()
.default(Expr::current_timestamp()), .default(Expr::current_timestamp()),
) )
.col(
ColumnDef::new(Alias::new("default_permissions"))
.big_integer()
.not_null()
.default(0),
)
.to_owned(), .to_owned(),
) )
.await?; .await?;
@@ -114,6 +120,11 @@ impl MigrationTrait for Migration {
.not_null() .not_null()
.default(Expr::current_timestamp()), .default(Expr::current_timestamp()),
) )
.col(
ColumnDef::new(Alias::new("default_permissions"))
.big_integer()
.null(),
)
// Indexes créés après // Indexes créés après
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()
@@ -299,6 +310,18 @@ impl MigrationTrait for Migration {
.not_null() .not_null()
.default(false), .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 // Indexes créés après
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()

View File

@@ -9,7 +9,7 @@ pub struct GroupResponse {
pub id: Uuid, pub id: Uuid,
pub server_id: Uuid, pub server_id: Uuid,
pub name: String, pub name: String,
pub permissions: i64, pub permissions: u64,
pub is_default: bool, pub is_default: bool,
pub created_at: String, pub created_at: String,
} }
@@ -31,7 +31,7 @@ impl From<group::Model> for GroupResponse {
pub struct CreateGroupRequest { pub struct CreateGroupRequest {
pub server_id: Uuid, pub server_id: Uuid,
pub name: String, pub name: String,
pub permissions: i64, pub permissions: u64,
} }
impl CreateGroupRequest { impl CreateGroupRequest {

View File

@@ -32,6 +32,7 @@ pub struct Model {
pub name: Option<String>, pub name: Option<String>,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc, pub updated_at: DateTimeUtc,
pub default_permissions: Option<u64>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -12,7 +12,7 @@ pub struct Model {
pub channel_id: Uuid, pub channel_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub role: String, pub role: String,
pub permissions: i64, pub permissions: u64,
pub joined_at: DateTimeUtc, pub joined_at: DateTimeUtc,
} }

View File

@@ -11,7 +11,7 @@ pub struct Model {
pub id: Uuid, pub id: Uuid,
pub server_id: Uuid, pub server_id: Uuid,
pub name: String, pub name: String,
pub permissions: i64, pub permissions: u64,
pub is_default: bool, pub is_default: bool,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
} }

View File

@@ -13,6 +13,7 @@ pub struct Model {
pub password: Option<String>, pub password: Option<String>,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc, pub updated_at: DateTimeUtc,
pub default_permissions: u64,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -15,6 +15,8 @@ pub struct Model {
pub joined_at: DateTimeUtc, pub joined_at: DateTimeUtc,
pub updated_at: DateTimeUtc, pub updated_at: DateTimeUtc,
pub is_admin: bool, pub is_admin: bool,
pub is_owner: bool,
pub permissions: u64,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -97,23 +97,12 @@ pub async fn category_create(
user: CurrentUser, user: CurrentUser,
Json(payload): Json<CreateCategoryRequest>, Json(payload): Json<CreateCategoryRequest>,
) -> Result<Json<CategoryResponse>, HTTPError> { ) -> Result<Json<CategoryResponse>, HTTPError> {
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, payload.server_id) .has_perm(user.id, payload.server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -149,23 +138,12 @@ pub async fn category_update(
.await? .await?
.ok_or_else(|| HTTPError::NotFound)?; .ok_or_else(|| HTTPError::NotFound)?;
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, category.server_id) .has_perm(user.id, category.server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -203,23 +181,12 @@ pub async fn category_delete(
.await? .await?
.ok_or_else(|| HTTPError::NotFound)?; .ok_or_else(|| HTTPError::NotFound)?;
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, category.server_id) .has_perm(user.id, category.server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);

View File

@@ -79,23 +79,12 @@ pub async fn channel_create(
Json(dto): Json<CreateChannelRequest>, Json(dto): Json<CreateChannelRequest>,
) -> Result<Json<ChannelResponse>, HTTPError> { ) -> Result<Json<ChannelResponse>, HTTPError> {
if let Some(server_id) = dto.server_id { if let Some(server_id) = dto.server_id {
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, server_id) .has_perm(user.id, server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -133,23 +122,17 @@ pub async fn channel_update(
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
if let Some(server_id) = channel.server_id { if let Some(server_id) = channel.server_id {
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .channel
.load_for_server_user(user.id, server_id) .has_perm(user.id, id, Permission::ManageChannel)
.await?
|| app_state
.repositories
.server
.has_perm(user.id, server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -189,23 +172,17 @@ pub async fn channel_delete(
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
if let Some(server_id) = channel.server_id { if let Some(server_id) = channel.server_id {
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.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
.repositories .repositories
.permission .channel
.load_for_server_user(user.id, server_id) .has_perm(user.id, id, Permission::ManageChannel)
.await?
|| app_state
.repositories
.server
.has_perm(user.id, server_id, Permission::ManageServer)
.await?; .await?;
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);

View File

@@ -87,23 +87,12 @@ pub async fn group_create(
user: CurrentUser, user: CurrentUser,
Json(payload): Json<CreateGroupRequest>, Json(payload): Json<CreateGroupRequest>,
) -> Result<Json<GroupResponse>, HTTPError> { ) -> Result<Json<GroupResponse>, HTTPError> {
let is_admin = state let has_permission = user.is_superuser
.repositories || state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, payload.server_id) .has_perm(user.id, payload.server_id, Permission::ManageRoles)
.await?; .await?;
ctx.has(Permission::ManageRoles, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -141,23 +130,12 @@ pub async fn group_update(
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
let is_admin = state let has_permission = user.is_superuser
.repositories || state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, group.server_id) .has_perm(user.id, group.server_id, Permission::ManageRoles)
.await?; .await?;
ctx.has(Permission::ManageRoles, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
@@ -197,23 +175,12 @@ pub async fn group_delete(
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
let is_admin = state let has_permission = user.is_superuser
.repositories || state
.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
.repositories .repositories
.permission .server
.load_for_server_user(user.id, group.server_id) .has_perm(user.id, group.server_id, Permission::ManageRoles)
.await?; .await?;
ctx.has(Permission::ManageRoles, None)
};
if !has_permission { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);

View File

@@ -88,22 +88,15 @@ pub async fn message_create(
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
if let Some(server_id) = channel.server_id { if let Some(server_id) = channel.server_id {
let is_admin = app_state let has_permission = user.is_superuser
.repositories || app_state
.permission
.is_server_admin(user.id, server_id)
.await?;
let is_superuser = user.is_superuser;
if !is_admin && !is_superuser {
let ctx = app_state
.repositories .repositories
.permission .channel
.load_for_server_user(user.id, server_id) .has_perm(user.id, channel.id, Permission::SendMessage)
.await?; .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? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
if let Some(server_id) = channel.server_id { let has_permission = user.is_superuser
let is_admin = app_state || app_state
.repositories .repositories
.permission .channel
.is_server_admin(user.id, server_id) .has_perm(user.id, channel.id, Permission::DeleteOthersMessage)
.await?; .await?;
let is_superuser = user.is_superuser;
if !is_admin && !is_superuser { if !has_permission {
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 {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
} }
} }

View File

@@ -1,8 +1,9 @@
use crate::interfaces::http::dto::{ 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, 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::api::{auth, category, channel, message, server, user};
use crate::network::http::web::ws_handler;
use utoipa::OpenApi; use utoipa::OpenApi;
#[derive(OpenApi)] #[derive(OpenApi)]
@@ -36,6 +37,7 @@ use utoipa::OpenApi;
user::get_me, user::get_me,
user::user_list, user::user_list,
user::user_detail, user::user_detail,
ws_handler::ws_doc,
), ),
components( components(
schemas( schemas(

View File

@@ -5,10 +5,11 @@ use crate::interfaces::http::dto::server::{
use crate::models::server; use crate::models::server;
use crate::network::http::context::CurrentUser; use crate::network::http::context::CurrentUser;
use crate::network::http::{AppRouter, HTTPError}; use crate::network::http::{AppRouter, HTTPError};
use crate::utils::permissions::Permission;
use axum::Json;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::get; use axum::routing::get;
use axum::Json;
use sea_orm::IntoActiveModel; use sea_orm::IntoActiveModel;
use uuid::Uuid; use uuid::Uuid;
@@ -107,14 +108,14 @@ pub async fn server_update(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(serializer): Json<CreateServerRequest>, Json(serializer): Json<CreateServerRequest>,
) -> Result<Json<ServerResponse>, HTTPError> { ) -> Result<Json<ServerResponse>, HTTPError> {
let is_admin = state let has_permission = user.is_superuser
.repositories || state
.permission .repositories
.is_server_admin(user.id, id) .server
.await?; .has_perm(user.id, id, Permission::ManageServer)
let is_superuser = user.is_superuser; .await?;
if !is_admin && !is_superuser { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
} }
@@ -152,14 +153,14 @@ pub async fn server_delete(
user: CurrentUser, user: CurrentUser,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let is_admin = state let has_permission = user.is_superuser
.repositories || state
.permission .repositories
.is_server_admin(user.id, id) .server
.await?; .has_perm(user.id, id, Permission::ManageServer)
let is_superuser = user.is_superuser; .await?;
if !is_admin && !is_superuser { if !has_permission {
return Err(HTTPError::Forbidden); return Err(HTTPError::Forbidden);
} }

View File

@@ -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::app::AppState;
use crate::hub::Client; use crate::hub::Client;
use crate::network::http::AppRouter; 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 { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new().route("/ws/", get(ws_handler))
.route("/ws/", get(ws_handler))
} }
async fn ws_handler( /// WebSocket documentation placeholder.
ws: WebSocketUpgrade, ///
State(app_state): State<AppState> /// This is not a real HTTP endpoint, but it documents the WebSocket behavior.
) -> impl IntoResponse { ///
/// **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<AppState>) -> impl IntoResponse {
// todo --- 1. VÉRIFICATION AVANT UPGRADE --- // todo --- 1. VÉRIFICATION AVANT UPGRADE ---
// C'est ici qu'on vérifierait le JWT par exemple. // C'est ici qu'on vérifierait le JWT par exemple.
// Si ça échoue, on peut retourner une erreur HTTP direct. // 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; 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 { let mut send_task = tokio::spawn(async move {
while let Some(msg) = rx.recv().await { while let Some(msg) = rx.recv().await {
if ws_sender.send(msg).await.is_err() { if ws_sender.send(msg).await.is_err() {

View File

@@ -1,11 +1,13 @@
use std::sync::Arc;
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
use crate::models::channel; use crate::models::channel;
use crate::repositories::permission::PermissionRepository;
use crate::repositories::RepositoryContext; use crate::repositories::RepositoryContext;
use crate::utils::permissions::Permission;
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait};
use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct ChannelRepository { pub struct ChannelRepository {
pub context: Arc<RepositoryContext> pub context: Arc<RepositoryContext>,
} }
impl ChannelRepository { impl ChannelRepository {
@@ -26,8 +28,23 @@ impl ChannelRepository {
} }
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> { 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); self.context.events.emit("channel_deleted", id);
Ok(()) Ok(())
} }
pub async fn has_perm(
&self,
user_id: uuid::Uuid,
channel_id: uuid::Uuid,
permission: Permission,
) -> Result<bool, DbErr> {
let repo = PermissionRepository {
context: self.context.clone(),
};
let stack = repo.get_channel_stack(user_id, channel_id).await?;
Ok(stack.has(permission))
}
} }

View File

@@ -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::repositories::RepositoryContext;
use crate::utils::permissions::PermissionContext; use crate::utils::permissions::PermissionStack;
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect}; use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -11,72 +11,100 @@ pub struct PermissionRepository {
} }
impl PermissionRepository { impl PermissionRepository {
pub async fn is_server_admin(&self, user_id: Uuid, server_id: Uuid) -> Result<bool, DbErr> { pub async fn get_server_stack(
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<PermissionContext, DbErr> {
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id
// collecter les group.permissions: i64 dans un Vec<i64>
let group_masks: Vec<i64> = 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(
&self, &self,
user_id: Uuid, user_id: Uuid,
server_id: Uuid, server_id: Uuid,
) -> Result<PermissionContext, DbErr> { ) -> Result<PermissionStack, DbErr> {
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id ET group.server_id = server_id let server = server::Entity::find_by_id(server_id)
let group_masks: Vec<i64> = group::Entity::find() .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<u64> = group::Entity::find()
.inner_join(group_member::Entity) .inner_join(group_member::Entity)
.filter(group_member::Column::UserId.eq(user_id))
.filter(group::Column::ServerId.eq(server_id)) .filter(group::Column::ServerId.eq(server_id))
.filter(group_member::Column::UserId.eq(user_id))
.select_only() .select_only()
.column(group::Column::Permissions) .column(group::Column::Permissions)
.into_tuple() .into_tuple()
.all(&self.context.db) .all(&self.context.db)
.await?; .await?;
// Requête 2 : SELECT channel_id, permissions FROM channel_user if !group_permissions.is_empty() {
// JOIN channel ON channel_user.channel_id = channel.id let groups_mask = group_permissions.iter().fold(0u64, |acc, &p| acc | p);
// WHERE user_id = user_id AND channel.server_id = server_id stack.groups = Some(groups_mask);
let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find() }
.inner_join(crate::models::channel::Entity)
.filter(channel_user::Column::UserId.eq(user_id)) Ok(stack)
.filter(crate::models::channel::Column::ServerId.eq(server_id)) }
.select_only()
.column(channel_user::Column::ChannelId) pub async fn get_channel_stack(
.column(channel_user::Column::Permissions) &self,
.into_tuple() user_id: Uuid,
.all(&self.context.db) channel_id: Uuid,
) -> Result<PermissionStack, DbErr> {
let channel = channel::Entity::find_by_id(channel_id)
.one(&self.context.db)
.await?; .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)
} }
} }

View File

@@ -1,8 +1,12 @@
use super::types::{ServerExplorerItem, ServerTree}; use super::types::{ServerExplorerItem, ServerTree};
use super::RepositoryContext; use super::RepositoryContext;
use crate::models::{category, channel, group, server}; use crate::models::{category, channel, group, server};
use crate::repositories::permission::PermissionRepository;
use crate::utils::permissions::Permission; 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 std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -33,7 +37,7 @@ impl ServerRepository {
let default_group = group::ActiveModel { let default_group = group::ActiveModel {
server_id: Set(server.id), server_id: Set(server.id),
name: Set("Membres".to_string()), name: Set("Membres".to_string()),
permissions: Set(Permission::default_permissions() as i64), permissions: Set(Permission::default_permissions()),
is_default: Set(true), is_default: Set(true),
..Default::default() ..Default::default()
}; };
@@ -108,4 +112,17 @@ impl ServerRepository {
Ok(ServerTree { items }) Ok(ServerTree { items })
} }
pub async fn has_perm(
&self,
user_id: Uuid,
server_id: Uuid,
permission: Permission,
) -> Result<bool, DbErr> {
let repo = PermissionRepository {
context: self.context.clone(),
};
let stack = repo.get_server_stack(user_id, server_id).await?;
Ok(stack.has(permission))
}
} }

View File

@@ -1,6 +1,3 @@
use std::collections::HashMap;
use uuid::Uuid;
/// # Système de Permissions par Bitmask /// # Système de Permissions par Bitmask
/// ///
/// Un bitmask (masque de bits) est une manière compacte et efficace de stocker plusieurs /// 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)] #[derive(Debug, Clone)]
pub struct PermissionContext { pub struct PermissionStack {
/// Union des masques de tous les groupes auxquels appartient l'utilisateur. /// Permissions au niveau du serveur (None = non configuré)
pub global: u64, pub server: Option<u64>,
/// Masques spécifiques par canal (si présents, ils remplacent totalement le masque global
/// pour ce canal spécifique). /// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe)
pub channel_overrides: HashMap<Uuid, u64>, pub groups: Option<u64>,
/// Permissions de l'utilisateur au niveau serveur (None = non défini)
pub server_user: Option<u64>,
/// Permissions de TOUS les groupes au niveau serveur (None = non défini)
pub server_groups: Option<u64>,
/// Permissions de l'utilisateur sur ce canal (None = non défini)
pub channel_user: Option<u64>,
/// Permissions de TOUS les groupes sur ce canal (None = non défini)
pub channel_groups: Option<u64>,
} }
impl PermissionContext { impl PermissionStack {
/// Crée un nouveau contexte à partir des masques bruts de la base de données. pub fn new() -> Self {
///
/// - `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();
Self { Self {
global, server: None,
channel_overrides, 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 /// ### Exemple :
/// est utilisée. Sinon, on utilise les permissions globales du serveur.
///
/// ### Exemple d'utilisation :
/// ```rust /// ```rust
/// if ctx.has(Permission::SendMessage, Some(channel_id)) { /// if stack.has(Permission::SendMessage) {
/// // Autorisé ! /// println!("L'utilisateur peut envoyer des messages !");
/// } /// }
/// ``` /// ```
#[inline] pub fn has(&self, permission: Permission) -> bool {
pub fn has(&self, permission: Permission, channel_id: Option<Uuid>) -> bool { let mask = self.resolve();
let mask = if let Some(cid) = channel_id {
self.channel_overrides
.get(&cid)
.copied()
.unwrap_or(self.global)
} else {
self.global
};
mask & (permission as u64) != 0 mask & (permission as u64) != 0
} }
/// Méthode utilitaire pour vérifier rapidement l'accès en lecture à un canal. /// Vérifie si TOUTES les permissions listées sont accordées (Strict).
pub fn can_see_channel(&self, channel_id: Uuid) -> bool { ///
self.has(Permission::ReadChannel, Some(channel_id)) /// 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::*; use super::*;
#[test] #[test]
fn test_permission_context() { fn test_permission_stack() {
let channel_id = Uuid::new_v4(); let mut stack = PermissionStack::new();
let other_channel_id = Uuid::new_v4();
// On imagine un utilisateur avec 2 groupes // 1. Test des permissions par défaut du serveur
let role_masks = vec![ stack.server = Some(Permission::ReadChannel as u64 | Permission::SendMessage as u64);
Permission::ReadChannel as i64 | Permission::SendMessage as i64, // Groupe 1 : lecture + envoi assert!(stack.has(Permission::ReadChannel));
Permission::VoiceSpeak as i64, // Groupe 2 : vocal assert!(stack.has(Permission::SendMessage));
]; assert!(!stack.has(Permission::VoiceSpeak));
// Et une permission spécifique sur un canal (Lecture + Envoi + Gestion) // 2. Test de l'union avec les groupes
let channel_permissions = vec![( stack.groups = Some(Permission::VoiceSpeak as u64);
channel_id, assert!(stack.has(Permission::ReadChannel));
Permission::ReadChannel as i64 assert!(stack.has(Permission::SendMessage));
| Permission::SendMessage as i64 assert!(stack.has(Permission::VoiceSpeak));
| Permission::ManageChannel as i64,
)];
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 // 4. Test spécifique au canal (Sans bypass admin)
assert!(ctx.has(Permission::ReadChannel, None)); stack.server_user = None;
assert!(ctx.has(Permission::SendMessage, None)); stack.channel_user = Some(Permission::ManageChannel as u64);
assert!(ctx.has(Permission::VoiceSpeak, None)); assert!(stack.has(Permission::ReadChannel)); // Vient du serveur
assert!(!ctx.has(Permission::ManageChannel, None)); assert!(stack.has(Permission::ManageChannel)); // Vient du channel_user
assert!(!stack.has(Permission::ManageRoles)); // Pas défini
// 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));
} }
} }