add test and permission check

This commit is contained in:
2026-09-24 21:10:11 +02:00
parent 844eaadee0
commit 910eb2b169
31 changed files with 1527 additions and 217 deletions
+112 -41
View File
@@ -6,36 +6,59 @@ use crate::domain::dto::message::{
use crate::domain::dto::reaction::{CreateReactionRequest, DeleteReactionQuery, ReactionResponse};
use crate::http::context::CurrentUser;
use crate::http::error::HTTPError;
use crate::models::{channel, channel_user};
use crate::http::permissions::check_channel_permission;
use crate::permissions::ChannelPermission;
use crate::routes::message::mapper;
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use uuid::Uuid;
pub(crate) async fn can_access(
pub(crate) async fn require_channel_permission(
state: &AppState,
channel_id: Uuid,
user_id: Uuid,
) -> Result<bool, HTTPError> {
let Some(channel) = channel::Entity::find_by_id(channel_id)
.one(&state.db)
.await?
else {
return Ok(false);
};
if channel.channel_type != channel::ChannelType::DM {
return Ok(true);
is_superuser: bool,
required: ChannelPermission,
) -> Result<(), HTTPError> {
let _ = is_superuser;
if check_channel_permission(state, user_id, channel_id, required).await? {
Ok(())
} else {
Err(HTTPError::Forbidden)
}
}
fn allows_channel_permission(granted: ChannelPermission, required: ChannelPermission) -> bool {
granted.contains(required)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_permissions_require_every_requested_bit() {
let granted = ChannelPermission::READ_CHANNEL | ChannelPermission::SEND_MESSAGE;
assert!(allows_channel_permission(
granted,
ChannelPermission::READ_CHANNEL
));
assert!(!allows_channel_permission(
granted,
ChannelPermission::SEND_MESSAGE | ChannelPermission::ATTACH_FILES
));
assert!(!allows_channel_permission(
granted,
ChannelPermission::EDIT_OTHERS_MESSAGES
));
assert!(!allows_channel_permission(
ChannelPermission::empty(),
ChannelPermission::READ_CHANNEL
));
}
Ok(channel_user::Entity::find()
.filter(channel_user::Column::ChannelId.eq(channel_id))
.filter(channel_user::Column::UserId.eq(user_id))
.one(&state.db)
.await?
.is_some())
}
/// Liste une fenêtre paginée de messages
@@ -64,11 +87,17 @@ pub async fn get_all(
}
let params = mapper::query_params_to_message_filter(filters);
if let Some(channel_id) = params.channel_id {
if !can_access(&state, channel_id, user.id).await? {
return Err(HTTPError::Forbidden);
}
}
let channel_id = params
.channel_id
.ok_or_else(|| HTTPError::BadRequest("channel_id is required".into()))?;
require_channel_permission(
&state,
channel_id,
user.id,
user.is_superuser,
ChannelPermission::READ_CHANNEL,
)
.await?;
let page = state.repositories.message.filter(params).await?;
let message_ids: Vec<_> = page.messages.iter().map(|message| message.id).collect();
let mut reactions = state
@@ -126,9 +155,14 @@ pub async fn get_by_id(
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
if !can_access(&state, message.channel_id, user.id).await? {
return Err(HTTPError::Forbidden);
}
require_channel_permission(
&state,
message.channel_id,
user.id,
user.is_superuser,
ChannelPermission::READ_CHANNEL,
)
.await?;
let reactions = state
.services
@@ -180,9 +214,11 @@ pub async fn create(
.get_by_id(payload.channel_id)
.await?
.ok_or(HTTPError::BadRequest("Channel not found".to_string()))?;
if !can_access(&state, channel.id, user.id).await? {
return Err(HTTPError::Forbidden);
let mut required = ChannelPermission::SEND_MESSAGE;
if !payload.file_ids.is_empty() {
required |= ChannelPermission::ATTACH_FILES;
}
require_channel_permission(&state, channel.id, user.id, user.is_superuser, required).await?;
if payload.content.trim().is_empty() && payload.file_ids.is_empty() {
return Err(HTTPError::BadRequest(
@@ -192,7 +228,7 @@ pub async fn create(
// Optionnel: vérifier reply_to_id
if let Some(reply_id) = payload.reply_to_id {
state
let parent = state
.repositories
.message
.get_by_id(reply_id)
@@ -200,6 +236,11 @@ pub async fn create(
.ok_or(HTTPError::BadRequest(
"Parent message not found".to_string(),
))?;
if parent.channel_id != channel.id {
return Err(HTTPError::BadRequest(
"Parent message belongs to another channel".into(),
));
}
}
let message = state
@@ -274,9 +315,19 @@ pub async fn update(
.ok_or(HTTPError::NotFound)?;
// Vérifier que l'utilisateur est l'auteur
if message.user_id != user.id && !user.is_superuser {
return Err(HTTPError::Forbidden);
}
let required = if message.user_id == user.id {
ChannelPermission::EDIT_OWN_MESSAGE
} else {
ChannelPermission::EDIT_OTHERS_MESSAGES
};
require_channel_permission(
&state,
message.channel_id,
user.id,
user.is_superuser,
required,
)
.await?;
let message = state
.services
@@ -332,9 +383,14 @@ pub async fn add_reaction(
.get_by_id(message_id)
.await?
.ok_or(HTTPError::NotFound)?;
if !can_access(&state, message.channel_id, user.id).await? {
return Err(HTTPError::Forbidden);
}
require_channel_permission(
&state,
message.channel_id,
user.id,
user.is_superuser,
ChannelPermission::READ_CHANNEL | ChannelPermission::ADD_REACTIONS,
)
.await?;
let (reaction, created) = state
.services
.message_reaction
@@ -378,9 +434,14 @@ pub async fn remove_reaction(
.get_by_id(message_id)
.await?
.ok_or(HTTPError::NotFound)?;
if !can_access(&state, message.channel_id, user.id).await? {
return Err(HTTPError::Forbidden);
}
require_channel_permission(
&state,
message.channel_id,
user.id,
user.is_superuser,
ChannelPermission::READ_CHANNEL,
)
.await?;
state
.services
.message_reaction
@@ -420,9 +481,19 @@ pub async fn delete(
.await?
.ok_or(HTTPError::NotFound)?;
if message.user_id != user.id && !user.is_superuser {
return Err(HTTPError::Forbidden);
}
let required = if message.user_id == user.id {
ChannelPermission::DELETE_OWN_MESSAGE
} else {
ChannelPermission::DELETE_OTHERS_MESSAGES
};
require_channel_permission(
&state,
message.channel_id,
user.id,
user.is_superuser,
required,
)
.await?;
let attachment_ids: Vec<_> = state
.repositories