Skip to main content

max / makenotwork

mt: forbid banning or muting the platform admin The platform admin is a config identity, not a community role, so in a community they don't own their role is None and the owner/mod target protections (which key on role) let a mod ban or mute them out of it. Reject a ban/mute whose target is the configured platform_admin_id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-07 15:08 UTC
Commit: b473090aee7c979ad36948bd596da2887db81344
Parent: 0116a14
2 files changed, +56 insertions, -0 deletions
@@ -224,6 +224,14 @@ pub(super) async fn ban_user_handler(
224 224
225 225 let target_id = get_user_by_username(&state.db, form.username.trim()).await?;
226 226
227 + // The platform admin is a config identity, not a community role, so in a
228 + // community they don't own their role is None and the owner/mod protections
229 + // below don't cover them — a mod could ban them out of the community. Reject
230 + // it explicitly (their mod actions still bypass bans anyway).
231 + if state.config.platform_admin_id == Some(target_id) {
232 + return Err((StatusCode::FORBIDDEN, "Cannot ban the platform administrator.").into_response());
233 + }
234 +
227 235 // Prevent banning owners
228 236 let target_role = get_role(&state.db, target_id, community.id).await?;
229 237
@@ -300,6 +308,12 @@ pub(super) async fn mute_user_handler(
300 308
301 309 let target_id = get_user_by_username(&state.db, form.username.trim()).await?;
302 310
311 + // See ban_user_handler: the platform admin holds no community role, so guard
312 + // them explicitly against a mod's mute.
313 + if state.config.platform_admin_id == Some(target_id) {
314 + return Err((StatusCode::FORBIDDEN, "Cannot mute the platform administrator.").into_response());
315 + }
316 +
303 317 // Prevent muting owners
304 318 let target_role = get_role(&state.db, target_id, community.id).await?;
305 319
@@ -210,6 +210,48 @@ async fn suspended_community_blocks_mod_mutations(_pool: sqlx::PgPool) {
210 210 assert!(!mt_db::queries::is_user_muted(&h.db, community_id, victim).await.unwrap());
211 211 }
212 212
213 + /// Regression (fuzz-2026-07-06 MED — mod can ban the platform admin): the
214 + /// platform admin is a config identity with no community role, so the owner/mod
215 + /// target-protection guards (which key on role) don't cover them. A mod must not
216 + /// be able to ban or mute them out of a community.
217 + #[sqlx::test]
218 + async fn mod_cannot_ban_or_mute_platform_admin(_pool: sqlx::PgPool) {
219 + let admin_id = uuid::Uuid::new_v4();
220 + let mut h = TestHarness::new_with_admin(admin_id).await;
221 +
222 + // The platform admin needs a users row so a mod can name them by username.
223 + sqlx::query(
224 + "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'theadmin', 'Admin')",
225 + )
226 + .bind(admin_id)
227 + .execute(&h.db)
228 + .await
229 + .unwrap();
230 +
231 + let owner = h.login_as("banadminowner").await;
232 + let community_id = h.create_community("Test", "test").await;
233 + h.add_membership(owner, community_id, "owner").await;
234 +
235 + let moduser = h.login_as("banadminmod").await;
236 + h.add_membership(moduser, community_id, "moderator").await;
237 +
238 + h.client.get("/p/test/moderation").await;
239 + let ban = h.client.post_form(
240 + "/p/test/moderation/ban",
241 + "username=theadmin&duration=permanent",
242 + ).await;
243 + assert_eq!(ban.status, StatusCode::FORBIDDEN, "a mod must not ban the platform admin");
244 +
245 + let mute = h.client.post_form(
246 + "/p/test/moderation/mute",
247 + "username=theadmin&duration=permanent",
248 + ).await;
249 + assert_eq!(mute.status, StatusCode::FORBIDDEN, "a mod must not mute the platform admin");
250 +
251 + assert!(!mt_db::queries::is_user_banned(&h.db, community_id, admin_id).await.unwrap());
252 + assert!(!mt_db::queries::is_user_muted(&h.db, community_id, admin_id).await.unwrap());
253 + }
254 +
213 255 #[sqlx::test]
214 256 async fn mod_cannot_ban_other_mod(_pool: sqlx::PgPool) {
215 257 let mut h = TestHarness::new().await;