From 1c44e2d0997842480c47d256e77db246654b4c23 Mon Sep 17 00:00:00 2001 From: weisd Date: Sun, 12 Jan 2025 01:41:22 +0800 Subject: [PATCH] extract_claims --- Cargo.lock | 5 +- Cargo.toml | 4 +- ecstore/src/bucket/tagging/mod.rs | 12 +++-- iam/Cargo.toml | 1 + iam/src/auth.rs | 18 +++++-- iam/src/auth/credentials.rs | 90 +++++++++++++++++-------------- iam/src/error.rs | 9 ++++ iam/src/lib.rs | 20 ++++++- iam/src/manager.rs | 56 +++++++++++++++++-- iam/src/utils.rs | 15 +++++- madmin/src/lib.rs | 2 + madmin/src/user.rs | 52 ++++++++++++++++++ rustfs/src/admin/handlers.rs | 62 ++++++++++++++++++++- rustfs/src/admin/handlers/user.rs | 79 ++++++++++++++++++++++++++- rustfs/src/admin/mod.rs | 21 +++++++- 15 files changed, 380 insertions(+), 66 deletions(-) create mode 100644 madmin/src/user.rs diff --git a/Cargo.lock b/Cargo.lock index b62f39259..b9050a570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1243,6 +1243,7 @@ dependencies = [ "itertools 0.14.0", "jsonwebtoken", "log", + "madmin", "rand", "serde", "serde_json", @@ -2508,7 +2509,7 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "s3s" version = "0.11.0-dev" -source = "git+https://github.com/Nugine/s3s.git?rev=3291c0ca0284971569499cbe75bd69ef7bde8321#3291c0ca0284971569499cbe75bd69ef7bde8321" +source = "git+https://github.com/Nugine/s3s.git?rev=05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8#05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8" dependencies = [ "arrayvec", "async-trait", @@ -2555,7 +2556,7 @@ dependencies = [ [[package]] name = "s3s-policy" version = "0.11.0-dev" -source = "git+https://github.com/Nugine/s3s.git?rev=3291c0ca0284971569499cbe75bd69ef7bde8321#3291c0ca0284971569499cbe75bd69ef7bde8321" +source = "git+https://github.com/Nugine/s3s.git?rev=05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8#05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8" dependencies = [ "indexmap 2.6.0", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0a866530a..6777770a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,10 +65,10 @@ protos = { path = "./common/protos" } rand = "0.8.5" rmp = "0.8.14" rmp-serde = "1.3.0" -s3s = { git = "https://github.com/Nugine/s3s.git", rev = "3291c0ca0284971569499cbe75bd69ef7bde8321", default-features = true, features = [ +s3s = { git = "https://github.com/Nugine/s3s.git", rev = "05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8", default-features = true, features = [ "tower", ] } -s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "3291c0ca0284971569499cbe75bd69ef7bde8321" } +s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8" } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" tempfile = "3.13.0" diff --git a/ecstore/src/bucket/tagging/mod.rs b/ecstore/src/bucket/tagging/mod.rs index a7cc219cb..f50b85a7f 100644 --- a/ecstore/src/bucket/tagging/mod.rs +++ b/ecstore/src/bucket/tagging/mod.rs @@ -7,9 +7,13 @@ pub fn decode_tags(tags: &str) -> Vec { let mut list = Vec::new(); for (k, v) in values { + if k.is_empty() || v.is_empty() { + continue; + } + list.push(Tag { - key: k.to_string(), - value: v.to_string(), + key: Some(k.to_string()), + value: Some(v.to_string()), }); } @@ -20,7 +24,9 @@ pub fn encode_tags(tags: Vec) -> String { let mut encoded = form_urlencoded::Serializer::new(String::new()); for tag in tags.iter() { - encoded.append_pair(tag.key.as_str(), tag.value.as_str()); + if let (Some(k), Some(v)) = (tag.key.as_ref(), tag.value.as_ref()) { + encoded.append_pair(k.as_str(), v.as_str()); + } } encoded.finish() diff --git a/iam/Cargo.toml b/iam/Cargo.toml index cc9e7ceb7..4b6628fc9 100644 --- a/iam/Cargo.toml +++ b/iam/Cargo.toml @@ -28,6 +28,7 @@ rand.workspace = true base64-simd = "0.8.0" jsonwebtoken = "9.3.0" tracing.workspace = true +madmin.workspace = true [dev-dependencies] test-case.workspace = true diff --git a/iam/src/auth.rs b/iam/src/auth.rs index 679dd2bb7..79d8eaa31 100644 --- a/iam/src/auth.rs +++ b/iam/src/auth.rs @@ -1,15 +1,25 @@ mod credentials; pub use credentials::Credentials; -pub use credentials::CredentialsBuilder; +pub use credentials::*; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] pub struct UserIdentity { pub version: i64, pub credentials: Credentials, - pub update_at: OffsetDateTime, + pub update_at: Option, +} + +impl UserIdentity { + pub fn new(credentials: Credentials) -> Self { + UserIdentity { + version: 1, + credentials, + update_at: Some(OffsetDateTime::now_utc()), + } + } } impl From for UserIdentity { @@ -17,7 +27,7 @@ impl From for UserIdentity { UserIdentity { version: 1, credentials: value, - update_at: OffsetDateTime::now_utc(), + update_at: Some(OffsetDateTime::now_utc()), } } } diff --git a/iam/src/auth/credentials.rs b/iam/src/auth/credentials.rs index e5d47140a..59edd2b71 100644 --- a/iam/src/auth/credentials.rs +++ b/iam/src/auth/credentials.rs @@ -1,6 +1,8 @@ use crate::policy::{Policy, Validator}; use crate::service_type::ServiceType; +use crate::utils::extract_claims; use crate::{utils, Error}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::cell::LazyCell; @@ -13,8 +15,8 @@ const ACCESS_KEY_MAX_LEN: usize = 20; const SECRET_KEY_MIN_LEN: usize = 8; const SECRET_KEY_MAX_LEN: usize = 40; -const ACCOUNT_ON: &str = "on"; -const ACCOUNT_OFF: &str = "off"; +pub const ACCOUNT_ON: &str = "on"; +pub const ACCOUNT_OFF: &str = "off"; #[cfg_attr(test, derive(PartialEq, Eq, Debug))] struct CredentialHeader { @@ -89,7 +91,7 @@ impl TryFrom<&str> for CredentialHeader { } } -#[derive(Serialize, Deserialize, Clone, Default)] +#[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Credentials { pub access_key: String, pub secret_key: String, @@ -109,44 +111,6 @@ impl Credentials { Self::check_key_value(header) } - pub fn get_new_credentials_with_metadata( - claims: &T, - token_secret: &str, - exp: Option, - ) -> crate::Result { - let ak = utils::gen_access_key(20).unwrap_or_default(); - let sk = utils::gen_secret_key(32).unwrap_or_default(); - - Self::create_new_credentials_with_metadata(&ak, &sk, claims, token_secret, exp) - } - - pub fn create_new_credentials_with_metadata( - ak: &str, - sk: &str, - claims: &T, - token_secret: &str, - exp: Option, - ) -> crate::Result { - if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN { - return Err(Error::InvalidAccessKeyLength); - } - - if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN { - return Err(Error::InvalidAccessKeyLength); - } - - let token = utils::generate_jwt(claims, token_secret).map_err(Error::JWTError)?; - - Ok(Self { - access_key: ak.to_owned(), - secret_key: sk.to_owned(), - session_token: token, - status: ACCOUNT_ON.to_owned(), - expiration: exp.map(|v| OffsetDateTime::now_utc().saturating_add(Duration::seconds(v as i64))), - ..Default::default() - }) - } - pub fn check_key_value(_header: CredentialHeader) -> crate::Result { todo!() } @@ -186,6 +150,50 @@ impl Credentials { } } +pub fn get_new_credentials_with_metadata( + claims: &T, + token_secret: &str, + exp: Option, +) -> crate::Result { + let ak = utils::gen_access_key(20).unwrap_or_default(); + let sk = utils::gen_secret_key(32).unwrap_or_default(); + + create_new_credentials_with_metadata(&ak, &sk, claims, token_secret, exp) +} + +pub fn create_new_credentials_with_metadata( + ak: &str, + sk: &str, + claims: &T, + token_secret: &str, + exp: Option, +) -> crate::Result { + if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN { + return Err(Error::InvalidAccessKeyLength); + } + + if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN { + return Err(Error::InvalidAccessKeyLength); + } + + let token = utils::generate_jwt(claims, token_secret).map_err(Error::JWTError)?; + + Ok(Credentials { + access_key: ak.to_owned(), + secret_key: sk.to_owned(), + session_token: token, + status: ACCOUNT_ON.to_owned(), + expiration: exp.map(|v| OffsetDateTime::now_utc().saturating_add(Duration::seconds(v as i64))), + ..Default::default() + }) +} + +pub fn get_claims_from_token_with_secret(token: &str, secret: &str) -> crate::Result { + let ms = extract_claims::(token, secret).map_err(Error::JWTError)?; + // TODO SessionPolicyName + Ok(ms.claims) +} + #[derive(Default)] pub struct CredentialsBuilder { session_policy: Option, diff --git a/iam/src/error.rs b/iam/src/error.rs index 70117183e..6233b2266 100644 --- a/iam/src/error.rs +++ b/iam/src/error.rs @@ -37,6 +37,15 @@ pub enum Error { #[error("jwt err {0}")] JWTError(jsonwebtoken::errors::Error), + + #[error("no access key")] + NoAccessKey, + + #[error("invalid token")] + InvalidToken, + + #[error("invalid access_key")] + InvalidAccessKey, } pub type Result = std::result::Result; diff --git a/iam/src/lib.rs b/iam/src/lib.rs index 419e45105..a4e05f6c4 100644 --- a/iam/src/lib.rs +++ b/iam/src/lib.rs @@ -3,7 +3,10 @@ use ecstore::store::ECStore; use log::debug; use manager::IamCache; use policy::{Args, Policy}; -use std::sync::{Arc, OnceLock}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, +}; use store::object::ObjectStore; use time::OffsetDateTime; @@ -85,6 +88,19 @@ pub async fn add_service_account(cred: Credentials) -> crate::Result crate::Result> { +pub async fn check_key(ak: &str) -> crate::Result<(Option, bool)> { + if let Some(sys_cred) = get_global_action_cred() { + if sys_cred.access_key == ak { + return Ok((Some(UserIdentity::new(sys_cred)), true)); + } + } + get()?.check_key(ak).await +} + +pub async fn list_users() -> crate::Result> { + get()?.get_users().await +} + +pub async fn get_user(ak: &str) -> crate::Result<(Option, bool)> { get()?.check_key(ak).await } diff --git a/iam/src/manager.rs b/iam/src/manager.rs index efe57c779..a2e983eca 100644 --- a/iam/src/manager.rs +++ b/iam/src/manager.rs @@ -8,7 +8,7 @@ use std::{ }; use ecstore::store_err::is_err_object_not_found; -use log::debug; +use log::{debug, warn}; use time::OffsetDateTime; use tokio::{ select, @@ -188,7 +188,7 @@ where OffsetDateTime::now_utc(), ); - Ok(user_entiry.update_at) + Ok(user_entiry.update_at.unwrap_or(OffsetDateTime::now_utc())) } pub async fn is_allowed<'a>(&self, args: Args<'a>) -> bool { @@ -209,7 +209,7 @@ where Ok((u.clone(), None)) } - pub async fn check_key(&self, ak: &str) -> crate::Result> { + pub async fn check_key(&self, ak: &str) -> crate::Result<(Option, bool)> { let user = self .cache .users @@ -219,8 +219,14 @@ where .or_else(|| self.cache.sts_accounts.load().get(ak).cloned()); match user { - Some(u) if u.credentials.is_valid() => Ok(Some(u)), - _ => Ok(None), + Some(u) => { + if u.credentials.is_valid() { + Ok((Some(u), true)) + } else { + Ok((Some(u), false)) + } + } + _ => Ok((None, false)), } } pub async fn policy_db_get(&self, name: &str, _groups: Option>) -> crate::Result> { @@ -263,4 +269,44 @@ where Ok(()) } + + // returns all users (not STS or service accounts) + pub async fn get_users(&self) -> crate::Result> { + let mut m = HashMap::new(); + + let users = self.cache.users.load(); + let policies = self.cache.user_policies.load(); + let group_members = self.cache.user_group_memeberships.load(); + + for (k, v) in users.iter() { + warn!("k: {}, v: {:?}", k, v.credentials); + + if v.credentials.is_temp() || v.credentials.is_service_account() { + continue; + } + + let mut u = madmin::UserInfo { + status: if v.credentials.is_valid() { + madmin::AccountStatus::Enabled + } else { + madmin::AccountStatus::Disabled + }, + updated_at: v.update_at, + ..Default::default() + }; + + if let Some(p) = policies.get(k) { + u.policy_name = Some(p.policies.clone()); + u.updated_at = Some(p.update_at); + } + + if let Some(members) = group_members.get(k) { + u.member_of = Some(members.iter().cloned().collect()); + } + + m.insert(k.clone(), u); + } + + Ok(m) + } } diff --git a/iam/src/utils.rs b/iam/src/utils.rs index f3cd2260b..1297ef87e 100644 --- a/iam/src/utils.rs +++ b/iam/src/utils.rs @@ -1,7 +1,7 @@ use crate::Error; -use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; +use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header}; use rand::{Rng, RngCore}; -use serde::Serialize; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; pub fn gen_access_key(length: usize) -> crate::Result { const ALPHA_NUMERIC_TABLE: [char; 36] = [ @@ -45,6 +45,17 @@ pub fn generate_jwt(claims: &T, secret: &str) -> Result( + token: &str, + secret: &str, +) -> Result, jsonwebtoken::errors::Error> { + jsonwebtoken::decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &jsonwebtoken::Validation::new(Algorithm::HS512), + ) +} + #[cfg(test)] mod tests { use super::{gen_access_key, gen_secret_key}; diff --git a/madmin/src/lib.rs b/madmin/src/lib.rs index 4f8f29ed8..2a9572f9b 100644 --- a/madmin/src/lib.rs +++ b/madmin/src/lib.rs @@ -5,6 +5,8 @@ pub mod metrics; pub mod net; pub mod service_commands; pub mod trace; +pub mod user; pub mod utils; pub use info_commands::*; +pub use user::*; diff --git a/madmin/src/user.rs b/madmin/src/user.rs new file mode 100644 index 000000000..5e635e927 --- /dev/null +++ b/madmin/src/user.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub enum AccountStatus { + #[serde(rename = "enabled")] + Enabled, + #[serde(rename = "disabled")] + #[default] + Disabled, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum UserAuthType { + #[serde(rename = "builtin")] + Builtin, + #[serde(rename = "ldap")] + Ldap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserAuthInfo { + #[serde(rename = "type")] + pub auth_type: UserAuthType, + + #[serde(rename = "authServer", skip_serializing_if = "Option::is_none")] + pub auth_server: Option, + + #[serde(rename = "authServerUserID", skip_serializing_if = "Option::is_none")] + pub auth_server_user_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct UserInfo { + #[serde(rename = "userAuthInfo", skip_serializing_if = "Option::is_none")] + pub auth_info: Option, + + #[serde(rename = "secretKey", skip_serializing_if = "Option::is_none")] + pub secret_key: Option, + + #[serde(rename = "policyName", skip_serializing_if = "Option::is_none")] + pub policy_name: Option, + + #[serde(rename = "status")] + pub status: AccountStatus, + + #[serde(rename = "memberOf", skip_serializing_if = "Option::is_none")] + pub member_of: Option>, + + #[serde(rename = "updatedAt")] + pub updated_at: Option, +} diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index ec0280e45..50d1bba9f 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -22,7 +22,8 @@ use ecstore::GLOBAL_Endpoints; use futures::{Stream, StreamExt}; use http::Uri; use hyper::StatusCode; -use iam::get_global_action_cred; +use iam::auth::create_new_credentials_with_metadata; +use iam::{auth, get_global_action_cred}; use madmin::metrics::RealtimeMetrics; use madmin::utils::parse_duration; use matchit::Params; @@ -107,6 +108,63 @@ fn get_token_signing_key() -> Option { } } +pub async fn check_key_valid(st: &str, ak: &str) -> S3Result<(auth::Credentials, bool)> { + let Some(mut cred) = get_global_action_cred() else { + return Err(s3_error!(InternalError, "action cred not init")); + }; + + if cred.access_key != ak { + let Ok(iam_store) = iam::get() else { return Err(s3_error!(InternalError, "iam not init")) }; + + match iam_store + .check_key(ak) + .await + .map_err(|_e| s3_error!(InternalError, "check key failed"))? + { + (Some(u), true) => { + cred = u.credentials; + } + (Some(u), false) => { + if u.credentials.status == "off" { + return Err(s3_error!(InvalidRequest, "ErrAccessKeyDisabled")); + } + + return Err(s3_error!(InvalidRequest, "check key failed")); + } + _ => { + return Err(s3_error!(InvalidRequest, "check key failed")); + } + } + + return Ok((cred, true)); + } + unimplemented!() +} + +pub fn check_claims_from_token(token: &str, cred: &auth::Credentials) -> crate::Result>> { + // if !token.is_empty() && cred.access_key.is_empty() { + // return Err(Error::NoAccessKey); + // } + + // if token.is_empty() && cred.is_temp() && !cred.is_service_account() { + // return Err(Error::InvalidToken); + // } + + // if !token.is_empty() && !cred.is_temp() { + // return Err(Error::InvalidToken); + // } + + // if !cred.is_service_account() && cred.is_temp() && token != cred.session_token { + // return Err(Error::InvalidToken); + // } + + // if cred.is_temp() || cred.is_expired() { + // return Err(Error::InvalidAccessKey); + // } + + unimplemented!() +} + pub struct AssumeRoleHandle {} #[async_trait::async_trait] impl Operation for AssumeRoleHandle { @@ -164,7 +222,7 @@ impl Operation for AssumeRoleHandle { claims.access_key = ak.clone(); - let mut cred = match iam::auth::Credentials::create_new_credentials_with_metadata(&ak, &sk, &claims, &secret, Some(exp)) { + let mut cred = match create_new_credentials_with_metadata(&ak, &sk, &claims, &secret, Some(exp)) { Ok(res) => res, Err(_er) => return Err(s3_error!(InvalidRequest, "")), }; diff --git a/rustfs/src/admin/handlers/user.rs b/rustfs/src/admin/handlers/user.rs index 3c961fa8a..0f6d0c063 100644 --- a/rustfs/src/admin/handlers/user.rs +++ b/rustfs/src/admin/handlers/user.rs @@ -1,17 +1,60 @@ +use futures::TryFutureExt; use http::StatusCode; +use iam::get_global_action_cred; use matchit::Params; -use s3s::{s3_error, Body, S3Request, S3Response, S3Result}; +use s3s::{s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result}; +use serde::Deserialize; +use serde_urlencoded::from_bytes; +use tracing::warn; use crate::admin::router::Operation; +#[derive(Debug, Deserialize, Default)] +pub struct AddUserQuery { + #[serde(rename = "accessKey")] + pub access_key: Option, +} + pub struct AddUser {} #[async_trait::async_trait] impl Operation for AddUser { - async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { + async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + let query = { + if let Some(query) = req.uri.query() { + let input: AddUserQuery = + from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed"))?; + input + } else { + AddUserQuery::default() + } + }; + + let ak = query.access_key.as_deref().unwrap_or_default(); + + if let Some(sys_cred) = get_global_action_cred() { + if sys_cred.access_key == ak { + return Err(s3_error!(InvalidArgument, "can't create user with system access key")); + } + } + + if let (Some(user), true) = iam::get_user(ak) + .await + .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {}", e)))? + { + if user.credentials.is_temp() || user.credentials.is_service_account() { + return Err(s3_error!(InvalidArgument, "can't create user with service account access key")); + } + } + + warn!("handle AddUser"); return Err(s3_error!(NotImplemented)); } } +fn check_claims_from_token(_token: &str) -> bool { + true +} + pub struct SetUserStatus {} #[async_trait::async_trait] impl Operation for SetUserStatus { @@ -19,3 +62,35 @@ impl Operation for SetUserStatus { return Err(s3_error!(NotImplemented)); } } + +pub struct ListUsers {} +#[async_trait::async_trait] +impl Operation for ListUsers { + async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle ListUsers"); + let users = iam::list_users() + .await + .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?; + + let body = serde_json::to_string(&users) + .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {}", e)))?; + Ok(S3Response::new((StatusCode::OK, Body::from(body)))) + } +} + +pub struct RemoveUser {} +#[async_trait::async_trait] +impl Operation for RemoveUser { + async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle RemoveUser"); + return Err(s3_error!(NotImplemented)); + } +} + +pub struct GetUserInfo {} +#[async_trait::async_trait] +impl Operation for GetUserInfo { + async fn call(&self, _req: S3Request, _params: Params<'_, '_>) -> S3Result> { + return Err(s3_error!(NotImplemented)); + } +} diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 713eab9e9..26d002f3c 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -126,13 +126,32 @@ fn regist_user_route(r: &mut S3Router) -> Result<()> { format!("{}{}", ADMIN_PREFIX, "/v3/accountinfo").as_str(), AdminOperation(&handlers::AccountInfoHandler {}), )?; + r.insert( Method::GET, + format!("{}{}", ADMIN_PREFIX, "/v3/list-users").as_str(), + AdminOperation(&user::ListUsers {}), + )?; + + r.insert( + Method::GET, + format!("{}{}", ADMIN_PREFIX, "/v3/user-info").as_str(), + AdminOperation(&user::GetUserInfo {}), + )?; + + r.insert( + Method::DELETE, + format!("{}{}", ADMIN_PREFIX, "/v3/remove-user").as_str(), + AdminOperation(&user::RemoveUser {}), + )?; + + r.insert( + Method::PUT, format!("{}{}", ADMIN_PREFIX, "/v3/add-user").as_str(), AdminOperation(&user::AddUser {}), )?; r.insert( - Method::GET, + Method::PUT, format!("{}{}", ADMIN_PREFIX, "/v3/set-user-status").as_str(), AdminOperation(&user::SetUserStatus {}), )?;