forked from github-starred/komodo
implement secret creating and deleting
This commit is contained in:
Generated
+1
@@ -795,6 +795,7 @@ dependencies = [
|
||||
"axum",
|
||||
"bollard",
|
||||
"monitor_types 0.1.0",
|
||||
"rand",
|
||||
"run_command",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user