Skip to content

Commit 36b247b

Browse files
KibnetMrZoidberg
andauthored
Feature/nested parameters (#3)
* Added support for nested parameters * Update README.md Changed the description to reflect the changes. * fixed test Co-authored-by: Mikhail Merkulov <[email protected]>
1 parent b4e63ae commit 36b247b

File tree

4 files changed

+108
-24
lines changed

4 files changed

+108
-24
lines changed

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,32 @@ Alternatively, you can configure Vault connection using next environmnt variable
4747
## Preparing secrets in Vault
4848

4949
You need to store your secrets with special naming rules.
50-
First of all, all secrets should use KV2 storage and have prefix `data/{app_alias}/`.
51-
For example, if your app has alias `sampleapp` and you want to have configuration option `ConnectionString` your secret path would be `data/sampleapp/ConnectionString`.
50+
First of all, all secrets should use KV2 storage and have prefix `{app_alias}/{env}`.
51+
For example, if your app has alias `sampleapp` and environment `producton` and you want to have configuration option `ConnectionString` your secret path would be `sampleapp/producton`.
5252

53-
All secret data should use JSON format with the only key `value` and secret data inside:
53+
All parameters are grouped and arranged in folders and can be managed within the group. All secret data should use JSON format with secret data inside:
5454
```json
5555
{
56-
"value": "secret value"
56+
"ConnectionString": "secret value",
57+
"Option1": "secret value 2",
58+
}
59+
```
60+
### Nested secrets
61+
62+
There are two ways to create nested parameters.
63+
1. Description of nesting directly in Json format.:
64+
```json
65+
{
66+
"DB":
67+
{
68+
"ConnectionString": "secret value"
69+
}
70+
}
71+
```
72+
2. Creating a parameter on the desired path "sampleapp/producton/DB":
73+
```json
74+
{
75+
"ConnectionString": "secret value"
5776
}
5877
```
5978

Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
namespace VaultSharp.Extensions.Configuration
1+
namespace VaultSharp.Extensions.Configuration
22
{
33
using System;
44
using System.Collections.Generic;
55
using System.Threading.Tasks;
66
using Microsoft.Extensions.Configuration;
77
using Microsoft.Extensions.Logging;
88
using Microsoft.VisualStudio.Threading;
9+
using Newtonsoft.Json.Linq;
910
using VaultSharp;
1011
using VaultSharp.Core;
1112
using VaultSharp.V1.AuthMethods;
@@ -73,21 +74,70 @@ private async Task LoadVaultDataAsync(IVaultClient vaultClient)
7374
await foreach (var secretData in this.ReadKeysAsync(vaultClient, this._source.BasePath))
7475
{
7576
var key = secretData.Key;
76-
key = key.Replace(this._source.BasePath, string.Empty, StringComparison.InvariantCultureIgnoreCase)
77+
key = key.Replace(this._source.BasePath, string.Empty, StringComparison.InvariantCultureIgnoreCase).TrimStart('/')
7778
.Replace('/', ':');
78-
if (secretData.SecretData.Data.ContainsKey("value"))
79+
var data = secretData.SecretData.Data;
80+
81+
this.SetData(data, key);
82+
}
83+
}
84+
85+
private void SetData<TValue>(IEnumerable<KeyValuePair<string, TValue>> data, string key)
86+
{
87+
foreach (var pair in data)
88+
{
89+
var nestedKey = string.IsNullOrEmpty(key) ? pair.Key : $"{key}:{pair.Key}";
90+
var nestedValue = pair.Value;
91+
switch (nestedValue)
7992
{
80-
this.Set(key, secretData.SecretData.Data["value"].ToString());
93+
case string sValue:
94+
this.Set(nestedKey, sValue);
95+
break;
96+
case JToken token:
97+
switch (token.Type)
98+
{
99+
case JTokenType.Object:
100+
this.SetData<JToken?>(token.Value<JObject>(), nestedKey);
101+
break;
102+
case JTokenType.String:
103+
this.Set(nestedKey, token.Value<string>());
104+
break;
105+
case JTokenType.None:
106+
case JTokenType.Array:
107+
case JTokenType.Constructor:
108+
case JTokenType.Property:
109+
case JTokenType.Comment:
110+
case JTokenType.Integer:
111+
case JTokenType.Float:
112+
case JTokenType.Boolean:
113+
case JTokenType.Null:
114+
case JTokenType.Undefined:
115+
case JTokenType.Date:
116+
case JTokenType.Raw:
117+
case JTokenType.Bytes:
118+
case JTokenType.Guid:
119+
case JTokenType.Uri:
120+
case JTokenType.TimeSpan:
121+
break;
122+
}
123+
124+
break;
81125
}
82126
}
83127
}
84128

85129
private async IAsyncEnumerable<KeyedSecretData> ReadKeysAsync(IVaultClient vaultClient, string path)
86130
{
87131
Secret<ListInfo>? keys = null;
132+
var folderPath = path;
133+
if (folderPath.EndsWith("/", StringComparison.InvariantCulture) == false)
134+
{
135+
folderPath += "/";
136+
}
137+
88138
try
89139
{
90-
keys = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(path, this._source.MountPoint).ConfigureAwait(false);
140+
keys = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(folderPath, this._source.MountPoint).ConfigureAwait(false);
91141
}
92142
catch (VaultApiException)
93143
{
@@ -98,18 +148,34 @@ private async IAsyncEnumerable<KeyedSecretData> ReadKeysAsync(IVaultClient vault
98148
{
99149
foreach (var key in keys.Data.Keys)
100150
{
101-
var keyData = this.ReadKeysAsync(vaultClient, path + key);
151+
var keyData = this.ReadKeysAsync(vaultClient, folderPath + key);
102152
await foreach (var secretData in keyData)
103153
{
104154
yield return secretData;
105155
}
106156
}
107157
}
108-
else
158+
159+
var valuePath = path;
160+
if (valuePath.EndsWith("/", StringComparison.InvariantCulture) == true)
161+
{
162+
valuePath = valuePath.TrimEnd('/');
163+
}
164+
165+
KeyedSecretData? keyedSecretData = null;
166+
try
167+
{
168+
var secretData = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(valuePath, null, this._source.MountPoint).ConfigureAwait(false);
169+
keyedSecretData = new KeyedSecretData(valuePath, secretData.Data);
170+
}
171+
catch (VaultApiException)
172+
{
173+
// this is folder, not a key
174+
}
175+
176+
if (keyedSecretData != null)
109177
{
110-
var secretData =
111-
await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(path, null, this._source.MountPoint).ConfigureAwait(false);
112-
yield return new KeyedSecretData(path, secretData.Data);
178+
yield return keyedSecretData;
113179
}
114180
}
115181

Source/VaultSharp.Extensions.Configuration/VaultConfigurationSource.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace VaultSharp.Extensions.Configuration
1+
namespace VaultSharp.Extensions.Configuration
22
{
33
using Microsoft.Extensions.Configuration;
44
using Microsoft.Extensions.Logging;
@@ -32,7 +32,7 @@ public VaultConfigurationSource(VaultOptions options, string basePath, string? m
3232
{
3333
this._logger = logger;
3434
this.Options = options;
35-
this.BasePath = "data/" + basePath + "/";
35+
this.BasePath = basePath;
3636
this.MountPoint = mountPoint ?? SecretsEngineDefaultPaths.KeyValueV2;
3737
}
3838

Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ private TestcontainersContainer PrepareVaultContainer()
2525
return testcontainersBuilder.Build();
2626
}
2727

28-
private async Task LoadDataAsync(Dictionary<string, string> values)
28+
private async Task LoadDataAsync(Dictionary<string, KeyValuePair<string,string>> values)
2929
{
3030
var authMethod = new TokenAuthMethodInfo("root");
3131

@@ -34,7 +34,7 @@ private async Task LoadDataAsync(Dictionary<string, string> values)
3434

3535
foreach (var pair in values)
3636
{
37-
var data = new Dictionary<string, object>() { ["value"] = pair.Value };
37+
var data = new Dictionary<string, object>() { [pair.Value.Key] = pair.Value.Value };
3838
await vaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(pair.Key, data).ConfigureAwait(false);
3939
}
4040
}
@@ -43,9 +43,9 @@ private async Task LoadDataAsync(Dictionary<string, string> values)
4343
public async Task Success_Test_TokenAuth()
4444
{
4545
// arrange
46-
Dictionary<string, string> values = new Dictionary<string, string>();
47-
values.Add("data/test/option1", "value1");
48-
values.Add("data/test/subsection/option2", "value2");
46+
Dictionary<string, KeyValuePair<string,string>> values = new Dictionary<string, KeyValuePair<string,string>>();
47+
values.Add("test", new KeyValuePair<string, string>("option1", "value1"));
48+
values.Add("test/subsection", new KeyValuePair<string, string>("option2", "value2"));
4949

5050
var container = this.PrepareVaultContainer();
5151
try
@@ -60,9 +60,8 @@ public async Task Success_Test_TokenAuth()
6060
var configurationRoot = builder.Build();
6161

6262
// assert
63-
configurationRoot.GetValue<string>("option1").Should().Be(values["data/test/option1"]);
64-
configurationRoot.GetSection("subsection").GetValue<string>("option2").Should()
65-
.Be(values["data/test/subsection/option2"]);
63+
configurationRoot.GetValue<string>("option1").Should().Be("value1");
64+
configurationRoot.GetSection("subsection").GetValue<string>("option2").Should().Be("value2");
6665
}
6766
finally
6867
{

0 commit comments

Comments
 (0)