From 07e74e1074d9023148800c92278ffbabb1aba9c6 Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Sat, 26 Jun 2021 22:16:37 +0100 Subject: [PATCH] Servers: New permissions implementation. Feature: Roles and group permissions. (server-side) --- set_version.sh | 2 +- src/database/entities/channel.rs | 16 ++++++ src/database/entities/server.rs | 24 ++++++++ src/database/migrations/scripts.rs | 36 ++++++++++-- src/database/permissions/channel.rs | 84 ++++++++++++++++++++-------- src/database/permissions/mod.rs | 9 +++ src/database/permissions/server.rs | 33 +++++++++-- src/routes/channels/group_create.rs | 1 + src/routes/servers/channel_create.rs | 10 +++- src/routes/servers/server_create.rs | 13 +++++ src/version.rs | 2 +- 11 files changed, 195 insertions(+), 35 deletions(-) diff --git a/set_version.sh b/set_version.sh index 4568624..d767014 100755 --- a/set_version.sh +++ b/set_version.sh @@ -1,3 +1,3 @@ #!/bin/bash -export version=0.5.1-alpha.0-patch.0 +export version=0.5.1-alpha.1 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index 5e134cf..ead1df5 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::database::*; use crate::notifications::events::ClientboundNotification; use crate::util::result::{Error, Result}; @@ -50,6 +52,9 @@ pub enum Channel { icon: Option<File>, #[serde(skip_serializing_if = "Option::is_none")] last_message: Option<LastMessage>, + + #[serde(skip_serializing_if = "Option::is_none")] + permissions: Option<i32>, }, TextChannel { #[serde(rename = "_id")] @@ -61,10 +66,16 @@ pub enum Channel { name: String, #[serde(skip_serializing_if = "Option::is_none")] description: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] icon: Option<File>, #[serde(skip_serializing_if = "Option::is_none")] last_message: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + default_permissions: Option<i32>, + #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] + role_permissions: HashMap<String, i32> }, VoiceChannel { #[serde(rename = "_id")] @@ -78,6 +89,11 @@ pub enum Channel { description: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] icon: Option<File>, + + #[serde(skip_serializing_if = "Option::is_none")] + default_permissions: Option<i32>, + #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] + role_permissions: HashMap<String, i32> }, } diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs index 0c325b7..9f312f7 100644 --- a/src/database/entities/server.rs +++ b/src/database/entities/server.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::database::*; use crate::notifications::events::ClientboundNotification; use crate::util::result::{Error, Result}; @@ -25,6 +27,23 @@ pub struct Member { pub nickname: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option<File>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub roles: Option<Vec<String>> +} + +pub type PermissionTuple = ( + i32, // server permission + i32 // channel permission +); + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Role { + pub name: String, + pub permissions: PermissionTuple, + #[serde(skip_serializing_if = "Option::is_none")] + pub colour: Option<String> + // Bri'ish API conventions } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -59,10 +78,15 @@ pub struct Server { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option<String>, + pub channels: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")] pub system_messages: Option<SystemMessageChannels>, + #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] + pub roles: HashMap<String, Role>, + pub default_permissions: PermissionTuple, + #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option<File>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/database/migrations/scripts.rs b/src/database/migrations/scripts.rs index a5d5ece..770c109 100644 --- a/src/database/migrations/scripts.rs +++ b/src/database/migrations/scripts.rs @@ -1,11 +1,8 @@ -use crate::database::{get_collection, get_db}; +use crate::database::{permissions, get_collection, get_db, PermissionTuple}; use futures::StreamExt; use log::info; -use mongodb::{ - bson::{doc, from_document}, - options::FindOptions, -}; +use mongodb::{bson::{doc, from_document, to_document}, options::FindOptions}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -14,7 +11,7 @@ struct MigrationInfo { revision: i32, } -pub const LATEST_REVISION: i32 = 5; +pub const LATEST_REVISION: i32 = 6; pub async fn migrate_database() { let migrations = get_collection("migrations"); @@ -157,6 +154,33 @@ pub async fn run_migrations(revision: i32) -> i32 { .expect("Failed to create channel_invites collection."); } + if revision <= 5 { + info!("Running migration [revision 5 / 2021-06-26]: Add permissions."); + + #[derive(Serialize)] + struct Server { + pub default_permissions: PermissionTuple, + } + + let server = Server { + default_permissions: ( + *permissions::server::DEFAULT_PERMISSION as i32, + *permissions::channel::DEFAULT_PERMISSION_SERVER as i32 + ) + }; + + get_collection("servers") + .update_many( + doc! { }, + doc! { + "$set": to_document(&server).unwrap() + }, + None + ) + .await + .expect("Failed to migrate servers."); + } + // Reminder to update LATEST_REVISION when adding new migrations. LATEST_REVISION } diff --git a/src/database/permissions/channel.rs b/src/database/permissions/channel.rs index 712bb96..f303090 100644 --- a/src/database/permissions/channel.rs +++ b/src/database/permissions/channel.rs @@ -1,5 +1,5 @@ use crate::database::*; -use crate::util::result::Result; +use crate::util::result::{Error, Result}; use super::PermissionCalculator; @@ -19,6 +19,25 @@ pub enum ChannelPermission { UploadFiles = 0b00000000000000000000000010000000, // 128 } +lazy_static! { + pub static ref DEFAULT_PERMISSION_DM: u32 = + ChannelPermission::View + + ChannelPermission::SendMessage + + ChannelPermission::ManageChannel + + ChannelPermission::VoiceCall + + ChannelPermission::InviteOthers + + ChannelPermission::EmbedLinks + + ChannelPermission::UploadFiles; + + pub static ref DEFAULT_PERMISSION_SERVER: u32 = + ChannelPermission::View + + ChannelPermission::SendMessage + + ChannelPermission::VoiceCall + + ChannelPermission::InviteOthers + + ChannelPermission::EmbedLinks + + ChannelPermission::UploadFiles; +} + impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 }); impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 }); @@ -62,11 +81,7 @@ impl<'a> PermissionCalculator<'a> { let perms = self.for_user(recipient).await?; if perms.get_send_message() { - return Ok(ChannelPermission::View - + ChannelPermission::SendMessage - + ChannelPermission::VoiceCall - + ChannelPermission::EmbedLinks - + ChannelPermission::UploadFiles); + return Ok(*DEFAULT_PERMISSION_DM); } return Ok(ChannelPermission::View as u32); @@ -75,36 +90,61 @@ impl<'a> PermissionCalculator<'a> { Ok(0) } - Channel::Group { recipients, .. } => { + Channel::Group { recipients, permissions, owner, .. } => { + if &self.perspective.id == owner { + return Ok(*DEFAULT_PERMISSION_DM) + } + if recipients .iter() .find(|x| *x == &self.perspective.id) .is_some() { - Ok(ChannelPermission::View - + ChannelPermission::SendMessage - + ChannelPermission::ManageChannel - + ChannelPermission::VoiceCall - + ChannelPermission::InviteOthers - + ChannelPermission::EmbedLinks - + ChannelPermission::UploadFiles) + if let Some(permissions) = permissions { + Ok(permissions.clone() as u32) + } else { + Ok(*DEFAULT_PERMISSION_DM) + } } else { Ok(0) } } - Channel::TextChannel { server, .. } - | Channel::VoiceChannel { server, .. } => { + Channel::TextChannel { server, default_permissions, role_permissions, .. } + | Channel::VoiceChannel { server, default_permissions, role_permissions, .. } => { let server = Ref::from_unchecked(server.clone()).fetch_server().await?; if self.perspective.id == server.owner { Ok(u32::MAX) } else { - Ok(ChannelPermission::View - + ChannelPermission::SendMessage - + ChannelPermission::VoiceCall - + ChannelPermission::InviteOthers - + ChannelPermission::EmbedLinks - + ChannelPermission::UploadFiles) + match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await { + Ok(member) => { + let mut perm = if let Some(permission) = default_permissions { + *permission as u32 + } else { + server.default_permissions.1 as u32 + }; + + if let Some(roles) = member.roles { + for role in roles { + if let Some(permission) = role_permissions.get(&role) { + perm |= *permission as u32; + } + + if let Some(server_role) = server.roles.get(&role) { + perm |= server_role.permissions.1 as u32; + } + } + } + + Ok(perm) + } + Err(error) => { + match &error { + Error::NotFound => Ok(0), + _ => Err(error) + } + } + } } } } diff --git a/src/database/permissions/mod.rs b/src/database/permissions/mod.rs index 9219906..d41e3a0 100644 --- a/src/database/permissions/mod.rs +++ b/src/database/permissions/mod.rs @@ -13,6 +13,7 @@ pub struct PermissionCalculator<'a> { relationship: Option<&'a RelationshipStatus>, channel: Option<&'a Channel>, server: Option<&'a Server>, + // member: Option<&'a Member>, has_mutual_connection: bool, } @@ -26,6 +27,7 @@ impl<'a> PermissionCalculator<'a> { relationship: None, channel: None, server: None, + // member: None, has_mutual_connection: false, } @@ -59,6 +61,13 @@ impl<'a> PermissionCalculator<'a> { } } + /* pub fn with_member(self, member: &'a Member) -> PermissionCalculator { + PermissionCalculator { + member: Some(&member), + ..self + } + } */ + pub fn with_mutual_connection(self) -> PermissionCalculator<'a> { PermissionCalculator { has_mutual_connection: true, diff --git a/src/database/permissions/server.rs b/src/database/permissions/server.rs index e0cad15..611d770 100644 --- a/src/database/permissions/server.rs +++ b/src/database/permissions/server.rs @@ -1,6 +1,7 @@ -use crate::util::result::Result; +use crate::util::result::{Error, Result}; use super::PermissionCalculator; +use super::Ref; use num_enum::TryFromPrimitive; use std::ops; @@ -22,6 +23,13 @@ pub enum ServerPermission { // 16 bits of space } +lazy_static! { + pub static ref DEFAULT_PERMISSION: u32 = + ServerPermission::View + + ServerPermission::ChangeNickname + + ServerPermission::ChangeAvatar; +} + impl_op_ex!(+ |a: &ServerPermission, b: &ServerPermission| -> u32 { *a as u32 | *b as u32 }); impl_op_ex_commutative!(+ |a: &u32, b: &ServerPermission| -> u32 { *a | *b as u32 }); @@ -52,9 +60,26 @@ impl<'a> PermissionCalculator<'a> { if self.perspective.id == server.owner { Ok(u32::MAX) } else { - Ok(ServerPermission::View - + ServerPermission::ChangeNickname - + ServerPermission::ChangeAvatar) + match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await { + Ok(member) => { + let mut perm = server.default_permissions.0 as u32; + if let Some(roles) = member.roles { + for role in roles { + if let Some(server_role) = server.roles.get(&role) { + perm |= server_role.permissions.0 as u32; + } + } + } + + Ok(perm) + } + Err(error) => { + match &error { + Error::NotFound => Ok(0), + _ => Err(error) + } + } + } } } diff --git a/src/routes/channels/group_create.rs b/src/routes/channels/group_create.rs index 1746e78..19d5bca 100644 --- a/src/routes/channels/group_create.rs +++ b/src/routes/channels/group_create.rs @@ -73,6 +73,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> { recipients: set.into_iter().collect::<Vec<String>>(), icon: None, last_message: None, + permissions: None }; channel.clone().publish().await?; diff --git a/src/routes/servers/channel_create.rs b/src/routes/servers/channel_create.rs index c96d1cd..3d8c2c3 100644 --- a/src/routes/servers/channel_create.rs +++ b/src/routes/servers/channel_create.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::database::*; use crate::util::result::{Error, Result}; @@ -76,6 +78,9 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue> description: info.description, icon: None, last_message: None, + + default_permissions: None, + role_permissions: HashMap::new() }, ChannelType::Voice => Channel::VoiceChannel { id: id.clone(), @@ -84,7 +89,10 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue> name: info.name, description: info.description, - icon: None + icon: None, + + default_permissions: None, + role_permissions: HashMap::new() } }; diff --git a/src/routes/servers/server_create.rs b/src/routes/servers/server_create.rs index b1adb2b..29425e4 100644 --- a/src/routes/servers/server_create.rs +++ b/src/routes/servers/server_create.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::database::*; use crate::util::result::{Error, Result}; @@ -51,6 +53,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> { name: info.name, description: info.description, + channels: vec![cid.clone()], system_messages: Some(SystemMessageChannels { user_joined: Some(cid.clone()), @@ -59,6 +62,12 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> { user_banned: Some(cid.clone()), }), + roles: HashMap::new(), + default_permissions: ( + *permissions::server::DEFAULT_PERMISSION as i32, + *permissions::channel::DEFAULT_PERMISSION_SERVER as i32 + ), + icon: None, banner: None, }; @@ -69,8 +78,12 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> { nonce: Some(info.nonce), name: "general".to_string(), description: None, + icon: None, last_message: None, + + default_permissions: None, + role_permissions: HashMap::new() } .publish() .await?; diff --git a/src/version.rs b/src/version.rs index 25c37d4..e0cea56 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "0.5.1-alpha.0-patch.0"; +pub const VERSION: &str = "0.5.1-alpha.1"; -- GitLab