From b62d80bd9e9b51b082136943359644e05a4f8275 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 9 Jan 2023 21:15:56 +0000 Subject: [PATCH] Implement basic channel permissioning for kicks, joins, chatting & topic setting --- src/channel.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------- src/channel/permissions.rs | 35 +++++++++++++++++++++++++++++++++++ src/channel/response.rs | 7 ++++++- src/client.rs | 8 ++++++++ src/messages.rs | 4 +++- src/persistence.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- src/persistence/events.rs | 20 +++++++++++++++++++- 7 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 src/channel/permissions.rs diff --git a/src/channel.rs b/src/channel.rs index 9498509..0dfff9f 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,3 +1,4 @@ +pub mod permissions; pub mod response; use std::collections::HashMap; @@ -12,7 +13,12 @@ use irc_proto::{Command, Message}; use tracing::{debug, error, info, instrument, Span}; use crate::{ - channel::response::{ChannelInviteResult, ChannelNamesList, ChannelTopic}, + channel::{ + permissions::Permission, + response::{ + ChannelInviteResult, ChannelJoinRejectionReason, ChannelNamesList, ChannelTopic, + }, + }, client::Client, connection::InitiatedConnection, messages::{ @@ -20,7 +26,7 @@ use crate::{ ChannelMemberList, ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientByNick, ServerDisconnect, UserKickedFromChannel, UserNickChange, }, - persistence::Persistence, + persistence::{events::FetchUserChannelPermissions, Persistence}, server::Server, }; @@ -32,7 +38,7 @@ pub struct ChannelId(pub i64); pub struct Channel { pub name: String, pub server: Addr, - pub clients: HashMap, InitiatedConnection>, + pub clients: HashMap, (Permission, InitiatedConnection)>, pub topic: Option, pub persistence: Addr, pub channel_id: ChannelId, @@ -93,11 +99,17 @@ impl Handler for Channel { fn handle(&mut self, msg: ChannelMessage, _ctx: &mut Self::Context) -> Self::Result { // ensure the user is actually in the channel by their handle, and grab their // nick & host if they are - let Some(sender) = self.clients.get(&msg.client) else { + let Some((permissions, sender)) = self.clients.get(&msg.client) else { error!("Received message from user not in channel"); return; }; + if !permissions.can_chatter() { + // TODO + error!("User cannot send message to channel"); + return; + } + // build the nick prefix for the message we're about to broadcast let nick = sender.to_nick(); @@ -106,7 +118,7 @@ impl Handler for Channel { channel_id: self.channel_id, sender: nick.to_string(), message: msg.message.to_string(), - receivers: self.clients.values().map(|v| v.user_id).collect(), + receivers: self.clients.values().map(|(_, v)| v.user_id).collect(), }); for client in self.clients.keys() { @@ -134,7 +146,7 @@ impl Handler for Channel { fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result { // grab the user's current info - let Some(sender) = self.clients.get_mut(&msg.client) else { + let Some((_, sender)) = self.clients.get_mut(&msg.client) else { return; }; @@ -149,53 +161,77 @@ impl Handler for Channel { /// /// Sends the current topic & user list, and returns a handle to the channel so the user can /// start sending us messages. +/// +/// This will return a `ChannelJoinRejectionReason` if the channel couldn't be joined. impl Handler for Channel { - type Result = MessageResult; + type Result = ResponseActFuture< + Self, + Result, ChannelJoinRejectionReason>, anyhow::Error>, + >; #[instrument(parent = &msg.span, skip_all)] - fn handle(&mut self, msg: ChannelJoin, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ChannelJoin, _ctx: &mut Self::Context) -> Self::Result { info!(self.name, msg.connection.nick, "User is joining channel"); - // persist the user's join to the database + // TODO: maybe do the lookup at channel `started` so we dont have to do a query every time + // a user attempts to join the channel self.persistence - .do_send(crate::persistence::events::ChannelJoined { + .send(FetchUserChannelPermissions { channel_id: self.channel_id, user_id: msg.connection.user_id, - span: msg.span.clone(), - }); - - self.clients - .insert(msg.client.clone(), msg.connection.clone()); - - // broadcast the user's join to everyone in the channel, including the joining user - for client in self.clients.keys() { - client.do_send(Broadcast { - span: Span::current(), - message: irc_proto::Message { - tags: None, - prefix: Some(msg.connection.to_nick()), - command: Command::JOIN(self.name.to_string(), None, None), - }, - }); - } - - // send the channel's topic to the joining user - for message in ChannelTopic::new(self).into_messages(self.name.to_string(), true) { - msg.client.do_send(Broadcast { - message, - span: Span::current(), - }); - } - - // send the user list to the user - for message in ChannelNamesList::new(self).into_messages(msg.connection.nick.to_string()) { - msg.client.do_send(Broadcast { - message, - span: Span::current(), - }); - } - - MessageResult(Ok(ctx.address())) + }) + .into_actor(self) + .map(move |res, this, ctx| { + let permissions: Permission = res.unwrap().unwrap_or(Permission::Normal); + + if !permissions.can_join() { + return Ok(Err(ChannelJoinRejectionReason::Banned)); + } + + // persist the user's join to the database + this.persistence + .do_send(crate::persistence::events::ChannelJoined { + channel_id: this.channel_id, + user_id: msg.connection.user_id, + span: msg.span.clone(), + }); + + this.clients + .insert(msg.client.clone(), (permissions, msg.connection.clone())); + + // broadcast the user's join to everyone in the channel, including the joining user + for client in this.clients.keys() { + client.do_send(Broadcast { + span: Span::current(), + message: irc_proto::Message { + tags: None, + prefix: Some(msg.connection.to_nick()), + command: Command::JOIN(this.name.to_string(), None, None), + }, + }); + } + + // send the channel's topic to the joining user + for message in ChannelTopic::new(this).into_messages(this.name.to_string(), true) { + msg.client.do_send(Broadcast { + message, + span: Span::current(), + }); + } + + // send the user list to the user + for message in + ChannelNamesList::new(this).into_messages(msg.connection.nick.to_string()) + { + msg.client.do_send(Broadcast { + message, + span: Span::current(), + }); + } + + Ok(Ok(ctx.address())) + }) + .boxed_local() } } @@ -205,19 +241,25 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelUpdateTopic, _ctx: &mut Self::Context) -> Self::Result { - let Some(client_info) = self.clients.get(&msg.client) else { + let Some((permissions, client_info)) = self.clients.get(&msg.client) else { return; }; debug!(msg.topic, "User is attempting to update channel topic"); + if !permissions.can_set_topic() { + // TODO + error!("User cannot set channel topic"); + return; + } + self.topic = Some(CurrentChannelTopic { topic: msg.topic, set_by: client_info.nick.to_string(), set_time: Utc::now(), }); - for (client, connection) in &self.clients { + for (client, (_, connection)) in &self.clients { for message in ChannelTopic::new(self).into_messages(connection.nick.to_string(), false) { client.do_send(Broadcast { @@ -234,18 +276,25 @@ impl Handler for Channel { type Result = (); fn handle(&mut self, msg: ChannelKickUser, _ctx: &mut Self::Context) -> Self::Result { - let Some(kicker) = self.clients.get(&msg.client) else { + let Some((permissions, kicker)) = self.clients.get(&msg.client) else { error!("Kicker is unknown"); return; }; + + if !permissions.can_kick() { + // TODO + error!("Kicker can not kick people from the channel"); + return; + } + let kicker = kicker.to_nick(); let kicked_user = self .clients .iter() - .find(|(_handle, client)| client.nick == msg.user) + .find(|(_handle, (_, client))| client.nick == msg.user) .map(|(k, v)| (k.clone(), v)); - let Some((kicked_user_handle, kicked_user_info)) = kicked_user else { + let Some((kicked_user_handle, (_, kicked_user_info))) = kicked_user else { error!(msg.user, "Attempted to kick unknown user"); return; }; @@ -290,7 +339,7 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelPart, ctx: &mut Self::Context) -> Self::Result { - let Some(client_info) = self.clients.remove(&msg.client) else { + let Some((_, client_info)) = self.clients.remove(&msg.client) else { return; }; @@ -322,7 +371,7 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelInvite, _ctx: &mut Self::Context) -> Self::Result { - let Some(source) = self.clients.get(&msg.client) else { + let Some((_, source)) = self.clients.get(&msg.client) else { return Box::pin(futures::future::ready(ChannelInviteResult::NotOnChannel)); }; @@ -382,7 +431,7 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ServerDisconnect, ctx: &mut Self::Context) -> Self::Result { - let Some(client_info) = self.clients.remove(&msg.client) else { + let Some((_, client_info)) = self.clients.remove(&msg.client) else { return; }; diff --git a/src/channel/permissions.rs b/src/channel/permissions.rs new file mode 100644 index 0000000..8ce47ac --- /dev/null +++ b/src/channel/permissions.rs @@ -0,0 +1,35 @@ +#[derive(Copy, Clone, Debug, Eq, PartialEq, sqlx::Type)] +#[repr(i16)] +pub enum Permission { + Ban = -1, + Normal = 0, + Voice = 1, + HalfOperator = 2, + Operator = i16::MAX, +} + +impl Permission { + /// Returns true, if the user is allowed to chat in the channel. + #[must_use] + pub fn can_chatter(self) -> bool { + self != Self::Ban + } + + /// Returns true, if the user is allowed to join the channel. + #[must_use] + pub fn can_join(self) -> bool { + self != Self::Ban + } + + /// Returns true, if the user is allowed to set the channel topic. + #[must_use] + pub const fn can_set_topic(self) -> bool { + (self as i16) >= (Self::HalfOperator as i16) + } + + /// Returns true, if the user is allowed to kick people from the channel. + #[must_use] + pub fn can_kick(self) -> bool { + self == Self::Operator + } +} diff --git a/src/channel/response.rs b/src/channel/response.rs index 98c718e..8457f0f 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -77,7 +77,7 @@ impl ChannelNamesList { nick_list: channel .clients .values() - .map(|v| v.nick.to_string()) + .map(|(_, v)| v.nick.to_string()) .collect(), } } @@ -162,3 +162,8 @@ impl ChannelInviteResult { }) } } + +#[derive(Copy, Clone, Debug)] +pub enum ChannelJoinRejectionReason { + Banned, +} diff --git a/src/client.rs b/src/client.rs index d8aaf32..ba93af0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -207,6 +207,14 @@ impl Handler for Client { ) .map(|result, this, _ctx| { for (channel_name, handle, messages) in result { + let handle = match handle { + Ok(v) => v, + Err(error) => { + error!(?error, "Failed to join user to channel"); + continue; + } + }; + this.channels.insert(channel_name.clone(), handle); for (source, message) in messages { diff --git a/src/messages.rs b/src/messages.rs index 4b54dba..d8fc843 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -51,7 +51,9 @@ pub struct ChannelList { /// Sent when the user attempts to join a channel. #[derive(Message)] -#[rtype(result = "Result>")] +#[rtype( + result = "Result, super::channel::response::ChannelJoinRejectionReason>>" +)] pub struct ChannelJoin { pub channel_name: String, pub client: Addr, diff --git a/src/persistence.rs b/src/persistence.rs index ec5ee13..b18f655 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -7,9 +7,12 @@ use chrono::Utc; use itertools::Itertools; use tracing::instrument; -use crate::persistence::events::{ - ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages, - FetchUserChannels, +use crate::{ + channel::permissions::Permission, + persistence::events::{ + ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages, + FetchUserChannelPermissions, FetchUserChannels, SetUserChannelPermissions, + }, }; /// Takes events destined for other actors and persists them to the database. @@ -123,6 +126,56 @@ impl Handler for Persistence { } } +impl Handler for Persistence { + type Result = ResponseFuture>; + + fn handle( + &mut self, + msg: FetchUserChannelPermissions, + _ctx: &mut Self::Context, + ) -> Self::Result { + let conn = self.database.clone(); + + Box::pin(async move { + sqlx::query_as( + "SELECT permissions + FROM channel_users + WHERE user = ? + AND channel = ?", + ) + .bind(msg.user_id.0) + .bind(msg.channel_id.0) + .fetch_optional(&conn) + .await + .unwrap() + .map(|(v,)| v) + }) + } +} + +impl Handler for Persistence { + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: SetUserChannelPermissions, _ctx: &mut Self::Context) -> Self::Result { + let conn = self.database.clone(); + + Box::pin(async move { + sqlx::query( + "UPDATE channel_users + SET permissions = ? + WHERE user = ? + AND channel = ?", + ) + .bind(msg.permissions) + .bind(msg.user_id.0) + .bind(msg.channel_id.0) + .execute(&conn) + .await + .unwrap(); + }) + } +} + impl Handler for Persistence { type Result = ResponseFuture>; diff --git a/src/persistence/events.rs b/src/persistence/events.rs index 2117825..266c432 100644 --- a/src/persistence/events.rs +++ b/src/persistence/events.rs @@ -1,7 +1,10 @@ use actix::Message; use tracing::Span; -use crate::{channel::ChannelId, connection::UserId}; +use crate::{ + channel::{permissions::Permission, ChannelId}, + connection::UserId, +}; #[derive(Message)] #[rtype(result = "i64")] @@ -33,6 +36,21 @@ pub struct FetchUserChannels { } #[derive(Message)] +#[rtype(result = "Option")] +pub struct FetchUserChannelPermissions { + pub channel_id: ChannelId, + pub user_id: UserId, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct SetUserChannelPermissions { + pub channel_id: ChannelId, + pub user_id: UserId, + pub permissions: Permission, +} + +#[derive(Message)] #[rtype(result = "()")] pub struct ChannelMessage { pub channel_id: ChannelId, -- libgit2 1.7.2