diff --git a/Client/Client.csproj b/Client/Client.csproj
new file mode 100644
index 0000000..7d86ef5
--- /dev/null
+++ b/Client/Client.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net6.0
+ enable
+ enable
+ HashiVaultCs
+ HashiVaultCs
+
+
+
+
+
+
+
+
+ True
+ True
+ Resource.resx
+
+
+ True
+ True
+ Resource.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resource.Designer.cs
+
+
+ ResXFileCodeGenerator
+ Resource.Designer.cs
+
+
+
+
diff --git a/Client/Clients/ApiUrl.cs b/Client/Clients/ApiUrl.cs
new file mode 100644
index 0000000..043c1f7
--- /dev/null
+++ b/Client/Clients/ApiUrl.cs
@@ -0,0 +1,10 @@
+namespace HashiVaultCs.Clients;
+
+public static class ApiUrl
+{
+ public const string AuthUserpassLogin = "/v1/auth/userpass/login/{username}";
+ public const string AuthApproleRoleId = "/v1/auth/approle/role/{rolename}/role-id";
+ public const string AuthApproleSecretId = "/v1/auth/approle/role/{rolename}/secret-id";
+ public const string AuthApproleLogin = "/v1/auth/approle/login";
+ public const string SecretsEngineDataPath = "/v1/{engine}/data/{path}";
+}
diff --git a/Client/Clients/Auth/ApproleClient.cs b/Client/Clients/Auth/ApproleClient.cs
new file mode 100644
index 0000000..441014a
--- /dev/null
+++ b/Client/Clients/Auth/ApproleClient.cs
@@ -0,0 +1,43 @@
+using HashiVaultCs.Interfaces.Auth;
+using HashiVaultCs.Models.Requests.Auth.Approle;
+
+namespace HashiVaultCs.Clients.Auth;
+
+public sealed class ApproleClient : MustInitialiseHttpVaultHeadersAndHostAbstraction, IApproleClient
+{
+ public ApproleClient(HttpVaultHeaders vault_headers, string base_address) : base(vault_headers, base_address) { }
+
+ public async Task LoginAsync(Login data, IImmutableDictionary headers, CancellationToken cancellationToken = default)
+ {
+ Uri request_uri = new(_base_uri, ApiUrl.AuthApproleLogin);
+ HttpVaultClient http_vault_client = new(HttpMethod.Post, _vault_headers, headers, request_uri, data);
+ JsonDocument response = await http_vault_client.SendAsync(cancellationToken);
+ return response.Deserialize() ?? new Secret();
+ }
+
+ public async Task RoleIdAsync(string rolename, IImmutableDictionary headers, CancellationToken cancellationToken = default)
+ {
+ string relative_url = ApiUrl.AuthApproleRoleId.FormatWith(new
+ {
+ rolename
+ });
+ Uri request_uri = new(_base_uri, relative_url);
+
+ HttpVaultClient http_vault_client = new(HttpMethod.Get, _vault_headers, headers, request_uri);
+ JsonDocument response = await http_vault_client.SendAsync(cancellationToken);
+ return response.Deserialize() ?? new Secret();
+ }
+
+ public async Task SecretIdAsync(string rolename, IImmutableDictionary headers, CancellationToken cancellationToken = default)
+ {
+ string relative_url = ApiUrl.AuthApproleSecretId.FormatWith(new
+ {
+ rolename
+ });
+ Uri request_uri = new(_base_uri, relative_url);
+
+ HttpVaultClient http_vault_client = new(HttpMethod.Post, _vault_headers, headers, request_uri, new { });
+ JsonDocument response = await http_vault_client.SendAsync(cancellationToken);
+ return response.Deserialize() ?? new Secret();
+ }
+}
diff --git a/Client/Clients/Auth/UserpassClient.cs b/Client/Clients/Auth/UserpassClient.cs
new file mode 100644
index 0000000..7e7b0ab
--- /dev/null
+++ b/Client/Clients/Auth/UserpassClient.cs
@@ -0,0 +1,22 @@
+using HashiVaultCs.Interfaces.Auth;
+using HashiVaultCs.Models.Requests.Auth.Userpass;
+
+namespace HashiVaultCs.Clients.Auth;
+
+public sealed class UserpassClient : MustInitialiseHttpVaultHeadersAndHostAbstraction, IUserpassClient
+{
+ public UserpassClient(HttpVaultHeaders vault_headers, string base_address) : base(vault_headers, base_address) { }
+
+ public async Task LoginAsync(string username, Login data, IImmutableDictionary headers, CancellationToken cancellationToken = default)
+ {
+ string relative_url = ApiUrl.AuthUserpassLogin.FormatWith(new
+ {
+ username
+ });
+ Uri request_uri = new(_base_uri, relative_url);
+
+ HttpVaultClient http_vault_client = new(HttpMethod.Post, _vault_headers, headers, request_uri, data);
+ JsonDocument response = await http_vault_client.SendAsync(cancellationToken);
+ return response.Deserialize() ?? new Secret(); // todo (2022-05-23|kibble): What to return when Deserialize() fails, this method shouldn't return empty secrets...
+ }
+}
diff --git a/Client/Clients/MustInitialiseHttpVaultHeadersAndHostAbstraction.cs b/Client/Clients/MustInitialiseHttpVaultHeadersAndHostAbstraction.cs
new file mode 100644
index 0000000..668b5c6
--- /dev/null
+++ b/Client/Clients/MustInitialiseHttpVaultHeadersAndHostAbstraction.cs
@@ -0,0 +1,14 @@
+using HashiVaultCs.Interfaces;
+
+namespace HashiVaultCs.Clients;
+
+public abstract class MustInitialiseHttpVaultHeadersAndHostAbstraction where T : IHttpVaultHeaders
+{
+ protected readonly T _vault_headers;
+ protected readonly Uri _base_uri;
+ public MustInitialiseHttpVaultHeadersAndHostAbstraction(T vault_headers, string base_address)
+ {
+ _vault_headers = vault_headers;
+ _base_uri = new Uri(base_address);
+ }
+}
diff --git a/Client/Clients/Secrets/DataClient.cs b/Client/Clients/Secrets/DataClient.cs
new file mode 100644
index 0000000..4524ce2
--- /dev/null
+++ b/Client/Clients/Secrets/DataClient.cs
@@ -0,0 +1,22 @@
+using HashiVaultCs.Interfaces.Secrets;
+
+namespace HashiVaultCs.Clients.Secrets;
+
+public sealed class DataClient : MustInitialiseHttpVaultHeadersAndHostAbstraction, IDataClient
+{
+ public DataClient(HttpVaultHeaders vault_headers, string base_address) : base(vault_headers, base_address) { }
+
+ public async Task GetAsync(string engine, string path, IImmutableDictionary headers, CancellationToken cancellationToken = default)
+ {
+ string relative_url = ApiUrl.SecretsEngineDataPath.FormatWith(new
+ {
+ engine,
+ path
+ });
+ Uri request_uri = new(_base_uri, relative_url);
+
+ HttpVaultClient http_vault_client = new(HttpMethod.Get, _vault_headers, headers, request_uri);
+ JsonDocument response = await http_vault_client.SendAsync(cancellationToken);
+ return response.Deserialize() ?? new Secret();
+ }
+}
diff --git a/Client/Extentions/HttpRequestMessageExtentions.cs b/Client/Extentions/HttpRequestMessageExtentions.cs
new file mode 100644
index 0000000..02eea86
--- /dev/null
+++ b/Client/Extentions/HttpRequestMessageExtentions.cs
@@ -0,0 +1,35 @@
+using HashiVaultCs;
+
+namespace HashiVaultCs.Extentions;
+
+public static class HttpRequestMessageExtentions
+{
+ public static void AddVaultHttpHeaders(this HttpRequestMessage http_request_message, HttpVaultHeaders vault_headers)
+ {
+ ArgumentNullException.ThrowIfNull(vault_headers);
+
+ if (vault_headers.RequestHeaderPresent is true)
+ {
+ http_request_message.Headers.Remove(vault_headers.Request.Key);
+ http_request_message.Headers.Add(vault_headers.Request.Key, vault_headers.Request.Value);
+ }
+
+ if (vault_headers.TokenHeaderPresent is true)
+ {
+ http_request_message.Headers.Remove(vault_headers.Token.Key);
+ http_request_message.Headers.Add(vault_headers.Token.Key, vault_headers.Token.Value);
+ }
+
+ if (vault_headers.NamespaceHeaderPresent is true)
+ {
+ http_request_message.Headers.Remove(vault_headers.Namespace.Key);
+ http_request_message.Headers.Add(vault_headers.Namespace.Key, vault_headers.Namespace.Value);
+ }
+
+ if (vault_headers.WrapTimeToLiveHeaderPresent is true)
+ {
+ http_request_message.Headers.Remove(vault_headers.WrapTimeToLive.Key);
+ http_request_message.Headers.Add(vault_headers.WrapTimeToLive.Key, vault_headers.WrapTimeToLive.Value);
+ }
+ }
+}
diff --git a/Client/HttpVaultClient.cs b/Client/HttpVaultClient.cs
new file mode 100644
index 0000000..0b3330c
--- /dev/null
+++ b/Client/HttpVaultClient.cs
@@ -0,0 +1,72 @@
+using HashiVaultCs.Interfaces;
+
+namespace HashiVaultCs;
+
+public sealed class HttpVaultClient : IHttpVaultClient
+{
+ private const string _media_type = "application/json";
+ private readonly TimeSpan _http_client_timeout = TimeSpan.FromSeconds(6);
+ private readonly HttpClient _http_client;
+ private readonly HttpRequestMessage _http_request_message;
+ private readonly List _supported_http_methods_list = new() { HttpMethod.Get, HttpMethod.Post };
+
+ public HttpVaultClient(HttpMethod method, HttpVaultHeaders vault_headers, IReadOnlyDictionary headers, Uri request_uri, object? content = null)
+ {
+ // Currently only HTTP Methods GET & POST is supported.
+ if (_supported_http_methods_list.Contains(method) is false)
+ {
+ throw new NotSupportedException($"{Resources.HttpVaultClient.Resource.MethodNotSupportedException}. {Resources.HttpVaultClient.Resource.SupportedHttpMethodsAre}: {string.Join(", ", _supported_http_methods_list)}");
+ }
+
+ // HTTP Headers should not be null, but can be empty (ImmutableDictionary.Empty).
+ if (headers?.Any() is null)
+ {
+ throw new ArgumentNullException(nameof(headers), Resources.HttpVaultClient.Resource.HeadersArgumentNullException);
+ }
+
+ // Generate HTTP Request Message and set content
+ _http_request_message = new(method, request_uri)
+ {
+ Content = content is null
+ ? null
+ : new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, _media_type)
+ };
+
+ // Build the HTTP Headers, both custom and Vault Specific HTTP Headers.
+ _http_request_message.AddVaultHttpHeaders(vault_headers);
+ foreach (KeyValuePair kvp in headers)
+ {
+ _http_request_message.Headers.Remove(kvp.Key);
+ _http_request_message.Headers.Add(kvp.Key, kvp.Value);
+ }
+
+ HttpClientHandler handler = new();
+ _http_client = new(handler)
+ {
+ Timeout = _http_client_timeout
+ };
+
+ _http_client.DefaultRequestHeaders.Accept.Clear();
+ _http_client.DefaultRequestHeaders.Accept.Add(new(_media_type));
+ }
+
+ ///
+ /// This method will throw an exception if the HTTP response status is anything but a success.
+ ///
+ /// Only valid JSON is returned from this method through the return of JsonDocument values.
+ ///
+ public async Task SendAsync(CancellationToken cancellationToken = default)
+ {
+ HttpResponseMessage http_response_message = await _http_client.SendAsync(_http_request_message, cancellationToken);
+ http_response_message.EnsureSuccessStatusCode();
+ string response_json = await http_response_message.Content.ReadAsStringAsync(cancellationToken);
+ JsonDocumentOptions jdo = new()
+ {
+ AllowTrailingCommas = false,
+ CommentHandling = JsonCommentHandling.Disallow,
+ MaxDepth = 32
+ };
+ JsonDocument document = JsonDocument.Parse(response_json, jdo);
+ return document;
+ }
+}
diff --git a/Client/HttpVaultHeaderKey.cs b/Client/HttpVaultHeaderKey.cs
new file mode 100644
index 0000000..c259de1
--- /dev/null
+++ b/Client/HttpVaultHeaderKey.cs
@@ -0,0 +1,9 @@
+namespace HashiVaultCs;
+
+public static class HttpVaultHeaderKey
+{
+ public const string Request = "X-Vault-Request";
+ public const string Token = "X-Vault-Token";
+ public const string Namespace = "X-Vault-Namespace";
+ public const string WrapTimeToLive = "X-Vault-Wrap-TTL";
+}
diff --git a/Client/HttpVaultHeaders.cs b/Client/HttpVaultHeaders.cs
new file mode 100644
index 0000000..bfeb2c6
--- /dev/null
+++ b/Client/HttpVaultHeaders.cs
@@ -0,0 +1,69 @@
+using HashiVaultCs.Interfaces;
+
+namespace HashiVaultCs;
+
+public sealed class HttpVaultHeaders : IHttpVaultHeaders
+{
+ public bool RequestHeaderPresent { get; private set; }
+ private KeyValuePair? _request;
+ public KeyValuePair Request => RequestHeaderPresent is false || _request is null
+ ? throw new InvalidOperationException($"{Resources.Resource.VaultHttpHeadersInvalidOperationException}: {nameof(Request)}")
+ : _request ?? throw new NullReferenceException();
+
+ public bool TokenHeaderPresent { get; private set; }
+ private KeyValuePair? _token;
+ public KeyValuePair Token => TokenHeaderPresent is false || _token is null
+ ? throw new InvalidOperationException($"{Resources.Resource.VaultHttpHeadersInvalidOperationException}: {nameof(Token)}")
+ : _token ?? throw new NullReferenceException();
+
+ public bool NamespaceHeaderPresent { get; private set; }
+ private KeyValuePair? _namespace;
+ public KeyValuePair Namespace => NamespaceHeaderPresent is false || _namespace is null
+ ? throw new InvalidOperationException($"{Resources.Resource.VaultHttpHeadersInvalidOperationException}: {nameof(Namespace)}")
+ : _namespace ?? throw new NullReferenceException();
+
+ public bool WrapTimeToLiveHeaderPresent { get; private set; }
+ private KeyValuePair? _wrapttl;
+ public KeyValuePair WrapTimeToLive => WrapTimeToLiveHeaderPresent is false || _wrapttl is null
+ ? throw new InvalidOperationException($"{Resources.Resource.VaultHttpHeadersInvalidOperationException}: {nameof(WrapTimeToLive)}")
+ : _wrapttl ?? throw new NullReferenceException();
+
+ HttpVaultHeaders IHttpVaultHeaders.Build(IReadOnlyDictionary headers) => Build(headers);
+
+ public static HttpVaultHeaders Build(IReadOnlyDictionary headers)
+ {
+ HttpVaultHeaders vault_http_headers = new();
+ foreach (KeyValuePair kvp in headers)
+ {
+ switch (kvp.Key)
+ {
+ case HttpVaultHeaderKey.Request:
+ {
+ vault_http_headers._request = new KeyValuePair(HttpVaultHeaderKey.Request, kvp.Value);
+ vault_http_headers.RequestHeaderPresent = true;
+ break;
+ }
+ case HttpVaultHeaderKey.Token:
+ {
+ vault_http_headers._token = new KeyValuePair(HttpVaultHeaderKey.Token, kvp.Value);
+ vault_http_headers.TokenHeaderPresent = true;
+ break;
+ }
+ case HttpVaultHeaderKey.Namespace:
+ {
+ vault_http_headers._namespace = new KeyValuePair(HttpVaultHeaderKey.Namespace, kvp.Value);
+ vault_http_headers.NamespaceHeaderPresent = true;
+ break;
+ }
+ case HttpVaultHeaderKey.WrapTimeToLive:
+ {
+ vault_http_headers._wrapttl = new KeyValuePair(HttpVaultHeaderKey.WrapTimeToLive, kvp.Value);
+ vault_http_headers.WrapTimeToLiveHeaderPresent = true;
+ break;
+ }
+ }
+ }
+
+ return vault_http_headers;
+ }
+}
diff --git a/Client/Interfaces/Auth/IApproleClient.cs b/Client/Interfaces/Auth/IApproleClient.cs
new file mode 100644
index 0000000..4564b4e
--- /dev/null
+++ b/Client/Interfaces/Auth/IApproleClient.cs
@@ -0,0 +1,10 @@
+using HashiVaultCs.Models.Requests.Auth.Approle;
+
+namespace HashiVaultCs.Interfaces.Auth;
+
+public interface IApproleClient : IVaultClient
+{
+ Task LoginAsync(Login data, IImmutableDictionary headers, CancellationToken cancellationToken = default);
+ Task RoleIdAsync(string rolename, IImmutableDictionary headers, CancellationToken cancellationToken = default);
+ Task SecretIdAsync(string rolename, IImmutableDictionary headers, CancellationToken cancellationToken = default);
+}
diff --git a/Client/Interfaces/Auth/IUserpassClient.cs b/Client/Interfaces/Auth/IUserpassClient.cs
new file mode 100644
index 0000000..fe2ded8
--- /dev/null
+++ b/Client/Interfaces/Auth/IUserpassClient.cs
@@ -0,0 +1,8 @@
+using HashiVaultCs.Models.Requests.Auth.Userpass;
+
+namespace HashiVaultCs.Interfaces.Auth;
+
+public interface IUserpassClient : IVaultClient
+{
+ Task LoginAsync(string username, Login data, IImmutableDictionary headers, CancellationToken cancellationToken = default);
+}
diff --git a/Client/Interfaces/IHttpVaultClient.cs b/Client/Interfaces/IHttpVaultClient.cs
new file mode 100644
index 0000000..a5c9964
--- /dev/null
+++ b/Client/Interfaces/IHttpVaultClient.cs
@@ -0,0 +1,6 @@
+namespace HashiVaultCs.Interfaces;
+
+public interface IHttpVaultClient
+{
+ Task SendAsync(CancellationToken cancellationToken = default);
+}
diff --git a/Client/Interfaces/IHttpVaultHeaders.cs b/Client/Interfaces/IHttpVaultHeaders.cs
new file mode 100644
index 0000000..8e668e1
--- /dev/null
+++ b/Client/Interfaces/IHttpVaultHeaders.cs
@@ -0,0 +1,20 @@
+using HashiVaultCs;
+
+namespace HashiVaultCs.Interfaces;
+
+public interface IHttpVaultHeaders
+{
+ bool RequestHeaderPresent { get; }
+ KeyValuePair Request { get; }
+
+ bool TokenHeaderPresent { get; }
+ KeyValuePair Token { get; }
+
+ bool NamespaceHeaderPresent { get; }
+ KeyValuePair Namespace { get; }
+
+ bool WrapTimeToLiveHeaderPresent { get; }
+ KeyValuePair WrapTimeToLive { get; }
+
+ HttpVaultHeaders Build(IReadOnlyDictionary headers);
+}
diff --git a/Client/Interfaces/IVaultClient.cs b/Client/Interfaces/IVaultClient.cs
new file mode 100644
index 0000000..41126bd
--- /dev/null
+++ b/Client/Interfaces/IVaultClient.cs
@@ -0,0 +1,3 @@
+namespace HashiVaultCs.Interfaces;
+
+public interface IVaultClient { }
diff --git a/Client/Interfaces/Secrets/IDataClient.cs b/Client/Interfaces/Secrets/IDataClient.cs
new file mode 100644
index 0000000..5282663
--- /dev/null
+++ b/Client/Interfaces/Secrets/IDataClient.cs
@@ -0,0 +1,6 @@
+namespace HashiVaultCs.Interfaces.Secrets;
+
+public interface IDataClient : IVaultClient
+{
+ Task GetAsync(string engine, string path, IImmutableDictionary headers, CancellationToken cancellationToken = default);
+}
diff --git a/Client/Models/Requests/Auth/Approle/Login.cs b/Client/Models/Requests/Auth/Approle/Login.cs
new file mode 100644
index 0000000..f9efd88
--- /dev/null
+++ b/Client/Models/Requests/Auth/Approle/Login.cs
@@ -0,0 +1,10 @@
+namespace HashiVaultCs.Models.Requests.Auth.Approle;
+
+public sealed class Login
+{
+ [JsonPropertyName("role_id")]
+ public string? RoleId { get; set; }
+
+ [JsonPropertyName("secret_id")]
+ public string? SecretId { get; set; }
+}
diff --git a/Client/Models/Requests/Auth/Userpass/Login.cs b/Client/Models/Requests/Auth/Userpass/Login.cs
new file mode 100644
index 0000000..52f7571
--- /dev/null
+++ b/Client/Models/Requests/Auth/Userpass/Login.cs
@@ -0,0 +1,7 @@
+namespace HashiVaultCs.Models.Requests.Auth.Userpass;
+
+public sealed class Login
+{
+ [JsonPropertyName("password")]
+ public string? Password { get; set; }
+}
diff --git a/Client/Models/Responses/Secret.cs b/Client/Models/Responses/Secret.cs
new file mode 100644
index 0000000..e228c67
--- /dev/null
+++ b/Client/Models/Responses/Secret.cs
@@ -0,0 +1,48 @@
+namespace HashiVaultCs.Models.Responses;
+
+///
+///
+///
+/// https://github.com/hashicorp/vault/blob/main/api/secret.go
+public sealed class Secret
+{
+ [JsonPropertyName("request_id")]
+ public string? RequestId { get; init; } = default!;
+
+ [JsonPropertyName("lease_id")]
+ public string? LeaseId { get; init; } = default!;
+
+ [JsonPropertyName("lease_duration")]
+ public int LeaseDuration { get; init; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; init; }
+
+ ///
+ /// Data is the actual contents of the secret.
+ /// The format of the data is arbitrary and up to the secret backend.
+ ///
+ [JsonPropertyName("data")]
+ public JsonDocument? Data { get; init; }
+
+ ///
+ /// Warnings contains any warnings related to the operation.
+ /// These are not issues that caused the command to fail,
+ /// but that the client should be aware of.
+ ///
+ [JsonPropertyName("warnings")]
+ public IEnumerable? Warnings { get; init; }
+
+ ///
+ /// Auth, if non-nil, means that there was authentication information attached to this response.
+ ///
+ [JsonPropertyName("auth")]
+ public SecretAuth? Auth { get; init; }
+
+ ///
+ /// WrapInfo, if non-nil, means that the initial response was wrapped in the
+ /// cubbyhole of the given token(which has a TTL of the given number of seconds)
+ ///
+ [JsonPropertyName("wrap_info")]
+ public SecretWrapInfo? WrapInfo { get; init; }
+}
diff --git a/Client/Models/Responses/SecretAuth.cs b/Client/Models/Responses/SecretAuth.cs
new file mode 100644
index 0000000..0cb83a6
--- /dev/null
+++ b/Client/Models/Responses/SecretAuth.cs
@@ -0,0 +1,42 @@
+namespace HashiVaultCs.Models.Responses;
+
+///
+///
+///
+/// https://github.com/hashicorp/vault/blob/main/api/secret.go
+public sealed class SecretAuth
+{
+ [JsonPropertyName("client_token")]
+ public string? ClientToken { get; init; } = default!;
+
+ [JsonPropertyName("accessor")]
+ public string? Accessor { get; init; } = default!;
+
+ [JsonPropertyName("policies")]
+ public IEnumerable? Policies { get; init; }
+
+ [JsonPropertyName("token_policies")]
+ public IEnumerable? TokenPolicies { get; init; }
+
+ [JsonPropertyName("identity_policies")]
+ public IEnumerable? IdentityPolicies { get; init; }
+
+ [JsonPropertyName("metadata")]
+ public IDictionary? Metadata { get; init; }
+
+ [JsonPropertyName("orphan")]
+ public bool Orphan { get; init; }
+
+ [JsonPropertyName("entity_id")]
+ public string? EntityID { get; init; } = default!;
+
+ [JsonPropertyName("lease_duration")]
+ public int LeaseDuration { get; init; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; init; }
+
+ [Obsolete("Currently unsupported")]
+ [JsonPropertyName("mfa_requirement")]
+ public object? MFARequirement { get; init; }
+}
diff --git a/Client/Models/Responses/SecretWrapInfo.cs b/Client/Models/Responses/SecretWrapInfo.cs
new file mode 100644
index 0000000..c619971
--- /dev/null
+++ b/Client/Models/Responses/SecretWrapInfo.cs
@@ -0,0 +1,26 @@
+namespace HashiVaultCs.Models.Responses;
+
+///
+///
+///
+/// https://github.com/hashicorp/vault/blob/main/api/secret.go
+public sealed class SecretWrapInfo
+{
+ [JsonPropertyName("token")]
+ public string? Token { get; init; } = default!;
+
+ [JsonPropertyName("accessor")]
+ public string? Accessor { get; init; } = default!;
+
+ [JsonPropertyName("ttl")]
+ public int TTL { get; init; }
+
+ [JsonPropertyName("creation_time")]
+ public DateTime CreationTime { get; init; }
+
+ [JsonPropertyName("creation_path")]
+ public string? CreationPath { get; init; } = default!;
+
+ [JsonPropertyName("wrapped_accessor")]
+ public string? WrappedAccessor { get; init; } = default!;
+}
diff --git a/Client/Resources/HttpVaultClient/Resource.Designer.cs b/Client/Resources/HttpVaultClient/Resource.Designer.cs
new file mode 100644
index 0000000..c081799
--- /dev/null
+++ b/Client/Resources/HttpVaultClient/Resource.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace HashiVaultCs.Resources.HttpVaultClient {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HashiVaultCs.Resources.HttpVaultClient.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Header value can not be null, but can be empty..
+ ///
+ internal static string HeadersArgumentNullException {
+ get {
+ return ResourceManager.GetString("HeadersArgumentNullException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to HTTP Method is not supported.
+ ///
+ internal static string MethodNotSupportedException {
+ get {
+ return ResourceManager.GetString("MethodNotSupportedException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Supported HTTP methods are.
+ ///
+ internal static string SupportedHttpMethodsAre {
+ get {
+ return ResourceManager.GetString("SupportedHttpMethodsAre", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Client/Resources/HttpVaultClient/Resource.resx b/Client/Resources/HttpVaultClient/Resource.resx
new file mode 100644
index 0000000..170641f
--- /dev/null
+++ b/Client/Resources/HttpVaultClient/Resource.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Header value can not be null, but can be empty.
+
+
+ HTTP Method is not supported
+
+
+ Supported HTTP methods are
+
+
\ No newline at end of file
diff --git a/Client/Resources/Resource.Designer.cs b/Client/Resources/Resource.Designer.cs
new file mode 100644
index 0000000..4d65593
--- /dev/null
+++ b/Client/Resources/Resource.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace HashiVaultCs.Resources {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HashiVaultCs.Resources.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to access field.
+ ///
+ internal static string VaultHttpHeadersInvalidOperationException {
+ get {
+ return ResourceManager.GetString("VaultHttpHeadersInvalidOperationException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Client/Resources/Resource.resx b/Client/Resources/Resource.resx
new file mode 100644
index 0000000..8f4fd81
--- /dev/null
+++ b/Client/Resources/Resource.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Unable to access field
+
+
\ No newline at end of file
diff --git a/Client/Usings.cs b/Client/Usings.cs
new file mode 100644
index 0000000..8fe4a64
--- /dev/null
+++ b/Client/Usings.cs
@@ -0,0 +1,7 @@
+global using FormatWith;
+global using HashiVaultCs.Extentions;
+global using HashiVaultCs.Models.Responses;
+global using System.Collections.Immutable;
+global using System.Text;
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
diff --git a/HashiVaultCs.sln b/HashiVaultCs.sln
new file mode 100644
index 0000000..cfead7a
--- /dev/null
+++ b/HashiVaultCs.sln
@@ -0,0 +1,76 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32516.85
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{DBBEEF2D-A83C-4D94-A3D2-16E41352EBB1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{45A81E28-0CE6-4313-BA86-0D816F986BD3}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FBCC82E1-EE2D-40D2-BD57-270645F1B78A}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Setup", "Setup", "{588FAB66-3809-467D-B044-211F79B78CBC}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\docker-compose.yml = Setup\docker-compose.yml
+ Setup\Dockerfile = Setup\Dockerfile
+ Setup\vault-db.sql = Setup\vault-db.sql
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vault", "vault", "{467DA82E-F0F8-40F7-97AD-1DDD4E82F6D3}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\vault\entrypoint.sh = Setup\vault\entrypoint.sh
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{D0A469B5-36AF-4B79-B2BD-76DAC965E08E}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\vault\config\default.hcl = Setup\vault\config\default.hcl
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "policies", "policies", "{471704EC-DB71-448C-A62E-DB8D750252AA}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\vault\policies\staging-manage.hcl = Setup\vault\policies\staging-manage.hcl
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "approles", "approles", "{924377C7-9EE3-4E56-BE9F-5A2B17DD25B0}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\vault\policies\approles\staging-approle.hcl = Setup\vault\policies\approles\staging-approle.hcl
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "userpass", "userpass", "{5C028607-C039-45FC-9471-868FEB650A53}"
+ ProjectSection(SolutionItems) = preProject
+ Setup\vault\policies\userpass\staging-userpass.hcl = Setup\vault\policies\userpass\staging-userpass.hcl
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {45A81E28-0CE6-4313-BA86-0D816F986BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45A81E28-0CE6-4313-BA86-0D816F986BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45A81E28-0CE6-4313-BA86-0D816F986BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45A81E28-0CE6-4313-BA86-0D816F986BD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DBBEEF2D-A83C-4D94-A3D2-16E41352EBB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DBBEEF2D-A83C-4D94-A3D2-16E41352EBB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DBBEEF2D-A83C-4D94-A3D2-16E41352EBB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DBBEEF2D-A83C-4D94-A3D2-16E41352EBB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {588FAB66-3809-467D-B044-211F79B78CBC} = {FBCC82E1-EE2D-40D2-BD57-270645F1B78A}
+ {467DA82E-F0F8-40F7-97AD-1DDD4E82F6D3} = {588FAB66-3809-467D-B044-211F79B78CBC}
+ {D0A469B5-36AF-4B79-B2BD-76DAC965E08E} = {467DA82E-F0F8-40F7-97AD-1DDD4E82F6D3}
+ {471704EC-DB71-448C-A62E-DB8D750252AA} = {467DA82E-F0F8-40F7-97AD-1DDD4E82F6D3}
+ {924377C7-9EE3-4E56-BE9F-5A2B17DD25B0} = {471704EC-DB71-448C-A62E-DB8D750252AA}
+ {5C028607-C039-45FC-9471-868FEB650A53} = {471704EC-DB71-448C-A62E-DB8D750252AA}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F83908E4-CB64-4919-984D-6E5AD97AB1E2}
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
index 138d134..c4946ca 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,337 @@
-# HashiVaultCs
\ No newline at end of file
+# HashiVaultC# (HashiVaultCs)
+
+This project is to help develop and understand how to build a HashiCorp Vault Client in csharp.
+
+It's funtionality is minimal but feel free to add to the project. I will do my best to keep up, and if you like the project, please give it a git-star. Thanks.
+
+Currently as a first version, this code does very little. The following API paths are the only endpoints that it can communicate with:
+- /v1/auth/userpass/login/{username}
+- /v1/auth/approle/role/{rolename}/role-id
+- /v1/auth/approle/role/{rolename}/secret-id
+- /v1/auth/approle/login
+- /v1/{engine}/data/{path}
+
+With these endpoints it is enough to:
+- Login using the userpass authentication method.
+- Use the vault token from the userpass login to obtain the role-id and generate a secret-id.
+- With the same userpass vault token, use the role-id and the secret-id to login under the approle authentication method and get a another token which can be then used to read from the kv secrets store.
+- Finally read values from the kv secrets engine store.
+
+To check out some sample code, just head over to the unit test(s) to see this in action, though there they are not complete yet.
+
+In the mean while I'll try to improve the documentation on how to use the code, and place it into a ```README.md``` (not yet created) inside of the ```Client``` project folder.
+
+Even if people don't end up using any of the code, I hope that the documentation below is enough to get the newer amongst us started with HashiCorp Vault (https://www.vaultproject.io/)
+
+I took inspiration from this article to get me started: https://www.infoq.com/articles/creating-http-sdks-dotnet-6/
+
+##### What's next...
+- I would like to add some basic reading of PKI secrets and getting certificates into my applications next.
+- After that I would like to add the Wrapping Token concept.
+- Get HTTPs finally working on localhost at least.
+- Finally wire it up in a way that can be used with Dependancy Injection.
+
+------
+
+## Getting Started
+
+You'll need a running Vault service, and although it is beyond the scope of this project, there is included a ```Dockerfile``` and ```docker-compose.yml``` included to help get going ith a postgresql service backend and utlising Vault-Unseal (https://github.com/omegion/vault-unseal).
+Include with this is the config files, entrypoint script which will need configuration (replace the vault shards), and the vault-db.sql for the postgres db.
+
+If you already have a running vault service, you will need to have access to the vault CLI, and then set the vault token environment variable as following:
+
+```bash
+export VAULT_TOKEN=
+```
+
+The vault token value can be the root vault token or other valid vault token.
+
+### Other Tools
+
+When using the CLI directly on the vault server (running alpine linux), there are a few tools that might need to be installed to make life easier.
+```bash
+apk add nano curl
+```
+
+------
+
+## Vault Configurations
+
+- Create a username/password authenication method
+ - This will be used to read role-id values and generate secret-id values from the staging approle secrets only, nothing more.
+ - You can see that the paths defined in the ACL policy ```setup/vault/config/policies/approles/staging-userpass.hcl``` will allow access to this part of the system.
+ - For the purpose of this documentation we will create a single username/password. However, this will not reflect the "real-world" configurations.
+ - In development, a unique username/password entry should be created per developer required (they can use this in their development environment such as docker desktop to start their applications)
+- Create a basic kv secrets store and enter some test key-value data entries (test=testing & heylo=world)
+- Create the approle authentication method
+ - Create the ACL policy that will allow access to the KV secrets store as path ```kv/data/staging/*``` - again this can be seen in the policy file ```setup/vault/policies/approle/staging-approle.hcl```
+ - Note the extra ```/data/``` path inserted after the ```kv/``` - this is to let the policy know that the path being accessed is for the version 2 secret store (kv-v2), and not version 1.
+ - For the purpose of documentation, we will create an approle named ```staging``` and apply the newly create policy to that approle entry.
+
+Where it mentions to run a commmand "Only once per database", this means that once the command has been run, you do not need to run it again and can omit that step. If you are working on an existing service using an existing database, you can safely assume that this step has already been done.
+
+------
+
+#### Username/Password Authentication Method
+
+Enabling username/password authentication method (Only once per database)
+```bash
+vault auth enable userpass
+```
+
+Create the policy that allows creation of rewrap tokens, list policies and read the newly created policy
+```bash
+vault policy write staging-userpass setup/vault/policies/userpass/staging-userpass.hcl
+vault policy list
+vault policy read staging-userpass
+```
+
+Create the userpass creditials with the newly created policy, and list usernames
+```bash
+vault write auth/userpass/users/ password= policies=staging-userpass
+vault list auth/userpass/users
+```
+
+#### Create Key Value Storage Engine
+
+```bash
+vault secrets enable -version=2 kv
+```
+
+Please note the lack of the extra ```/data/``` path, as this not required when writing or reading a version 2 variant of the kv secrets store.
+```bash
+vault kv put kv/staging/service-svc test=testing heylo=world
+```
+
+#### App Role Configurations
+
+Enabling approle authentication method (Only once per database)
+```bash
+vault auth enable approle
+```
+
+Create the policy for the approle, list policies and read the newly created policy
+```bash
+vault policy write staging-approle setup/vault/policies/approle/staging-approle.hcl
+vault policy list
+vault policy read staging-approle
+```
+
+Create named role and associated policies
+```bash
+vault write auth/approle/role/staging \
+ secret_id_ttl=30m \
+ token_num_uses=100 \
+ token_ttl=30m \
+ token_max_ttl=30m \
+ secret_id_num_uses=100 \
+ policies=staging-approle
+vault read auth/approle/role/staging
+```
+
+Let's test the secret-id generation. Any results from this are not used later in the documentation, so these steps are optional.
+
+Fetch the RoleID of the AppRole.
+```bash
+vault read auth/approle/role/staging/role-id
+```
+
+Generate/Retrieve a SecretID.
+```bash
+vault write -f auth/approle/role/staging/secret-id
+```
+
+Generate token
+```bash
+vault write auth/approle/login role_id= secret_id=
+```
+
+The generation of the token above is just to test if the process has worked.
+An example of the commands can be seen below, to provide more context of it's use:
+
+```bash
+
+/ # vault read auth/approle/role/staging/role-id
+Key Value
+--- -----
+role_id 59b5cc05-d7f7-71c3-6ba6-4bc8fbbd2168
+
+/ # vault write -f auth/approle/role/staging/secret-id
+Key Value
+--- -----
+secret_id daa0a954-2791-04e1-79d0-204e4b8ae64f
+secret_id_accessor 5451d54e-1318-2a74-5edd-9ebf018e9941
+secret_id_ttl 1m
+
+/ # vault write auth/approle/login role_id=59b5cc05-d7f7-71c3-6ba6-4bc8fbbd2168 secret_id=daa0a954-2791-04e1-79d0-204e4b8ae64f
+Key Value
+--- -----
+token hvs.CAESIBfa3idmMGpTzS7KuxTCpWQXcEmOryi6SlB5A-Dl1V67Gh4KHGh2cy5xT2ZNTHJ0Y0RrQlU0REhLNDlnSlZRSXg
+token_accessor HSxzYQi5Crl1w2LSvwghJWgz
+token_duration 30m
+token_renewable true
+token_policies ["default","staging-approle"]
+identity_policies []
+policies ["default","staging-approle"]
+token_meta_role_name staging
+
+```
+...
+##### Extracting the vault token from the above response!
+You can see that in the response above, that there is a key field simply defined as ```token```, and that the value starting with ```hvs.CAESIBf...``` is the token that we can save for later use as the VAULT_TOKEN environment variable.
+
+---
+
+#### Obtain secret-id token as newly created user
+
+The final part, is to get a vault token that is useable for the duration specified in approle creation command, of 30 minutes (30m).
+
+The steps are as followed:
+
+Make sure we are logged out of the vault by clearing any tokens.
+```bash
+unset VAULT_TOKEN
+rm -Rf ~/.vault-token
+```
+
+Login to the vault using our userpass authentication creditials (this will create the ```~/.vault-token``` file.)
+```bash
+vault login -method=userpass username=
+/ # Password (will be hidden):
+```
+
+Get the role-id and generate a secret-id for the approle 'staging' as logged in user
+```bash
+vault read auth/approle/role/staging/role-id
+vault write -f auth/approle/role/staging/secret-id
+```
+
+Generate a token using the previously create role-id and newly created secret-id.
+Important: Save the ```token``` value as the '*approle-token*' for use later...
+```bash
+vault write auth/approle/login role_id= secret_id=
+```
+
+Logout of the vault as the newly created user by removing the vault-token from the home directory
+```bash
+rm -Rf ~/.vault-token
+```
+
+Using the '*approle-token*' that you just obtained, set the VAULT_TOKEN environment varaible to this value
+```bash
+export VAULT_TOKEN=
+```
+
+With the newly set VAULT_TOKEN, attempt to access the values in the kv/staging/* secrets store, again noting the lack of the extra ```/data/``` path as it is not required.
+```bash
+vault kv get kv/staging/service-svc
+```
+
+Sample of what to expect to see:
+```bash
+/ # vault kv get kv/staging/service-svc
+======= Secret Path =======
+kv/data/staging/service-svc
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2022-05-22T13:02:52.183297Z
+custom_metadata
+deletion_time n/a
+destroyed false
+version 1
+
+==== Data ====
+Key Value
+--- -----
+heylo world
+test testing
+```
+
+
+### Removing Vault Configurations
+
+Remove named role
+```bash
+vault delete auth/approle/role/staging
+```
+
+Disable approle authentication method
+```bash
+vault auth disable approle
+```
+
+Disable kv secret engine
+```bash
+vault secrets disable kv
+```
+
+Remove userpass entry
+```bash
+vault delete auth/userpass/users/
+```
+
+Disable userpass authentication method
+```bash
+vault auth disable userpass
+```
+
+Remove ACL Policies
+```bash
+vault policy delete staging-approle
+vault policy delete staging-userpass
+```
+
+-----
+
+## Collated Commands to CURL Requests
+
+The following is the same set of commands as above, but using CURL and the API to access the same set of features and requests.
+
+Using a pre-shared username password combo, get the ```client_token``` which will only allow access to the approle authentication method to read a role-id and write a secret-id only.
+```bash
+vault login -method=userpass username=
+curl --request POST --data "{ \"password\": \"\" }" http://localhost:8200/v1/auth/userpass/login/
+```
+
+Using the ```client_token``` as the vault token value, read the role-id and write/retrieve a secret-id
+```bash
+vault read auth/approle/role/staging/role-id
+curl --header "X-Vault-Token: ..." --request GET http://localhost:8200/v1/auth/approle/role/staging/role-id
+
+vault write -f auth/approle/role/staging/secret-id
+curl --header "X-Vault-Token: ..." --request POST --data "{}" http://localhost:8200/v1/auth/approle/role/staging/secret-id
+```
+
+Create a token using the role-id and the newly generated secret-id, get a new ```client_token``` which will have the ability to read and write from the kv secrets storage engine.
+```bash
+vault write auth/approle/login role_id= secret_id=
+curl --header "X-Vault-Token: ..." --request POST --data "{ \"role_id\": \"\", \"secret_id\": \"\" }" http://localhost:8200/v1/auth/approle/login
+```
+
+Use the ```client_token``` from the previous command as a newly generated vault token, read from the kv secrets storage engine.
+```bash
+vault kv get kv/staging/service-svc
+curl --header "X-Vault-Token: ..." --request GET http://localhost:8200/v1/kv/data/staging/service-svc
+```
+
+Below is the CURL commands being ran and the responses, so you can see the context of how it should work.
+- The username/password have been redacted.
+- Please note the change in the vault token for the last command.
+```bash
+curl --request POST --data "{ \"password\": \"\" }" http://localhost:8200/v1/auth/userpass/login/
+{"request_id":"23610a44-3b4f-5f5d-f2b0-c452117ba89b","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":null,"warnings":null,"auth":{"client_token":"hvs.CAESII17x5GQYw0QhYKi95kxUUjdhi0Q-X1G6fbddsTxQcPAGh4KHGh2cy5vRWJwUzNvdWlpMXExc3NZT0ltbkk5MjA","accessor":"al1urMYAjwT3E8Gc6e3DtuHb","policies":["default","staging-userpass"],"token_policies":["default","staging-userpass"],"metadata":{"username":"kibble"},"lease_duration":2764800,"renewable":true,"entity_id":"b5d38ffe-8c15-46e6-931f-86cc074c8c2e","token_type":"service","orphan":true,"mfa_requirement":null,"num_uses":0}}
+
+curl --header "X-Vault-Token: hvs.CAESII17x5GQYw0QhYKi95kxUUjdhi0Q-X1G6fbddsTxQcPAGh4KHGh2cy5vRWJwUzNvdWlpMXExc3NZT0ltbkk5MjA" --request GET http://localhost:8200/v1/auth/approle/role/staging/role-id
+{"request_id":"e060d82e-2224-7995-f539-a993696e8d6b","lease_id":"","renewable":false,"lease_duration":0,"data":{"role_id":"f2d9ae61-8055-93a7-5913-cbc176864d2a"},"wrap_info":null,"warnings":null,"auth":null}
+
+curl --header "X-Vault-Token: hvs.CAESII17x5GQYw0QhYKi95kxUUjdhi0Q-X1G6fbddsTxQcPAGh4KHGh2cy5vRWJwUzNvdWlpMXExc3NZT0ltbkk5MjA" --request POST --data "{}" http://localhost:8200/v1/auth/approle/role/staging/secret-id
+{"request_id":"27a8497f-de31-f902-e19b-955b8ff435b6","lease_id":"","renewable":false,"lease_duration":0,"data":{"secret_id":"afbb0d99-bcdc-f722-ec3d-a4a465a674b9","secret_id_accessor":"9dc3edfc-228a-b216-d627-094f3ce9aad4","secret_id_ttl":1800},"wrap_info":null,"warnings":null,"auth":null}
+
+curl --header "X-Vault-Token: hvs.CAESII17x5GQYw0QhYKi95kxUUjdhi0Q-X1G6fbddsTxQcPAGh4KHGh2cy5vRWJwUzNvdWlpMXExc3NZT0ltbkk5MjA" --request POST --data "{ \"role_id\": \"f2d9ae61-8055-93a7-5913-cbc176864d2a\", \"secret_id\": \"afbb0d99-bcdc-f722-ec3d-a4a465a674b9\" }" http://localhost:8200/v1/auth/approle/login
+{"request_id":"cad4f7d8-63f1-b09c-ecf1-23c016419c70","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":null,"warnings":null,"auth":{"client_token":"hvs.CAESILacrRZCXR-e1GTcitvww7EdFdq7C4ftIRDro6lkE3wZGh4KHGh2cy5iTWhvMDhvcFVFMzQzODZtYm9wVEZEcVA","accessor":"4IsCFPg9kJQdNaVnknLFeHvx","policies":["default","staging-approle"],"token_policies":["default","staging-approle"],"metadata":{"role_name":"staging"},"lease_duration":1800,"renewable":true,"entity_id":"dc5683f8-830a-c1fa-2363-15148b72f772","token_type":"service","orphan":true,"mfa_requirement":null,"num_uses":100}}
+
+curl --header "X-Vault-Token: hvs.CAESILacrRZCXR-e1GTcitvww7EdFdq7C4ftIRDro6lkE3wZGh4KHGh2cy5iTWhvMDhvcFVFMzQzODZtYm9wVEZEcVA" --request GET http://localhost:8200/v1/kv/data/staging/service-svc
+{"request_id":"30f5d4ec-ccb1-fcc2-f69d-142218651368","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"heylo":"world","test":"testing"},"metadata":{"created_time":"2022-05-22T13:42:53.8885279Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":3}},"wrap_info":null,"warnings":null,"auth":null}
+```
diff --git a/Setup/Dockerfile b/Setup/Dockerfile
new file mode 100644
index 0000000..ebe60fd
--- /dev/null
+++ b/Setup/Dockerfile
@@ -0,0 +1,23 @@
+ARG GO_VERSION=1.16-alpine3.12
+
+FROM golang:${GO_VERSION} AS builder
+LABEL org.opencontainers.image.source="https://github.com/omegion/vault-unseal"
+WORKDIR /app
+RUN apk update && \
+ apk add ca-certificates gettext git make curl unzip && \
+ rm -rf /tmp/* && \
+ rm -rf /var/cache/apk/* && \
+ rm -rf /var/tmp/* && \
+ git clone https://github.com/omegion/vault-unseal.git
+RUN cd /app/vault-unseal && make build-for-container
+
+FROM vault:latest
+ENV IMAGE_NAME="vault-svc"
+LABEL name="vault-svc"
+COPY --from=builder /app/vault-unseal/dist/vault-unseal-linux /bin/vault-unseal
+COPY vault/config/default.hcl /vault/config/default.hcl
+COPY vault/entrypoint.sh /vault/entrypoint.sh
+RUN chmod 755 /vault/entrypoint.sh
+
+EXPOSE 8200
+ENTRYPOINT ["/vault/entrypoint.sh"]
diff --git a/Setup/docker-compose.yml b/Setup/docker-compose.yml
new file mode 100644
index 0000000..1dc3b47
--- /dev/null
+++ b/Setup/docker-compose.yml
@@ -0,0 +1,47 @@
+version: '3.7'
+
+## docker-compose build --no-cache && docker-compose -p vault-stack -f docker-compose.yml up -d
+## docker-compose -p vault-stack -f docker-compose.yml down
+
+services:
+
+ vault-svc:
+ container_name: vault-svc
+ build:
+ context: .
+ dockerfile: Dockerfile
+ restart: unless-stopped
+ expose:
+ - 8200
+ ports:
+ - 8200:8200
+ environment:
+ VAULT_LOG_LEVEL: trace
+ VAULT_ADDR: http://localhost:8200
+ VAULT_API_ADDR: http://localhost:8200
+ cap_add:
+ - IPC_LOCK
+ volumes:
+ - vault-server-config:/vault/config/
+ - vault-server-file:/vault/file
+ - vault-server-logs:/vault/logs/
+ - vault-server-plugins:/vault/plugins/
+
+
+volumes:
+
+ vault-server-config:
+ name: vault-server-config
+ driver: local
+
+ vault-server-file:
+ name: vault-server-file
+ driver: local
+
+ vault-server-logs:
+ name: vault-server-logs
+ driver: local
+
+ vault-server-plugins:
+ name: vault-server-plugins
+ driver: local
diff --git a/Setup/vault-db.sql b/Setup/vault-db.sql
new file mode 100644
index 0000000..d6db3da
--- /dev/null
+++ b/Setup/vault-db.sql
@@ -0,0 +1,21 @@
+DROP INDEX IF EXISTS parent_path_idx;
+DROP TABLE IF EXISTS vault_kv_store;
+DROP TABLE IF EXISTS vault_ha_locks;
+
+CREATE TABLE vault_kv_store (
+ parent_path TEXT COLLATE "C" NOT NULL,
+ path TEXT COLLATE "C",
+ key TEXT COLLATE "C",
+ value BYTEA,
+ CONSTRAINT pkey PRIMARY KEY (path, key)
+);
+
+CREATE TABLE vault_ha_locks (
+ ha_key TEXT COLLATE "C" NOT NULL,
+ ha_identity TEXT COLLATE "C" NOT NULL,
+ ha_value TEXT COLLATE "C",
+ valid_until TIMESTAMP WITH TIME ZONE NOT NULL,
+ CONSTRAINT ha_key PRIMARY KEY (ha_key)
+);
+
+CREATE INDEX parent_path_idx ON vault_kv_store (parent_path);
diff --git a/Setup/vault/config/default.hcl b/Setup/vault/config/default.hcl
new file mode 100644
index 0000000..9593c5e
--- /dev/null
+++ b/Setup/vault/config/default.hcl
@@ -0,0 +1,19 @@
+storage "postgresql" {
+ connection_url = "postgres://username:password@hostname:5432/database?sslmode=require"
+ table = "vault_kv_store"
+ ha_enabled = true
+ max_idle_connections = 1
+ max_parallel = 4
+}
+
+listener "tcp" {
+ address = "0.0.0.0:8200"
+ tls_disable = true
+}
+
+api_addr = "http://localhost:8200"
+ui = true
+
+disable_cache = true
+disable_mlock = true
+disable_sealwrap = true
diff --git a/Setup/vault/config/vault-cluster-keys.json b/Setup/vault/config/vault-cluster-keys.json
new file mode 100644
index 0000000..669d36e
--- /dev/null
+++ b/Setup/vault/config/vault-cluster-keys.json
@@ -0,0 +1,31 @@
+{
+ "keys": [
+ "dc761cbad9c291e6704010e73d66c7f59d3ceb65bff5ca8e216e222a6b239536e6",
+ "8818fbf9dc92e29491bbcc229f04d8169768aff51956b6ab8e7f858318e98f19e8",
+ "0f5349d2b998cf5165875a5f40c375295def2ae74b6134ca35953f7e353b47b2af",
+ "66dc75cf37a03372ad175f7895bfa3445a861c76cf6b41facc156473c9951f85f9",
+ "3c690fdd6e477cd9b9732e4e93d68a83e8dc84fe7489e53cf2333b2386486ff1f6",
+ "7ef1e03e087d893212ef0570dbc6db14249e3d9c44dfbab72cd615b973dc58b642",
+ "585dd96715b6602b3d63469bad4665c01eb882a1df59748386bf7e059a10fbb895",
+ "a9540105ef8f156c49f5543dbb0c7dd0f1497ec4017d1463e18092ceefa319b7b2",
+ "b059b9a6b77812dd68f14ec3392800864150f2956d49fb0afca3484ae8b53c6eec",
+ "ee6c77e769379f9f32482400222e3d59e60ee8c7679d0f5c1b288d39650fdf9f5a",
+ "43103b60831506633b84dc92acd8129a29a34f9d6676b3a27a47e91dfcb81aae1a",
+ "bfa209a58ce593f50392a3c6517e33a4375f8336978837cd90741bfd69fd34df08"
+ ],
+ "keys_base64": [
+ "3HYcutnCkeZwQBDnPWbH9Z0862W/9cqOIW4iKmsjlTbm",
+ "iBj7+dyS4pSRu8winwTYFpdor/UZVrarjn+Fgxjpjxno",
+ "D1NJ0rmYz1Flh1pfQMN1KV3vKudLYTTKNZU/fjU7R7Kv",
+ "Ztx1zzegM3KtF194lb+jRFqGHHbPa0H6zBVkc8mVH4X5",
+ "PGkP3W5HfNm5cy5Ok9aKg+jchP50ieU88jM7I4ZIb/H2",
+ "fvHgPgh9iTIS7wVw28bbFCSePZxE37q3LNYVuXPcWLZC",
+ "WF3ZZxW2YCs9Y0abrUZlwB64gqHfWXSDhr9+BZoQ+7iV",
+ "qVQBBe+PFWxJ9VQ9uwx90PFJfsQBfRRj4YCSzu+jGbey",
+ "sFm5prd4Et1o8U7DOSgAhkFQ8pVtSfsK/KNISui1PG7s",
+ "7mx352k3n58ySCQAIi49WeYO6MdnnQ9cGyiNOWUP359a",
+ "QxA7YIMVBmM7hNySrNgSmimjT51mdrOiekfpHfy4Gq4a",
+ "v6IJpYzlk/UDkqPGUX4zpDdfgzaXiDfNkHQb/Wn9NN8I"
+ ],
+ "root_token": "hvs.fzlW81QcX9Gxlfd9s9v1Nltg"
+}
\ No newline at end of file
diff --git a/Setup/vault/entrypoint.sh b/Setup/vault/entrypoint.sh
new file mode 100644
index 0000000..189b1e6
--- /dev/null
+++ b/Setup/vault/entrypoint.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+VAULT_RETRIES=3
+
+## Update these vault shards with your own. This script is taken over with the image during a ```docker build .```
+VAULT_SHARD_ONE=dc761cbad9c291e6704010e73d66c7f59d3ceb65bff5ca8e216e222a6b239536e6
+VAULT_SHARD_TWO=8818fbf9dc92e29491bbcc229f04d8169768aff51956b6ab8e7f858318e98f19e8
+
+export VAULT_ADDR='http://localhost:8200'
+export VAULT_API_ADDR='http://localhost:8200'
+
+/bin/vault server -config=/vault/config/default.hcl -log-level=trace &
+echo "Vault is starting...";
+
+until /bin/vault status > /dev/null 2>&1 || [ "$VAULT_RETRIES" -eq 0 ]; do
+ echo "Waiting for vault to start...: $((VAULT_RETRIES--))";
+ sleep 1
+done
+
+if [ "$VAULT_RETRIES" -eq 0 ]; then
+ echo "Vault is either sealed or in error state, attempting to unseal...";
+ /bin/vault-unseal unseal --address http://localhost:8200 --shard=$VAULT_SHARD_ONE --shard=$VAULT_SHARD_TWO
+fi
+
+echo "Entering 10m sleep loop...";
+while true; do
+ sleep 10m
+ echo "Ping :)";
+ if [ /bin/vault status > /dev/null 2>&1 ]; then
+ exit;
+ fi
+done
diff --git a/Setup/vault/policies/approles/staging-approle.hcl b/Setup/vault/policies/approles/staging-approle.hcl
new file mode 100644
index 0000000..1402b1f
--- /dev/null
+++ b/Setup/vault/policies/approles/staging-approle.hcl
@@ -0,0 +1,11 @@
+path "kv/data/staging/*" {
+ capabilities = [ "create", "read", "delete", "update", "list" ]
+}
+
+path "kv/staging/*" {
+ capabilities = [ "create", "read", "update", "delete", "list" ]
+}
+
+path "kv/*" {
+ capabilities = [ "list" ]
+}
diff --git a/Setup/vault/policies/staging-manage.hcl b/Setup/vault/policies/staging-manage.hcl
new file mode 100644
index 0000000..06382ab
--- /dev/null
+++ b/Setup/vault/policies/staging-manage.hcl
@@ -0,0 +1,4 @@
+# Allow a management of staging kv
+path "kv/data/staging/*" {
+ capabilities = ["create", "read", "update", "delete", "list"]
+}
\ No newline at end of file
diff --git a/Setup/vault/policies/userpass/staging-userpass.hcl b/Setup/vault/policies/userpass/staging-userpass.hcl
new file mode 100644
index 0000000..09f21eb
--- /dev/null
+++ b/Setup/vault/policies/userpass/staging-userpass.hcl
@@ -0,0 +1,14 @@
+path "auth/approle/role/staging/secret*" {
+ capabilities = [ "create" ]
+ min_wrapping_ttl = "1m"
+ max_wrapping_ttl = "3m"
+}
+
+path "auth/approle/role/staging/secret-id" {
+ capabilities = [ "update" ]
+}
+
+path "auth/approle/role/staging/role-id" {
+ capabilities = [ "read" ]
+}
+
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
new file mode 100644
index 0000000..ea4e0eb
--- /dev/null
+++ b/Tests/Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/UnitTest.cs b/Tests/UnitTest.cs
new file mode 100644
index 0000000..6823259
--- /dev/null
+++ b/Tests/UnitTest.cs
@@ -0,0 +1,80 @@
+namespace Tests;
+
+[TestClass]
+public class UnitTest
+{
+
+ private readonly string _base_address;
+ private readonly string _username;
+ private readonly string _password;
+ private readonly string _rolename;
+ private readonly string _engine;
+ private readonly string _secrets_path;
+
+ public UnitTest()
+ {
+ _base_address = "http://vault-svc:8200";
+ _username = "username";
+ _password = "password";
+ _rolename = "staging";
+ _engine = "kv";
+ _secrets_path = "staging/service-svc";
+ }
+
+ [TestMethod]
+ public void UserpassClientUnitTest()
+ {
+ Dictionary vault_headers = new();
+
+ UserpassClient userpass_client = new(HttpVaultHeaders.Build(vault_headers), _base_address);
+ Secret response = userpass_client.LoginAsync(_username, new HashiVaultCs.Models.Requests.Auth.Userpass.Login
+ {
+ Password = _password
+ }, ImmutableDictionary.Empty).Result;
+
+ _ = response;
+ }
+
+ [TestMethod]
+ public void TestMethod()
+ {
+ Dictionary vault_headers = new();
+
+ UserpassClient userpass_client = new(HttpVaultHeaders.Build(vault_headers), _base_address);
+ Secret userpass_login_response = userpass_client.LoginAsync(_username, new HashiVaultCs.Models.Requests.Auth.Userpass.Login
+ {
+ Password = _password
+ }, ImmutableDictionary.Empty).Result;
+
+ Assert.IsNotNull(userpass_login_response.Auth?.ClientToken);
+
+ vault_headers.Remove(HttpVaultHeaderKey.Token);
+ vault_headers.Add(HttpVaultHeaderKey.Token, userpass_login_response.Auth.ClientToken);
+
+ ApproleClient approle_client = new(HttpVaultHeaders.Build(vault_headers), _base_address);
+
+ Secret approle_roleid_response = approle_client.RoleIdAsync(_rolename, ImmutableDictionary.Empty).Result;
+ string? role_id = approle_roleid_response.Data?.RootElement.GetProperty("role_id").GetString();
+ Assert.IsNotNull(role_id);
+
+ Secret approle_secretid_response = approle_client.SecretIdAsync(_rolename, ImmutableDictionary.Empty).Result;
+ string? secret_id = approle_secretid_response.Data?.RootElement.GetProperty("secret_id").GetString();
+ Assert.IsNotNull(secret_id);
+
+ Secret approle_login_response = approle_client.LoginAsync(new HashiVaultCs.Models.Requests.Auth.Approle.Login
+ {
+ RoleId = role_id,
+ SecretId = secret_id
+ }, ImmutableDictionary.Empty).Result;
+ Assert.IsNotNull(approle_login_response.Auth?.ClientToken);
+
+ vault_headers.Remove(HttpVaultHeaderKey.Token);
+ vault_headers.Add(HttpVaultHeaderKey.Token, approle_login_response.Auth.ClientToken);
+
+ DataClient data_client = new(HttpVaultHeaders.Build(vault_headers), _base_address);
+ Secret data_get_response = data_client.GetAsync(_engine, _secrets_path, ImmutableDictionary.Empty).Result;
+ string? response_json = data_get_response.Data?.RootElement.GetProperty("data").GetRawText();
+ _ = response_json;
+
+ }
+}
\ No newline at end of file
diff --git a/Tests/Usings.cs b/Tests/Usings.cs
new file mode 100644
index 0000000..1a3bc4c
--- /dev/null
+++ b/Tests/Usings.cs
@@ -0,0 +1,6 @@
+global using HashiVaultCs;
+global using HashiVaultCs.Clients.Auth;
+global using HashiVaultCs.Clients.Secrets;
+global using HashiVaultCs.Models.Responses;
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+global using System.Collections.Immutable;