From 00a89ccb4829ea714a3f2969da737a422f291ec3 Mon Sep 17 00:00:00 2001 From: beckerinj Date: Fri, 2 Dec 2022 02:40:44 -0500 Subject: [PATCH] implement secret creating and deleting --- Cargo.lock | 1 + core/src/api/mod.rs | 5 ++ core/src/api/permissions.rs | 58 ++++++++++++++++---- core/src/api/secret.rs | 106 ++++++++++++++++++++++++++++++++++++ core/src/auth/secret.rs | 11 ++-- lib/db_client/src/lib.rs | 10 ++++ lib/helpers/Cargo.toml | 3 +- lib/helpers/src/lib.rs | 9 +++ lib/types/src/lib.rs | 2 - 9 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 core/src/api/secret.rs diff --git a/Cargo.lock b/Cargo.lock index e695e1dc2..6e0913de1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,6 +795,7 @@ dependencies = [ "axum", "bollard", "monitor_types 0.1.0", + "rand", "run_command", "serde", "serde_derive", diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 07af0d09a..470630495 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -16,6 +16,7 @@ use crate::{ mod build; mod deployment; mod permissions; +mod secret; mod server; type PeripheryExtension = Extension>; @@ -30,6 +31,7 @@ pub fn router() -> Router { .nest("/deployment", deployment::router()) .nest("/server", server::router()) .nest("/permissions", permissions::router()) + .nest("/secret", secret::router()) .layer(Extension(Arc::new(PeripheryClient::new()))) .layer(middleware::from_fn(auth_request)) } @@ -44,6 +46,9 @@ async fn get_user( .await? .ok_or(anyhow!("did not find user"))?; user.password = None; + for secret in &mut user.secrets { + secret.hash = String::new(); + } Ok(Json(user)) } diff --git a/core/src/api/permissions.rs b/core/src/api/permissions.rs index a9c3e423f..57c79461f 100644 --- a/core/src/api/permissions.rs +++ b/core/src/api/permissions.rs @@ -2,34 +2,49 @@ use anyhow::{anyhow, Context}; use axum::{routing::post, Extension, Json, Router}; use db::DbExtension; use helpers::handle_anyhow_error; -use mungos::{doc, Deserialize, Update}; +use mungos::{doc, Deserialize, Document, Update}; use types::{Build, Deployment, PermissionLevel, PermissionsTarget, Server}; use crate::auth::RequestUserExtension; #[derive(Deserialize)] -struct PermissionsUpdate { +struct PermissionsUpdateBody { user_id: String, permission: PermissionLevel, target_type: PermissionsTarget, target_id: String, } +#[derive(Deserialize)] +struct ModifyUserEnabledBody { + user_id: String, + enabled: bool, +} + pub fn router() -> Router { - Router::new().route( - "/update", - post(|db, user, update| async { - update_permissions(db, user, update) - .await - .map_err(handle_anyhow_error) - }), - ) + Router::new() + .route( + "/update", + post(|db, user, update| async { + update_permissions(db, user, update) + .await + .map_err(handle_anyhow_error) + }), + ) + .route( + "/modify_enabled", + post(|db, user, body| async { + modify_user_enabled(db, user, body) + .await + .map_err(handle_anyhow_error) + }), + ) } async fn update_permissions( Extension(db): DbExtension, Extension(user): RequestUserExtension, - Json(update): Json, + Json(update): Json, ) -> anyhow::Result { if !user.is_admin { return Err(anyhow!( @@ -117,3 +132,24 @@ async fn update_permissions( } } } + +async fn modify_user_enabled( + Extension(db): DbExtension, + Extension(user): RequestUserExtension, + Json(ModifyUserEnabledBody { user_id, enabled }): Json, +) -> anyhow::Result<()> { + if !user.is_admin { + return Err(anyhow!( + "user does not have permissions for this action (not admin)" + )); + } + db.users + .find_one_by_id(&user_id) + .await + .context("failed at mongo query to find target user")? + .ok_or(anyhow!("did not find any user with user_id {user_id}"))?; + db.users + .update_one::(&user_id, Update::Set(doc! { "enabled": enabled })) + .await?; + Ok(()) +} diff --git a/core/src/api/secret.rs b/core/src/api/secret.rs new file mode 100644 index 000000000..ddfecfd61 --- /dev/null +++ b/core/src/api/secret.rs @@ -0,0 +1,106 @@ +use anyhow::{anyhow, Context}; +use async_timing_util::unix_timestamp_ms; +use axum::{ + extract::Path, + routing::{delete, post}, + Extension, Json, Router, +}; +use db::DbExtension; +use helpers::{generate_secret, handle_anyhow_error}; +use mungos::{doc, to_bson, Deserialize, Document, Update}; +use types::ApiSecret; + +use crate::auth::RequestUserExtension; + +const SECRET_LENGTH: usize = 40; +const BCRYPT_COST: u32 = 10; + +#[derive(Deserialize)] +struct CreateSecretBody { + name: String, + expires: Option, +} + +#[derive(Deserialize)] +struct DeleteSecretPath { + name: String, +} + +pub fn router() -> Router { + Router::new() + .route( + "/create", + post(|db, user, secret| async { + create(db, user, secret).await.map_err(handle_anyhow_error) + }), + ) + .route( + "/delete/:name", + delete(|db, user, secret_id| async { + delete_one(db, user, secret_id) + .await + .map_err(handle_anyhow_error) + }), + ) +} + +impl Into for CreateSecretBody { + fn into(self) -> ApiSecret { + ApiSecret { + name: self.name, + expires: self.expires, + created_at: unix_timestamp_ms() as i64, + ..Default::default() + } + } +} + +async fn create( + Extension(db): DbExtension, + Extension(req_user): RequestUserExtension, + Json(secret): Json, +) -> anyhow::Result { + let user = db.get_user(&req_user.id).await?; + for s in &user.secrets { + if s.name == secret.name { + return Err(anyhow!("secret with name {} already exists", secret.name)); + } + } + let mut secret: ApiSecret = secret.into(); + let secret_str = generate_secret(SECRET_LENGTH); + secret.hash = + bcrypt::hash(&secret_str, BCRYPT_COST).context("failed at hashing secret string")?; + db.users + .update_one::( + &req_user.id, + Update::Custom(doc! { + "$push": { + "secrets": to_bson(&secret).context("failed at converting secret to bson")? + } + }), + ) + .await + .context("failed at mongo update query")?; + Ok(secret_str) +} + +async fn delete_one( + Extension(db): DbExtension, + Extension(user): RequestUserExtension, + Path(DeleteSecretPath { name }): Path, +) -> anyhow::Result<()> { + db.users + .update_one::( + &user.id, + Update::Custom(doc! { + "$pull": { + "secrets": { + "name": name + } + } + }), + ) + .await + .context("failed at mongo update query")?; + Ok(()) +} diff --git a/core/src/auth/secret.rs b/core/src/auth/secret.rs index 9a37f2ffb..c2a073e50 100644 --- a/core/src/auth/secret.rs +++ b/core/src/auth/secret.rs @@ -9,7 +9,7 @@ use super::JwtExtension; #[derive(Deserialize)] pub struct SecretLoginBody { - username: String, + user_id: String, secret: String, } @@ -23,15 +23,14 @@ pub fn router() -> Router { pub async fn login( Extension(db): DbExtension, Extension(jwt): JwtExtension, - Json(SecretLoginBody { username, secret }): Json, + Json(SecretLoginBody { user_id, secret }): Json, ) -> anyhow::Result { let user = db .users - .find_one(doc! { "username": &username }, None) + .find_one_by_id(&user_id) .await .context("failed at mongo query")? - .ok_or(anyhow!("did not find user with username {username}"))?; - let user_id = user.id.ok_or(anyhow!("user does not have id"))?.to_string(); + .ok_or(anyhow!("did not find user with id {user_id}"))?; let ts = unix_timestamp_ms() as i64; for s in user.secrets { if let Some(expires) = s.expires { @@ -39,7 +38,7 @@ pub async fn login( db.users .update_one::( &user_id, - Update::Custom(doc! { "$pull": { "secrets": { "_id": s.id } } }), + Update::Custom(doc! { "$pull": { "secrets": { "name": s.name } } }), ) .await .context("failed to remove expired secret")?; diff --git a/lib/db_client/src/lib.rs b/lib/db_client/src/lib.rs index 5442b76ac..bedbbd32a 100644 --- a/lib/db_client/src/lib.rs +++ b/lib/db_client/src/lib.rs @@ -51,6 +51,16 @@ impl DbClient { Extension(Arc::new(client)) } + pub async fn get_user(&self, user_id: &str) -> anyhow::Result { + let user = self + .users + .find_one_by_id(user_id) + .await + .context(format!("failed at mongo query for user {user_id}"))? + .ok_or(anyhow!("user at {user_id} doesn't exist"))?; + Ok(user) + } + pub async fn get_deployment(&self, deployment_id: &str) -> anyhow::Result { let deployment = self .deployments diff --git a/lib/helpers/Cargo.toml b/lib/helpers/Cargo.toml index f0c0be850..c601be536 100644 --- a/lib/helpers/Cargo.toml +++ b/lib/helpers/Cargo.toml @@ -15,4 +15,5 @@ serde = "1.0" serde_derive = "1.0" serde_json = "1.0" toml = "0.5" -run_command = { version = "0.0.5", features = ["async_tokio"] } \ No newline at end of file +run_command = { version = "0.0.5", features = ["async_tokio"] } +rand = "0.8" \ No newline at end of file diff --git a/lib/helpers/src/lib.rs b/lib/helpers/src/lib.rs index 8560afeb1..e0c82d0f1 100644 --- a/lib/helpers/src/lib.rs +++ b/lib/helpers/src/lib.rs @@ -3,6 +3,7 @@ use std::{fs::File, io::Read, net::SocketAddr, str::FromStr}; use anyhow::Context; use async_timing_util::unix_timestamp_ms; use axum::http::StatusCode; +use rand::{distributions::Alphanumeric, Rng}; use run_command::{async_run_command, CommandOutput}; use serde::de::DeserializeOwned; use types::Log; @@ -58,3 +59,11 @@ pub fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) { format!("Internal Error: {err:#?}"), ) } + +pub fn generate_secret(length: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} diff --git a/lib/types/src/lib.rs b/lib/types/src/lib.rs index 791ae3bd6..3deecf471 100644 --- a/lib/types/src/lib.rs +++ b/lib/types/src/lib.rs @@ -49,8 +49,6 @@ pub struct User { #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct ApiSecret { - #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] - pub id: Option, pub name: String, pub hash: String, pub created_at: i64,