From c471119d217ab39539ffabced0caeed3ff9640ea Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 12 Jan 2023 22:56:08 +0000 Subject: [PATCH] Load permission list at channel boot & send prefixes in LIST response --- src/channel.rs | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------- src/channel/permissions.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- src/channel/response.rs | 8 +++++++- src/connection.rs | 3 ++- src/persistence.rs | 27 ++++++++++++++------------- src/persistence/events.rs | 7 ++++--- src/server.rs | 8 ++++++-- 7 files changed, 296 insertions(+), 150 deletions(-) diff --git a/src/channel.rs b/src/channel.rs index d77662f..a914628 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -20,14 +20,14 @@ use crate::{ }, }, client::Client, - connection::InitiatedConnection, + connection::{InitiatedConnection, UserId}, messages::{ Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic, FetchClientByNick, ServerDisconnect, UserKickedFromChannel, UserNickChange, }, persistence::{ - events::{FetchUserChannelPermissions, SetUserChannelPermissions}, + events::{FetchAllUserChannelPermissions, SetUserChannelPermissions}, Persistence, }, server::Server, @@ -41,7 +41,8 @@ pub struct ChannelId(pub i64); pub struct Channel { pub name: String, pub server: Addr, - pub clients: HashMap, (Permission, InitiatedConnection)>, + pub permissions: HashMap, + pub clients: HashMap, InitiatedConnection>, pub topic: Option, pub persistence: Addr, pub channel_id: ChannelId, @@ -57,12 +58,29 @@ impl Actor for Channel { name: self.name.to_string(), }) .into_actor(self) + .then(|res, this, ctx| { + match res { + Ok(channel_id) => { + this.channel_id.0 = channel_id; + } + Err(error) => { + error!(%error, "Failed to create channel in database"); + ctx.terminate(); + } + } + + this.persistence + .send(FetchAllUserChannelPermissions { + channel_id: this.channel_id, + }) + .into_actor(this) + }) .map(|res, this, ctx| match res { - Ok(channel_id) => { - this.channel_id.0 = channel_id; + Ok(permissions) => { + this.permissions = permissions; } Err(error) => { - error!(%error, "Failed to create channel in database"); + error!(%error, "Failed to fetch channel permissions"); ctx.terminate(); } }), @@ -72,6 +90,17 @@ impl Actor for Channel { impl Supervised for Channel {} +impl Channel { + /// Grabs the user's permissions from the permission cache, defaulting to `Normal`. + #[must_use] + pub fn get_user_permissions(&self, user_id: UserId) -> Permission { + self.permissions + .get(&user_id) + .copied() + .unwrap_or(Permission::Normal) + } +} + /// Broadcast a raw IRC message to all clients connected to this channel. impl Handler for Channel { type Result = (); @@ -102,12 +131,12 @@ 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((permissions, sender)) = self.clients.get(&msg.client) else { + let Some(sender) = self.clients.get(&msg.client) else { error!("Received message from user not in channel"); return; }; - if !permissions.can_chatter() { + if !self.get_user_permissions(sender.user_id).can_chatter() { // TODO error!("User cannot send message to channel"); return; @@ -122,7 +151,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 +163,7 @@ impl Handler for Channel { // broadcast the message to `client` client.do_send(Broadcast { span: Span::current(), - message: irc_proto::Message { + message: Message { tags: None, prefix: Some(nick.clone()), command: Command::PRIVMSG(self.name.to_string(), msg.message.clone()), @@ -149,7 +178,7 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelSetMode, ctx: &mut Self::Context) -> Self::Result { - let Some((permissions, client)) = self.clients.get(&msg.client).cloned() else { + let Some(client) = self.clients.get(&msg.client).cloned() else { return; }; @@ -166,50 +195,11 @@ impl Handler for Channel { continue; }; - // TODO: this should allow setting perms not currently in the channel, this probably - // ties into fetching all user permissions on boot of the channel - let Some((_, (affected_user_perms, affected_user))) = - self.clients.iter_mut().find(|(_, (_, connection))| { - connection.nick == affected_nick - }) else { - error!("Unknown user to set perms on"); - continue; - }; - - let new_affected_user_perms = if add { - user_mode - } else if *affected_user_perms == user_mode { - Permission::Normal - } else { - error!("Removing the given permission would do nothing"); - continue; - }; - - if !permissions.can_set_permission(new_affected_user_perms, *affected_user_perms) { - error!( - ?permissions, - ?new_affected_user_perms, - ?affected_user_perms, - "User is not allowed to set permissions for this user" - ); - - continue; - } - - self.persistence.do_send(SetUserChannelPermissions { - channel_id: self.channel_id, - user_id: affected_user.user_id, - permissions: new_affected_user_perms, - }); - - *affected_user_perms = new_affected_user_perms; - - ctx.notify(Broadcast { - message: Message { - tags: None, - prefix: Some(client.to_nick()), - command: Command::ChannelMODE(self.name.to_string(), vec![mode]), - }, + ctx.notify(SetUserMode { + requester: client.clone(), + add, + affected_nick, + user_mode, span: Span::current(), }); } else { @@ -219,13 +209,87 @@ impl Handler for Channel { } } +/// Called by other users to set a permission on a user. +/// +/// Users are currently only able to set permissions of users that are currently at a lower +/// permission level to themselves, and can only set permissions to levels lower than their +/// own. +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: SetUserMode, ctx: &mut Self::Context) -> Self::Result { + let permissions = self.get_user_permissions(msg.requester.user_id); + + // TODO: this should allow setting perms not currently in the channel + let Some((_, affected_user)) = self.clients.iter().find(|(_, connection)| connection.nick == msg.affected_nick) else { + error!("Unknown user to set perms on"); + return; + }; + + // grab the permissions of the user we're trying to affect + let affected_user_perms = self.get_user_permissions(affected_user.user_id); + + // calculate the new permissions that should be set on the user + let new_affected_user_perms = if msg.add { + msg.user_mode + } else if affected_user_perms == msg.user_mode { + Permission::Normal + } else { + error!("Removing the given permission would do nothing"); + return; + }; + + // check if the caller can set these permissions on the user + if !permissions.can_set_permission(new_affected_user_perms, affected_user_perms) { + error!( + ?permissions, + ?new_affected_user_perms, + ?affected_user_perms, + "User is not allowed to set permissions for this user" + ); + + return; + } + + // persist the permissions change both locally and to the database + self.permissions + .insert(affected_user.user_id, new_affected_user_perms); + self.persistence.do_send(SetUserChannelPermissions { + channel_id: self.channel_id, + user_id: affected_user.user_id, + permissions: new_affected_user_perms, + }); + + // broadcast the change for all nicks that the affected user is connected with + let all_connected_for_user_id = self + .clients + .values() + .filter(|connection| connection.user_id == affected_user.user_id); + for connection in all_connected_for_user_id { + let Some(mode) = msg.user_mode.into_mode(msg.add, connection.nick.to_string()) else { + continue; + }; + + ctx.notify(Broadcast { + message: Message { + tags: None, + prefix: Some(connection.to_nick()), + command: Command::ChannelMODE(self.name.to_string(), vec![mode.clone()]), + }, + span: Span::current(), + }); + } + } +} + /// Received when a user changes their nick. impl Handler for Channel { type Result = (); 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; }; @@ -243,74 +307,88 @@ impl Handler for Channel { /// /// This will return a `ChannelJoinRejectionReason` if the channel couldn't be joined. impl Handler for Channel { - type Result = ResponseActFuture< - Self, - Result, ChannelJoinRejectionReason>, anyhow::Error>, - >; + type Result = MessageResult; #[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"); - // 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 + let mut permissions = self + .permissions + .get(&msg.connection.user_id) + .copied() + .unwrap_or(Permission::Normal); + + if !permissions.can_join() { + return MessageResult(Ok(Err(ChannelJoinRejectionReason::Banned))); + } + + // persist the user's join to the database self.persistence - .send(FetchUserChannelPermissions { + .do_send(crate::persistence::events::ChannelJoined { channel_id: self.channel_id, user_id: msg.connection.user_id, - }) - .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() + span: msg.span.clone(), + }); + + // we need to send out the set user channel permissions after the channel joined persistence + // event has been sent so the user's row exists + if self.permissions.is_empty() { + // the first person to ever join the channel should get founder permissions + permissions = Permission::Founder; + + self.permissions.insert(msg.connection.user_id, permissions); + + self.persistence.do_send(SetUserChannelPermissions { + channel_id: self.channel_id, + user_id: msg.connection.user_id, + permissions, + }); + } + + 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: Message { + tags: None, + prefix: Some(msg.connection.to_nick()), + command: Command::JOIN(self.name.to_string(), None, None), + }, + }); + + if let Some(mode) = permissions.into_mode(true, msg.connection.nick.to_string()) { + client.do_send(Broadcast { + span: Span::current(), + message: Message { + tags: None, + prefix: Some(msg.connection.to_nick()), + command: Command::ChannelMODE(self.name.to_string(), vec![mode]), + }, + }); + } + } + + // 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(Ok(ctx.address()))) } } @@ -320,13 +398,16 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelUpdateTopic, _ctx: &mut Self::Context) -> Self::Result { - let Some((permissions, client_info)) = self.clients.get(&msg.client) else { + let Some(client_info) = self.clients.get(&msg.client) else { return; }; debug!(msg.topic, "User is attempting to update channel topic"); - if !permissions.can_set_topic() { + if !self + .get_user_permissions(client_info.user_id) + .can_set_topic() + { // TODO error!("User cannot set channel topic"); return; @@ -338,7 +419,7 @@ impl Handler for Channel { 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 { @@ -355,12 +436,12 @@ impl Handler for Channel { type Result = (); fn handle(&mut self, msg: ChannelKickUser, _ctx: &mut Self::Context) -> Self::Result { - let Some((permissions, kicker)) = self.clients.get(&msg.client) else { + let Some(kicker) = self.clients.get(&msg.client) else { error!("Kicker is unknown"); return; }; - if !permissions.can_kick() { + if !self.get_user_permissions(kicker.user_id).can_kick() { // TODO error!("Kicker can not kick people from the channel"); return; @@ -371,9 +452,9 @@ impl Handler for Channel { 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; }; @@ -418,7 +499,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; }; @@ -431,7 +512,7 @@ impl Handler for Channel { }); let message = Broadcast { - message: irc_proto::Message { + message: Message { tags: None, prefix: Some(client_info.to_nick()), command: Command::PART(self.name.to_string(), msg.message), @@ -450,7 +531,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)); }; @@ -510,13 +591,13 @@ 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; }; let message = Broadcast { span: Span::current(), - message: irc_proto::Message { + message: Message { tags: None, prefix: Some(client_info.to_nick()), command: Command::QUIT(msg.message), @@ -534,3 +615,13 @@ pub struct CurrentChannelTopic { pub set_by: String, pub set_time: DateTime, } + +#[derive(actix::Message)] +#[rtype(result = "()")] +pub struct SetUserMode { + requester: InitiatedConnection, + add: bool, + affected_nick: String, + user_mode: Permission, + span: Span, +} diff --git a/src/channel/permissions.rs b/src/channel/permissions.rs index aa538af..af63c85 100644 --- a/src/channel/permissions.rs +++ b/src/channel/permissions.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use irc_proto::ChannelMode; +use irc_proto::{ChannelMode, Mode}; #[derive(Copy, Clone, Debug, Eq, PartialEq, sqlx::Type)] #[repr(i16)] @@ -28,6 +28,48 @@ impl TryFrom for Permission { } impl Permission { + /// A list of (mode)prefix used to inform clients of which modes set which prefixes. + pub const SUPPORTED_PREFIXES: &'static str = "(qohv)~@%+"; + + /// Builds the mode message that's used to set (or unset) this permission. + #[must_use] + pub fn into_mode(self, add: bool, nick: String) -> Option> { + >::from(self).map(|v| { + if add { + Mode::Plus(v, Some(nick)) + } else { + Mode::Minus(v, Some(nick)) + } + }) + } + + /// Grabs the prefix that is used to represent a permission. + #[must_use] + pub const fn into_prefix(self) -> &'static str { + match self { + Self::Ban | Self::Normal => "", + Self::Voice => "+", + Self::HalfOperator => "%", + Self::Operator => "@", + Self::Founder => "~", + } + } +} + +impl From for Option { + fn from(value: Permission) -> Self { + match value { + Permission::Ban => Some(ChannelMode::Ban), + Permission::Normal => None, + Permission::Voice => Some(ChannelMode::Voice), + Permission::HalfOperator => Some(ChannelMode::Halfop), + Permission::Operator => Some(ChannelMode::Oper), + Permission::Founder => Some(ChannelMode::Founder), + } + } +} + +impl Permission { /// Returns true, if the user is allowed to chat in the channel. #[must_use] pub fn can_chatter(self) -> bool { diff --git a/src/channel/response.rs b/src/channel/response.rs index 8457f0f..b0d2357 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -77,7 +77,13 @@ impl ChannelNamesList { nick_list: channel .clients .values() - .map(|(_, v)| v.nick.to_string()) + .map(|v| { + format!( + "{}{}", + channel.get_user_permissions(v.user_id).into_prefix(), + v.nick + ) + }) .collect(), } } diff --git a/src/connection.rs b/src/connection.rs index d930e3d..ff37ef8 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -30,7 +30,8 @@ pub type MessageSink = FramedWrite, irc_proto::Irc pub const SUPPORTED_CAPABILITIES: &[&str] = &[concatcp!("sasl=", AuthStrategy::SUPPORTED)]; -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, sqlx::Type)] +#[sqlx(transparent)] pub struct UserId(pub i64); #[derive(Default)] diff --git a/src/persistence.rs b/src/persistence.rs index 00c9411..7f806ce 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -1,6 +1,6 @@ pub mod events; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use actix::{AsyncContext, Context, Handler, ResponseFuture, WrapFuture}; use chrono::Utc; @@ -9,9 +9,11 @@ use tracing::instrument; use crate::{ channel::permissions::Permission, + connection::UserId, persistence::events::{ - ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages, - FetchUserChannelPermissions, FetchUserChannels, ReserveNick, SetUserChannelPermissions, + ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, + FetchAllUserChannelPermissions, FetchUnseenMessages, FetchUserChannels, ReserveNick, + SetUserChannelPermissions, }, }; @@ -126,29 +128,28 @@ impl Handler for Persistence { } } -impl Handler for Persistence { - type Result = ResponseFuture>; +impl Handler for Persistence { + type Result = ResponseFuture>; fn handle( &mut self, - msg: FetchUserChannelPermissions, + msg: FetchAllUserChannelPermissions, _ctx: &mut Self::Context, ) -> Self::Result { let conn = self.database.clone(); Box::pin(async move { - sqlx::query_as( - "SELECT permissions + sqlx::query_as::<_, (UserId, Permission)>( + "SELECT user, permissions FROM channel_users - WHERE user = ? - AND channel = ?", + WHERE channel = ?", ) - .bind(msg.user_id.0) .bind(msg.channel_id.0) - .fetch_optional(&conn) + .fetch_all(&conn) .await .unwrap() - .map(|(v,)| v) + .into_iter() + .collect() }) } } diff --git a/src/persistence/events.rs b/src/persistence/events.rs index 2fbdd95..1591817 100644 --- a/src/persistence/events.rs +++ b/src/persistence/events.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use actix::Message; use tracing::Span; @@ -36,10 +38,9 @@ pub struct FetchUserChannels { } #[derive(Message)] -#[rtype(result = "Option")] -pub struct FetchUserChannelPermissions { +#[rtype(result = "HashMap")] +pub struct FetchAllUserChannelPermissions { pub channel_id: ChannelId, - pub user_id: UserId, } #[derive(Message)] diff --git a/src/server.rs b/src/server.rs index 548e56a..ea70404 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,7 +15,7 @@ use tokio_stream::StreamExt; use tracing::{debug, instrument, warn, Span}; use crate::{ - channel::{Channel, ChannelId}, + channel::{permissions::Permission, Channel, ChannelId}, client::Client, config::Config, connection::InitiatedConnection, @@ -91,7 +91,10 @@ impl Handler for Server { ), ( Response::RPL_ISUPPORT, - vec!["D".into(), "are supported by this server".into()], + vec![ + format!("PREFIX={}", Permission::SUPPORTED_PREFIXES).into(), + "are supported by this server".into(), + ], ), ]; @@ -163,6 +166,7 @@ impl Handler for Server { Supervisor::start_in_arbiter(&arbiter, move |_ctx| Channel { name: channel_name, + permissions: HashMap::new(), clients: HashMap::new(), topic: None, server, -- libgit2 1.7.2