// There are some placeholder functions, that will have to be implemented before the first release. // They are marked with `todo!()` to indicate that they need to be implemented. // We want to keep these functions in the codebase. // That is why we choose to suppress unused warnings for now. // TODO #![allow(unused)] use super::structs::KvV2WriteRequest; use crate::{ common::HttpError, engines::{ kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath }, storage::sealing::Secret, DbPool }; use axum::{ Extension, Json, extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, NoContent, Response}, }; use log::{debug, error, info, warn}; use serde::Deserialize; use time::{OffsetDateTime, UtcDateTime}; #[derive(Deserialize)] pub struct GetDataQuery { #[serde(default)] /// Version of secret requested to be read. /// Default `0`, to get the most recent version. pub version: u32, } /// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations struct SecretDataInternal { pub created_time: OffsetDateTime, pub deletion_time: Option, pub version_number: i64, pub secret_path: String, pub nonce: Vec, pub encrypted_data: Vec, } impl SecretDataInternal { pub async fn into_external(self) -> KvSecretData { let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await; KvSecretData { created_time: self.created_time, deletion_time: self.deletion_time, version_number: self.version_number, secret_path: self.secret_path, secret_data: secret.unwrap(), } } } pub async fn get_data( State(pool): State, Query(params): Query, Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { debug!("Get request: Engine: {engine_path}, path: {path}",); let res = if params.version != 0 { // With specific version sqlx::query_as!( SecretDataInternal, r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL AND version_number = $3"#, engine_path, path, params.version).fetch_one(&pool).await } else { // Without specific version sqlx::query_as!( SecretDataInternal, r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL ORDER BY version_number DESC LIMIT 1"#, engine_path, path).fetch_one(&pool).await }; match res { Ok(secret_content) => { let secret_content = secret_content.into_external().await; let inner = secret_content.secret_data; let data = Wrapper { data: serde_json::from_str(&inner).unwrap(), }; let return_secret = KvSecretRes { data, options: None, version: Some(secret_content.version_number), }; let return_secret = Json(return_secret); info!("{return_secret:?}"); Ok(return_secret.into_response()) } Err(e) => match e { sqlx::Error::RowNotFound => { warn!("Secret not found (could be correct behavior) {e:?}"); Ok(HttpError::simple( StatusCode::NOT_FOUND, "Secret not found within kv2 engine", )) } _ => panic!("Unhandled error: {e:?}"), }, } } pub async fn post_data( State(pool): State, Path(kv_path): Path, Extension(EnginePath(engine_path)): Extension, Json(secret): Json, ) -> Result { debug!( "Engine: {}, Secret: {}, Version: {:?}, path: {}", engine_path, kv_path, secret.version, //.unwrap_or(0), kv_path ); let created_time = time::UtcDateTime::now(); let ts = created_time.unix_timestamp(); let content = serde_json::to_string(&secret.data).unwrap(); let Secret { nonce, protected_data } = Secret::encrypt(&content).await.unwrap(); let nonce = nonce.as_slice(); let mut tx = pool.begin().await.unwrap(); let _ = sqlx::query!(" INSERT INTO kv2_metadata (engine_path, secret_path, cas_required, created_time, max_versions, updated_time) VALUES ($1, $2, 0, $3, 100, $3) ON CONFLICT(engine_path, secret_path) DO NOTHING; ", engine_path, kv_path, ts).execute(&mut *tx).await.unwrap(); let res_r = sqlx::query_file!( "src/engines/kv/post_secret.sql", engine_path, kv_path, nonce, protected_data, ts, secret.version, ) .fetch_one(&mut *tx) .await .unwrap(); tx.commit().await.expect("FAILED TO WRITE TX!"); let res = KvV2WriteResponse { created_time: created_time.into(), custom_metadata: None, deletion_time: None, destroyed: false, version: res_r.version_number, }; Ok(Json(res).into_response()) } /// TODO: soft delete the secret version at path. can be undone with undelete_secret // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-latest-version-of-secret // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-secret-versions pub async fn delete_data( State(pool): State, Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { debug!("Secret: {path}, path: {path}"); let del_time = UtcDateTime::now().unix_timestamp(); let mut tx = pool.begin().await.unwrap(); // TODO: Find a better way let latest_version = sqlx::query!( r#" SELECT version_number AS latest_version FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL ORDER BY version_number DESC LIMIT 1"#, engine_path, path, ) .fetch_optional(&mut *tx) .await .unwrap(); let latest_version = match latest_version { Some(v) => v.latest_version, None => { return Err(HttpError::simple( StatusCode::NOT_FOUND, "No secret version found which could be deleted", )); } }; let u = sqlx::query!( r#" UPDATE kv2_secret_version SET deletion_time = $4 WHERE engine_path = $1 AND secret_path = $2 AND version_number = $3 "#, engine_path, path, latest_version, del_time ) .execute(&mut *tx) .await; if let Err(e) = u { error!( "Strange - a version to be deleted has been found but could not be found to set deletion.\n\t{e:?}" ); // Not committed transactions will be aborted upon drop // tx.rollback().await.unwrap(); return Err(HttpError::simple( StatusCode::INTERNAL_SERVER_ERROR, "A version to be deleted was found but could not be deleted", )); } tx.commit().await.unwrap(); info!("Secret {path} version {latest_version} of {engine_path} engine deleted! {u:?}"); Ok(NoContent.into_response()) } pub async fn patch_data( State(pool): State, Path(kv_path): Path, Extension(EnginePath(engine_path)): Extension, Json(secret): Json, ) -> &'static str { todo!("not implemented") }