WIP feat (kv2): Method implementations and test improvements

This commit is contained in:
Laurenz 2025-03-24 10:34:38 +01:00
parent b5e086bd0a
commit 491ca2fd54
20 changed files with 1337 additions and 852 deletions

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE kv2_secret_version\n SET deletion_time = $4\n WHERE engine_path = $1 AND secret_path = $2\n AND version_number = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "047ebbce6fa0073cc810b189e8db3ff5e4eb347f1c1d9e5408220411a9e08b00"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT secret_data, created_time, deletion_time, version_number, secret_path\n FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL\n ORDER BY version_number DESC LIMIT 1",
"describe": {
"columns": [
{
"name": "secret_data",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "created_time",
"ordinal": 1,
"type_info": "Datetime"
},
{
"name": "deletion_time",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "version_number",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "secret_path",
"ordinal": 4,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
true,
false,
false
]
},
"hash": "0b6d152060335c7c8cc6e781eac810a3fc1c2ad752e3f856a66e75b73b2ab3c6"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT version_number AS latest_version FROM kv2_secret_version\n WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL\n ORDER BY version_number DESC LIMIT 1",
"describe": {
"columns": [
{
"name": "latest_version",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "414c74a3c017bde424fe44bbc251fea384b0dbedd1541900d147e0814c1f33d8"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT engine_type FROM secret_engines WHERE mount_point = $1",
"describe": {
"columns": [
{
"name": "engine_type",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "9265f0195bbacd15061927c2a6034e3725a25068fd3faa08cc1d02e7c926f1c2"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO kv2_metadata (engine_path, secret_path, cas_required, created_time, max_versions, updated_time)\n VALUES ($1, $2, 0, $3, 100, $3)\n ON CONFLICT(engine_path, secret_path) DO NOTHING;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "af57fe92ead35790b02f38f34e1614cd1accb2da61f1d9a07eeefb0fc31ec318"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\nWITH latest_version AS (\n SELECT MAX(version_number) AS max_version\n FROM kv2_secret_version\n WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path\n)\nINSERT INTO kv2_secret_version (engine_path, secret_path, secret_data, created_time, version_number)\nVALUES (\n $1, -- engine_path\n $2, -- secret_path\n $3, -- secret_data\n $4, -- created_time\n CASE -- Use provided version if given\n WHEN $5 IS NOT NULL THEN $5 -- version_number (optional)\n ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 0)\n END -- version_number logic\n)\nRETURNING version_number;\n",
"describe": {
"columns": [
{
"name": "version_number",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 5
},
"nullable": [
false
]
},
"hash": "c6beeb7d8672039df5258ada802920aae8f16db215dda5ab447dbe832f4a6703"
}

1404
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,23 +7,25 @@ edition = "2021"
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 = ["zeroize_derive"] }
chrono = { version = "0.4.38", features = ["serde"] } time = { version = "0.3.39", features = ["serde", "formatting"]}
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
tower = { version = "0.4.13", features = [] } tower = { version = "0.5.2", features = [] }
axum = "0.7.5" axum = "0.8.1"
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 = "4.0.0"
# serde_with = "3.8.1" # serde_with = "3.8.1"
dotenvy = "0.15.7"
# utoipa = { version = "4.2.0", features = ["axum_extras"] } # utoipa = { version = "4.2.0", features = ["axum_extras"] }
sqlx = { version = "0.7.4", features = [ sqlx = { version = "0.8.3", features = [
"sqlite", "sqlite",
# "postgres", # "postgres",
# "any", # "any",
"macros", "macros",
"runtime-tokio", "runtime-tokio",
"tls-rustls", "tls-rustls",
"time"
] } ] }
[lints] [lints]

View file

@ -4,13 +4,26 @@ go 1.21.9
require github.com/hashicorp/vault-client-go v0.4.3 require github.com/hashicorp/vault-client-go v0.4.3
require github.com/hashicorp/vault/api v1.16.0
require ( require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
) )

View file

@ -1,29 +1,77 @@
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc= github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4=
github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -4,15 +4,18 @@ import (
"context" "context"
"log" "log"
"os" "os"
"reflect"
"testing" "testing"
"time" "time"
"github.com/hashicorp/vault-client-go" // "github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema" // "github.com/hashicorp/vault-client-go/schema"
vault "github.com/hashicorp/vault/api"
) )
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"
@ -20,93 +23,86 @@ var mountpath2 = "/some"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
ctx = context.Background() ctx = context.Background()
var err error var err error
config := vault.DefaultConfig()
config.Address = "http://localhost:8200"
config.Timeout = 30*time.Second
// prepare a client with the given base address // prepare a client with the given base address
client, err = vault.New( client, err = vault.NewClient(config)
vault.WithAddress("http://localhost:8200"),
vault.WithRequestTimeout(30*time.Second),
)
if err != nil { if err != nil {
log.Fatal(err) log.Fatalf("unable to initialize Vault client: %v", err)
} }
log.Println("client prepared") log.Println("client prepared")
// authenticate with a root token (insecure) // authenticate with a root token (insecure)
if err := client.SetToken("my-token"); err != nil { client.SetToken("my-token")
log.Fatal(err)
}
exitCode := m.Run() // run all tests and get code exitCode := m.Run() // run all tests and get code
os.Exit(exitCode) os.Exit(exitCode)
} }
func kv2Write(t *testing.T, mount string, path string) {
data := map[string]any{
"password1": "123abc",
"password2": "horse horse horse battery staple correct",
}
t.Logf("Attempting to write to KV2 %s path %s:\t", mount, path)
v, err := client.KVv2(mount).Put(ctx, path, data)
if err != nil {
t.Fatal("ERROR writing secret:\n\t", err)
}
t.Log("Success (unchecked)\n\t", v)
res, err := client.KVv2(mount).Get(ctx, path)
if err != nil {
t.Fatal("ERROR checking/reading secret (request failed)\n\t", err)
}
if !reflect.DeepEqual(res.Data, data) {
t.Fatal("AAAAH", res.Data)
t.Fatalf("ERROR secret received does not match what was outght to be written.\n\tWritten: %s\n\tReceived: %s\n", data, res.Data)
// t.Fatal("\tWritten: ", newVar.Data)
// t.Fatal("\tReceived:", res.Data.Data)
}
t.Logf("SUCCESS writing to KV2 %s path %s\n", mount, path)
}
func kv2Delete(t *testing.T, mount string, path string) {
err := client.KVv2(mount).Delete(ctx, path) // currently disregarding modifier options
if err != nil {
log.Fatal("ERROR deleting secret:\n\t", err)
}
res, err := client.KVv2(mount).Get(ctx, path)
if res != nil || err == nil {
t.Fatal("ERROR checking/reading secret (request failed)\n\t", res, err)
}
t.Logf("SUCCESS deleting KV2 secret %s path %s\n", mount, path)
}
// 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 // @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{ t.Logf("Writing to first KV2 engine at %s...", mountpath)
Data: map[string]any{ kv2Write(t, mountpath, "foo")
"password1": "123abc", kv2Write(t, mountpath, "bar")
"password2": "horse horse horse battery staple correct", t.Logf("Writing to second KV2 engine at %s...", mountpath2)
}}, kv2Write(t, mountpath2, "foo")
vault.WithMountPath(mountpath), kv2Write(t, mountpath2, "bar")
) t.Logf("Deleting...")
if err != nil { kv2Delete(t, mountpath, "foo")
log.Fatal("kv2: Failed to write secret:\n\t", err)
}
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath)
// Path bar
_, err = client.Secrets.KvV2Write(ctx, "bar", schema.KvV2WriteRequest{
Data: map[string]any{
"password1": "abc123",
"password2": "correct horse battery staple",
}},
vault.WithMountPath(mountpath),
)
if err != nil {
log.Fatal("kv2: Failed to write secret:\n\t", err)
}
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath)
} }
func TestWriteSecret2(t *testing.T) { // func TestDeleteSecret(t *testing.T) {
// Path foo // _, err := client.Secrets.KvV2Delete(ctx, "foo") // currently disregarding modifier options
_, err := client.Secrets.KvV2Write(ctx, "foo", schema.KvV2WriteRequest{ // if err != nil {
Data: map[string]any{ // log.Fatal("kv2: Failed to delete secret:\n\t", err)
"password1": "123abc", // }
"password2": "horse horse horse battery staple correct", // }
}},
vault.WithMountPath(mountpath2),
)
if err != nil {
log.Fatal("kv2: Failed to write secret:\n\t", err)
}
log.Println("kv2: Tried to write Secret at foo at mountpath: ", mountpath2)
// Path bar // func TestReadSecret(t *testing.T) {
_, err = client.Secrets.KvV2Write(ctx, "bar", schema.KvV2WriteRequest{ // _, err := client.Secrets.KvV2Read(ctx, "bar")
Data: map[string]any{ // if err != nil {
"password1": "abc123", // log.Fatal("kv2: Failed to read secret:\n\t", err)
"password2": "correct horse battery staple", // }
}}, // }
vault.WithMountPath(mountpath2),
)
if err != nil {
log.Fatal("kv2: Failed to write secret:\n\t", err)
}
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) {
_, err := client.Secrets.KvV2Read(ctx, "bar")
if err != nil {
log.Fatal("kv2: Failed to read secret:\n\t", err)
}
}

View file

@ -1,7 +1,8 @@
-- Add migration script here -- Add migration script here
CREATE TABLE metadata ( CREATE TABLE kv2_metadata (
secret_path TEXT PRIMARY KEY NOT NULL, engine_path TEXT NOT NULL,
secret_path TEXT NOT NULL,
cas_required INTEGER NOT NULL, -- no bool datatype in sqlite cas_required INTEGER NOT NULL, -- no bool datatype in sqlite
created_time TIMESTAMP NOT NULL, created_time TIMESTAMP NOT NULL,
@ -10,19 +11,20 @@ CREATE TABLE metadata (
-- current_version INTEGER NOT NULL, -- current_version INTEGER NOT NULL,
-- oldest_version INTEGER NOT NULL, -- oldest_version INTEGER NOT NULL,
updated_time TIMESTAMP NOT NULL, updated_time TIMESTAMP NOT NULL,
custom_data TEXT custom_data TEXT,
PRIMARY KEY (engine_path, secret_path)
); );
CREATE TABLE secret_versions ( CREATE TABLE kv2_secret_version (
secret_data TEXT NOT NULL, engine_path TEXT NOT NULL,
created_time TIMESTAMP NOT NULL,
deletion_time TIMESTAMP,
version_number INTEGER NOT NULL DEFAULT 0,
secret_path TEXT NOT NULL, secret_path TEXT NOT NULL,
PRIMARY KEY (secret_path, version_number),
FOREIGN KEY (secret_path) REFERENCES metadata(secret_path)
);
CREATE INDEX idx_secret_versions_secret_path ON secret_versions (secret_path); version_number INTEGER NOT NULL,
secret_data TEXT NOT NULL,
created_time DATETIME NOT NULL,
deletion_time DATETIME,
PRIMARY KEY (engine_path, secret_path, version_number),
FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path)
);

View file

@ -1,5 +0,0 @@
-- Add migration script here
INSERT INTO metadata VALUES ("bar", false, DateTime('now'), "123", 4, DateTime('now'), "customData");
INSERT INTO secret_versions VALUES ("secret_data", DateTime('now'), DateTime('now'), 1, "bar");

View file

@ -20,6 +20,9 @@ struct EngineMapperState {
kv_v2: Router, kv_v2: Router,
} }
#[derive(Clone)]
struct EnginePath(String);
/// Secret engine router /// Secret engine router
pub fn secrets_router(pool: DatabaseDriver) -> Router<DatabaseDriver> { pub fn secrets_router(pool: DatabaseDriver) -> Router<DatabaseDriver> {
// State containing the pool and engine routers // State containing the pool and engine routers
@ -28,6 +31,7 @@ pub fn secrets_router(pool: DatabaseDriver) -> Router<DatabaseDriver> {
kv_v2: kv::kv_router(pool.clone()), kv_v2: kv::kv_router(pool.clone()),
}; };
// Problem solved via fallback route
Router::new().fallback(engine_handler).with_state(state) Router::new().fallback(engine_handler).with_state(state)
} }
@ -55,6 +59,7 @@ async fn engine_handler(
async fn call_router(engine: Router, mount_path: String, mut req: Request) -> Response { async fn call_router(engine: Router, mount_path: String, mut req: Request) -> Response {
let rui = req.uri().path().replace(&mount_path, "").parse().unwrap(); let rui = req.uri().path().replace(&mount_path, "").parse().unwrap();
*req.uri_mut() = rui; *req.uri_mut() = rui;
let mount_path = EnginePath(mount_path);
engine engine
.layer(Extension(mount_path)) .layer(Extension(mount_path))
@ -70,7 +75,7 @@ fn unknown_engine(engine_type: String) -> impl IntoResponse {
error!("Engine type {} not implemented", engine_type); error!("Engine type {} not implemented", engine_type);
HttpError::simple( HttpError::simple(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("Engine type {} not implemented", engine_type), format!("Engine type {engine_type} not implemented"),
) )
} }

View file

@ -7,37 +7,41 @@ pub mod structs;
// #[cfg(test)] // #[cfg(test)]
// mod tests; // mod tests;
use std::{collections::HashMap, convert::Infallible};
use crate::{ use crate::{
common::HttpError,
engines::kv::{logic::body_to_json, structs::*}, engines::kv::{logic::body_to_json, structs::*},
storage::DatabaseDriver, storage::DatabaseDriver,
}; };
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::IntoResponse, http::StatusCode,
response::{IntoResponse, NoContent, Response},
routing::*, routing::*,
Json, Router, Extension, Json, Router,
}; };
use log::{info, error}; use log::{error, info, warn};
use sqlx::Row; use time::UtcDateTime;
use super::EnginePath;
pub fn kv_router(pool: DatabaseDriver) -> Router { pub fn kv_router(pool: DatabaseDriver) -> Router {
Router::new() Router::new()
.route("/config", get(get_config)) .route("/config", get(get_config))
.route("/config", post(post_config)) .route("/config", post(post_config))
.route("/data/*path", get(get_data)) .route("/data/{*path}", get(get_data))
// .route("/:mount_path/data/*path/", get(get_data)) // .route("/:mount_path/data/*path/", get(get_data))
.route("/data/*path", post(post_data)) .route("/data/{*path}", post(post_data))
.route("/data/*path", delete(delete_data)) .route("/data/{*path}", put(post_data))
.route("/delete/*path", post(delete_path)) .route("/data/{*path}", delete(delete_data))
.route("/destroy/*path", post(destroy_path)) .route("/delete/{*path}", post(delete_path))
.route("/metadata/*path", get(get_meta)) .route("/destroy/{*path}", post(destroy_path))
.route("/metadata/{*path}", get(get_meta))
// .route("/:mount_path/metadata/*path/", get(get_meta)) // .route("/:mount_path/metadata/*path/", get(get_meta))
.route("/metadata/*path", post(post_meta)) .route("/metadata/{*path}", post(post_meta))
.route("/metadata/*path", delete(delete_meta)) .route("/metadata/{*path}", delete(delete_meta))
.route("/subkeys/*path", get(get_subkeys)) .route("/subkeys/{*path}", get(get_subkeys))
.route("/undelete/*path", post(post_undelete)) .route("/undelete/{*path}", post(post_undelete))
.with_state(pool) .with_state(pool)
} }
@ -52,88 +56,114 @@ async fn post_config() -> &'static str {
async fn get_data( async fn get_data(
State(pool): State<DatabaseDriver>, State(pool): State<DatabaseDriver>,
Path(path): Path<String>, Path(path): Path<String>,
) -> Result<impl IntoResponse, Infallible> { Extension(EnginePath(engine_path)): Extension<EnginePath>,
match sqlx::query("SELECT * FROM secret_versions WHERE secret_path = $1") ) -> Result<Response, ()> {
.bind(path) log::trace!("AAAAAAAAAAAAAAAAAAAA! {path} of engine {engine_path}");
match sqlx::query_as!(
KvSecretData,
r#"SELECT secret_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) .fetch_one(&pool)
.await .await
{ {
Ok(v) => { Ok(secret_content) => {
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 // // TODO: use 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 {
data: secret_content, let data = Wrapper {
options: None, data: serde_json::from_str(&secret_content.secret_data).unwrap(),
}; };
let return_secret = KvSecretRes {
data,
options: None,
version: Some(secret_content.version_number)
};
let return_secret = Json(return_secret);
info!("{:?}", return_secret); info!("{:?}", return_secret);
Ok(Json(return_secret)) Ok(return_secret.into_response())
} }
Err(e) => match e { Err(e) => match e {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
error!("{:?}", e); error!("Row not found {:?}", e);
let error_data: HashMap<String, String> = HashMap::from([("error".to_string(), "Secret not found".to_string())]); // let error_data: HashMap<String, String> =
let error_secret = KvSecretReq{data: error_data, options: None}; // HashMap::from([("error".to_string(), "Secret not found".to_string())]);
Ok(Json(error_secret)) // let error_secret = KvSecretRes {
}, // data: error_data,
_ => panic!("{:?}", e), // options: None,
// };
Ok(HttpError::simple(
StatusCode::NOT_FOUND,
"Secret not found within kv2 engine",
))
}
_ => panic!("{e:?}"),
}, },
} }
} }
async fn post_data( async fn post_data(
State(pool): State<DatabaseDriver>, State(pool): State<DatabaseDriver>,
Path(path): Path<String>, Path(kv_path): Path<String>,
body: String, Extension(EnginePath(engine_path)): Extension<EnginePath>,
) -> &'static str { Json(secret): Json<KvV2WriteRequest>,
// Insert Metadata first -> Else: Error because of foreign key constraint ) -> Result<Response, ()> {
log::debug!(
"Engine: {}, Secret: {}, Content: {}, Version: {:?}, path: {}",
engine_path,
kv_path,
secret.data,
secret.version, //.unwrap_or(0),
kv_path
);
// let mut body_json = body_to_json(body); let created_time = time::UtcDateTime::now();
let ts = created_time.unix_timestamp();
// let secret: KvSecret = KvSecret { let mut tx = pool.begin().await.unwrap();
// data: body_json["data"]["password1"].take().to_string(),
// version: body_json["data"]["version"].take().as_i64(),
// };
// 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 res_m = sqlx::query!("
// let deletion_time = "05-03-2024 12:00:00"; INSERT INTO kv2_metadata (engine_path, secret_path, cas_required, created_time, max_versions, updated_time)
// let version = "0"; VALUES ($1, $2, 0, $3, 100, $3)
// match sqlx::query!( ON CONFLICT(engine_path, secret_path) DO NOTHING;
// "INSERT INTO secret_versions VALUES ($1, $2, $3, $4, $5)", ", engine_path, kv_path, ts).execute(&mut *tx).await.unwrap();
// secret.data,
// created_time,
// deletion_time,
// version,
// version
// )
// .execute(&pool)
// .await
// {
// Ok(v) => {
// trace!("{:?}", v);
// "Success"
// }
// Err(e) => {
// trace!("{:?}", e);
// "Error"
// }
// }
todo!("not implemented") let res_r = sqlx::query_file!(
"src/engines/kv/post_secret.sql",
engine_path,
kv_path,
secret.data,
ts,
secret.version,
)
.fetch_one(&mut *tx)
.await
.unwrap();
tx.commit().await.expect("FAILED TO WRITE TX!");
warn!("test: {res_m:?} {res_r:?} {}", res_r.version_number);
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())
} }
/* mock for return /* mock for return
@ -166,8 +196,70 @@ async fn post_data(
/// 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
// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-secret-versions // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-secret-versions
async fn delete_data() -> &'static str { async fn delete_data(
todo!("not implemented") State(pool): State<DatabaseDriver>,
Path(path): Path<String>,
Extension(EnginePath(engine_path)): Extension<EnginePath>,
) -> Result<Response, Response> {
log::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 commited 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())
} }
async fn delete_path() -> &'static str { async fn delete_path() -> &'static str {
@ -187,7 +279,7 @@ async fn post_meta(
Path((mount_path, kv_path)): Path<(String, String)>, Path((mount_path, kv_path)): Path<(String, String)>,
body: String, body: String,
) -> &'static str { ) -> &'static str {
let mut body_json = body_to_json(body); let body_json = body_to_json(body);
let meta_data: SecretMeta = Default::default(); let meta_data: SecretMeta = Default::default();
todo!("not implemented") todo!("not implemented")
} }

View file

@ -0,0 +1,25 @@
WITH latest AS (
SELECT version_number AS version
FROM kv2_secret_version
WHERE engine_path = '/kv-v2' AND secret_path = 'foo' AND deletion_time IS NULL
ORDER BY version_number DESC
LIMIT 1
),
update_deleted AS (
UPDATE kv2_secret_version
SET deletion_time = CURRENT_TIMESTAMP
WHERE engine_path = '/kv-v2' AND secret_path = 'foo'
AND version_number = (SELECT version FROM latest)
RETURNING version_number AS deleted_version
),
new_latest AS (
SELECT version_number AS new_latest_version
FROM kv2_secret_version
WHERE engine_path = '/kv-v2' AND secret_path = 'foo' AND deletion_time IS NULL
ORDER BY version_number DESC
LIMIT 1
)
SELECT
(SELECT deleted_version FROM update_deleted) AS deleted_version,
(SELECT new_latest_version FROM new_latest) AS new_latest_version;

View file

@ -6,13 +6,13 @@ use super::structs::*;
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")] #[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
/// serialize secret to JSON String /// serialize secret to JSON String
pub fn serialize_secret_json(secret: &KvSecretReq) -> Result<String, serde_json::Error> { pub fn serialize_secret_json(secret: &KvSecretRes) -> Result<String, serde_json::Error> {
serde_json::to_string(&secret) serde_json::to_string(&secret)
} }
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")] #[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
/// deserialize JSON String to secret /// deserialize JSON String to secret
pub fn deserialize_secret_struct(raw: &String) -> Result<KvSecretReq, serde_json::Error> { pub fn deserialize_secret_struct(raw: &str) -> Result<KvSecretRes, serde_json::Error> {
serde_json::from_str(raw) serde_json::from_str(raw)
} }
@ -24,7 +24,7 @@ pub fn serialize_metadata_json(secret: &SecretMeta) -> Result<String, serde_json
#[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")] #[deprecated(note = "Use Axum functionality with structs instead, also, this should be inlined if it is actually needed")]
/// deserialize JSON String to metadata /// deserialize JSON String to metadata
pub fn deserialize_metadata_struct(raw: &String) -> Result<SecretMeta, serde_json::Error> { pub fn deserialize_metadata_struct(raw: &str) -> Result<SecretMeta, serde_json::Error> {
serde_json::from_str(raw) serde_json::from_str(raw)
} }

View file

@ -0,0 +1,18 @@
WITH latest_version AS (
SELECT MAX(version_number) AS max_version
FROM kv2_secret_version
WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path
)
INSERT INTO kv2_secret_version (engine_path, secret_path, secret_data, created_time, version_number)
VALUES (
$1, -- engine_path
$2, -- secret_path
$3, -- secret_data
$4, -- created_time
CASE -- Use provided version if given
WHEN $5 IS NOT NULL THEN $5 -- version_number (optional)
ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 0)
END -- version_number logic
)
RETURNING version_number;

View file

@ -1,46 +1,92 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::{collections::HashMap, vec}; use std::{collections::HashMap, vec};
use time::{OffsetDateTime, UtcDateTime, serde::rfc3339};
// #[derive(Serialize, Deserialize, Debug)]
// pub struct KvSecretData {
// pub secret_data: String,
// #[serde(with = "rfc3339")]
// pub created_time: UtcDateTime,
// #[serde(with = "rfc3339::option")]
// pub deletion_time: Option<UtcDateTime>,
// pub version_number: i64,
// pub secret_path: String,
// }
#[derive(Serialize, Deserialize, Debug, Clone)]
/// For SQLite support
pub struct KvSecretData {
pub secret_data: String,
#[serde(with = "rfc3339")]
pub created_time: OffsetDateTime,
#[serde(with = "rfc3339::option")]
pub deletion_time: Option<OffsetDateTime>,
pub version_number: i64,
pub secret_path: String,
}
// impl From<KvSecretDataDBO> for KvSecretData {
// fn from(value: KvSecretDataDBO) -> Self {
// Self {
// secret_data: value.secret_data,
// created_time: value.created_time.to_offset(UtcOffset::UTC),
// deletion_time: value.deletion_time.map(|v| v.to_utc()),
// version_number: value.version_number,
// secret_path: value.secret_path,
// }
// }
// }
#[derive(serde::Serialize, Deserialize, Debug)]
pub struct Wrapper<T> {
pub data: T,
}
pub type KvSecretData = HashMap<String, String>;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct KvSecretReq { pub struct KvSecretRes {
/// Map (required) /// Map (required)
pub data: KvSecretData, pub data: Wrapper<serde_json::Value>,
/// Map (optional), may contain `cas` integer /// Map (optional), may contain `cas` integer
/// Set the `cas` value to use a Check-And-Set operation
// #[serde_as(as = "serde_with::EnumMap")] // #[serde_as(as = "serde_with::EnumMap")]
pub options: Option<HashMap<String, String>>, pub options: Option<HashMap<String, String>>,
// Version does not exist for create/update operations // Version does not exist for create/update operations
// pub version: Option<i64>, pub version: Option<i64>,
// TODO add all fields // TODO add all fields
} }
#[derive(Deserialize)]
pub struct KvV2WriteRequest {
pub data: serde_json::Value,
pub options: Option<serde_json::Value>,
pub version: Option<i32>,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct KvSecretResData { pub struct KvV2WriteResponse {
pub created_time: DateTime<Utc>, #[serde(with = "rfc3339")]
pub created_time: OffsetDateTime,
pub custom_metadata: Option<HashMap<String, String>>, pub custom_metadata: Option<HashMap<String, String>>,
pub deletion_time: Option<DateTime<Utc>>, #[serde(with = "rfc3339::option")]
pub deletion_time: Option<OffsetDateTime>,
pub destroyed: bool, pub destroyed: bool,
pub version: i64, pub version: i64,
} }
#[derive(Serialize, Debug)]
pub struct KvSecretRes {
pub data: KvSecretResData,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct VersionMeta { pub struct VersionMeta {
pub created_time: DateTime<Utc>, pub created_time: UtcDateTime,
pub deletion_time: Option<DateTime<Utc>>, // optional deletion time pub deletion_time: Option<UtcDateTime>, // optional deletion time
pub destroyed: bool, pub destroyed: bool,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SecretMeta { pub struct SecretMeta {
pub cas_required: bool, pub cas_required: bool,
pub created_time: DateTime<Utc>, pub created_time: UtcDateTime,
pub current_version: i64, pub current_version: i64,
/// In Hashicorp: /// In Hashicorp:
/// If not set, the backend's configured delete_version_after is used. /// If not set, the backend's configured delete_version_after is used.
@ -50,7 +96,7 @@ pub struct SecretMeta {
// TODO https://developer.hashicorp.com/vault/docs/concepts/duration-format // TODO https://developer.hashicorp.com/vault/docs/concepts/duration-format
pub max_versions: i64, pub max_versions: i64,
pub oldest_version: i64, pub oldest_version: i64,
pub updated_time: DateTime<Utc>, pub updated_time: UtcDateTime,
/// User-provided key-value pairs that are used to describe arbitrary and version-agnostic information about a secret. /// 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 custom_metadata: Option<HashMap<String, String>>,
pub versions: Vec<VersionMeta>, pub versions: Vec<VersionMeta>,
@ -58,7 +104,7 @@ pub struct SecretMeta {
impl Default for SecretMeta { impl Default for SecretMeta {
fn default() -> Self { fn default() -> Self {
let current = Utc::now(); let current = UtcDateTime::now();
SecretMeta { SecretMeta {
cas_required: false, cas_required: false,
created_time: current, created_time: current,

View file

@ -22,14 +22,15 @@ mod sys;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let _ = dotenvy::dotenv();
// 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"); // TODO: Remove to respect user configuration
// env::set_var("DATABASE_URL", "sqlite:test.db"); // TODO: move to .env // env::set_var("DATABASE_URL", "sqlite:test.db"); // TODO: move to .env
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());
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");
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");