From 3eaf31d64f6d7eddca0af94935aeec0b71a7de57 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 4 Mar 2026 22:35:16 +0100 Subject: [PATCH] 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. --- src/commands.rs | 142 ++++++++++++++++++++++++++++++++++-------------- src/db.rs | 116 ++++++++++++++++++++++++++++++++++++++- src/types.rs | 2 + 3 files changed, 216 insertions(+), 44 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index c251049..a517603 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,7 +1,7 @@ use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig}; use bcrypt::verify as bcrypt_verify; use serde_json::{json, Value}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; 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_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 mut guard = state.write().await; @@ -161,20 +188,7 @@ async fn handle_init_command( send_error(client_id, Arc::clone(&state), "loggedin").await; return; } - let Some(resolved_room_name) = resolve_room_name(&guard, &room_name) else { - 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, - ) { + 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; @@ -262,41 +276,54 @@ async fn handle_join_command( client_id: ClientId, command: &Command, state: Arc>, - _config: Arc, + config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } 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 (from_room, user_name, actual_room_name) = { - let mut guard = state.write().await; + let (resolved_room, room_meta, falukant_user_id, gender_id, age, right_type_ids, rights) = { + let guard = state.read().await; 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; return; }; - let (name_for_check, falukant_user_id, gender_id, age, right_type_ids) = { - let Some(client) = guard.clients.get(&client_id) else { - return; - }; - ( - client.user_name.clone(), - client.falukant_user_id, - client.gender_id, - client.age, - client.right_type_ids.clone(), - ) + 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; }; - if !room_access_allowed( - guard.room_meta.get(&resolved_room), - &name_for_check, - falukant_user_id, - gender_id, - age, - &right_type_ids, - &password, - ) { + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + ( + resolved_room, + room_meta, + client.falukant_user_id, + client.gender_id, + client.age, + client.right_type_ids.clone(), + client.rights.clone(), + ) + }; + if !room_access_allowed( + Some(&room_meta), + falukant_user_id, + gender_id, + age, + &right_type_ids, + &rights, + &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); send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; 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>, - _user_name: &str, falukant_user_id: Option, user_gender_id: Option, age: Option, - user_right_type_ids: &std::collections::HashSet, + user_right_type_ids: &HashSet, + user_rights: &HashSet, provided_password: &str, + config: &ServerConfig, ) -> bool { let Some(room) = room_meta else { 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 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 !room_password.is_empty() && !password_matches(room_password, provided_password) { return false; @@ -835,6 +882,17 @@ fn room_access_allowed( 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 } diff --git a/src/db.rs b/src/db.rs index 780a962..c51ddb4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -161,6 +161,18 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result, & let has_required_user_right = column_exists(client.clone(), "chat", "room", "required_user_right_id") .await .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 { "r.gender_restriction_id" } else { @@ -171,8 +183,26 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result, & } else { "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!( - "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 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, & for row in rows { rooms.push(RoomMeta { name: row.get::<_, String>("title"), - password: row.get::<_, Option>("password_hash"), + password: row.get::<_, Option>("room_password"), gender_restriction_id: row.get::<_, Option>("gender_restriction_id"), required_user_right_id: row.get::<_, Option>("required_user_right_id"), min_age: row.get::<_, Option>("min_age"), max_age: row.get::<_, Option>("max_age"), is_public: row.get::<_, bool>("is_public"), owner_id: row.get::<_, Option>("owner_id"), + room_type_id: row.get::<_, Option>("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, falukant_user_id: i32) -> } 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>("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, room_owner_id: i32) -> Option { + 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>("falukant_user_id") +} diff --git a/src/types.rs b/src/types.rs index c584c3a..f62fb1e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -93,4 +93,6 @@ pub(crate) struct RoomMeta { pub(crate) max_age: Option, pub(crate) is_public: bool, pub(crate) owner_id: Option, + pub(crate) room_type_id: Option, + pub(crate) friends_of_owner_only: bool, }