Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea5189ad8 | ||
|
|
ee8f6d8e65 | ||
|
|
6bc9bbbf50 | ||
|
|
021b4c42b6 | ||
|
|
075f13777a | ||
|
|
eab776a8d1 | ||
|
|
620df86c06 | ||
|
|
a6de2133fd | ||
|
|
363cd11aa5 | ||
|
|
40b7e74088 | ||
|
|
805fd182cd | ||
| cd3c13f30e | |||
| c97c09cde5 |
14 changed files with 586 additions and 494 deletions
38
Cargo.lock
generated
38
Cargo.lock
generated
|
|
@ -459,15 +459,6 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fluent-uri"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -820,29 +811,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "json-patch"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc"
|
|
||||||
dependencies = [
|
|
||||||
"jsonptr",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jsonptr"
|
|
||||||
version = "0.4.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627"
|
|
||||||
dependencies = [
|
|
||||||
"fluent-uri",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -1334,7 +1302,6 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"json-patch",
|
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -1544,6 +1511,7 @@ dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
|
|
@ -1607,6 +1575,7 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
|
"sqlx-postgres",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
@ -1625,6 +1594,7 @@ dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"digest",
|
"digest",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
|
@ -1666,6 +1636,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
|
|
@ -1701,6 +1672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
|
"chrono",
|
||||||
"flume",
|
"flume",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
|
zeroize = { version = "1.7.0", features = ["derive"]}
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
tower = { version = "0.4.13", features = [] }
|
tower = { version = "0.4.13", features = [] }
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
serde = "1.0.201"
|
serde = "1.0.201"
|
||||||
serde_json = "1.0.117"
|
serde_json = "1.0.117"
|
||||||
json-patch = "2.0.0"
|
# json-patch = "2.0.0"
|
||||||
# serde_with = "3.8.1"
|
# serde_with = "3.8.1"
|
||||||
|
|
||||||
# utoipa = { version = "4.2.0", features = ["axum_extras"] }
|
# utoipa = { version = "4.2.0", features = ["axum_extras"] }
|
||||||
|
|
@ -21,6 +21,7 @@ sqlx = { version = "0.7.4", features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
# "postgres",
|
# "postgres",
|
||||||
# "any",
|
# "any",
|
||||||
|
"chrono",
|
||||||
"macros",
|
"macros",
|
||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
"tls-rustls",
|
"tls-rustls",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
# RUN go build -o /app
|
# RUN go build -o /app
|
||||||
RUN go build
|
RUN go build
|
||||||
|
# CMD export GOCACHE=off
|
||||||
CMD go test tests/*
|
CMD go test tests/*
|
||||||
|
|
||||||
# FROM docker.io/library/alpine:3.19
|
# FROM docker.io/library/alpine:3.19
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# Testing module proposals
|
|
||||||
|
|
||||||
- Basic API calls
|
|
||||||
- Login/logout
|
|
||||||
- kv IO test module
|
|
||||||
- Token
|
|
||||||
- Lookup
|
|
||||||
- Generation
|
|
||||||
|
|
||||||
(- TLS module)
|
|
||||||
(- Auth module)
|
|
||||||
|
|
||||||
|
|
||||||
==> Nur KvV2 testen
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
var client *vault.Client
|
var client *vault.Client
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
|
|
||||||
// Apparently used as a default if mountpath is an empty string (client library)
|
// Apparently used as a default if mountpath is an empty string (client library)
|
||||||
var mountpath = "/kv-v2"
|
var mountpath = "/kv-v2"
|
||||||
var mountpath2 = "/some"
|
var mountpath2 = "/some"
|
||||||
|
|
@ -40,7 +41,6 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#create-update-secret
|
// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#create-update-secret
|
||||||
// @Philip der Path steht in der KvV2Write Methode
|
|
||||||
func TestWriteSecret(t *testing.T) {
|
func TestWriteSecret(t *testing.T) {
|
||||||
// Path foo
|
// Path foo
|
||||||
_, err := client.Secrets.KvV2Write(ctx, "foo", schema.KvV2WriteRequest{
|
_, err := client.Secrets.KvV2Write(ctx, "foo", schema.KvV2WriteRequest{
|
||||||
|
|
@ -66,7 +66,7 @@ func TestWriteSecret(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("kv2: Failed to write secret:\n\t", err)
|
log.Fatal("kv2: Failed to write secret:\n\t", err)
|
||||||
}
|
}
|
||||||
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath)
|
log.Println("kv2: Tried to write Secret at bar at mountpath: ", mountpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteSecret2(t *testing.T) {
|
func TestWriteSecret2(t *testing.T) {
|
||||||
|
|
@ -97,16 +97,55 @@ func TestWriteSecret2(t *testing.T) {
|
||||||
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath2)
|
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteSecret(t *testing.T) {
|
|
||||||
_, err := client.Secrets.KvV2Delete(ctx, "foo") // currently disregarding modifier options
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("kv2: Failed to delete secret:\n\t", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadSecret(t *testing.T) {
|
func TestReadSecret(t *testing.T) {
|
||||||
_, err := client.Secrets.KvV2Read(ctx, "bar")
|
_, err := client.Secrets.KvV2Read(ctx, "bar")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("kv2: Failed to read secret:\n\t", err)
|
log.Fatal("kv2: Failed to read secret:\n\t", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadMeta(t *testing.T) {
|
||||||
|
_, err := client.Secrets.KvV2ReadMetadata(ctx, "bar")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("kv2: Failed to read metadata:\n\t", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteAndReadMeta(t *testing.T) {
|
||||||
|
meta := schema.KvV2WriteMetadataRequest{
|
||||||
|
MaxVersions: 5,
|
||||||
|
CasRequired: false,
|
||||||
|
DeleteVersionAfter: "3h25m19s",
|
||||||
|
CustomMetadata: map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "123",
|
||||||
|
"baz": "5c07d823-3810-48f6-a147-4c06b5219e84",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := client.Secrets.KvV2WriteMetadata(ctx, "newMeta", meta)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("kv2: Failed to write metadata:\n\t", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the metadata
|
||||||
|
_, err2 := client.Secrets.KvV2ReadMetadata(ctx, "newMeta")
|
||||||
|
if err2 != nil {
|
||||||
|
log.Fatal("kv2: Failed to read metadata:\n\t", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// does NOT revert destruction
|
||||||
|
func TestDestroySecret(t *testing.T) {
|
||||||
|
_, err := client.Secrets.KvV2DestroyVersions(ctx, "bar", schema.KvV2DestroyVersionsRequest{Versions: []int32{1}})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("kv2: Failed to destroy secret:\n\t", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// does NOT revert destruction
|
||||||
|
func TestDestroySecret2(t *testing.T) {
|
||||||
|
_, err := client.Secrets.KvV2DestroyVersions(ctx, "bar", schema.KvV2DestroyVersionsRequest{Versions: []int32{1, 2}})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("kv2: Failed to destroy secret:\n\t", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
-- Add migration script here
|
-- Add migration script here
|
||||||
|
|
||||||
INSERT INTO metadata VALUES ("bar", false, DateTime('now'), "123", 4, DateTime('now'), "customData");
|
INSERT INTO metadata VALUES ("bar", false, DateTime('now'), "30d", 4, DateTime('now'), '{"foo": "customData"}');
|
||||||
|
|
||||||
INSERT INTO secret_versions VALUES ("secret_data", DateTime('now'), DateTime('now'), 1, "bar");
|
INSERT INTO secret_versions VALUES ("secret_data", DateTime('now'), DateTime('now'), 1, "bar");
|
||||||
|
INSERT INTO secret_versions VALUES ("more_secret_data", DateTime('now'), datetime('now', '+30 day'), 2, "bar");
|
||||||
|
|
||||||
|
INSERT INTO metadata VALUES ("foo", false, DateTime('now'), "30d", 4, DateTime('now'), '{"foo": "customData"}');
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
// TODO: Remove
|
// pub mod logic;
|
||||||
#![allow(dead_code)]
|
pub mod db_structs;
|
||||||
|
pub mod http_structs;
|
||||||
pub mod logic;
|
|
||||||
pub mod structs;
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
// #[cfg(test)]
|
||||||
// mod tests;
|
// mod tests;
|
||||||
|
|
||||||
use std::{collections::HashMap, convert::Infallible};
|
use crate::{engines::kv::http_structs::*, storage::DatabaseDriver};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
engines::kv::{logic::body_to_json, structs::*},
|
|
||||||
storage::DatabaseDriver,
|
|
||||||
};
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{self, Path, State},
|
||||||
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::*,
|
routing::*,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use log::{info, error};
|
|
||||||
use sqlx::Row;
|
use chrono::{DateTime, Utc};
|
||||||
|
use db_structs::*;
|
||||||
|
use log::{error, info};
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
use sqlx::{Row, Sqlite};
|
||||||
|
use std::{collections::HashMap, convert::Infallible};
|
||||||
|
|
||||||
pub fn kv_router(pool: DatabaseDriver) -> Router {
|
pub fn kv_router(pool: DatabaseDriver) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -61,28 +61,39 @@ async fn get_data(
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
let version: i64 = v.get("version_number");
|
let version: i64 = v.get("version_number");
|
||||||
let secret_content: HashMap<String, String> = HashMap::from([
|
let secret_content: HashMap<String, String> = HashMap::from([
|
||||||
// TODO: use sqlx to parse the row to a struct, do not do it manually
|
// Consider using sqlx to parse the row to a struct, do not do it manually
|
||||||
("secret_data".to_string(), v.get("secret_data")),
|
("secret_data".to_string(), v.get("secret_data")),
|
||||||
("created_time".to_string(), v.get("created_time")),
|
("created_time".to_string(), v.get("created_time")),
|
||||||
("deletion_time".to_string(), v.get("deletion_time")),
|
("deletion_time".to_string(), v.get("deletion_time")),
|
||||||
("version_number".to_string(), version.to_string()),
|
("version_number".to_string(), version.to_string()),
|
||||||
("secret_path".to_string(), v.get("secret_path")),
|
("secret_path".to_string(), v.get("secret_path")),
|
||||||
]);
|
]);
|
||||||
let return_secret = KvSecretReq {
|
let return_secret = KvSecretRes::new(KvSecretResData {
|
||||||
data: secret_content,
|
created_time: DateTime::parse_from_rfc3339(
|
||||||
options: None,
|
secret_content
|
||||||
};
|
.get("created_time")
|
||||||
|
.unwrap_or(&"".to_string()),
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_utc(), // TODO
|
||||||
|
custom_metadata: None,
|
||||||
|
deletion_time: None,
|
||||||
|
destroyed: false,
|
||||||
|
version: version,
|
||||||
|
});
|
||||||
info!("{:?}", return_secret);
|
info!("{:?}", return_secret);
|
||||||
|
|
||||||
Ok(Json(return_secret))
|
Ok((StatusCode::OK, Json(return_secret)).into_response())
|
||||||
}
|
}
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
sqlx::Error::RowNotFound => {
|
sqlx::Error::RowNotFound => {
|
||||||
error!("{:?}", e);
|
error!("{:?}", e);
|
||||||
let error_data: HashMap<String, String> = HashMap::from([("error".to_string(), "Secret not found".to_string())]);
|
let error_struct: ErrorStruct = ErrorStruct { err: e.to_string() };
|
||||||
let error_secret = KvSecretReq{data: error_data, options: None};
|
error!("{:?}", error_struct.err);
|
||||||
Ok(Json(error_secret))
|
Ok(error_struct.into_response()) // API doesn't specify return value in case of error. Error struct correct? Else send empty secret back?
|
||||||
},
|
// let error_secret = KvSecretRes{data: None, options: None};
|
||||||
|
// Ok(Json())
|
||||||
|
}
|
||||||
_ => panic!("{:?}", e),
|
_ => panic!("{:?}", e),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -91,77 +102,74 @@ async fn get_data(
|
||||||
async fn post_data(
|
async fn post_data(
|
||||||
State(pool): State<DatabaseDriver>,
|
State(pool): State<DatabaseDriver>,
|
||||||
Path(path): Path<String>,
|
Path(path): Path<String>,
|
||||||
body: String,
|
extract::Json(payload): extract::Json<KvSecretReq>,
|
||||||
) -> &'static str {
|
) -> Result<impl IntoResponse, Infallible> {
|
||||||
// Insert Metadata first -> Else: Error because of foreign key constraint
|
// Insert Metadata first -> Else: Error because of foreign key constraint
|
||||||
|
// In a later implementation, a Metadata with default values from the config will be created
|
||||||
|
|
||||||
// let mut body_json = body_to_json(body);
|
log::debug!(
|
||||||
|
"Secret: {}, Content: {:?}, Version: {:?}, path: {}",
|
||||||
// let secret: KvSecret = KvSecret {
|
path,
|
||||||
// data: body_json["data"]["password1"].take().to_string(),
|
payload.data,
|
||||||
// version: body_json["data"]["version"].take().as_i64(),
|
payload.options,
|
||||||
// };
|
path
|
||||||
// log::debug!(
|
|
||||||
// "Secret: {}, Content: {}, Version: {}, path: {}",
|
|
||||||
// path,
|
|
||||||
// secret.data,
|
|
||||||
// secret.version.unwrap_or(0),
|
|
||||||
// path
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let created_time = "05-03-2024 12:00:00";
|
|
||||||
// let deletion_time = "05-03-2024 12:00:00";
|
|
||||||
// let version = "0";
|
|
||||||
// match sqlx::query!(
|
|
||||||
// "INSERT INTO secret_versions VALUES ($1, $2, $3, $4, $5)",
|
|
||||||
// secret.data,
|
|
||||||
// created_time,
|
|
||||||
// deletion_time,
|
|
||||||
// version,
|
|
||||||
// version
|
|
||||||
// )
|
|
||||||
// .execute(&pool)
|
|
||||||
// .await
|
|
||||||
// {
|
|
||||||
// Ok(v) => {
|
|
||||||
// trace!("{:?}", v);
|
|
||||||
// "Success"
|
|
||||||
// }
|
|
||||||
// Err(e) => {
|
|
||||||
// trace!("{:?}", e);
|
|
||||||
// "Error"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
todo!("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* mock for return
|
|
||||||
async fn post_data(
|
|
||||||
Path(kv_path): Path<String>,
|
|
||||||
Extension(mount_path): Extension<String>,
|
|
||||||
Json(body): Json<KvSecretReq>,
|
|
||||||
) -> Json<KvSecretRes> {
|
|
||||||
trace!(
|
|
||||||
"Secret: {}, Content: {:#?}, path: {}",
|
|
||||||
kv_path,
|
|
||||||
body.data,
|
|
||||||
// body.version.unwrap_or(0),
|
|
||||||
mount_path,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = KvSecretRes {
|
let mut highest_num = 0;
|
||||||
data: KvSecretResData {
|
match sqlx::query("SELECT version_number FROM secret_versions WHERE secret_path = $1")
|
||||||
created_time: chrono::Utc::now(),
|
.bind(&path)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
for curr_ver in v {
|
||||||
|
let curr_num = curr_ver.get("version_number");
|
||||||
|
if highest_num < curr_num {
|
||||||
|
// should be the max of the available version numbers
|
||||||
|
highest_num = curr_num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = highest_num + 1;
|
||||||
|
log::debug!("{:?}", version);
|
||||||
|
let data = serde_json::to_string(&payload.data).unwrap();
|
||||||
|
log::debug!("Received data: {:?}", data);
|
||||||
|
let created_time = Utc::now();
|
||||||
|
let created_time_string = created_time.to_string();
|
||||||
|
let deletion_time = "12-12-2024 12:00:00"; // TODO
|
||||||
|
|
||||||
|
match sqlx::query("INSERT INTO secret_versions VALUES ($1, $2, $3, $4, $5)")
|
||||||
|
.bind(&data)
|
||||||
|
.bind(created_time_string)
|
||||||
|
.bind(deletion_time)
|
||||||
|
.bind(&version)
|
||||||
|
.bind(&path)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
info!("{:?}", v);
|
||||||
|
|
||||||
|
let return_struct = KvSecretResData {
|
||||||
|
created_time: created_time,
|
||||||
custom_metadata: None,
|
custom_metadata: None,
|
||||||
deletion_time: None,
|
deletion_time: None,
|
||||||
destroyed: false,
|
destroyed: false,
|
||||||
version: 1,
|
version: version,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
return Ok((StatusCode::OK, Json(return_struct)).into_response());
|
||||||
Json(res)
|
}
|
||||||
} */
|
Err(e) => {
|
||||||
|
error!("{:?}", e);
|
||||||
|
return Ok((StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string())).into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// TODO: soft delete the secret version at path. can be undone with undelete_secret
|
/// 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-latest-version-of-secret
|
||||||
|
|
@ -174,22 +182,192 @@ async fn delete_path() -> &'static str {
|
||||||
todo!("not implemented")
|
todo!("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn destroy_path() -> &'static str {
|
async fn destroy_path(
|
||||||
todo!("not implemented")
|
State(pool): State<DatabaseDriver>,
|
||||||
|
Path(kv_path): Path<String>,
|
||||||
|
Json(body): Json<KvSecretDestrReq>,
|
||||||
|
) -> Result<impl IntoResponse, Infallible> {
|
||||||
|
let versions = body.versions;
|
||||||
|
|
||||||
|
for ver in versions {
|
||||||
|
let res_wrapper = sqlx::query::<Sqlite>(
|
||||||
|
"DELETE FROM secret_versions where secret_path = $1 AND version_number = $2",
|
||||||
|
)
|
||||||
|
.bind(&kv_path)
|
||||||
|
.bind(ver)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res_wrapper {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
log::debug!(
|
||||||
|
"No rows were deleted for version {} at path {}",
|
||||||
|
ver,
|
||||||
|
kv_path
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::debug!("Deleted version {} at path {}", ver, kv_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to delete version {} at path {}: {}",
|
||||||
|
ver,
|
||||||
|
kv_path,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_meta() -> &'static str {
|
async fn get_meta(
|
||||||
todo!("not implemented")
|
State(pool): State<DatabaseDriver>,
|
||||||
|
Path(kv_path): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, Infallible> {
|
||||||
|
log::debug!("Path: {}", kv_path);
|
||||||
|
|
||||||
|
let mut metadata_res: KvMetaRes = KvMetaRes::default();
|
||||||
|
|
||||||
|
let dbmeta = sqlx::query_as::<_, DbSecretMeta>("SELECT * FROM metadata where secret_path = $1")
|
||||||
|
.bind(&kv_path)
|
||||||
|
.fetch_optional(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match dbmeta {
|
||||||
|
Ok(Some(dbmeta)) => {
|
||||||
|
metadata_res.data = KvMetaResData {
|
||||||
|
created_time: dbmeta.created_time,
|
||||||
|
|
||||||
|
// map the custom_data to a Hashmap
|
||||||
|
custom_metadata: dbmeta.custom_data.map(|data| {
|
||||||
|
serde_json::from_str::<HashMap<String, String>>(&data)
|
||||||
|
.unwrap_or_else(|_| HashMap::new())
|
||||||
|
}),
|
||||||
|
|
||||||
|
cas_required: dbmeta.cas_required,
|
||||||
|
max_versions: dbmeta.max_versions,
|
||||||
|
updated_time: dbmeta.updated_time,
|
||||||
|
delete_version_after: dbmeta.delete_version_after,
|
||||||
|
current_version: 0,
|
||||||
|
oldest_version: 0,
|
||||||
|
versions: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_data = sqlx::query_as::<_, DbSecretVersionMeta>("SELECT version_number, created_time, deletion_time FROM secret_versions WHERE secret_path = $1")
|
||||||
|
.bind(&kv_path)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await;
|
||||||
|
log::debug!("found version_data: {:?}", version_data);
|
||||||
|
|
||||||
|
if let Ok(version_data) = version_data {
|
||||||
|
// 1. iterate through all version data
|
||||||
|
// 2. put all version numbers as keys in the hashmap. the rest of the values values should be the value
|
||||||
|
let mut parsed_versions: HashMap<i64, KvMetaResVersionData> = HashMap::new();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
for curr_ver in version_data {
|
||||||
|
let curr_num = curr_ver.version_number;
|
||||||
|
let data = KvMetaResVersionData {
|
||||||
|
created_time: curr_ver.created_time,
|
||||||
|
deletion_time: curr_ver.deletion_time,
|
||||||
|
destroyed: if curr_ver.deletion_time < now {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if metadata_res.data.current_version < curr_num {
|
||||||
|
// the max of the available version numbers
|
||||||
|
metadata_res.data.current_version = curr_num;
|
||||||
|
}
|
||||||
|
if metadata_res.data.oldest_version > curr_num {
|
||||||
|
// the min of the available version numbers
|
||||||
|
metadata_res.data.oldest_version = curr_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_versions.insert(curr_num, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_res.data.versions = parsed_versions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None) => {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, Json("No metadata found")).into_response());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Database error: {}", e);
|
||||||
|
return Ok((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json("Internal server error"),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_string = serde_json::to_string(&metadata_res).unwrap();
|
||||||
|
log::debug!("Returning response: {}", json_string);
|
||||||
|
|
||||||
|
Ok((StatusCode::OK, Json(metadata_res)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// currently only writes the metadata - Not case if already exists
|
||||||
async fn post_meta(
|
async fn post_meta(
|
||||||
State(pool): State<DatabaseDriver>,
|
State(pool): State<DatabaseDriver>,
|
||||||
Path((mount_path, kv_path)): Path<(String, String)>,
|
Path(kv_path): Path<String>,
|
||||||
body: String,
|
Json(body): Json<KvMetaReq>,
|
||||||
) -> &'static str {
|
// extract::Json(body): extract::Json<http_structs::KvMetaReq>,
|
||||||
let mut body_json = body_to_json(body);
|
) -> Result<impl IntoResponse, Infallible> {
|
||||||
let meta_data: SecretMeta = Default::default();
|
let now = Utc::now();
|
||||||
todo!("not implemented")
|
let custom_metadata: String = match serde_json::to_string(&body.custom_metadata) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!(
|
||||||
|
"could not serialize custom_metadata: {:?}\ndropping data",
|
||||||
|
body.custom_metadata
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_metadata = DbSecretMeta {
|
||||||
|
cas_required: body.cas_required.unwrap_or(false),
|
||||||
|
secret_path: kv_path,
|
||||||
|
created_time: now,
|
||||||
|
delete_version_after: Some("30d".to_string()),
|
||||||
|
max_versions: body.max_versions,
|
||||||
|
updated_time: now,
|
||||||
|
|
||||||
|
custom_data: Some(custom_metadata),
|
||||||
|
};
|
||||||
|
|
||||||
|
match sqlx::query("INSERT INTO metadata (cas_required, secret_path, created_time, delete_version_after, max_versions, updated_time, custom_data) VALUES ($1, $2, $3, $4, $5, $6, $7)")
|
||||||
|
.bind(new_metadata.cas_required)
|
||||||
|
.bind(new_metadata.secret_path)
|
||||||
|
.bind(new_metadata.created_time)
|
||||||
|
.bind(new_metadata.delete_version_after.unwrap())
|
||||||
|
.bind(new_metadata.max_versions)
|
||||||
|
.bind(new_metadata.updated_time)
|
||||||
|
.bind(new_metadata.custom_data.unwrap())
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => {
|
||||||
|
info!("{:?}", result);
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
log::error!("Failed to insert metadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{:?}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_meta() -> &'static str {
|
async fn delete_meta() -> &'static str {
|
||||||
|
|
|
||||||
35
src/engines/kv/db_structs.rs
Normal file
35
src/engines/kv/db_structs.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
#[derive(FromRow, Debug)]
|
||||||
|
pub struct DbSecretMeta {
|
||||||
|
pub secret_path: String,
|
||||||
|
pub cas_required: bool,
|
||||||
|
pub created_time: DateTime<Utc>,
|
||||||
|
// Consider implementation of duration type for further development:
|
||||||
|
// https://developer.hashicorp.com/vault/docs/concepts/duration-format
|
||||||
|
/// In Hashicorp:
|
||||||
|
/// If not set, the backend's configured delete_version_after is used.
|
||||||
|
/// Cannot be greater than the backend's delete_version_after
|
||||||
|
pub delete_version_after: Option<String>,
|
||||||
|
|
||||||
|
///In Hashicorp:
|
||||||
|
/// The number of versions to keep per key.
|
||||||
|
/// If not set, the backend’s configured max version is used.
|
||||||
|
/// Once a key has more than the configured allowed versions,
|
||||||
|
/// the oldest version will be permanently deleted.
|
||||||
|
pub max_versions: i64,
|
||||||
|
pub updated_time: DateTime<Utc>,
|
||||||
|
/// User-provided key-value pairs that are used to describe arbitrary and version-agnostic information about a secret.
|
||||||
|
pub custom_data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, FromRow)]
|
||||||
|
/// Metadata concerning a specific secret version
|
||||||
|
/// contained by [KvMetaRes]
|
||||||
|
pub struct DbSecretVersionMeta {
|
||||||
|
pub version_number: i64,
|
||||||
|
pub created_time: DateTime<Utc>,
|
||||||
|
pub deletion_time: DateTime<Utc>,
|
||||||
|
}
|
||||||
143
src/engines/kv/http_structs.rs
Normal file
143
src/engines/kv/http_structs.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{Response, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
pub type KvSecretData = HashMap<String, String>;
|
||||||
|
|
||||||
|
// This file contains structures for serializing HTTP Responses (Res) and deserializing Requests (Req) for the KV engine
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
// #[zeroize(drop)]
|
||||||
|
/// HTTP Request to create or update a secret
|
||||||
|
pub struct KvSecretReq {
|
||||||
|
/// Map (required)
|
||||||
|
pub data: KvSecretData,
|
||||||
|
/// Map (optional), may contain `cas` integer
|
||||||
|
// #[serde_as(as = "serde_with::EnumMap")]
|
||||||
|
pub options: Option<HashMap<String, String>>,
|
||||||
|
// Version does not exist for create/update operations
|
||||||
|
// pub version: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zeroize for KvSecretReq {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
// Zero out each field individually
|
||||||
|
self.data = HashMap::new();
|
||||||
|
self.options = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for KvSecretReq {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
/// HTTP Response to creating or updating a secret
|
||||||
|
/// Contained by [`KvSecretRes`]
|
||||||
|
pub struct KvSecretResData {
|
||||||
|
pub created_time: DateTime<Utc>,
|
||||||
|
pub custom_metadata: Option<HashMap<String, String>>,
|
||||||
|
pub deletion_time: Option<DateTime<Utc>>,
|
||||||
|
pub destroyed: bool,
|
||||||
|
pub version: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
/// HTTP Response to creating or updating a secret
|
||||||
|
/// Container of [`KvSecretResData`]
|
||||||
|
pub struct KvSecretRes {
|
||||||
|
pub data: KvSecretResData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KvSecretRes {
|
||||||
|
pub fn new(data: KvSecretResData) -> Self {
|
||||||
|
KvSecretRes { data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ErrorStruct {
|
||||||
|
pub err: String,
|
||||||
|
}
|
||||||
|
impl ErrorStruct {
|
||||||
|
pub fn into_response(self) -> Response<Body> {
|
||||||
|
let body = self.err;
|
||||||
|
(StatusCode::NOT_FOUND, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
/// HTTP Request to destroy secret versions
|
||||||
|
pub struct KvSecretDestrReq {
|
||||||
|
pub versions: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
/// HTTP Response to Reading a Secret metadata
|
||||||
|
/// Container of [`KvMetaResData`]
|
||||||
|
pub struct KvMetaRes {
|
||||||
|
pub data: KvMetaResData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KvMetaRes {
|
||||||
|
fn default() -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
data: KvMetaResData {
|
||||||
|
cas_required: false,
|
||||||
|
created_time: now,
|
||||||
|
delete_version_after: Some("".to_string()),
|
||||||
|
max_versions: 0,
|
||||||
|
updated_time: now,
|
||||||
|
custom_metadata: Some(HashMap::new()),
|
||||||
|
current_version: 0,
|
||||||
|
oldest_version: 0,
|
||||||
|
|
||||||
|
versions: HashMap::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
/// Metadata concerning a specific secret version
|
||||||
|
/// contained by [KvMetaRes]
|
||||||
|
pub struct KvMetaResVersionData {
|
||||||
|
pub created_time: DateTime<Utc>,
|
||||||
|
pub deletion_time: DateTime<Utc>,
|
||||||
|
pub destroyed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
/// contained by [KvMetaRes]
|
||||||
|
pub struct KvMetaResData {
|
||||||
|
pub cas_required: bool,
|
||||||
|
pub created_time: DateTime<Utc>,
|
||||||
|
pub current_version: i64,
|
||||||
|
pub delete_version_after: Option<String>,
|
||||||
|
pub max_versions: i64,
|
||||||
|
pub oldest_version: i64,
|
||||||
|
pub updated_time: DateTime<Utc>,
|
||||||
|
pub custom_metadata: Option<HashMap<String, String>>,
|
||||||
|
pub versions: HashMap<i64, KvMetaResVersionData>,
|
||||||
|
// here, the key to a version is the version number
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Deserialize)]
|
||||||
|
/// HTTP Request to post metadatas
|
||||||
|
pub struct KvMetaReq {
|
||||||
|
pub cas_required: Option<bool>,
|
||||||
|
// pub cas_required: bool,
|
||||||
|
pub delete_version_after: Option<String>,
|
||||||
|
pub max_versions: i64,
|
||||||
|
// pub updated_time: Option<DateTime<Utc>>,
|
||||||
|
pub custom_metadata: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,8 @@
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::structs::*;
|
use super::{db_structs::SecretMeta, http_structs::*};
|
||||||
|
|
||||||
// TODO create default function
|
|
||||||
|
|
||||||
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
|
|
||||||
/// serialize secret to JSON String
|
|
||||||
pub fn serialize_secret_json(secret: &KvSecretReq) -> Result<String, serde_json::Error> {
|
|
||||||
serde_json::to_string(&secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
|
|
||||||
/// deserialize JSON String to secret
|
|
||||||
pub fn deserialize_secret_struct(raw: &String) -> Result<KvSecretReq, serde_json::Error> {
|
|
||||||
serde_json::from_str(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
|
|
||||||
/// serialize metadata to JSON String
|
|
||||||
pub fn serialize_metadata_json(secret: &SecretMeta) -> Result<String, serde_json::Error> {
|
|
||||||
serde_json::to_string(&secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
|
|
||||||
/// deserialize JSON String to metadata
|
|
||||||
pub fn deserialize_metadata_struct(raw: &String) -> Result<SecretMeta, serde_json::Error> {
|
|
||||||
serde_json::from_str(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Consider leaving this here - JSON merge patch is also used in the official implementation
|
||||||
#[deprecated(note = "Propably not needed (remove deprecation if actually needed)")]
|
#[deprecated(note = "Propably not needed (remove deprecation if actually needed)")]
|
||||||
/// Consider:
|
/// Consider:
|
||||||
/// Instead of patching JSON, we should apply the modified fields directly to the database
|
/// Instead of patching JSON, we should apply the modified fields directly to the database
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use zeroize::ZeroizeOnDrop;
|
|
||||||
use std::{collections::HashMap, vec};
|
|
||||||
|
|
||||||
pub type KvSecretData = HashMap<String, String>;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct KvSecretReq {
|
|
||||||
/// Map (required)
|
|
||||||
pub data: KvSecretData,
|
|
||||||
/// Map (optional), may contain `cas` integer
|
|
||||||
// #[serde_as(as = "serde_with::EnumMap")]
|
|
||||||
pub options: Option<HashMap<String, String>>,
|
|
||||||
// Version does not exist for create/update operations
|
|
||||||
// pub version: Option<i64>,
|
|
||||||
// TODO add all fields
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
pub struct KvSecretResData {
|
|
||||||
pub created_time: DateTime<Utc>,
|
|
||||||
pub custom_metadata: Option<HashMap<String, String>>,
|
|
||||||
pub deletion_time: Option<DateTime<Utc>>,
|
|
||||||
pub destroyed: bool,
|
|
||||||
pub version: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
pub struct KvSecretRes {
|
|
||||||
pub data: KvSecretResData,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct VersionMeta {
|
|
||||||
pub created_time: DateTime<Utc>,
|
|
||||||
pub deletion_time: Option<DateTime<Utc>>, // optional deletion time
|
|
||||||
pub destroyed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct SecretMeta {
|
|
||||||
pub cas_required: bool,
|
|
||||||
pub created_time: DateTime<Utc>,
|
|
||||||
pub current_version: i64,
|
|
||||||
/// In Hashicorp:
|
|
||||||
/// If not set, the backend's configured delete_version_after is used.
|
|
||||||
/// Cannot be greater than the backend's delete_version_after
|
|
||||||
// TODO: implement duration type
|
|
||||||
pub delete_version_after: String,
|
|
||||||
// TODO https://developer.hashicorp.com/vault/docs/concepts/duration-format
|
|
||||||
pub max_versions: i64,
|
|
||||||
pub oldest_version: i64,
|
|
||||||
pub updated_time: DateTime<Utc>,
|
|
||||||
/// User-provided key-value pairs that are used to describe arbitrary and version-agnostic information about a secret.
|
|
||||||
pub custom_metadata: Option<HashMap<String, String>>,
|
|
||||||
pub versions: Vec<VersionMeta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SecretMeta {
|
|
||||||
fn default() -> Self {
|
|
||||||
let current = Utc::now();
|
|
||||||
SecretMeta {
|
|
||||||
cas_required: false,
|
|
||||||
created_time: current,
|
|
||||||
current_version: 1,
|
|
||||||
delete_version_after: "24h00m00s".to_string(),
|
|
||||||
max_versions: 10,
|
|
||||||
oldest_version: 1,
|
|
||||||
updated_time: current,
|
|
||||||
custom_metadata: None,
|
|
||||||
versions: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +1,68 @@
|
||||||
use std::collections::HashMap;
|
// This file is deprecated. Currently, all tests are in written in go.
|
||||||
|
|
||||||
use chrono::Utc;
|
// use std::collections::HashMap;
|
||||||
use tests::{
|
|
||||||
logic::patch_metadata,
|
|
||||||
structs::{SecretMeta, VersionMeta},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
// use chrono::Utc;
|
||||||
|
// use tests::{
|
||||||
|
// logic::patch_metadata,
|
||||||
|
// structs::{SecretMeta, VersionMeta},
|
||||||
|
// };
|
||||||
|
|
||||||
#[test]
|
// use super::*;
|
||||||
#[cfg(target_feature = "_disabled")]
|
|
||||||
fn print_serialized_test() {
|
|
||||||
let temp_secret = TempSecret {
|
|
||||||
content: String::from("Hallo"),
|
|
||||||
version: 12,
|
|
||||||
};
|
|
||||||
let serialized = serialize_secret_json(&temp_secret);
|
|
||||||
println!("string serialized: {:?}", serialized);
|
|
||||||
let deserialized = deserialize_secret_struct(&serialized.unwrap());
|
|
||||||
println!(
|
|
||||||
"Struct field from deserialized: {}",
|
|
||||||
deserialized.unwrap().content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
#[cfg(target_feature = "_disabled")]
|
|
||||||
fn test_patching() {
|
|
||||||
// TODO add more assertions
|
|
||||||
let mut base = create_mock_meta();
|
|
||||||
println!("OLD metadata: {:?}", base);
|
|
||||||
let overwrite: SecretMeta = SecretMeta {
|
|
||||||
max_versions: 10,
|
|
||||||
versions: vec![VersionMeta {
|
|
||||||
created_time: Utc::now(),
|
|
||||||
deletion_time: Some(Utc::now()),
|
|
||||||
destroyed: true,
|
|
||||||
}],
|
|
||||||
cas_required: true,
|
|
||||||
delete_version_after: "10m".to_string(),
|
|
||||||
current_version: 4,
|
|
||||||
oldest_version: 2,
|
|
||||||
updated_time: Utc::now(),
|
|
||||||
created_time: Utc::now(),
|
|
||||||
custom_metadata: Some(HashMap::new()),
|
|
||||||
};
|
|
||||||
let mut patched: Option<SecretMeta> = None; // Laurenz here
|
|
||||||
match patch_metadata(&mut base, &overwrite) {
|
|
||||||
Ok(meta) => {
|
|
||||||
println!("NEW metadata: {:?}", meta);
|
|
||||||
println!("patched successfully");
|
|
||||||
patched = Some(meta);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("error patching metadata: {}", e);
|
|
||||||
panic!("Patching failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(patched_meta) = patched {
|
// #[test]
|
||||||
assert!(patched_meta.current_version == 4);
|
// #[cfg(target_feature = "_disabled")]
|
||||||
assert!(patched_meta.versions[0].destroyed == true);
|
// fn print_serialized_test() {
|
||||||
} else {
|
// let temp_secret = TempSecret {
|
||||||
panic!("patched was not initialized");
|
// content: String::from("Hallo"),
|
||||||
}
|
// version: 12,
|
||||||
}
|
// };
|
||||||
|
// let serialized = serialize_secret_json(&temp_secret);
|
||||||
|
// println!("string serialized: {:?}", serialized);
|
||||||
|
// let deserialized = deserialize_secret_struct(&serialized.unwrap());
|
||||||
|
// println!(
|
||||||
|
// "Struct field from deserialized: {}",
|
||||||
|
// deserialized.unwrap().content
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// #[test]
|
||||||
|
// #[cfg(target_feature = "_disabled")]
|
||||||
|
// fn test_patching() {
|
||||||
|
// // TODO add more assertions
|
||||||
|
// let mut base = create_mock_meta();
|
||||||
|
// println!("OLD metadata: {:?}", base);
|
||||||
|
// let overwrite: SecretMeta = SecretMeta {
|
||||||
|
// max_versions: 10,
|
||||||
|
// versions: vec![VersionMeta {
|
||||||
|
// created_time: Utc::now(),
|
||||||
|
// deletion_time: Some(Utc::now()),
|
||||||
|
// destroyed: true,
|
||||||
|
// }],
|
||||||
|
// cas_required: true,
|
||||||
|
// delete_version_after: "10m".to_string(),
|
||||||
|
// current_version: 4,
|
||||||
|
// oldest_version: 2,
|
||||||
|
// updated_time: Utc::now(),
|
||||||
|
// created_time: Utc::now(),
|
||||||
|
// custom_metadata: Some(HashMap::new()),
|
||||||
|
// };
|
||||||
|
// let mut patched: Option<SecretMeta> = None;
|
||||||
|
// match patch_metadata(&mut base, &overwrite) {
|
||||||
|
// Ok(meta) => {
|
||||||
|
// println!("NEW metadata: {:?}", meta);
|
||||||
|
// println!("patched successfully");
|
||||||
|
// patched = Some(meta);
|
||||||
|
// }
|
||||||
|
// Err(e) => {
|
||||||
|
// log::error!("error patching metadata: {}", e);
|
||||||
|
// panic!("Patching failed");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Some(patched_meta) = patched {
|
||||||
|
// assert!(patched_meta.current_version == 4);
|
||||||
|
// assert!(patched_meta.versions[0].destroyed == true);
|
||||||
|
// } else {
|
||||||
|
// panic!("patched was not initialized");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,9 @@ mod sys;
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// To be configured via environment variables
|
// To be configured via environment variables
|
||||||
// choose from (highest to lowest): error, warn, info, debug, trace, off
|
// choose from (highest to lowest): error, warn, info, debug, trace, off
|
||||||
env::set_var("RUST_LOG", "trace"); // TODO: Remove to respect user configuration
|
env::set_var("RUST_LOG", "trace");
|
||||||
// env::set_var("DATABASE_URL", "sqlite:test.db"); // TODO: move to .env
|
// env::set_var("DATABASE_URL", "sqlite:test.db"); // Format for the env var config. Consider moving to an .env file
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// Listen on all IPv4 and IPv6 interfaces on port 8200 by default
|
// Listen on all IPv4 and IPv6 interfaces on port 8200 by default
|
||||||
let listen_addr = env::var("LISTEN_ADDR").unwrap_or("[::]:8200".to_string()); // Do not change
|
let listen_addr = env::var("LISTEN_ADDR").unwrap_or("[::]:8200".to_string()); // Do not change
|
||||||
let listen_addr = SocketAddr::from_str(&listen_addr).expect("Failed to parse LISTEN_ADDR");
|
let listen_addr = SocketAddr::from_str(&listen_addr).expect("Failed to parse LISTEN_ADDR");
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use base::create_mock_meta;
|
|
||||||
#[test]
|
|
||||||
fn test_update_secret() {
|
|
||||||
let db: sled::Db = sled::open("sled_db").unwrap();
|
|
||||||
update_secret(&db, "foo", TempSecret{version: -99, content: "cool".to_string()});
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_get_secret() {
|
|
||||||
let db: sled::Db = sled::open("sled_db").unwrap();
|
|
||||||
get_secret(&db, "foo");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_delete_secret(){
|
|
||||||
let db: sled::Db = sled::open("sled_db").unwrap();
|
|
||||||
delete_secret(&db, "foo");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_meta(){
|
|
||||||
let db: sled::Db = sled::open("sled_db").unwrap();
|
|
||||||
println!("writing metadata:");
|
|
||||||
update_secret_meta(&db, "metatest", create_mock_meta());
|
|
||||||
println!("getting metadata:");
|
|
||||||
get_secretmeta(&db, "metatest");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use sled::Db;
|
|
||||||
use base::{deserialize_metadata_struct, deserialize_secret_struct, serialize_metadata_json, serialize_secret_json, SecretMeta, TempSecret};
|
|
||||||
|
|
||||||
/// [TODO] Currently no proper versioning
|
|
||||||
/// inserts a secret. If there was already a secret in the given path, the version is incremented
|
|
||||||
fn update_secret(db: &Db, path: &str, mut secret: TempSecret) {
|
|
||||||
match get_secret(db, path) {
|
|
||||||
Some(old_secret) => {
|
|
||||||
// case secret found. TODO save it somewhere for versioning
|
|
||||||
secret.version = old_secret.version + 1;
|
|
||||||
#[cfg(test)]
|
|
||||||
print!("something was found. new version {} \n", secret.version)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// case new secret
|
|
||||||
secret.version = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if let secret_json = serialize_secret_json(&secret) {
|
|
||||||
// let _res = db.insert(path, secret_json); // maybe this can be handled cleaner
|
|
||||||
match serialize_secret_json(&secret) {
|
|
||||||
Ok(secret_json) => {
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("String: {:?}", secret_json.clone());
|
|
||||||
let as_ivec = sled::IVec::from(secret_json.into_bytes()); // maybe outsource this in a fn later
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("ivec: {:?}", as_ivec);
|
|
||||||
match db.insert(path, as_ivec) {
|
|
||||||
Ok(_) => println!("Secret inserted"),
|
|
||||||
Err(e) => eprintln!("Failed to insert secret: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Failed to serialize secret: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// !TODO eliminate redundancy: refactor get and update functions to accept generic types!
|
|
||||||
|
|
||||||
// read and return a secret from the DB
|
|
||||||
//if there is no secret, return None
|
|
||||||
fn get_secret(db: &Db, path: &str) -> Option<TempSecret>{
|
|
||||||
let raw_secret;
|
|
||||||
match db.get(path) {
|
|
||||||
Ok(Some(ivec)) => {
|
|
||||||
raw_secret = ivec;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error on retrieving secret: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// outsource this in a fn later. TODO maybe deal with unwrap
|
|
||||||
let as_str = String::from_utf8(raw_secret.to_vec()).unwrap();
|
|
||||||
match deserialize_secret_struct(&as_str) {
|
|
||||||
Ok(secret) => {
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("got some secret: {:?}", secret);
|
|
||||||
return Some(secret);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("error on secret deserialization: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO write abstract get_something fn
|
|
||||||
// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-metadata
|
|
||||||
fn get_secretmeta(db: &Db, path: &str) -> Option<SecretMeta>{
|
|
||||||
let raw_metadata;
|
|
||||||
match db.get(path) {
|
|
||||||
Ok(Some(ivec)) => {
|
|
||||||
raw_metadata = ivec;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error on retrieving metadata: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let as_str = String::from_utf8(raw_metadata.to_vec()).unwrap();
|
|
||||||
match deserialize_metadata_struct(&as_str) {
|
|
||||||
Ok(meta) => {
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("got some metadata: {:?}", meta);
|
|
||||||
return Some(meta);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("error on secret deserialization: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently early version (copied from update_secret)
|
|
||||||
fn update_secret_meta(db: &Db, path: &str, mut meta: SecretMeta) {
|
|
||||||
match get_secretmeta(db, path) {
|
|
||||||
Some(meta) => {
|
|
||||||
// case secret found. TODO save it somewhere for versioning
|
|
||||||
#[cfg(test)]
|
|
||||||
print!("something was found. new version {:?} \n", meta)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match serialize_metadata_json(&meta) {
|
|
||||||
Ok(meta_json) => {
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("String: {:?}", meta_json.clone());
|
|
||||||
let as_ivec = sled::IVec::from(meta_json.into_bytes()); // maybe outsource this in a fn later
|
|
||||||
#[cfg(test)]
|
|
||||||
println!("ivec: {:?}", as_ivec);
|
|
||||||
match db.insert(path, as_ivec) {
|
|
||||||
Ok(_) => println!("Metadata inserted"),
|
|
||||||
Err(e) => eprintln!("Failed to insert meta: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Failed to serialize meta: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
|
|
||||||
/// hard delete secret at path
|
|
||||||
fn delete_secret(db: &Db, path: &str) {
|
|
||||||
let rem = db.remove(path);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue