From 958099d68435a5b3a3116d43bc285b29da392c05 Mon Sep 17 00:00:00 2001 From: Colin Reeder Date: Tue, 12 Jan 2021 00:05:20 -0700 Subject: [PATCH] Only allow removing moderators newer than you --- .../20210112062722_more-timestamps/down.sql | 4 + .../20210112062722_more-timestamps/up.sql | 4 + res/lang/en.ftl | 1 + src/apub_util/ingest.rs | 4 +- src/routes/api/communities.rs | 102 +++++++++++++----- 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 migrations/20210112062722_more-timestamps/down.sql create mode 100644 migrations/20210112062722_more-timestamps/up.sql diff --git a/migrations/20210112062722_more-timestamps/down.sql b/migrations/20210112062722_more-timestamps/down.sql new file mode 100644 index 0000000..db4e53a --- /dev/null +++ b/migrations/20210112062722_more-timestamps/down.sql @@ -0,0 +1,4 @@ +BEGIN; + ALTER TABLE community DROP COLUMN created_local; + ALTER TABLE community_moderator DROP COLUMN created_local; +COMMIT; diff --git a/migrations/20210112062722_more-timestamps/up.sql b/migrations/20210112062722_more-timestamps/up.sql new file mode 100644 index 0000000..1a28a0a --- /dev/null +++ b/migrations/20210112062722_more-timestamps/up.sql @@ -0,0 +1,4 @@ +BEGIN; + ALTER TABLE community ADD COLUMN created_local TIMESTAMPTZ; + ALTER TABLE community_moderator ADD COLUMN created_local TIMESTAMPTZ; +COMMIT; diff --git a/res/lang/en.ftl b/res/lang/en.ftl index fbacec9..bef8c3f 100644 --- a/res/lang/en.ftl +++ b/res/lang/en.ftl @@ -3,6 +3,7 @@ comment_empty = Comment may not be empty comment_not_yours = That's not your comment community_edit_denied = You are not authorized to modify this community community_moderators_not_local = Community moderators can only be listed for local communities +community_moderators_remove_must_be_older = You can only remove moderators that are newer than you community_name_disallowed_chars = Community name contains disallowed characters email_content_forgot_password = Hi { $username }, if you requested a password reset from lotide, use this code: { $key } email_not_configured = Email is not configured on this server diff --git a/src/apub_util/ingest.rs b/src/apub_util/ingest.rs index 95cb9c6..baf7d6e 100644 --- a/src/apub_util/ingest.rs +++ b/src/apub_util/ingest.rs @@ -184,7 +184,7 @@ pub async fn ingest_object( if let Some(row) = row { let local: bool = row.get(0); if local { - db.execute("INSERT INTO community_follow (community, follower, local, ap_id, accepted) VALUES ($1, $2, FALSE, $3, TRUE) ON CONFLICT (community, follower) DO NOTHING", &[&community_id, &follower_local_id, &activity_ap_id.as_str()]).await?; + db.execute("INSERT INTO community_follow (community, follower, local, ap_id, accepted, created_local) VALUES ($1, $2, FALSE, $3, TRUE, current_timestamp) ON CONFLICT (community, follower) DO NOTHING", &[&community_id, &follower_local_id, &activity_ap_id.as_str()]).await?; crate::apub_util::spawn_enqueue_send_community_follow_accept( community_id, @@ -235,7 +235,7 @@ pub async fn ingest_object( .and_then(|key| key.signature_algorithm.as_deref()); let id = CommunityLocalID(db.query_one( - "INSERT INTO community (name, local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description_html) VALUES ($1, FALSE, $2, $3, $4, $5, $6, $7) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description_html=$7 RETURNING id", + "INSERT INTO community (name, local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description_html, created_local) VALUES ($1, FALSE, $2, $3, $4, $5, $6, $7, current_timestamp) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description_html=$7 RETURNING id", &[&name, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description_html], ).await?.get(0)); diff --git a/src/routes/api/communities.rs b/src/routes/api/communities.rs index ceeafda..a201189 100644 --- a/src/routes/api/communities.rs +++ b/src/routes/api/communities.rs @@ -27,6 +27,13 @@ struct RespYourFollowInfo { accepted: bool, } +#[derive(Serialize)] +struct RespModeratorInfo<'a> { + #[serde(flatten)] + base: RespMinimalAuthorInfo<'a>, + moderator_since: Option, +} + fn get_community_description_fields<'a>( description_text: &'a str, description_html: Option<&'a str>, @@ -210,7 +217,7 @@ async fn route_unstable_communities_create( let row = trans .query_one( - "INSERT INTO community (name, local, private_key, public_key, created_by) VALUES ($1, TRUE, $2, $3, $4) RETURNING id", + "INSERT INTO community (name, local, private_key, public_key, created_by, created_local) VALUES ($1, TRUE, $2, $3, $4, current_timestamp) RETURNING id", &[&body.name, &private_key, &public_key, &user.raw()], ) .await?; @@ -219,7 +226,7 @@ async fn route_unstable_communities_create( trans .execute( - "INSERT INTO community_moderator (community, person) VALUES ($1, $2)", + "INSERT INTO community_moderator (community, person, created_local) VALUES ($1, $2, current_timestamp)", &[&community_id, &user], ) .await?; @@ -464,7 +471,7 @@ async fn route_unstable_communities_moderators_list( })?; let rows = db.query( - "SELECT id, username, local, ap_id, avatar FROM person WHERE id IN (SELECT person FROM community_moderator WHERE community=$1)", + "SELECT person.id, person.username, person.local, person.ap_id, person.avatar, community_moderator.created_local FROM person, community_moderator WHERE person.id = community_moderator.person AND community_moderator.community = $1 ORDER BY community_moderator.created_local ASC NULLS FIRST", &[&community_id], ).await?; @@ -475,15 +482,21 @@ async fn route_unstable_communities_moderators_list( let local = row.get(2); let ap_id = row.get(3); - RespMinimalAuthorInfo { - id, - username: Cow::Borrowed(row.get(1)), - local, - host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname), - remote_url: ap_id.map(|x| x.into()), - avatar: row.get::<_, Option<&str>>(4).map(|url| RespAvatarInfo { - url: ctx.process_avatar_href(url, id), - }), + let moderator_since: Option> = row.get(5); + + RespModeratorInfo { + base: RespMinimalAuthorInfo { + id, + username: Cow::Borrowed(row.get(1)), + local, + host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname), + remote_url: ap_id.map(|x| x.into()), + avatar: row.get::<_, Option<&str>>(4).map(|url| RespAvatarInfo { + url: ctx.process_avatar_href(url, id), + }), + }, + + moderator_since: moderator_since.map(|time| time.to_rfc3339()), } }) .collect(); @@ -545,7 +558,7 @@ async fn route_unstable_communities_moderators_add( })?; db.execute( - "INSERT INTO community_moderator (community, person) VALUES ($1, $2)", + "INSERT INTO community_moderator (community, person, created_local) VALUES ($1, $2, current_timestamp)", &[&community_id, &user_id], ) .await?; @@ -560,34 +573,65 @@ async fn route_unstable_communities_moderators_remove( ) -> Result, crate::Error> { let (community_id, user_id) = params; - let db = ctx.db_pool.get().await?; + let mut db = ctx.db_pool.get().await?; let lang = crate::get_lang_for_req(&req); let login_user = crate::require_login(&req, &db).await?; - ({ - let row = db - .query_opt( - "SELECT 1 FROM community_moderator WHERE community=$1 AND person=$2", - &[&community_id, &login_user], - ) - .await?; - match row { + let self_moderator_row = db + .query_opt( + "SELECT created_local FROM community_moderator WHERE community=$1 AND person=$2", + &[&community_id, &login_user], + ) + .await?; + + let self_moderator_since: Option> = ({ + match self_moderator_row { None => Err(crate::Error::UserError(crate::simple_response( hyper::StatusCode::FORBIDDEN, lang.tr("must_be_moderator", None).into_owned(), ))), - Some(_) => Ok(()), + Some(row) => Ok(row.get(0)), } })?; - db.execute( - "DELETE FROM community_moderator WHERE community=$1 AND person=$2", - &[&community_id, &user_id], - ) - .await?; + { + let trans = db.transaction().await?; + let row = trans.query_opt( + "DELETE FROM community_moderator WHERE community=$1 AND person=$2 RETURNING (created_local >= $3)", + &[&community_id, &user_id, &self_moderator_since], + ) + .await?; - Ok(crate::empty_response()) + let is_allowed = match self_moderator_since { + None => true, // self was moderator before timestamps existed, can remove anyone + Some(_) => { + match row { + None => true, // was already removed, ok + Some(row) => { + let res: Option = row.get(0); + match res { + None => false, // other has no timestamp, must be older + Some(value) => value, + } + } + } + } + }; + + if is_allowed { + trans.commit().await?; + Ok(crate::empty_response()) + } else { + trans.rollback().await?; + + Err(crate::Error::UserError(crate::simple_response( + hyper::StatusCode::FORBIDDEN, + lang.tr("community_moderators_remove_must_be_older", None) + .into_owned(), + ))) + } + } } async fn route_unstable_communities_unfollow( -- 2.30.1