Skip to content

Commit 6051b4f

Browse files
mahadi99xybeforan
andauthored
feat: created a httpclient service for vault credentials (#933)
* feat: created a httpclient service for vault credentials * Update src/TRE-API/Controllers/PostgrsCredentialsController.cs Co-authored-by: Jon Couldridge <[email protected]> * Update src/TRE-API/Controllers/VaultCredentialsController.cs Co-authored-by: Jon Couldridge <[email protected]> * Update src/TRE-API/Controllers/VaultCredentialsController.cs Co-authored-by: Jon Couldridge <[email protected]> * Rename PostgrsCredentialsController.cs to PostgresCredentialsController.cs * Update PostgresCredentialsController.cs --------- Co-authored-by: Jon Couldridge <[email protected]>
1 parent 831bcc7 commit 6051b4f

File tree

7 files changed

+584
-0
lines changed

7 files changed

+584
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace BL.Models.Settings
8+
{
9+
public class VaultSettings
10+
{
11+
public string BaseUrl { get; set; } = "http://localhost:8200";
12+
public string Token { get; set; } = string.Empty;
13+
public int TimeoutSeconds { get; set; } = 30;
14+
public string SecretEngine { get; set; } = "secret"; // KV v2 engine name
15+
public bool EnableRetry { get; set; } = true;
16+
public int MaxRetryAttempts { get; set; } = 3;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace BL.Services.Contract
8+
{
9+
public interface IVaultCredentialsService
10+
{
11+
Task<bool> AddCredentialAsync(string path, Dictionary<string, object> credential);
12+
Task<bool> RemoveCredentialAsync(string path);
13+
Task<Dictionary<string, object>> GetCredentialAsync(string path);
14+
Task<bool> UpdateCredentialAsync(string path, Dictionary<string, object> credential);
15+
Task<string> GetConnectionStringAsync(string databaseName);
16+
Task<bool> StoreConnectionStringAsync(string databaseName, string server, string database, string username, string password, int port = 5432);
17+
}
18+
}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
using BL.Models.Settings;
2+
using BL.Services.Contract;
3+
using Newtonsoft.Json;
4+
using Serilog;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Net.Http.Headers;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace BL.Services
13+
{
14+
public class VaultCredentialsService : IVaultCredentialsService, IDisposable
15+
{
16+
private readonly HttpClient _httpClient;
17+
private readonly VaultSettings _vaultSettings;
18+
private bool _disposed = false;
19+
20+
public VaultCredentialsService(HttpClient httpClient, VaultSettings vaultSettings)
21+
{
22+
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
23+
_vaultSettings = vaultSettings ?? throw new ArgumentNullException(nameof(vaultSettings));
24+
25+
ConfigureHttpClient();
26+
}
27+
28+
private void ConfigureHttpClient()
29+
{
30+
_httpClient.BaseAddress = new Uri(_vaultSettings.BaseUrl);
31+
_httpClient.DefaultRequestHeaders.Clear();
32+
_httpClient.DefaultRequestHeaders.Add("X-Vault-Token", _vaultSettings.Token);
33+
_httpClient.DefaultRequestHeaders.Accept.Add(
34+
new MediaTypeWithQualityHeaderValue("application/json"));
35+
_httpClient.Timeout = TimeSpan.FromSeconds(_vaultSettings.TimeoutSeconds);
36+
}
37+
38+
public async Task<bool> AddCredentialAsync(string path, Dictionary<string, object> credential)
39+
{
40+
return await ExecuteWithRetryAsync(async () =>
41+
{
42+
Log.Information("Adding credential to vault path: {Path}", path);
43+
44+
var payload = new VaultSecretPayload { Data = credential };
45+
var jsonContent = JsonConvert.SerializeObject(payload);
46+
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
47+
48+
var response = await _httpClient.PostAsync($"v1/{_vaultSettings.SecretEngine}/data/{path}", content);
49+
50+
if (response.IsSuccessStatusCode)
51+
{
52+
Log.Information("Successfully added credential to vault path: {Path}", path);
53+
return true;
54+
}
55+
56+
var errorContent = await response.Content.ReadAsStringAsync();
57+
Log.Error("Failed to add credential to vault. Status: {Status}, Error: {Error}",
58+
response.StatusCode, errorContent);
59+
60+
return false;
61+
});
62+
}
63+
64+
public async Task<bool> RemoveCredentialAsync(string path)
65+
{
66+
return await ExecuteWithRetryAsync(async () =>
67+
{
68+
Log.Information("Removing credential from vault path: {Path}", path);
69+
70+
var response = await _httpClient.DeleteAsync($"v1/{_vaultSettings.SecretEngine}/data/{path}");
71+
72+
if (response.IsSuccessStatusCode)
73+
{
74+
Log.Information("Successfully removed credential from vault path: {Path}", path);
75+
return true;
76+
}
77+
78+
var errorContent = await response.Content.ReadAsStringAsync();
79+
Log.Error("Failed to remove credential from vault. Status: {Status}, Error: {Error}",
80+
response.StatusCode, errorContent);
81+
82+
return false;
83+
});
84+
}
85+
86+
public async Task<Dictionary<string, object>> GetCredentialAsync(string path)
87+
{
88+
return await ExecuteWithRetryAsync(async () =>
89+
{
90+
Log.Information("Retrieving credential from vault path: {Path}", path);
91+
92+
var response = await _httpClient.GetAsync($"v1/{_vaultSettings.SecretEngine}/data/{path}");
93+
94+
if (response.IsSuccessStatusCode)
95+
{
96+
var jsonContent = await response.Content.ReadAsStringAsync();
97+
var vaultResponse = JsonConvert.DeserializeObject<VaultSecretResponse>(jsonContent);
98+
99+
Log.Information("Successfully retrieved credential from vault path: {Path}", path);
100+
return vaultResponse?.Data?.Data ?? new Dictionary<string, object>();
101+
}
102+
103+
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
104+
{
105+
Log.Warning("Credential not found at vault path: {Path}", path);
106+
return new Dictionary<string, object>();
107+
}
108+
109+
var errorContent = await response.Content.ReadAsStringAsync();
110+
Log.Error("Failed to retrieve credential from vault. Status: {Status}, Error: {Error}",
111+
response.StatusCode, errorContent);
112+
113+
return new Dictionary<string, object>();
114+
});
115+
}
116+
117+
public async Task<bool> UpdateCredentialAsync(string path, Dictionary<string, object> credential)
118+
{
119+
return await ExecuteWithRetryAsync(async () =>
120+
{
121+
Log.Information("Updating credential at vault path: {Path}", path);
122+
123+
var payload = new VaultSecretPayload { Data = credential };
124+
var jsonContent = JsonConvert.SerializeObject(payload);
125+
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
126+
127+
var response = await _httpClient.PutAsync($"v1/{_vaultSettings.SecretEngine}/data/{path}", content);
128+
129+
if (response.IsSuccessStatusCode)
130+
{
131+
Log.Information("Successfully updated credential at vault path: {Path}", path);
132+
return true;
133+
}
134+
135+
var errorContent = await response.Content.ReadAsStringAsync();
136+
Log.Error("Failed to update credential in vault. Status: {Status}, Error: {Error}",
137+
response.StatusCode, errorContent);
138+
139+
return false;
140+
});
141+
}
142+
143+
public async Task<string> GetConnectionStringAsync(string databaseName)
144+
{
145+
try
146+
{
147+
var credentials = await GetCredentialAsync($"database/{databaseName}");
148+
149+
if (credentials.Count == 0)
150+
{
151+
Log.Warning("No database credentials found for: {DatabaseName}", databaseName);
152+
return string.Empty;
153+
}
154+
155+
var server = credentials.GetValueOrDefault("server", "localhost").ToString();
156+
var port = credentials.GetValueOrDefault("port", "5432").ToString();
157+
var database = credentials.GetValueOrDefault("database", databaseName).ToString();
158+
var username = credentials.GetValueOrDefault("username", "").ToString();
159+
var password = credentials.GetValueOrDefault("password", "").ToString();
160+
161+
var connectionString = $"Server={server};Port={port};Database={database};User Id={username};Password={password};Include Error Detail=true;";
162+
163+
Log.Information("Successfully built connection string for database: {DatabaseName}", databaseName);
164+
return connectionString;
165+
}
166+
catch (Exception ex)
167+
{
168+
Log.Error(ex, "Failed to get connection string for database: {DatabaseName}", databaseName);
169+
return string.Empty;
170+
}
171+
}
172+
173+
public async Task<bool> StoreConnectionStringAsync(string databaseName, string server, string database, string username, string password, int port = 5432)
174+
{
175+
var credentials = new Dictionary<string, object>
176+
{
177+
["server"] = server,
178+
["port"] = port,
179+
["database"] = database,
180+
["username"] = username,
181+
["password"] = password,
182+
["created_at"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
183+
["type"] = "postgresql"
184+
};
185+
186+
return await AddCredentialAsync($"database/{databaseName}", credentials);
187+
}
188+
189+
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
190+
{
191+
if (!_vaultSettings.EnableRetry)
192+
{
193+
return await operation();
194+
}
195+
196+
int attempts = 0;
197+
Exception lastException = null;
198+
199+
while (attempts < _vaultSettings.MaxRetryAttempts)
200+
{
201+
try
202+
{
203+
return await operation();
204+
}
205+
catch (HttpRequestException ex)
206+
{
207+
lastException = ex;
208+
attempts++;
209+
210+
if (attempts < _vaultSettings.MaxRetryAttempts)
211+
{
212+
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempts)); // Exponential backoff
213+
Log.Warning("Vault operation failed, retrying in {Delay}ms. Attempt {Attempt}/{MaxAttempts}. Error: {Error}",
214+
delay.TotalMilliseconds, attempts, _vaultSettings.MaxRetryAttempts, ex.Message);
215+
216+
await Task.Delay(delay);
217+
}
218+
}
219+
}
220+
221+
Log.Error(lastException, "Vault operation failed after {MaxAttempts} attempts", _vaultSettings.MaxRetryAttempts);
222+
return default(T);
223+
}
224+
225+
public void Dispose()
226+
{
227+
if (!_disposed)
228+
{
229+
_httpClient?.Dispose();
230+
_disposed = true;
231+
}
232+
}
233+
}
234+
235+
// Supporting classes
236+
public class VaultSecretPayload
237+
{
238+
[JsonProperty("data")]
239+
public Dictionary<string, object> Data { get; set; } = new();
240+
}
241+
242+
public class VaultSecretResponse
243+
{
244+
[JsonProperty("data")]
245+
public VaultSecretData Data { get; set; } = new();
246+
}
247+
248+
public class VaultSecretData
249+
{
250+
[JsonProperty("data")]
251+
public Dictionary<string, object> Data { get; set; } = new();
252+
253+
[JsonProperty("metadata")]
254+
public VaultMetadata Metadata { get; set; } = new();
255+
}
256+
257+
public class VaultMetadata
258+
{
259+
[JsonProperty("created_time")]
260+
public DateTime? CreatedTime { get; set; }
261+
262+
[JsonProperty("deletion_time")]
263+
public DateTime? DeletionTime { get; set; }
264+
265+
[JsonProperty("destroyed")]
266+
public bool Destroyed { get; set; }
267+
268+
[JsonProperty("version")]
269+
public int Version { get; set; }
270+
}
271+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Swashbuckle.AspNetCore.Annotations;
4+
5+
namespace TRE_API.Controllers
6+
{
7+
/// <summary>
8+
/// API controller for managing postgres credentials and authentication operations
9+
/// </summary>
10+
[Route("api/tre-credentials/postgres")]
11+
[ApiController]
12+
[SwaggerTag("Postgrescredentials", "Manage Postgres Database Credentials")]
13+
[Produces("application/json")]
14+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
15+
public class PostgresCredentialsController : ControllerBase
16+
{
17+
}
18+
}

0 commit comments

Comments
 (0)