From cbd3d0fa1911d5f677b695b6cbc4a4be5797ed08 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 11 Feb 2025 10:32:44 +1300 Subject: [PATCH] Add 7 new management commands: listadmins, listmoderators, grantmoderator, revokemoderator listusers, grantuser, revokeuser --- src/lib.rs | 5 ++ src/web/management/auth.rs | 8 +-- src/web/management/mod.rs | 139 +++++++++++++++++++++++++++++-------- 3 files changed, 119 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a722524..eb44a1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -904,3 +904,8 @@ pub fn is_moderator(pubkey: Pubkey) -> bool { Ok(Some(moderator)) => moderator, } } + +/// Is the pubkey an admin? +pub fn is_admin(pubkey: Pubkey) -> bool { + GLOBALS.config.read().admin_keys.contains(&pubkey) +} diff --git a/src/web/management/auth.rs b/src/web/management/auth.rs index 3e076f7..7fd80e6 100644 --- a/src/web/management/auth.rs +++ b/src/web/management/auth.rs @@ -5,15 +5,15 @@ use http::header::AUTHORIZATION; use http_body_util::BodyExt; use hyper::body::Incoming; use hyper::Request; -use pocket_types::Event; +use pocket_types::{Event, Pubkey}; use secp256k1::hashes::{sha256, Hash}; use serde_json::Value; -fn s_err(s: &str) -> Result { +fn s_err(s: &str) -> Result<(Pubkey, Value), Error> { Err(ChorusError::ManagementAuthFailure(s.to_owned()).into()) } -pub async fn check_auth(request: Request) -> Result { +pub async fn check_auth(request: Request) -> Result<(Pubkey, Value), Error> { // Must be POST if request.method() != hyper::Method::POST { return s_err("Management RPC only supports POST method"); @@ -112,5 +112,5 @@ pub async fn check_auth(request: Request) -> Result { return s_err("Authorization event payload missing"); } - Ok(serde_json::from_slice(&body)?) + Ok((event.pubkey(), serde_json::from_slice(&body)?)) } diff --git a/src/web/management/mod.rs b/src/web/management/mod.rs index 2adf201..5571d65 100644 --- a/src/web/management/mod.rs +++ b/src/web/management/mod.rs @@ -32,8 +32,8 @@ pub async fn handle( _peer: HashedPeer, request: Request, ) -> Result>, Error> { - let command: Value = match auth::check_auth(request).await { - Ok(v) => v, + let (pubkey, command) = match auth::check_auth(request).await { + Ok((pk, v)) => (pk, v), Err(e) => { let result = json!({ "result": {}, @@ -43,7 +43,7 @@ pub async fn handle( } }; - match handle_inner(command) { + match handle_inner(pubkey, command) { Ok(Some(value)) => respond(value, StatusCode::OK), Ok(None) => { let result = json!({ @@ -80,7 +80,7 @@ pub async fn handle( } } -pub fn handle_inner(command: Value) -> Result, Error> { +pub fn handle_inner(pubkey: Pubkey, command: Value) -> Result, Error> { let obj = match command.as_object() { Some(o) => o, None => return Err(ChorusError::BadRequest("Command was not a JSON object").into()), @@ -107,11 +107,16 @@ pub fn handle_inner(command: Value) -> Result, Error> { "listbannedpubkeys", "supportedmethods", "numconnections", - "uptime" + "uptime", + "listadmins", + "listmoderators", + "grantmoderator", + "revokemoderator", + "listusers", + "grantuser", + "revokeuser", ] }))), - - // Pubkeys "banpubkey" => { let pk = get_pubkey_param(obj)?; crate::mark_pubkey_approval(GLOBALS.store.get().unwrap(), pk, false)?; @@ -154,7 +159,6 @@ pub fn handle_inner(command: Value) -> Result, Error> { "result": pubkeys }))) } - // Events "banevent" => { let id = get_id_param(obj)?; crate::mark_event_approval(GLOBALS.store.get().unwrap(), id, false)?; @@ -197,39 +201,116 @@ pub fn handle_inner(command: Value) -> Result, Error> { "result": ids }))) } - - "listeventsneedingmoderation" => Err(ChorusError::NotImplemented.into()), - - // Kinds - "allowkind" => Err(ChorusError::NotImplemented.into()), - "disallowkind" => Err(ChorusError::NotImplemented.into()), - "listbannedkinds" => Err(ChorusError::NotImplemented.into()), - "listallowedkinds" => Err(ChorusError::NotImplemented.into()), - - // IP addresses - "blockip" => Err(ChorusError::NotImplemented.into()), - "unblockip" => Err(ChorusError::NotImplemented.into()), - "listblockedips" => Err(ChorusError::NotImplemented.into()), - - // Config - "changerelayname" => Err(ChorusError::NotImplemented.into()), - "changerelaydescription" => Err(ChorusError::NotImplemented.into()), - "changerelayicon" => Err(ChorusError::NotImplemented.into()), - - // System "numconnections" => { let num = &GLOBALS.num_connections; Ok(Some(json!({ "result": num, }))) } - "uptime" => { let uptime_in_secs = GLOBALS.start_time.elapsed().as_secs(); Ok(Some(json!({ "result": uptime_in_secs, }))) } + "listadmins" => { + let keys = GLOBALS.config.read().admin_hex_keys.clone(); + Ok(Some(json!({ + "result": keys + }))) + } + "listmoderators" => { + let moderators: Vec = + crate::dump_authorized_users(GLOBALS.store.get().unwrap())? + .iter() + .filter_map(|(pk, moderator)| { + if *moderator { + Some(pk.as_hex_string()) + } else { + None + } + }) + .collect(); + Ok(Some(json!({ + "result": moderators + }))) + } + "grantmoderator" => { + if !crate::is_admin(pubkey) { + Ok(Some(json!({ + "result": {}, + "error": "Unauthorized: Only admins can grant moderator status" + }))) + } else { + let pk = get_pubkey_param(obj)?; + crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, true)?; + Ok(None) + } + } + "revokemoderator" => { + if !crate::is_admin(pubkey) { + Ok(Some(json!({ + "result": {}, + "error": "Unauthorized: Only admins can revoke moderator status" + }))) + } else { + let pk = get_pubkey_param(obj)?; + + // Do not do this if they aren't already an authorized user + if !crate::is_authorized_user(pk) { + Ok(None) + } else { + crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, false)?; + Ok(None) + } + } + } + "listusers" => { + let users: Vec = crate::dump_authorized_users(GLOBALS.store.get().unwrap())? + .iter() + .map(|(pk, _moderator)| pk.as_hex_string()) + .collect(); + Ok(Some(json!({ + "result": users + }))) + } + "grantuser" => { + if !crate::is_admin(pubkey) { + Ok(Some(json!({ + "result": {}, + "error": "Unauthorized: Only admins can grant user status" + }))) + } else { + let pk = get_pubkey_param(obj)?; + crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, false)?; + Ok(None) + } + } + "revokeuser" => { + if !crate::is_admin(pubkey) { + Ok(Some(json!({ + "result": {}, + "error": "Unauthorized: Only admins can revoke user status" + }))) + } else { + let pk = get_pubkey_param(obj)?; + crate::rm_authorized_user(GLOBALS.store.get().unwrap(), pk)?; + Ok(None) + } + } + + // Commands we do not support (yet) + "listeventsneedingmoderation" => Err(ChorusError::NotImplemented.into()), + "allowkind" => Err(ChorusError::NotImplemented.into()), + "disallowkind" => Err(ChorusError::NotImplemented.into()), + "listbannedkinds" => Err(ChorusError::NotImplemented.into()), + "listallowedkinds" => Err(ChorusError::NotImplemented.into()), + "blockip" => Err(ChorusError::NotImplemented.into()), + "unblockip" => Err(ChorusError::NotImplemented.into()), + "listblockedips" => Err(ChorusError::NotImplemented.into()), + "changerelayname" => Err(ChorusError::NotImplemented.into()), + "changerelaydescription" => Err(ChorusError::NotImplemented.into()), + "changerelayicon" => Err(ChorusError::NotImplemented.into()), _ => Err(ChorusError::NotImplemented.into()), }