Skip to content

Commit 2971ccd

Browse files
author
Mark Saroufim
authored
Add user ban system for blocking submissions (#471)
* Add user ban system for blocking submissions Adds is_banned column to user_info, ban check in prepare_submission (blocks all entry points: Discord, CLI, Web), admin Discord commands (/admin ban, /admin unban), and admin API endpoints (POST/DELETE /admin/ban/{user_id}). * Fix test mock to set is_user_banned=False for submission tests
1 parent 01a0072 commit 2971ccd

File tree

6 files changed

+152
-0
lines changed

6 files changed

+152
-0
lines changed

src/kernelbot/api/main.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,32 @@ async def _stream_submission_response(
426426
pass
427427

428428

429+
@app.post("/admin/ban/{user_id}")
430+
async def admin_ban_user(
431+
user_id: str,
432+
_: Annotated[None, Depends(require_admin)],
433+
db_context=Depends(get_db),
434+
) -> dict:
435+
with db_context as db:
436+
found = db.ban_user(user_id)
437+
if not found:
438+
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
439+
return {"status": "ok", "user_id": user_id, "banned": True}
440+
441+
442+
@app.delete("/admin/ban/{user_id}")
443+
async def admin_unban_user(
444+
user_id: str,
445+
_: Annotated[None, Depends(require_admin)],
446+
db_context=Depends(get_db),
447+
) -> dict:
448+
with db_context as db:
449+
found = db.unban_user(user_id)
450+
if not found:
451+
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
452+
return {"status": "ok", "user_id": user_id, "banned": False}
453+
454+
429455
@app.post("/{leaderboard_name}/{gpu_type}/{submission_mode}")
430456
async def run_submission( # noqa: C901
431457
leaderboard_name: str,

src/kernelbot/cogs/admin_cog.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ def __init__(self, bot: "ClusterBot"):
123123
name="set-forum-ids", description="Sets forum IDs"
124124
)(self.set_forum_ids)
125125

126+
self.ban_user = bot.admin_group.command(
127+
name="ban", description="Ban a user from making submissions"
128+
)(self.ban_user)
129+
130+
self.unban_user = bot.admin_group.command(
131+
name="unban", description="Unban a user"
132+
)(self.unban_user)
133+
126134
self.export_to_hf = bot.admin_group.command(
127135
name="export-hf", description="Export competition data to Hugging Face dataset"
128136
)(self.export_to_hf)
@@ -154,6 +162,44 @@ async def is_creator_check(
154162
return True
155163
return False
156164

165+
@discord.app_commands.describe(user_id="Discord user ID to ban")
166+
@with_error_handling
167+
async def ban_user(self, interaction: discord.Interaction, user_id: str):
168+
if not await self.admin_check(interaction):
169+
await send_discord_message(
170+
interaction, "You need to have Admin permissions to run this command", ephemeral=True
171+
)
172+
return
173+
174+
with self.bot.leaderboard_db as db:
175+
if db.ban_user(user_id):
176+
await send_discord_message(
177+
interaction, f"User `{user_id}` has been banned.", ephemeral=True
178+
)
179+
else:
180+
await send_discord_message(
181+
interaction, f"User `{user_id}` not found.", ephemeral=True
182+
)
183+
184+
@discord.app_commands.describe(user_id="Discord user ID to unban")
185+
@with_error_handling
186+
async def unban_user(self, interaction: discord.Interaction, user_id: str):
187+
if not await self.admin_check(interaction):
188+
await send_discord_message(
189+
interaction, "You need to have Admin permissions to run this command", ephemeral=True
190+
)
191+
return
192+
193+
with self.bot.leaderboard_db as db:
194+
if db.unban_user(user_id):
195+
await send_discord_message(
196+
interaction, f"User `{user_id}` has been unbanned.", ephemeral=True
197+
)
198+
else:
199+
await send_discord_message(
200+
interaction, f"User `{user_id}` not found.", ephemeral=True
201+
)
202+
157203
@discord.app_commands.describe(
158204
directory="Directory of the kernel definition. Also used as the leaderboard's name",
159205
gpu="The GPU to submit to. Leave empty for interactive selection/multiple GPUs",

src/libkernelbot/leaderboard_db.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,59 @@ def validate_cli_id(self, cli_id: str) -> Optional[dict[str, str]]:
14451445
raise KernelBotError("Error validating CLI ID") from e
14461446

14471447

1448+
def ban_user(self, user_id: str) -> bool:
1449+
"""Ban a user by their ID. Returns True if the user was found and banned."""
1450+
try:
1451+
self.cursor.execute(
1452+
"""
1453+
UPDATE leaderboard.user_info
1454+
SET is_banned = TRUE
1455+
WHERE id = %s
1456+
""",
1457+
(str(user_id),),
1458+
)
1459+
self.connection.commit()
1460+
return self.cursor.rowcount > 0
1461+
except psycopg2.Error as e:
1462+
self.connection.rollback()
1463+
logger.exception("Error banning user %s", user_id, exc_info=e)
1464+
raise KernelBotError("Error banning user") from e
1465+
1466+
def unban_user(self, user_id: str) -> bool:
1467+
"""Unban a user by their ID. Returns True if the user was found and unbanned."""
1468+
try:
1469+
self.cursor.execute(
1470+
"""
1471+
UPDATE leaderboard.user_info
1472+
SET is_banned = FALSE
1473+
WHERE id = %s
1474+
""",
1475+
(str(user_id),),
1476+
)
1477+
self.connection.commit()
1478+
return self.cursor.rowcount > 0
1479+
except psycopg2.Error as e:
1480+
self.connection.rollback()
1481+
logger.exception("Error unbanning user %s", user_id, exc_info=e)
1482+
raise KernelBotError("Error unbanning user") from e
1483+
1484+
def is_user_banned(self, user_id: str) -> bool:
1485+
"""Check if a user is banned."""
1486+
try:
1487+
self.cursor.execute(
1488+
"""
1489+
SELECT is_banned FROM leaderboard.user_info
1490+
WHERE id = %s
1491+
""",
1492+
(str(user_id),),
1493+
)
1494+
row = self.cursor.fetchone()
1495+
return row[0] if row else False
1496+
except psycopg2.Error as e:
1497+
self.connection.rollback()
1498+
logger.exception("Error checking ban status for user %s", user_id, exc_info=e)
1499+
raise KernelBotError("Error checking ban status") from e
1500+
14481501
def set_rate_limit(self, leaderboard_name: str, mode_category: str, max_per_hour: int) -> RateLimitItem:
14491502
try:
14501503
self.cursor.execute(

src/libkernelbot/submission.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def prepare_submission( # noqa: C901
4949
"The bot is currently not accepting any new submissions, please try again later."
5050
)
5151

52+
with backend.db as db:
53+
if db.is_user_banned(str(req.user_id)):
54+
raise KernelBotError("You are banned from making submissions.")
55+
5256
if profanity.contains_profanity(req.file_name):
5357
raise KernelBotError("Please provide a non-rude filename")
5458

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
add_is_banned_to_user_info
3+
"""
4+
5+
from yoyo import step
6+
7+
__depends__ = {'20260317_01_rate-limits'}
8+
9+
steps = [
10+
step(
11+
# forward
12+
"""
13+
ALTER TABLE leaderboard.user_info
14+
ADD COLUMN is_banned BOOLEAN NOT NULL DEFAULT FALSE
15+
""",
16+
# backward
17+
"""
18+
ALTER TABLE leaderboard.user_info
19+
DROP COLUMN is_banned;
20+
"""
21+
)
22+
]

tests/test_submission.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def mock_backend():
3131
"name": "test_board",
3232
}
3333
db_context.get_leaderboard_gpu_types.return_value = ["A100", "V100"]
34+
db_context.is_user_banned.return_value = False
3435

3536
return backend
3637

0 commit comments

Comments
 (0)