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",
]
[[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",

View File

@@ -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"

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

View File

@@ -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()

View File

@@ -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<group::Model> for GroupResponse {
pub struct CreateGroupRequest {
pub server_id: Uuid,
pub name: String,
pub permissions: i64,
pub permissions: u64,
}
impl CreateGroupRequest {

View File

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

View File

@@ -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,
}

View File

@@ -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,
}

View File

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

View File

@@ -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)]

View File

@@ -97,23 +97,12 @@ pub async fn category_create(
user: CurrentUser,
Json(payload): Json<CreateCategoryRequest>,
) -> Result<Json<CategoryResponse>, 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);

View File

@@ -79,23 +79,12 @@ pub async fn channel_create(
Json(dto): Json<CreateChannelRequest>,
) -> Result<Json<ChannelResponse>, 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);

View File

@@ -87,23 +87,12 @@ pub async fn group_create(
user: CurrentUser,
Json(payload): Json<CreateGroupRequest>,
) -> Result<Json<GroupResponse>, 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);

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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<Uuid>,
Json(serializer): Json<CreateServerRequest>,
) -> Result<Json<ServerResponse>, 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<Uuid>,
) -> Result<StatusCode, 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);
}

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::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<AppState>
) -> 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<AppState>) -> 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);
}
}

View File

@@ -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<RepositoryContext>
pub context: Arc<RepositoryContext>,
}
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(())
}
}
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::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<bool, DbErr> {
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(
pub async fn get_server_stack(
&self,
user_id: Uuid,
server_id: Uuid,
) -> Result<PermissionContext, DbErr> {
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id ET group.server_id = server_id
let group_masks: Vec<i64> = group::Entity::find()
) -> Result<PermissionStack, DbErr> {
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<u64> = 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<PermissionStack, DbErr> {
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)
}
}

View File

@@ -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<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
///
/// 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<Uuid, u64>,
pub struct PermissionStack {
/// Permissions au niveau du serveur (None = non configuré)
pub server: Option<u64>,
/// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe)
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 {
/// 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<Uuid>) -> 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
}
}