implement secret creating and deleting

This commit is contained in:
beckerinj
2022-12-02 02:40:44 -05:00
parent 4e518d90ad
commit 00a89ccb48
9 changed files with 185 additions and 20 deletions
Generated
+1
View File
@@ -795,6 +795,7 @@ dependencies = [
"axum",
"bollard",
"monitor_types 0.1.0",
"rand",
"run_command",
"serde",
"serde_derive",
+5
View File
@@ -16,6 +16,7 @@ use crate::{
mod build;
mod deployment;
mod permissions;
mod secret;
mod server;
type PeripheryExtension = Extension<Arc<PeripheryClient>>;
@@ -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))
}
+47 -11
View File
@@ -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<PermissionsUpdate>,
Json(update): Json<PermissionsUpdateBody>,
) -> anyhow::Result<String> {
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<ModifyUserEnabledBody>,
) -> 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::<Document>(&user_id, Update::Set(doc! { "enabled": enabled }))
.await?;
Ok(())
}
+106
View File
@@ -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<i64>,
}
#[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<ApiSecret> 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<CreateSecretBody>,
) -> anyhow::Result<String> {
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::<Document>(
&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<DeleteSecretPath>,
) -> anyhow::Result<()> {
db.users
.update_one::<Document>(
&user.id,
Update::Custom(doc! {
"$pull": {
"secrets": {
"name": name
}
}
}),
)
.await
.context("failed at mongo update query")?;
Ok(())
}
+5 -6
View File
@@ -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<SecretLoginBody>,
Json(SecretLoginBody { user_id, secret }): Json<SecretLoginBody>,
) -> anyhow::Result<String> {
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::<Document>(
&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")?;
+10
View File
@@ -51,6 +51,16 @@ impl DbClient {
Extension(Arc::new(client))
}
pub async fn get_user(&self, user_id: &str) -> anyhow::Result<User> {
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<Deployment> {
let deployment = self
.deployments
+2 -1
View File
@@ -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"] }
run_command = { version = "0.0.5", features = ["async_tokio"] }
rand = "0.8"
+9
View File
@@ -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()
}
-2
View File
@@ -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<ObjectId>,
pub name: String,
pub hash: String,
pub created_at: i64,