From 82f6e6215f491565b86c76909ac3b0364fbacf61 Mon Sep 17 00:00:00 2001 From: Paul Makles <paulmakles@gmail.com> Date: Thu, 9 Apr 2020 11:38:32 +0100 Subject: [PATCH] Implement channel permissions. --- src/database/guild.rs | 6 +- src/database/mod.rs | 1 + src/database/mutual.rs | 35 +++++ src/database/permissions.rs | 141 +++++++++++++++---- src/guards/auth.rs | 66 +++++---- src/routes/channel.rs | 267 +++++++++++++++++++----------------- src/routes/mod.rs | 20 +++ src/routes/user.rs | 64 ++------- 8 files changed, 360 insertions(+), 240 deletions(-) create mode 100644 src/database/mutual.rs diff --git a/src/database/guild.rs b/src/database/guild.rs index cdbdef2..5279061 100644 --- a/src/database/guild.rs +++ b/src/database/guild.rs @@ -8,7 +8,7 @@ pub fn find_member_permissions<C: Into<Option<String>>>( id: String, guild: String, channel: C, -) -> u8 { +) -> u32 { let col = get_collection("guilds"); match col.find_one( @@ -31,10 +31,10 @@ pub fn find_member_permissions<C: Into<Option<String>>>( Ok(result) => { if let Some(doc) = result { if doc.get_str("owner").unwrap() == id { - return u8::MAX; + return u32::MAX; } - doc.get_i32("default_permissions").unwrap() as u8 + doc.get_i32("default_permissions").unwrap() as u32 } else { 0 } diff --git a/src/database/mod.rs b/src/database/mod.rs index ecc10c5..a5e7741 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -27,6 +27,7 @@ pub fn get_collection(collection: &str) -> Collection { pub mod channel; pub mod guild; pub mod message; +pub mod mutual; pub mod permissions; pub mod user; diff --git a/src/database/mutual.rs b/src/database/mutual.rs new file mode 100644 index 0000000..aad8036 --- /dev/null +++ b/src/database/mutual.rs @@ -0,0 +1,35 @@ +use super::get_collection; + +use bson::{bson, doc}; +use mongodb::options::FindOneOptions; + +/*pub struct MutualGuild { + +} + +pub fn find_mutual_guilds(user_id: String, target_id: String) -> Vec<> { + +}*/ + +pub fn has_mutual_connection(user_id: String, target_id: String) -> bool { + let col = get_collection("guilds"); + if let Ok(result) = col.find_one( + doc! { + "$and": [ + { "members": { "$elemMatch": { "id": user_id } } }, + { "members": { "$elemMatch": { "id": target_id } } }, + ] + }, + FindOneOptions::builder() + .projection(doc! { "_id": 1 }) + .build(), + ) { + if result.is_some() { + true + } else { + false + } + } else { + false + } +} diff --git a/src/database/permissions.rs b/src/database/permissions.rs index 48f62b7..1a31abf 100644 --- a/src/database/permissions.rs +++ b/src/database/permissions.rs @@ -1,31 +1,99 @@ +use super::mutual::has_mutual_connection; +use crate::database::user::UserRelationship; +use crate::guards::auth::UserRef; +use crate::guards::channel::ChannelRef; +use crate::guards::guild::GuildRef; + +use bson::{bson, doc}; +use num_enum::TryFromPrimitive; + +#[derive(Debug, PartialEq, Eq, TryFromPrimitive)] +#[repr(u8)] +pub enum Relationship { + FRIEND = 0, + OUTGOING = 1, + INCOMING = 2, + BLOCKED = 3, + BLOCKEDOTHER = 4, + NONE = 5, + SELF = 6, +} + +#[derive(Debug, PartialEq, Eq, TryFromPrimitive)] +#[repr(u32)] +pub enum Permission { + ACCESS = 1, + CREATE_INVITE = 2, + KICK_MEMBERS = 4, + BAN_MEMBERS = 8, + READ_MESSAGES = 16, + SEND_MESSAGES = 32, + MANAGE_MESSAGES = 64, + MANAGE_CHANNELS = 128, + MANAGE_SERVER = 256, + MANAGE_ROLES = 512, +} + bitfield! { - pub struct MemberPermissions(MSB0 [u8]); + pub struct MemberPermissions(MSB0 [u32]); u8; - pub get_access, set_access: 7; - pub get_create_invite, set_create_invite: 6; - pub get_kick_members, set_kick_members: 5; - pub get_ban_members, set_ban_members: 4; - pub get_read_messages, set_read_messages: 3; - pub get_send_messages, set_send_messages: 2; + pub get_access, set_access: 31; + pub get_create_invite, set_create_invite: 30; + pub get_kick_members, set_kick_members: 29; + pub get_ban_members, set_ban_members: 28; + pub get_read_messages, set_read_messages: 27; + pub get_send_messages, set_send_messages: 26; + pub get_manage_messages, set_manage_messages: 25; + pub get_manage_channels, set_manage_channels: 24; + pub get_manage_server, set_manage_server: 23; + pub get_manage_roles, set_manage_roles: 22; } -use super::get_collection; -use crate::guards::channel::ChannelRef; -use crate::guards::guild::GuildRef; +pub fn get_relationship_internal( + user_id: &str, + target_id: &str, + relationships: &Option<Vec<UserRelationship>>, +) -> Relationship { + if user_id == target_id { + return Relationship::SELF; + } -use bson::{bson, doc}; -use mongodb::options::FindOneOptions; + if let Some(arr) = &relationships { + for entry in arr { + if entry.id == target_id { + match entry.status { + 0 => return Relationship::FRIEND, + 1 => return Relationship::OUTGOING, + 2 => return Relationship::INCOMING, + 3 => return Relationship::BLOCKED, + 4 => return Relationship::BLOCKEDOTHER, + _ => return Relationship::NONE, + } + } + } + } + + Relationship::NONE +} + +pub fn get_relationship(a: &UserRef, b: &UserRef) -> Relationship { + if a.id == b.id { + return Relationship::SELF; + } + + get_relationship_internal(&a.id, &b.id, &a.fetch_relationships()) +} pub struct PermissionCalculator { - pub user_id: String, + pub user: UserRef, pub channel: Option<ChannelRef>, pub guild: Option<GuildRef>, } impl PermissionCalculator { - pub fn new(user_id: String) -> PermissionCalculator { + pub fn new(user: UserRef) -> PermissionCalculator { PermissionCalculator { - user_id, + user, channel: None, guild: None, } @@ -45,7 +113,7 @@ impl PermissionCalculator { } } - pub fn calculate(self) -> u8 { + pub fn calculate(self) -> u32 { let guild = if let Some(value) = self.guild { Some(value) } else if let Some(channel) = &self.channel { @@ -70,14 +138,14 @@ impl PermissionCalculator { doc! { "members": { "$elemMatch": { - "id": &self.user_id, + "id": &self.user.id, } } }, - doc! { } + doc! {}, ) { - if guild.owner == self.user_id { - return u8::MAX; + if guild.owner == self.user.id { + return u32::MAX; } permissions = guild.default_permissions; @@ -87,29 +155,42 @@ impl PermissionCalculator { if let Some(channel) = &self.channel { match channel.channel_type { 0 => { + // ? check user is part of the channel if let Some(arr) = &channel.recipients { + let mut other_user = ""; for item in arr { - if item == &self.user_id { + if item == &self.user.id { permissions = 49; - break; + } else { + other_user = item; } } + + let relationships = self.user.fetch_relationships(); + let relationship = + get_relationship_internal(&self.user.id, &other_user, &relationships); + + if relationship == Relationship::BLOCKED + || relationship == Relationship::BLOCKEDOTHER + { + permissions = 1; + } else if has_mutual_connection(self.user.id, other_user.to_string()) { + permissions = 49; + } } - }, - 1 => { - unreachable!() - }, + } + 1 => unreachable!(), 2 => { // nothing implemented yet - }, + } _ => {} } } - permissions as u8 + permissions as u32 } - pub fn as_permission(self) -> MemberPermissions<[u8; 1]> { - MemberPermissions([ self.calculate() ]) + pub fn as_permission(self) -> MemberPermissions<[u32; 1]> { + MemberPermissions([self.calculate()]) } } diff --git a/src/guards/auth.rs b/src/guards/auth.rs index 66df8ea..0d30470 100644 --- a/src/guards/auth.rs +++ b/src/guards/auth.rs @@ -1,14 +1,14 @@ +use bson::{bson, doc, from_bson, Document}; +use mongodb::options::FindOneOptions; use rocket::http::{RawStr, Status}; use rocket::request::{self, FromParam, FromRequest, Request}; use rocket::Outcome; -use bson::{bson, doc, from_bson, Document}; use serde::{Deserialize, Serialize}; -use mongodb::options::FindOneOptions; use crate::database; use database::user::{User, UserRelationship}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserRef { pub id: String, pub username: String, @@ -29,11 +29,13 @@ impl UserRef { let user = database::get_collection("users") .find_one( doc! { "_id": &self.id }, - FindOneOptions::builder().projection(doc! { "relations": 1 }).build(), + FindOneOptions::builder() + .projection(doc! { "relations": 1 }) + .build(), ) .expect("Failed to fetch user relationships from database.") .expect("Missing user document."); - + if let Ok(arr) = user.get_array("relations") { let mut relationships = vec![]; for item in arr { @@ -77,13 +79,15 @@ impl<'a, 'r> FromRequest<'a, 'r> for UserRef { .unwrap(); if let Some(user) = result { - Outcome::Success( - UserRef { - id: user.get_str("_id").unwrap().to_string(), - username: user.get_str("username").unwrap().to_string(), - email_verified: user.get_document("email_verification").unwrap().get_bool("verified").unwrap(), - } - ) + Outcome::Success(UserRef { + id: user.get_str("_id").unwrap().to_string(), + username: user.get_str("username").unwrap().to_string(), + email_verified: user + .get_document("email_verification") + .unwrap() + .get_bool("verified") + .unwrap(), + }) } else { Outcome::Failure((Status::Forbidden, AuthError::Invalid)) } @@ -124,26 +128,28 @@ impl<'r> FromParam<'r> for UserRef { fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> { let col = database::get_db().collection("users"); let result = database::get_collection("users") - .find_one( - doc! { "_id": param.to_string() }, - FindOneOptions::builder() - .projection(doc! { - "_id": 1, - "username": 1, - "email_verification.verified": 1, - }) - .build(), - ) - .unwrap(); + .find_one( + doc! { "_id": param.to_string() }, + FindOneOptions::builder() + .projection(doc! { + "_id": 1, + "username": 1, + "email_verification.verified": 1, + }) + .build(), + ) + .unwrap(); if let Some(user) = result { - Ok( - UserRef { - id: user.get_str("_id").unwrap().to_string(), - username: user.get_str("username").unwrap().to_string(), - email_verified: user.get_document("email_verification").unwrap().get_bool("verified").unwrap(), - } - ) + Ok(UserRef { + id: user.get_str("_id").unwrap().to_string(), + username: user.get_str("username").unwrap().to_string(), + email_verified: user + .get_document("email_verification") + .unwrap() + .get_bool("verified") + .unwrap(), + }) } else { Err(param) } diff --git a/src/routes/channel.rs b/src/routes/channel.rs index 5dc57be..5c92f3f 100644 --- a/src/routes/channel.rs +++ b/src/routes/channel.rs @@ -1,8 +1,7 @@ use super::Response; -use crate::database::{self, channel::Channel, message::Message, user::User, PermissionCalculator}; -use crate::guards::channel::ChannelRef; +use crate::database::{self, message::Message, Permission, PermissionCalculator}; use crate::guards::auth::UserRef; -use crate::websocket; +use crate::guards::channel::ChannelRef; use bson::{bson, doc, from_bson, Bson::UtcDatetime}; use chrono::prelude::*; @@ -20,19 +19,17 @@ pub enum ChannelType { } macro_rules! with_permissions { - ($user: expr, $target: expr) => { - { - let permissions = PermissionCalculator::new($user.id.clone()) - .channel($target.clone()) - .as_permission(); - - if !permissions.get_access() { - return None; - } + ($user: expr, $target: expr) => {{ + let permissions = PermissionCalculator::new($user.clone()) + .channel($target.clone()) + .as_permission(); - permissions + if !permissions.get_access() { + return None; } - }; + + permissions + }}; } /// fetch channel information @@ -41,34 +38,28 @@ pub fn channel(user: UserRef, target: ChannelRef) -> Option<Response> { with_permissions!(user, target); match target.channel_type { - 0..=1 => Some(Response::Success( - json!({ - "id": target.id, - "type": target.channel_type, - "recipients": target.recipients, - }) - )), + 0..=1 => Some(Response::Success(json!({ + "id": target.id, + "type": target.channel_type, + "recipients": target.recipients, + }))), 2 => { - if let Some(info) = target.fetch_data( - doc! { - "name": 1, - "description": 1, - } - ) { - Some(Response::Success( - json!({ - "id": target.id, - "type": target.channel_type, - "guild": target.guild, - "name": info.get_str("name").unwrap(), - "description": info.get_str("description").unwrap_or(""), - }) - )) + if let Some(info) = target.fetch_data(doc! { + "name": 1, + "description": 1, + }) { + Some(Response::Success(json!({ + "id": target.id, + "type": target.channel_type, + "guild": target.guild, + "name": info.get_str("name").unwrap(), + "description": info.get_str("description").unwrap_or(""), + }))) } else { None } - }, - _ => unreachable!() + } + _ => unreachable!(), } } @@ -77,38 +68,49 @@ pub fn channel(user: UserRef, target: ChannelRef) -> Option<Response> { /// or close DM conversation #[delete("/<target>")] pub fn delete(user: UserRef, target: ChannelRef) -> Option<Response> { - with_permissions!(user, target); + let permissions = with_permissions!(user, target); + + if !permissions.get_manage_channels() { + return Some(Response::LackingPermission(Permission::MANAGE_CHANNELS)); + } let col = database::get_collection("channels"); - Some(match target.channel_type { + match target.channel_type { 0 => { - col.update_one( + if col.update_one( doc! { "_id": target.id }, doc! { "$set": { "active": false } }, None, - ) - .expect("Failed to update channel."); - - Response::Result(super::Status::Ok) + ).is_ok() { + Some(Response::Result(super::Status::Ok)) + } else { + Some(Response::InternalServerError(json!({ "error": "Failed to close channel." }))) + } } 1 => { // ? TODO: group dm - Response::Result(super::Status::Ok) + Some(Response::Result(super::Status::Ok)) } 2 => { // ? TODO: guild - Response::Result(super::Status::Ok) + Some(Response::Result(super::Status::Ok)) } - _ => Response::InternalServerError(json!({ "error": "Unknown error has occurred." })), - }) + _ => Some(Response::InternalServerError( + json!({ "error": "Unknown error has occurred." }), + )), + } } /// fetch channel messages #[get("/<target>/messages")] pub fn messages(user: UserRef, target: ChannelRef) -> Option<Response> { - with_permissions!(user, target); + let permissions = with_permissions!(user, target); + + if !permissions.get_read_messages() { + return Some(Response::LackingPermission(Permission::READ_MESSAGES)); + } let col = database::get_collection("messages"); let result = col.find(doc! { "channel": target.id }, None).unwrap(); @@ -136,8 +138,16 @@ pub struct SendMessage { /// send a message to a channel #[post("/<target>/messages", data = "<message>")] -pub fn send_message(user: UserRef, target: ChannelRef, message: Json<SendMessage>) -> Option<Response> { - with_permissions!(user, target); +pub fn send_message( + user: UserRef, + target: ChannelRef, + message: Json<SendMessage>, +) -> Option<Response> { + let permissions = with_permissions!(user, target); + + if !permissions.get_send_messages() { + return Some(Response::LackingPermission(Permission::SEND_MESSAGES)); + } let content: String = message.content.chars().take(2000).collect(); let nonce: String = message.nonce.chars().take(32).collect(); @@ -201,7 +211,11 @@ pub fn send_message(user: UserRef, target: ChannelRef, message: Json<SendMessage /// get a message #[get("/<target>/messages/<message>")] pub fn get_message(user: UserRef, target: ChannelRef, message: Message) -> Option<Response> { - with_permissions!(user, target); + let permissions = with_permissions!(user, target); + + if !permissions.get_read_messages() { + return Some(Response::LackingPermission(Permission::READ_MESSAGES)); + } let prev = // ! CHECK IF USER HAS PERMISSION TO VIEW EDITS OF MESSAGES @@ -243,87 +257,90 @@ pub fn edit_message( ) -> Option<Response> { with_permissions!(user, target); - Some(if message.author != user.id { - Response::Unauthorized(json!({ "error": "You did not send this message." })) - } else { - let col = database::get_collection("messages"); + if message.author != user.id { + return Some(Response::Unauthorized( + json!({ "error": "You did not send this message." }), + )); + } - let time = if let Some(edited) = message.edited { - edited.0 - } else { - Ulid::from_string(&message.id).unwrap().datetime() - }; + let col = database::get_collection("messages"); + let time = if let Some(edited) = message.edited { + edited.0 + } else { + Ulid::from_string(&message.id).unwrap().datetime() + }; - let edited = Utc::now(); - match col.update_one( - doc! { "_id": message.id.clone() }, - doc! { - "$set": { - "content": edit.content.clone(), - "edited": UtcDatetime(edited.clone()) - }, - "$push": { - "previous_content": { - "content": message.content, - "time": time, - } - }, + let edited = Utc::now(); + match col.update_one( + doc! { "_id": message.id.clone() }, + doc! { + "$set": { + "content": edit.content.clone(), + "edited": UtcDatetime(edited.clone()) }, - None, - ) { - Ok(_) => { - /*websocket::queue_message( - get_recipients(&target), - json!({ - "type": "message_update", - "data": { - "id": message.id, - "channel": target.id, - "content": edit.content.clone(), - "edited": edited.timestamp() - }, - }) - .to_string(), - );*/ - - Response::Result(super::Status::Ok) - } - Err(_) => { - Response::InternalServerError(json!({ "error": "Failed to update message." })) - } + "$push": { + "previous_content": { + "content": message.content, + "time": time, + } + }, + }, + None, + ) { + Ok(_) => { + /*websocket::queue_message( + get_recipients(&target), + json!({ + "type": "message_update", + "data": { + "id": message.id, + "channel": target.id, + "content": edit.content.clone(), + "edited": edited.timestamp() + }, + }) + .to_string(), + );*/ + + Some(Response::Result(super::Status::Ok)) } - }) + Err(_) => Some(Response::InternalServerError( + json!({ "error": "Failed to update message." }), + )), + } } /// delete a message #[delete("/<target>/messages/<message>")] pub fn delete_message(user: UserRef, target: ChannelRef, message: Message) -> Option<Response> { - with_permissions!(user, target); + let permissions = with_permissions!(user, target); - Some(if message.author != user.id { - Response::Unauthorized(json!({ "error": "You did not send this message." })) - } else { - let col = database::get_collection("messages"); - - match col.delete_one(doc! { "_id": message.id.clone() }, None) { - Ok(_) => { - /*websocket::queue_message( - get_recipients(&target), - json!({ - "type": "message_delete", - "data": { - "id": message.id, - "channel": target.id - }, - }) - .to_string(), - );*/ - - Response::Result(super::Status::Ok) - } - Err(_) => { - Response::InternalServerError(json!({ "error": "Failed to delete message." })) - } + if !permissions.get_manage_messages() { + if message.author != user.id { + return Some(Response::LackingPermission(Permission::MANAGE_MESSAGES)); + } + } + + let col = database::get_collection("messages"); + + match col.delete_one(doc! { "_id": message.id.clone() }, None) { + Ok(_) => { + /*websocket::queue_message( + get_recipients(&target), + json!({ + "type": "message_delete", + "data": { + "id": message.id, + "channel": target.id + }, + }) + .to_string(), + );*/ + + Some(Response::Result(super::Status::Ok)) } - }) + Err(_) => Some(Response::InternalServerError( + json!({ "error": "Failed to delete message." }), + )), + } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ce019e4..96a19ec 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -3,6 +3,8 @@ pub use rocket::response::Redirect; use rocket::Rocket; use rocket_contrib::json::JsonValue; +use crate::database::Permission; + pub mod account; pub mod channel; pub mod guild; @@ -21,6 +23,8 @@ pub enum Response { BadRequest(JsonValue), #[response(status = 401)] Unauthorized(JsonValue), + #[response(status = 401)] + LackingPermission(Permission), #[response(status = 404)] NotFound(JsonValue), #[response(status = 406)] @@ -37,6 +41,22 @@ pub enum Response { InternalServerError(JsonValue), } +use rocket::http::ContentType; +use rocket::request::Request; +use std::io::Cursor; + +impl<'a> rocket::response::Responder<'a> for Permission { + fn respond_to(self, _: &Request) -> rocket::response::Result<'a> { + rocket::response::Response::build() + .header(ContentType::JSON) + .sized_body(Cursor::new(format!( + "{{\"error\":\"Lacking {:?} permission.\"}}", + self + ))) + .ok() + } +} + pub fn mount(rocket: Rocket) -> Rocket { rocket .mount("/api", routes![root::root]) diff --git a/src/routes/user.rs b/src/routes/user.rs index 01dcc3f..713043f 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,7 +1,8 @@ use super::Response; -use crate::database::{self, channel::Channel, user::UserRelationship}; -use crate::routes::channel; +use crate::database::{self, channel::Channel}; +use crate::database::{get_relationship, get_relationship_internal, Relationship}; use crate::guards::auth::UserRef; +use crate::routes::channel; use bson::{bson, doc, from_bson}; use mongodb::options::FindOptions; @@ -9,61 +10,20 @@ use rocket_contrib::json::Json; use serde::{Deserialize, Serialize}; use ulid::Ulid; -enum Relationship { - FRIEND = 0, - OUTGOING = 1, - INCOMING = 2, - BLOCKED = 3, - BLOCKEDOTHER = 4, - NONE = 5, - SELF = 6, -} - -fn get_relationship_internal(user_id: &str, target_id: &str, relationships: &Option<Vec<UserRelationship>>) -> Relationship { - if user_id == target_id { - return Relationship::SELF; - } - - if let Some(arr) = &relationships { - for entry in arr { - if entry.id == target_id { - match entry.status { - 0 => return Relationship::FRIEND, - 1 => return Relationship::OUTGOING, - 2 => return Relationship::INCOMING, - 3 => return Relationship::BLOCKED, - 4 => return Relationship::BLOCKEDOTHER, - _ => return Relationship::NONE, - } - } - } - } - - Relationship::NONE -} - -fn get_relationship(a: &UserRef, b: &UserRef) -> Relationship { - if a.id == b.id { - return Relationship::SELF; - } - - get_relationship_internal(&a.id, &b.id, &a.fetch_relationships()) -} - /// retrieve your user information #[get("/@me")] pub fn me(user: UserRef) -> Response { if let Some(info) = user.fetch_data(doc! { "email": 1 }) { - Response::Success( - json!({ - "id": user.id, - "username": user.username, - "email": info.get_str("email").unwrap(), - "verified": user.email_verified, - }) - ) + Response::Success(json!({ + "id": user.id, + "username": user.username, + "email": info.get_str("email").unwrap(), + "verified": user.email_verified, + })) } else { - Response::InternalServerError(json!({ "error": "Failed to fetch information from database." })) + Response::InternalServerError( + json!({ "error": "Failed to fetch information from database." }), + ) } } -- GitLab