Repository: python-discord/bot

Issue:
Moderation commands don't compare role hierachy of caller and "moderated"
**Originally posted by Thomas Petersson:**

Since the bot straight out bans, kicks, etc. after posting an infraction, won't it be possible for moderators to ban admins. Given the bots role is above admins.  This makes it more of a trust based implementation over restricted.  

In my opinion admins should not be able to moderate other admins, and moderators should definitely not be able to moderate admins (owners are above the bot in the hierarchy and can't be effected)

![attempted murder](https://i.imgur.com/JjnP1BO.png)

If it was not for the fact that joseph is an owner (and super duper owner) and the bot is under owner role in the hierarchy that may have very well gone through. (We were curious, didn't intend to try to ban Joseph)


Discussion:
**Comment from Johannes Christ:**

This is a honeypot to find untrustworthy mods. Don't tell the other mods. :secret:
Should this be enforced for all moderation commands or just a subset?

Resolving patch:
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -132,6 +132,11 @@ async def kick(self, ctx: Context, user: Member, *, reason: str = None):
         **`reason`:** The reason for the kick.
         """
 
+        if not await self.respect_role_hierarchy(ctx, user, 'kick'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(ctx, user, type="kick", reason=reason)
         if response_object is None:
             return
@@ -187,6 +192,12 @@ async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str
         **`reason`:** The reason for the ban.
         """
 
+        member = ctx.guild.get_member(user.id)
+        if not await self.respect_role_hierarchy(ctx, member, 'ban'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(ctx, user, type="ban", reason=reason)
         if response_object is None:
             return
@@ -370,6 +381,12 @@ async def tempban(
         **`reason`:** The reason for the temporary ban.
         """
 
+        member = ctx.guild.get_member(user.id)
+        if not await self.respect_role_hierarchy(ctx, member, 'tempban'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(
             ctx, user, type="ban", reason=reason, duration=duration
         )
@@ -475,6 +492,11 @@ async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None):
         **`reason`:** The reason for the kick.
         """
 
+        if not await self.respect_role_hierarchy(ctx, user, 'shadowkick'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True)
         if response_object is None:
             return
@@ -523,6 +545,12 @@ async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reaso
         **`reason`:** The reason for the ban.
         """
 
+        member = ctx.guild.get_member(user.id)
+        if not await self.respect_role_hierarchy(ctx, member, 'shadowban'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True)
         if response_object is None:
             return
@@ -662,6 +690,12 @@ async def shadow_tempban(
         **`reason`:** The reason for the temporary ban.
         """
 
+        member = ctx.guild.get_member(user.id)
+        if not await self.respect_role_hierarchy(ctx, member, 'shadowtempban'):
+            # Ensure ctx author has a higher top role than the target user
+            # Warning is sent to ctx by the helper method
+            return
+
         response_object = await post_infraction(
             ctx, user, type="ban", reason=reason, duration=duration, hidden=True
         )
@@ -1334,6 +1368,29 @@ async def __error(self, ctx, error):
             if User in error.converters:
                 await ctx.send(str(error.errors[0]))
 
+    async def respect_role_hierarchy(self, ctx: Context, target: Member, infraction_type: str) -> bool:
+        """
+        Check if the highest role of the invoking member is greater than that of the target member
+
+        If this check fails, a warning is sent to the invoking ctx
+
+        Implement as a method rather than a check in order to avoid having to reimplement parameter
+        checks & conversions in a dedicated check decorater
+        """
+
+        actor = ctx.author
+        target_is_lower = target.top_role < actor.top_role
+        if not target_is_lower:
+            log.info(
+                f"{actor} ({actor.id}) attempted to {infraction_type} "
+                f"{target} ({target.id}), who has an equal or higher top role"
+            )
+            await ctx.send(
+                f":x: {actor.mention}, you may not {infraction_type} someone with an equal or higher top role"
+            )
+
+        return target_is_lower
+
 
 def setup(bot):
     bot.add_cog(Moderation(bot))


Test patch: