Skip to content

Commit b8ce188

Browse files
Rewrite permission checks (#325)
Co-authored-by: jamesbt365 <jamesbt365@gmail.com>
1 parent cb75eda commit b8ce188

File tree

9 files changed

+255
-76
lines changed

9 files changed

+255
-76
lines changed

src/builtins/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ pub async fn on_error<U, E: std::fmt::Display + std::fmt::Debug>(
173173
ctx.send(CreateReply::default().content(response).ephemeral(true))
174174
.await?;
175175
}
176+
crate::FrameworkError::PermissionFetchFailed { ctx } => {
177+
ctx.say("An error occurred when fetching permissions.")
178+
.await?;
179+
}
176180
crate::FrameworkError::NotAnOwner { ctx } => {
177181
let response = "Only bot owners can call this command";
178182
ctx.send(CreateReply::default().content(response).ephemeral(true))

src/dispatch/common.rs

Lines changed: 16 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,6 @@
22
33
use crate::serenity_prelude as serenity;
44

5-
/// Retrieves user permissions in the given channel. If unknown, returns None. If in DMs, returns
6-
/// `Permissions::all()`.
7-
async fn user_permissions(
8-
ctx: &serenity::Context,
9-
guild_id: Option<serenity::GuildId>,
10-
channel_id: serenity::ChannelId,
11-
user_id: serenity::UserId,
12-
) -> Option<serenity::Permissions> {
13-
let guild_id = match guild_id {
14-
Some(x) => x,
15-
None => return Some(serenity::Permissions::all()), // no permission checks in DMs
16-
};
17-
18-
let guild = guild_id.to_partial_guild(ctx).await.ok()?;
19-
20-
// Use to_channel so that it can fallback on HTTP for threads (which aren't in cache usually)
21-
let channel = match channel_id.to_channel(ctx).await {
22-
Ok(serenity::Channel::Guild(channel)) => channel,
23-
Ok(_other_channel) => {
24-
tracing::warn!(
25-
"guild message was supposedly sent in a non-guild channel. Denying invocation"
26-
);
27-
return None;
28-
}
29-
Err(_) => return None,
30-
};
31-
32-
let member = guild.member(ctx, user_id).await.ok()?;
33-
34-
Some(guild.user_permissions_in(&channel, &member))
35-
}
36-
37-
/// Retrieves the set of permissions that are lacking, relative to the given required permission set
38-
///
39-
/// Returns None if permissions couldn't be retrieved
40-
async fn missing_permissions<U, E>(
41-
ctx: crate::Context<'_, U, E>,
42-
user: serenity::UserId,
43-
required_permissions: serenity::Permissions,
44-
) -> Option<serenity::Permissions> {
45-
if required_permissions.is_empty() {
46-
return Some(serenity::Permissions::empty());
47-
}
48-
49-
let permissions = user_permissions(
50-
ctx.serenity_context(),
51-
ctx.guild_id(),
52-
ctx.channel_id(),
53-
user,
54-
)
55-
.await;
56-
Some(required_permissions - permissions?)
57-
}
58-
595
/// See [`check_permissions_and_cooldown`]. Runs the check only for a single command. The caller
606
/// should call this multiple time for each parent command to achieve the check inheritance logic.
617
async fn check_permissions_and_cooldown_single<'a, U, E>(
@@ -111,35 +57,29 @@ async fn check_permissions_and_cooldown_single<'a, U, E>(
11157
}
11258

11359
// Make sure that user has required permissions
114-
match missing_permissions(ctx, ctx.author().id, cmd.required_permissions).await {
115-
Some(missing_permissions) if missing_permissions.is_empty() => {}
116-
Some(missing_permissions) => {
117-
return Err(crate::FrameworkError::MissingUserPermissions {
118-
ctx,
119-
missing_permissions: Some(missing_permissions),
120-
})
121-
}
122-
// Better safe than sorry: when perms are unknown, restrict access
123-
None => {
60+
if let Some((user_missing_permissions, bot_missing_permissions)) =
61+
super::permissions::calculate_missing(
62+
ctx,
63+
cmd.required_permissions,
64+
cmd.required_bot_permissions,
65+
)
66+
.await
67+
{
68+
if !user_missing_permissions.is_empty() {
12469
return Err(crate::FrameworkError::MissingUserPermissions {
12570
ctx,
126-
missing_permissions: None,
127-
})
71+
missing_permissions: Some(user_missing_permissions),
72+
});
12873
}
129-
}
13074

131-
// Before running any pre-command checks, make sure the bot has the permissions it needs
132-
match missing_permissions(ctx, ctx.framework().bot_id(), cmd.required_bot_permissions).await {
133-
Some(missing_permissions) if missing_permissions.is_empty() => {}
134-
Some(missing_permissions) => {
75+
if !bot_missing_permissions.is_empty() {
13576
return Err(crate::FrameworkError::MissingBotPermissions {
13677
ctx,
137-
missing_permissions,
138-
})
78+
missing_permissions: bot_missing_permissions,
79+
});
13980
}
140-
// When in doubt, just let it run. Not getting fancy missing permissions errors is better
141-
// than the command not executing at all
142-
None => {}
81+
} else {
82+
return Err(crate::FrameworkError::PermissionFetchFailed { ctx });
14383
}
14484

14585
// Only continue if command checks returns true

src/dispatch/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Contains all code to dispatch incoming events onto framework commands
22
33
mod common;
4+
mod permissions;
45
mod prefix;
56
mod slash;
67

src/dispatch/permissions.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Module for calculating permission checks for commands
2+
3+
use crate::serenity_prelude as serenity;
4+
5+
mod application;
6+
mod prefix;
7+
8+
/// Simple POD type to hold the results of permission lookups.
9+
struct PermissionsInfo {
10+
/// The Permissions of the author, if requested.
11+
author_permissions: Option<serenity::Permissions>,
12+
/// The Permissions of the bot, if requested.
13+
bot_permissions: Option<serenity::Permissions>,
14+
}
15+
16+
impl PermissionsInfo {
17+
/// Returns the fake permissions info from a DM.
18+
fn dm_permissions() -> Self {
19+
Self {
20+
author_permissions: Some(serenity::Permissions::dm_permissions()),
21+
bot_permissions: Some(serenity::Permissions::dm_permissions()),
22+
}
23+
}
24+
}
25+
26+
/// Retrieves the permissions for the context author and the bot.
27+
async fn get_author_and_bot_permissions<U, E>(
28+
ctx: crate::Context<'_, U, E>,
29+
skip_author: bool,
30+
skip_bot: bool,
31+
) -> Option<PermissionsInfo> {
32+
// No permission checks in DMs.
33+
let Some(guild_id) = ctx.guild_id() else {
34+
return Some(PermissionsInfo::dm_permissions());
35+
};
36+
37+
match ctx {
38+
crate::Context::Application(ctx) => {
39+
Some(application::get_author_and_bot_permissions(ctx.interaction))
40+
}
41+
crate::Context::Prefix(ctx) => {
42+
prefix::get_author_and_bot_permissions(ctx, guild_id, skip_author, skip_bot).await
43+
}
44+
}
45+
}
46+
47+
/// Retrieves the set of permissions that are lacking, relative to the given required permission set
48+
///
49+
/// Returns None if permissions couldn't be retrieved.
50+
pub(super) async fn calculate_missing<U, E>(
51+
ctx: crate::Context<'_, U, E>,
52+
author_required_permissions: serenity::Permissions,
53+
bot_required_permissions: serenity::Permissions,
54+
) -> Option<(serenity::Permissions, serenity::Permissions)> {
55+
// If both user and bot are None, return empty permissions
56+
if author_required_permissions.is_empty() && bot_required_permissions.is_empty() {
57+
return Some((
58+
serenity::Permissions::empty(),
59+
serenity::Permissions::empty(),
60+
));
61+
}
62+
63+
// Fetch permissions, returning None if an error occurred
64+
let permissions = get_author_and_bot_permissions(
65+
ctx,
66+
author_required_permissions.is_empty(),
67+
bot_required_permissions.is_empty(),
68+
)
69+
.await?;
70+
71+
let author_missing_perms = permissions
72+
.author_permissions
73+
.map(|permissions| author_required_permissions - permissions)
74+
.unwrap_or_default();
75+
76+
let bot_missing_perms = permissions
77+
.bot_permissions
78+
.map(|permissions| bot_required_permissions - permissions)
79+
.unwrap_or_default();
80+
81+
Some((author_missing_perms, bot_missing_perms))
82+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Application command permissions calculation
2+
use crate::serenity_prelude as serenity;
3+
4+
use super::PermissionsInfo;
5+
6+
/// Gets the permissions of the ctx author and the bot.
7+
pub(super) fn get_author_and_bot_permissions(
8+
interaction: &serenity::CommandInteraction,
9+
) -> PermissionsInfo {
10+
let err = "member is Some if interaction is in guild";
11+
let author_member = interaction.member.as_ref().expect(err);
12+
13+
let err = "should always be some as inside interaction";
14+
let author_permissions = author_member.permissions.expect(err);
15+
16+
let err = "should always be some according to discord docs";
17+
let bot_permissions = interaction.app_permissions.expect(err);
18+
19+
PermissionsInfo {
20+
author_permissions: Some(author_permissions),
21+
bot_permissions: Some(bot_permissions),
22+
}
23+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! The cache variant of prefix permissions calculation
2+
3+
use crate::{serenity_prelude as serenity, PrefixContext};
4+
5+
use crate::dispatch::permissions::PermissionsInfo;
6+
7+
/// Gets the permissions of the ctx author and the bot.
8+
pub(in crate::dispatch::permissions) async fn get_author_and_bot_permissions<U, E>(
9+
ctx: PrefixContext<'_, U, E>,
10+
guild_id: serenity::GuildId,
11+
skip_author: bool,
12+
skip_bot: bool,
13+
) -> Option<PermissionsInfo> {
14+
// Should only fail if the guild is not cached, which is fair to bail on.
15+
let guild = ctx.cache().guild(guild_id)?;
16+
17+
let author_permissions = if skip_author {
18+
None
19+
} else {
20+
let err_msg = "should only fail if the guild is not cached";
21+
Some(ctx.msg.author_permissions(ctx.cache()).expect(err_msg))
22+
};
23+
24+
let bot_permissions = if skip_bot {
25+
None
26+
} else {
27+
let channel_id = ctx.channel_id();
28+
let bot_user_id = ctx.framework.bot_id();
29+
Some(get_bot_permissions(&guild, channel_id, bot_user_id)?)
30+
};
31+
32+
Some(PermissionsInfo {
33+
author_permissions,
34+
bot_permissions,
35+
})
36+
}
37+
38+
/// Gets the permissions for the bot.
39+
fn get_bot_permissions(
40+
guild: &serenity::Guild,
41+
channel_id: serenity::ChannelId,
42+
bot_id: serenity::UserId,
43+
) -> Option<serenity::Permissions> {
44+
// Should never fail, as the bot member is always cached
45+
let bot_member = guild.members.get(&bot_id)?;
46+
47+
if let Some(channel) = guild.channels.get(&channel_id) {
48+
Some(guild.user_permissions_in(channel, bot_member))
49+
} else if let Some(thread) = guild.threads.iter().find(|th| th.id == channel_id) {
50+
let err = "parent id should always be Some for thread";
51+
let parent_channel_id = thread.parent_id.expect(err);
52+
53+
let parent_channel = guild.channels.get(&parent_channel_id)?;
54+
Some(guild.user_permissions_in(parent_channel, bot_member))
55+
} else {
56+
// The message was either:
57+
// - Sent in a guild with broken caching
58+
// - Not set in a channel or thread?
59+
tracing::warn!("Could not find channel/thread ({channel_id}) for permissions check in cache for guild: {}", guild.id);
60+
None
61+
}
62+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//! The cache variant of prefix permissions calculation
2+
3+
use crate::{serenity_prelude as serenity, PrefixContext};
4+
5+
use crate::dispatch::permissions::PermissionsInfo;
6+
7+
/// Gets the permissions of the ctx author and the bot.
8+
pub(in crate::dispatch::permissions) async fn get_author_and_bot_permissions<U, E>(
9+
ctx: PrefixContext<'_, U, E>,
10+
guild_id: serenity::GuildId,
11+
skip_author: bool,
12+
skip_bot: bool,
13+
) -> Option<PermissionsInfo> {
14+
let http = ctx.http();
15+
let guild = guild_id.to_partial_guild(http).await.ok()?;
16+
let guild_channel = {
17+
let channel = ctx.http().get_channel(ctx.channel_id()).await.ok()?;
18+
channel.guild().expect("channel should be a guild channel")
19+
};
20+
21+
let bot_permissions = if skip_bot {
22+
None
23+
} else {
24+
let bot_member = guild.id.member(http, ctx.framework.bot_id).await.ok()?;
25+
Some(guild.user_permissions_in(&guild_channel, &bot_member))
26+
};
27+
28+
let author_permissions = if skip_author {
29+
None
30+
} else {
31+
let err = "should always be Some in MessageCreateEvent";
32+
let author_member = ctx.msg.member.as_ref().expect(err);
33+
Some(guild.partial_member_permissions_in(&guild_channel, ctx.author().id, author_member))
34+
};
35+
36+
Some(PermissionsInfo {
37+
author_permissions,
38+
bot_permissions,
39+
})
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//! Prefix command permissions calculation
2+
3+
#[cfg(feature = "cache")]
4+
mod cache;
5+
#[cfg(not(feature = "cache"))]
6+
mod http;
7+
8+
#[cfg(feature = "cache")]
9+
pub(super) use cache::get_author_and_bot_permissions;
10+
11+
#[cfg(not(feature = "cache"))]
12+
pub(super) use http::get_author_and_bot_permissions;

0 commit comments

Comments
 (0)