feat(core/permissions): initial commit

This commit is contained in:
Paul Makles
2023-04-23 22:11:59 +01:00
parent 39fec310f9
commit f633fccbca
9 changed files with 897 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
[package]
name = "revolt-permissions"
version = "0.0.1"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
description = "Revolt Backend: Permission Logic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde" ]
schemas = [ "dep:schemars" ]
try-from-primitive = [ "dep:num_enum" ]
[dev-dependencies]
# Async
async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Utility
auto_ops = "0.3.0"
once_cell = "1.17"
num_enum = { version = "0.6.1", optional = true }
# Async
async-trait = "0.1.51"
# Serialisation
serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }

View File

@@ -0,0 +1,128 @@
use crate::{
ChannelPermission, ChannelType, PermissionQuery, PermissionValue, RelationshipStatus,
UserPermission, ALLOW_IN_TIMEOUT, DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY,
};
/// Calculate permissions against a user
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> u32 {
if query.are_we_privileged().await {
return u32::MAX;
}
if query.are_the_users_same().await {
return u32::MAX;
}
let mut permissions = 0_u32;
match query.user_relationship().await {
RelationshipStatus::Friend => return u32::MAX,
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return UserPermission::Access as u32
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
}
_ => {}
}
if query.have_mutual_connection().await {
permissions = UserPermission::Access + UserPermission::ViewProfile;
if query.user_is_bot().await || query.are_we_a_bot().await {
permissions += UserPermission::SendMessage as u32;
}
permissions
} else {
permissions
}
}
/// Calculate permissions against a server
pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await || query.are_we_server_owner().await {
return ChannelPermission::GrantAllSafe.into();
}
if !query.are_we_a_member().await {
return 0_u64.into();
}
let mut permissions: PermissionValue = query.get_default_server_permissions().await.into();
for role_override in query.get_our_server_role_overrides().await {
permissions.apply(role_override);
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
}
/// Calculate permissions against a channel
pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await {
return ChannelPermission::GrantAllSafe.into();
}
match query.get_channel_type().await {
ChannelType::SavedMessages => {
if query.do_we_own_the_channel().await {
DEFAULT_PERMISSION_SAVED_MESSAGES.into()
} else {
0_u64.into()
}
}
ChannelType::DirectMessage => {
if query.are_we_part_of_the_channel().await {
query.set_recipient_as_user().await;
let permissions = calculate_user_permissions(query).await;
if (permissions & UserPermission::SendMessage as u32)
== UserPermission::SendMessage as u32
{
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
}
} else {
0_u64.into()
}
}
ChannelType::Group => {
if query.do_we_own_the_channel().await {
ChannelPermission::GrantAllSafe.into()
} else if query.are_we_part_of_the_channel().await {
(*DEFAULT_PERMISSION_VIEW_ONLY
| query.get_default_channel_permissions().await.allow)
.into()
} else {
0_u64.into()
}
}
ChannelType::ServerChannel => {
query.set_server_from_channel().await;
if query.are_we_a_member().await {
let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_channel_permissions().await);
for role_override in query.get_our_channel_role_overrides().await {
permissions.apply(role_override);
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
} else {
0_u64.into()
}
}
ChannelType::Unknown => 0_u64.into(),
}
}

View File

@@ -0,0 +1,16 @@
#[macro_use]
extern crate auto_ops;
#[macro_use]
extern crate async_trait;
mod r#impl;
mod models;
mod r#trait;
pub use models::*;
pub use r#impl::*;
pub use r#trait::*;
#[cfg(test)]
mod test;

View File

@@ -0,0 +1,137 @@
use once_cell::sync::Lazy;
use std::ops::Add;
/// Abstract channel type
pub enum ChannelType {
SavedMessages,
DirectMessage,
Group,
ServerChannel,
Unknown,
}
/// Permission value on Revolt
///
/// This should be restricted to the lower 52 bits to prevent any
/// potential issues with Javascript. Also leave empty spaces for
/// future permission flags to be added.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))]
#[repr(u64)]
pub enum ChannelPermission {
// * Generic permissions
/// Manage the channel or channels on the server
ManageChannel = 1 << 0,
/// Manage the server
ManageServer = 1 << 1,
/// Manage permissions on servers or channels
ManagePermissions = 1 << 2,
/// Manage roles on server
ManageRole = 1 << 3,
/// Manage server customisation (includes emoji)
ManageCustomisation = 1 << 4,
// % 1 bit reserved
// * Member permissions
/// Kick other members below their ranking
KickMembers = 1 << 6,
/// Ban other members below their ranking
BanMembers = 1 << 7,
/// Timeout other members below their ranking
TimeoutMembers = 1 << 8,
/// Assign roles to members below their ranking
AssignRoles = 1 << 9,
/// Change own nickname
ChangeNickname = 1 << 10,
/// Change or remove other's nicknames below their ranking
ManageNicknames = 1 << 11,
/// Change own avatar
ChangeAvatar = 1 << 12,
/// Remove other's avatars below their ranking
RemoveAvatars = 1 << 13,
// % 7 bits reserved
// * Channel permissions
/// View a channel
ViewChannel = 1 << 20,
/// Read a channel's past message history
ReadMessageHistory = 1 << 21,
/// Send a message in a channel
SendMessage = 1 << 22,
/// Delete messages in a channel
ManageMessages = 1 << 23,
/// Manage webhook entries on a channel
ManageWebhooks = 1 << 24,
/// Create invites to this channel
InviteOthers = 1 << 25,
/// Send embedded content in this channel
SendEmbeds = 1 << 26,
/// Send attachments and media in this channel
UploadFiles = 1 << 27,
/// Masquerade messages using custom nickname and avatar
Masquerade = 1 << 28,
/// React to messages with emojis
React = 1 << 29,
// * Voice permissions
/// Connect to a voice channel
Connect = 1 << 30,
/// Speak in a voice call
Speak = 1 << 31,
/// Share video in a voice call
Video = 1 << 32,
/// Mute other members with lower ranking in a voice call
MuteMembers = 1 << 33,
/// Deafen other members with lower ranking in a voice call
DeafenMembers = 1 << 34,
/// Move members between voice channels
MoveMembers = 1 << 35,
// * Misc. permissions
// % Bits 36 to 52: free area
// % Bits 53 to 64: do not use
// * Grant all permissions
/// Safely grant all permissions
GrantAllSafe = 0x000F_FFFF_FFFF_FFFF,
/// Grant all permissions
GrantAll = u64::MAX,
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 });
impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 });
pub static ALLOW_IN_TIMEOUT: Lazy<u64> =
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
pub static DEFAULT_PERMISSION_VIEW_ONLY: Lazy<u64> =
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION_VIEW_ONLY.add(
ChannelPermission::SendMessage
+ ChannelPermission::InviteOthers
+ ChannelPermission::SendEmbeds
+ ChannelPermission::UploadFiles
+ ChannelPermission::Connect
+ ChannelPermission::Speak,
)
});
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React)
});
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION.add(
ChannelPermission::React
+ ChannelPermission::ChangeNickname
+ ChannelPermission::ChangeAvatar,
)
});

View File

@@ -0,0 +1,58 @@
mod channel;
mod server;
mod user;
pub use channel::*;
pub use server::*;
pub use user::*;
/// Holds a permission value to manipulate.
#[derive(Debug)]
pub struct PermissionValue(u64);
impl PermissionValue {
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.allow(v.allow);
self.revoke(v.deny);
}
/// Allow given permissions
pub fn allow(&mut self, v: u64) {
self.0 |= v;
}
/// Revoke given permissions
pub fn revoke(&mut self, v: u64) {
self.0 &= !v;
}
/// Restrict to given permissions
pub fn restrict(&mut self, v: u64) {
self.0 &= v;
}
}
impl From<i64> for PermissionValue {
fn from(v: i64) -> Self {
Self(v as u64)
}
}
impl From<u64> for PermissionValue {
fn from(v: u64) -> Self {
Self(v)
}
}
impl From<PermissionValue> for u64 {
fn from(v: PermissionValue) -> Self {
v.0
}
}
impl From<ChannelPermission> for PermissionValue {
fn from(v: ChannelPermission) -> Self {
(v as u64).into()
}
}

View File

@@ -0,0 +1,56 @@
/// Representation of a single permission override
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Override {
/// Allow bit flags
pub allow: u64,
/// Disallow bit flags
pub deny: u64,
}
/// Representation of a single permission override
/// as it appears on models and in the database
#[derive(/*JsonSchema, */ Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OverrideField {
/// Allow bit flags
a: i64,
/// Disallow bit flags
d: i64,
}
impl Override {
/// Into allows
pub fn allows(&self) -> u64 {
self.allow
}
/// Into denies
pub fn denies(&self) -> u64 {
self.deny
}
}
impl From<Override> for OverrideField {
fn from(v: Override) -> Self {
Self {
a: v.allow as i64,
d: v.deny as i64,
}
}
}
impl From<OverrideField> for Override {
fn from(v: OverrideField) -> Self {
Self {
allow: v.a as u64,
deny: v.d as u64,
}
}
}
/*impl From<OverrideField> for Bson {
fn from(v: OverrideField) -> Self {
Self::Document(bson::to_document(&v).unwrap())
}
}*/

View File

@@ -0,0 +1,25 @@
/// User's relationship with another user (or themselves)
pub enum RelationshipStatus {
None,
User,
Friend,
Outgoing,
Incoming,
Blocked,
BlockedOther,
}
/// User permission definitions
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))]
#[repr(u32)]
pub enum UserPermission {
Access = 1 << 0,
ViewProfile = 1 << 1,
SendMessage = 1 << 2,
Invite = 1 << 3,
}
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });

View File

@@ -0,0 +1,377 @@
use crate::{
calculate_channel_permissions, calculate_user_permissions, ChannelPermission, ChannelType,
Override, PermissionQuery, RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_SERVER, DEFAULT_PERMISSION_VIEW_ONLY,
};
#[async_std::test]
async fn validate_user_permissions() {
/// Scenario in which we are friends with a user
/// and we have a DM channel open with them
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_user_permissions(&mut query).await;
assert_eq!(perms, u32::MAX);
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(value, *DEFAULT_PERMISSION_DIRECT_MESSAGE);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
false
}
async fn are_the_users_same(&mut self) -> bool {
false
}
async fn user_relationship(&mut self) -> RelationshipStatus {
RelationshipStatus::Friend
}
async fn user_is_bot(&mut self) -> bool {
false
}
async fn have_mutual_connection(&mut self) -> bool {
false
}
async fn are_we_server_owner(&mut self) -> bool {
unreachable!()
}
async fn are_we_a_member(&mut self) -> bool {
unreachable!()
}
async fn get_default_server_permissions(&mut self) -> u64 {
unreachable!()
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn are_we_timed_out(&mut self) -> bool {
unreachable!()
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::DirectMessage
}
async fn get_default_channel_permissions(&mut self) -> Override {
unreachable!()
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn do_we_own_the_channel(&mut self) -> bool {
unreachable!()
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
true
}
async fn set_recipient_as_user(&mut self) {
// no-op
}
async fn set_server_from_channel(&mut self) {
unreachable!()
}
}
}
#[async_std::test]
async fn validate_group_permissions() {
/// Scenario in which we are in a group channel with only talking permission
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(
value,
*DEFAULT_PERMISSION_VIEW_ONLY | ChannelPermission::SendMessage as u64
);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
unreachable!()
}
async fn are_the_users_same(&mut self) -> bool {
unreachable!()
}
async fn user_relationship(&mut self) -> RelationshipStatus {
unreachable!()
}
async fn user_is_bot(&mut self) -> bool {
unreachable!()
}
async fn have_mutual_connection(&mut self) -> bool {
unreachable!()
}
async fn are_we_server_owner(&mut self) -> bool {
unreachable!()
}
async fn are_we_a_member(&mut self) -> bool {
unreachable!()
}
async fn get_default_server_permissions(&mut self) -> u64 {
unreachable!()
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn are_we_timed_out(&mut self) -> bool {
unreachable!()
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::Group
}
async fn get_default_channel_permissions(&mut self) -> Override {
Override {
allow: ChannelPermission::SendMessage as u64,
deny: 0,
}
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn do_we_own_the_channel(&mut self) -> bool {
false
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
true
}
async fn set_recipient_as_user(&mut self) {
unreachable!()
}
async fn set_server_from_channel(&mut self) {
unreachable!()
}
}
}
#[async_std::test]
async fn validate_server_permissions() {
/// Scenario in which we are in a server channel where:
/// - the server grants reading history and sending messages by default
/// - we have a role that allows us to upload files and react but denies reading history
/// - however the channel disallows sending messages
/// - and removes our role specific react permission
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(
value,
ChannelPermission::ViewChannel as u64 | ChannelPermission::UploadFiles as u64
);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
unreachable!()
}
async fn are_the_users_same(&mut self) -> bool {
unreachable!()
}
async fn user_relationship(&mut self) -> RelationshipStatus {
unreachable!()
}
async fn user_is_bot(&mut self) -> bool {
unreachable!()
}
async fn have_mutual_connection(&mut self) -> bool {
unreachable!()
}
async fn are_we_server_owner(&mut self) -> bool {
false
}
async fn are_we_a_member(&mut self) -> bool {
true
}
async fn get_default_server_permissions(&mut self) -> u64 {
ChannelPermission::ViewChannel as u64
| ChannelPermission::SendMessage as u64
| ChannelPermission::ReadMessageHistory as u64
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
vec![Override {
allow: ChannelPermission::UploadFiles as u64 | ChannelPermission::React as u64,
deny: ChannelPermission::ReadMessageHistory as u64,
}]
}
async fn are_we_timed_out(&mut self) -> bool {
false
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel
}
async fn get_default_channel_permissions(&mut self) -> Override {
Override {
allow: 0,
deny: ChannelPermission::SendMessage as u64,
}
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
vec![Override {
allow: 0,
deny: ChannelPermission::React as u64,
}]
}
async fn do_we_own_the_channel(&mut self) -> bool {
unreachable!()
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
unreachable!()
}
async fn set_recipient_as_user(&mut self) {
unreachable!()
}
async fn set_server_from_channel(&mut self) {
// no-op
}
}
}
#[async_std::test]
async fn validate_timed_out_member() {
/// Scenario in which we are in a server that we have been timed out from
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(value, *DEFAULT_PERMISSION_VIEW_ONLY);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
unreachable!()
}
async fn are_the_users_same(&mut self) -> bool {
unreachable!()
}
async fn user_relationship(&mut self) -> RelationshipStatus {
unreachable!()
}
async fn user_is_bot(&mut self) -> bool {
unreachable!()
}
async fn have_mutual_connection(&mut self) -> bool {
unreachable!()
}
async fn are_we_server_owner(&mut self) -> bool {
false
}
async fn are_we_a_member(&mut self) -> bool {
true
}
async fn get_default_server_permissions(&mut self) -> u64 {
*DEFAULT_PERMISSION_SERVER
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn are_we_timed_out(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel
}
async fn get_default_channel_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn do_we_own_the_channel(&mut self) -> bool {
unreachable!()
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
unreachable!()
}
async fn set_recipient_as_user(&mut self) {
unreachable!()
}
async fn set_server_from_channel(&mut self) {
// no-op
}
}
}

View File

@@ -0,0 +1,67 @@
use crate::{ChannelType, Override, RelationshipStatus};
#[async_trait]
pub trait PermissionQuery {
// * For calculating user permission
/// Is our perspective user privileged?
async fn are_we_privileged(&mut self) -> bool;
/// Is our perspective user a bot?
async fn are_we_a_bot(&mut self) -> bool;
/// Is our perspective user and the currently selected user the same?
async fn are_the_users_same(&mut self) -> bool;
/// Get the relationship with have with the currently selected user
async fn user_relationship(&mut self) -> RelationshipStatus;
/// Whether the currently selected user is a bot
async fn user_is_bot(&mut self) -> bool;
/// Do we have a mutual connection with the currently selected user?
async fn have_mutual_connection(&mut self) -> bool;
// * For calculating server permission
/// Is our perspective user the server's owner?
async fn are_we_server_owner(&mut self) -> bool;
/// Is our perspective user a member of the server?
async fn are_we_a_member(&mut self) -> bool;
/// Get default server permission
async fn get_default_server_permissions(&mut self) -> u64;
/// Get the ordered role overrides (from lowest to highest) for this member in this server
async fn get_our_server_role_overrides(&mut self) -> Vec<Override>;
/// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool;
// * For calculating channel permission
/// Get the type of the channel
async fn get_channel_type(&mut self) -> ChannelType;
/// Get the default channel permissions
/// Group channel defaults should be mapped to an allow-only override
async fn get_default_channel_permissions(&mut self) -> Override;
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override>;
/// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool;
/// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool;
/// Set the current user as the recipient of this channel
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
async fn set_recipient_as_user(&mut self);
/// Set the current server as the server owning this channel
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self);
}