Enhance room access validation and database structure in yourchat2
Updated the room access logic in `handle_init_command` and `handle_join_command` to improve validation against user rights and room ownership. Introduced new fields in the `RoomMeta` structure for room type and friends-only access. Modified database queries to accommodate these changes, ensuring robust access control based on user relationships and room settings.
This commit is contained in:
112
src/commands.rs
112
src/commands.rs
@@ -1,7 +1,7 @@
|
|||||||
use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig};
|
use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig};
|
||||||
use bcrypt::verify as bcrypt_verify;
|
use bcrypt::verify as bcrypt_verify;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -148,6 +148,33 @@ async fn handle_init_command(
|
|||||||
};
|
};
|
||||||
let room_name = normalize_room_name(command.room.as_deref().unwrap_or("lobby"));
|
let room_name = normalize_room_name(command.room.as_deref().unwrap_or("lobby"));
|
||||||
let room_password = command.password.clone().unwrap_or_default();
|
let room_password = command.password.clone().unwrap_or_default();
|
||||||
|
let (resolved_room_name, room_meta) = {
|
||||||
|
let guard = state.read().await;
|
||||||
|
let Some(resolved_room_name) = resolve_room_name(&guard, &room_name) else {
|
||||||
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(room_meta) = guard.room_meta.get(&resolved_room_name).cloned() else {
|
||||||
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
(resolved_room_name, room_meta)
|
||||||
|
};
|
||||||
|
if !room_access_allowed(
|
||||||
|
Some(&room_meta),
|
||||||
|
profile.falukant_user_id,
|
||||||
|
profile.gender_id,
|
||||||
|
profile.age,
|
||||||
|
&profile.right_type_ids,
|
||||||
|
&profile.rights,
|
||||||
|
&room_password,
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let (token, user_name, actual_room_name) = {
|
let (token, user_name, actual_room_name) = {
|
||||||
let mut guard = state.write().await;
|
let mut guard = state.write().await;
|
||||||
@@ -161,20 +188,7 @@ async fn handle_init_command(
|
|||||||
send_error(client_id, Arc::clone(&state), "loggedin").await;
|
send_error(client_id, Arc::clone(&state), "loggedin").await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(resolved_room_name) = resolve_room_name(&guard, &room_name) else {
|
if !guard.room_meta.contains_key(&resolved_room_name) {
|
||||||
drop(guard);
|
|
||||||
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !room_access_allowed(
|
|
||||||
guard.room_meta.get(&resolved_room_name),
|
|
||||||
&profile.display_name,
|
|
||||||
profile.falukant_user_id,
|
|
||||||
profile.gender_id,
|
|
||||||
profile.age,
|
|
||||||
&profile.right_type_ids,
|
|
||||||
&room_password,
|
|
||||||
) {
|
|
||||||
drop(guard);
|
drop(guard);
|
||||||
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
return;
|
return;
|
||||||
@@ -262,41 +276,54 @@ async fn handle_join_command(
|
|||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
command: &Command,
|
command: &Command,
|
||||||
state: Arc<RwLock<ChatState>>,
|
state: Arc<RwLock<ChatState>>,
|
||||||
_config: Arc<ServerConfig>,
|
config: Arc<ServerConfig>,
|
||||||
) {
|
) {
|
||||||
if !state::authorize(client_id, command, Arc::clone(&state)).await {
|
if !state::authorize(client_id, command, Arc::clone(&state)).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let room = normalize_room_name(command.room.as_deref().or(command.name.as_deref()).unwrap_or("lobby"));
|
let room = normalize_room_name(command.room.as_deref().or(command.name.as_deref()).unwrap_or("lobby"));
|
||||||
let password = command.password.clone().unwrap_or_default();
|
let password = command.password.clone().unwrap_or_default();
|
||||||
let (from_room, user_name, actual_room_name) = {
|
let (resolved_room, room_meta, falukant_user_id, gender_id, age, right_type_ids, rights) = {
|
||||||
let mut guard = state.write().await;
|
let guard = state.read().await;
|
||||||
let Some(resolved_room) = resolve_room_name(&guard, &room) else {
|
let Some(resolved_room) = resolve_room_name(&guard, &room) else {
|
||||||
drop(guard);
|
|
||||||
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let (name_for_check, falukant_user_id, gender_id, age, right_type_ids) = {
|
let Some(room_meta) = guard.room_meta.get(&resolved_room).cloned() else {
|
||||||
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
let Some(client) = guard.clients.get(&client_id) else {
|
let Some(client) = guard.clients.get(&client_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
client.user_name.clone(),
|
resolved_room,
|
||||||
|
room_meta,
|
||||||
client.falukant_user_id,
|
client.falukant_user_id,
|
||||||
client.gender_id,
|
client.gender_id,
|
||||||
client.age,
|
client.age,
|
||||||
client.right_type_ids.clone(),
|
client.right_type_ids.clone(),
|
||||||
|
client.rights.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if !room_access_allowed(
|
if !room_access_allowed(
|
||||||
guard.room_meta.get(&resolved_room),
|
Some(&room_meta),
|
||||||
&name_for_check,
|
|
||||||
falukant_user_id,
|
falukant_user_id,
|
||||||
gender_id,
|
gender_id,
|
||||||
age,
|
age,
|
||||||
&right_type_ids,
|
&right_type_ids,
|
||||||
|
&rights,
|
||||||
&password,
|
&password,
|
||||||
) {
|
&config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (from_room, user_name, actual_room_name) = {
|
||||||
|
let mut guard = state.write().await;
|
||||||
|
if !guard.room_meta.contains_key(&resolved_room) {
|
||||||
drop(guard);
|
drop(guard);
|
||||||
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||||
return;
|
return;
|
||||||
@@ -779,18 +806,25 @@ async fn handle_unknown_command(client_id: ClientId, command: &Command, state: A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn room_access_allowed(
|
async fn room_access_allowed(
|
||||||
room_meta: Option<&RoomMeta>,
|
room_meta: Option<&RoomMeta>,
|
||||||
_user_name: &str,
|
|
||||||
falukant_user_id: Option<i32>,
|
falukant_user_id: Option<i32>,
|
||||||
user_gender_id: Option<i32>,
|
user_gender_id: Option<i32>,
|
||||||
age: Option<i32>,
|
age: Option<i32>,
|
||||||
user_right_type_ids: &std::collections::HashSet<i32>,
|
user_right_type_ids: &HashSet<i32>,
|
||||||
|
user_rights: &HashSet<String>,
|
||||||
provided_password: &str,
|
provided_password: &str,
|
||||||
|
config: &ServerConfig,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let Some(room) = room_meta else {
|
let Some(room) = room_meta else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let is_admin = user_rights.contains("admin");
|
||||||
|
let has_required_right = room
|
||||||
|
.required_user_right_id
|
||||||
|
.filter(|v| *v > 0)
|
||||||
|
.map(|v| user_right_type_ids.contains(&v))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if let Some(required_gender) = room.gender_restriction_id {
|
if let Some(required_gender) = room.gender_restriction_id {
|
||||||
if required_gender > 0 && user_gender_id != Some(required_gender) {
|
if required_gender > 0 && user_gender_id != Some(required_gender) {
|
||||||
@@ -798,6 +832,19 @@ fn room_access_allowed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !room.is_public {
|
||||||
|
let Some(owner_id) = room.owner_id else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(fid) = falukant_user_id else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let is_owner = db::user_is_room_owner(config, owner_id, fid).await;
|
||||||
|
if !is_owner && !is_admin && !has_required_right {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(room_password) = &room.password {
|
if let Some(room_password) = &room.password {
|
||||||
if !room_password.is_empty() && !password_matches(room_password, provided_password) {
|
if !room_password.is_empty() && !password_matches(room_password, provided_password) {
|
||||||
return false;
|
return false;
|
||||||
@@ -835,6 +882,17 @@ fn room_access_allowed(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if room.friends_of_owner_only {
|
||||||
|
let Some(owner_id) = room.owner_id else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(fid) = falukant_user_id else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !db::is_friend_of_room_owner(config, owner_id, fid).await && !is_admin {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
116
src/db.rs
116
src/db.rs
@@ -161,6 +161,18 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
|||||||
let has_required_user_right = column_exists(client.clone(), "chat", "room", "required_user_right_id")
|
let has_required_user_right = column_exists(client.clone(), "chat", "room", "required_user_right_id")
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let has_friends_of_owner_only = column_exists(client.clone(), "chat", "room", "friends_of_owner_only")
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_room_type = column_exists(client.clone(), "chat", "room", "room_type_id")
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_password_plain = column_exists(client.clone(), "chat", "room", "password")
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_password_hash = column_exists(client.clone(), "chat", "room", "password_hash")
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
let gender_column = if has_gender_restriction {
|
let gender_column = if has_gender_restriction {
|
||||||
"r.gender_restriction_id"
|
"r.gender_restriction_id"
|
||||||
} else {
|
} else {
|
||||||
@@ -171,8 +183,26 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
|||||||
} else {
|
} else {
|
||||||
"NULL::int"
|
"NULL::int"
|
||||||
};
|
};
|
||||||
|
let friends_only_column = if has_friends_of_owner_only {
|
||||||
|
"r.friends_of_owner_only"
|
||||||
|
} else {
|
||||||
|
"false"
|
||||||
|
};
|
||||||
|
let room_type_column = if has_room_type {
|
||||||
|
"r.room_type_id"
|
||||||
|
} else {
|
||||||
|
"NULL::int"
|
||||||
|
};
|
||||||
|
let room_password_column = match (has_password_hash, has_password_plain) {
|
||||||
|
(true, true) => "COALESCE(NULLIF(r.password_hash, ''), NULLIF(r.\"password\", ''))",
|
||||||
|
(true, false) => "NULLIF(r.password_hash, '')",
|
||||||
|
(false, true) => "NULLIF(r.\"password\", '')",
|
||||||
|
(false, false) => "NULL::text",
|
||||||
|
};
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT r.title, r.password_hash, r.is_public, r.owner_id, r.min_age, r.max_age, {gender_column} AS gender_restriction_id, {required_right_column} AS required_user_right_id
|
"SELECT r.title, {room_password_column} AS room_password, r.is_public, r.owner_id, r.min_age, r.max_age,
|
||||||
|
{gender_column} AS gender_restriction_id, {required_right_column} AS required_user_right_id,
|
||||||
|
{room_type_column} AS room_type_id, {friends_only_column} AS friends_of_owner_only
|
||||||
FROM chat.room r
|
FROM chat.room r
|
||||||
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id"
|
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id"
|
||||||
);
|
);
|
||||||
@@ -182,13 +212,15 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
|||||||
for row in rows {
|
for row in rows {
|
||||||
rooms.push(RoomMeta {
|
rooms.push(RoomMeta {
|
||||||
name: row.get::<_, String>("title"),
|
name: row.get::<_, String>("title"),
|
||||||
password: row.get::<_, Option<String>>("password_hash"),
|
password: row.get::<_, Option<String>>("room_password"),
|
||||||
gender_restriction_id: row.get::<_, Option<i32>>("gender_restriction_id"),
|
gender_restriction_id: row.get::<_, Option<i32>>("gender_restriction_id"),
|
||||||
required_user_right_id: row.get::<_, Option<i32>>("required_user_right_id"),
|
required_user_right_id: row.get::<_, Option<i32>>("required_user_right_id"),
|
||||||
min_age: row.get::<_, Option<i32>>("min_age"),
|
min_age: row.get::<_, Option<i32>>("min_age"),
|
||||||
max_age: row.get::<_, Option<i32>>("max_age"),
|
max_age: row.get::<_, Option<i32>>("max_age"),
|
||||||
is_public: row.get::<_, bool>("is_public"),
|
is_public: row.get::<_, bool>("is_public"),
|
||||||
owner_id: row.get::<_, Option<i32>>("owner_id"),
|
owner_id: row.get::<_, Option<i32>>("owner_id"),
|
||||||
|
room_type_id: row.get::<_, Option<i32>>("room_type_id"),
|
||||||
|
friends_of_owner_only: row.get::<_, bool>("friends_of_owner_only"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,3 +415,83 @@ async fn load_community_rights(client: Arc<PgClient>, falukant_user_id: i32) ->
|
|||||||
}
|
}
|
||||||
Ok((rights, right_ids))
|
Ok((rights, right_ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn user_is_room_owner(config: &ServerConfig, room_owner_id: i32, falukant_user_id: i32) -> bool {
|
||||||
|
if room_owner_id <= 0 || falukant_user_id <= 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if room_owner_id == falukant_user_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(client) = config.db_client.clone() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT falukant_user_id FROM chat.\"user\" WHERE id = $1 LIMIT 1",
|
||||||
|
&[&room_owner_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(row)) => row.get::<_, Option<i32>>("falukant_user_id") == Some(falukant_user_id),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_friend_of_room_owner(config: &ServerConfig, room_owner_id: i32, falukant_user_id: i32) -> bool {
|
||||||
|
if room_owner_id <= 0 || falukant_user_id <= 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if user_is_room_owner(config, room_owner_id, falukant_user_id).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(client) = config.db_client.clone() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let owner_falukant_id = match resolve_owner_falukant_id(client.clone(), room_owner_id).await {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if owner_falukant_id == falukant_user_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for query in [
|
||||||
|
"SELECT 1 FROM community.user_friend uf WHERE ((uf.user_id = $1 AND uf.friend_user_id = $2) OR (uf.user_id = $2 AND uf.friend_user_id = $1)) LIMIT 1",
|
||||||
|
"SELECT 1 FROM community.friend f WHERE ((f.user_id = $1 AND f.friend_user_id = $2) OR (f.user_id = $2 AND f.friend_user_id = $1)) LIMIT 1",
|
||||||
|
"SELECT 1
|
||||||
|
FROM community.friendship f
|
||||||
|
WHERE ((f.user1_id = $1 AND f.user2_id = $2) OR (f.user1_id = $2 AND f.user2_id = $1))
|
||||||
|
AND COALESCE(f.accepted, false) = true
|
||||||
|
AND COALESCE(f.denied, false) = false
|
||||||
|
AND COALESCE(f.withdrawn, false) = false
|
||||||
|
LIMIT 1",
|
||||||
|
"SELECT 1 FROM community.contact c WHERE ((c.user_id = $1 AND c.contact_user_id = $2) OR (c.user_id = $2 AND c.contact_user_id = $1)) LIMIT 1",
|
||||||
|
] {
|
||||||
|
match client.query_opt(query, &[&falukant_user_id, &owner_falukant_id]).await {
|
||||||
|
Ok(Some(_)) => return true,
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_owner_falukant_id(client: Arc<PgClient>, room_owner_id: i32) -> Option<i32> {
|
||||||
|
if room_owner_id <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Ok(Some(_)) = client
|
||||||
|
.query_opt("SELECT 1 FROM community.\"user\" WHERE id = $1 LIMIT 1", &[&room_owner_id])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Some(room_owner_id);
|
||||||
|
}
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT falukant_user_id FROM chat.\"user\" WHERE id = $1 LIMIT 1",
|
||||||
|
&[&room_owner_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()??;
|
||||||
|
row.get::<_, Option<i32>>("falukant_user_id")
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,4 +93,6 @@ pub(crate) struct RoomMeta {
|
|||||||
pub(crate) max_age: Option<i32>,
|
pub(crate) max_age: Option<i32>,
|
||||||
pub(crate) is_public: bool,
|
pub(crate) is_public: bool,
|
||||||
pub(crate) owner_id: Option<i32>,
|
pub(crate) owner_id: Option<i32>,
|
||||||
|
pub(crate) room_type_id: Option<i32>,
|
||||||
|
pub(crate) friends_of_owner_only: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user