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