-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add import from azure app configuration (#69)
- Loading branch information
Showing
17 changed files
with
753 additions
and
127 deletions.
There are no files selected for viewing
72 changes: 72 additions & 0 deletions
72
...AzureAppConfigurationEmulator.Authentication.Hmac/HmacAuthenticatingHttpMessageHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
using System.Net.Http.Headers; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
namespace AzureAppConfigurationEmulator.Authentication.Hmac; | ||
|
||
public class HmacAuthenticatingHttpMessageHandler(string credential, string secret) : DelegatingHandler | ||
{ | ||
protected override async Task<HttpResponseMessage> SendAsync( | ||
HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(HmacAuthenticatingHttpMessageHandler)}.{nameof(SendAsync)}"); | ||
|
||
var contentHash = await ComputeContentHash(request, cancellationToken); | ||
activity?.SetTag(Telemetry.ContentHash, contentHash); | ||
|
||
var date = DateTimeOffset.UtcNow; | ||
activity?.SetTag(Telemetry.Date, date); | ||
|
||
request.Headers.Authorization = GetAuthenticationHeaderValue(request, contentHash, date); | ||
request.Headers.Date = date; | ||
request.Headers.Add("x-ms-content-sha256", contentHash); | ||
|
||
return await base.SendAsync(request, cancellationToken); | ||
} | ||
|
||
private static async Task<string> ComputeContentHash( | ||
HttpRequestMessage request, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(HmacAuthenticatingHttpMessageHandler)}.{nameof(ComputeContentHash)}"); | ||
|
||
using var stream = new MemoryStream(); | ||
|
||
if (request.Content is not null) | ||
{ | ||
await request.Content.CopyToAsync(stream, cancellationToken); | ||
} | ||
|
||
using var sha256 = SHA256.Create(); | ||
|
||
return Convert.ToBase64String(await sha256.ComputeHashAsync(stream, cancellationToken)); | ||
} | ||
|
||
private string ComputeHash(string value) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(HmacAuthenticatingHttpMessageHandler)}.{nameof(ComputeHash)}"); | ||
|
||
using var hmac = new HMACSHA256(Convert.FromBase64String(secret)); | ||
|
||
return Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(value))); | ||
} | ||
|
||
private AuthenticationHeaderValue GetAuthenticationHeaderValue( | ||
HttpRequestMessage request, | ||
string contentHash, | ||
DateTimeOffset date) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(HmacAuthenticatingHttpMessageHandler)}.{nameof(GetAuthenticationHeaderValue)}"); | ||
|
||
const string signedHeaders = "date;host;x-ms-content-sha256"; | ||
|
||
var stringToSign = $"{request.Method.Method.ToUpper()}\n{request.RequestUri?.PathAndQuery}\n{date:R};{request.RequestUri?.Authority};{contentHash}"; | ||
activity?.SetTag(Telemetry.StringToSign, stringToSign); | ||
|
||
var signature = ComputeHash(stringToSign); | ||
activity?.SetTag(Telemetry.Signature, signature); | ||
|
||
return new AuthenticationHeaderValue("HMAC-SHA256", $"Credential={credential}&SignedHeaders={signedHeaders}&Signature={signature}"); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/AzureAppConfigurationEmulator.Authentication.Hmac/Telemetry.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System.Diagnostics; | ||
|
||
namespace AzureAppConfigurationEmulator.Authentication.Hmac; | ||
|
||
public static class Telemetry | ||
{ | ||
public const string ContentHash = $"{Namespace}.content_hash"; | ||
public const string Date = $"{Namespace}.date"; | ||
public const string Signature = $"{Namespace}.signature"; | ||
public const string StringToSign = $"{Namespace}.string_to_sign"; | ||
|
||
private const string Namespace = "azure_app_configuration_emulator.authentication.hmac"; | ||
|
||
public static ActivitySource ActivitySource { get; } = new("AzureAppConfigurationEmulator.Authentication.Hmac"); | ||
} |
19 changes: 19 additions & 0 deletions
19
src/AzureAppConfigurationEmulator.Common/Abstractions/IConfigurationClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using AzureAppConfigurationEmulator.Common.Constants; | ||
using AzureAppConfigurationEmulator.Common.Models; | ||
|
||
namespace AzureAppConfigurationEmulator.Common.Abstractions; | ||
|
||
public interface IConfigurationClient | ||
{ | ||
public IAsyncEnumerable<ConfigurationSetting> GetConfigurationSettings( | ||
string key = KeyFilter.Any, | ||
string label = LabelFilter.Any, | ||
DateTimeOffset? moment = default, | ||
CancellationToken cancellationToken = default); | ||
|
||
public IAsyncEnumerable<string> GetKeys( | ||
CancellationToken cancellationToken = default); | ||
|
||
public IAsyncEnumerable<string?> GetLabels( | ||
CancellationToken cancellationToken = default); | ||
} |
133 changes: 133 additions & 0 deletions
133
src/AzureAppConfigurationEmulator.Common/Clients/ConfigurationClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
using System.Runtime.CompilerServices; | ||
using System.Text.Json; | ||
using AzureAppConfigurationEmulator.Common.Abstractions; | ||
using AzureAppConfigurationEmulator.Common.Constants; | ||
using AzureAppConfigurationEmulator.Common.Headers; | ||
using AzureAppConfigurationEmulator.Common.Models; | ||
|
||
namespace AzureAppConfigurationEmulator.Common.Clients; | ||
|
||
public class ConfigurationClient( | ||
HttpClient httpClient, | ||
IConfigurationSettingFactory configurationSettingFactory) : IConfigurationClient | ||
{ | ||
public async IAsyncEnumerable<ConfigurationSetting> GetConfigurationSettings( | ||
string key = KeyFilter.Any, | ||
string label = LabelFilter.Any, | ||
DateTimeOffset? moment = default, | ||
[EnumeratorCancellation] CancellationToken cancellationToken = default) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ConfigurationClient)}.{nameof(GetConfigurationSettings)}"); | ||
|
||
LinkHeaderValue? link = null; | ||
|
||
do | ||
{ | ||
var uri = link?.Next?.SingleOrDefault() ?? | ||
new Uri($"/kv?key={key}&label={label}&api-version=1.0", UriKind.Relative); | ||
|
||
using var request = new HttpRequestMessage(HttpMethod.Get, uri); | ||
|
||
if (moment.HasValue) | ||
{ | ||
request.Headers.Add("Accept-Datetime", moment.Value.ToString("R")); | ||
} | ||
|
||
using var response = await httpClient.SendAsync(request, cancellationToken); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
link = response.Headers.TryGetValues("Link", out var values) | ||
? LinkHeaderValue.Parse(string.Join(", ", values)) | ||
: null; | ||
|
||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); | ||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); | ||
|
||
foreach (var element in document.RootElement.GetProperty("items").EnumerateArray()) | ||
{ | ||
yield return configurationSettingFactory.Create( | ||
element.GetProperty("etag").GetString()!, | ||
element.GetProperty("key").GetString()!, | ||
element.GetProperty("last_modified").GetDateTimeOffset(), | ||
element.GetProperty("locked").GetBoolean(), | ||
element.TryGetProperty("label", out var labelElement) | ||
? labelElement.GetString() | ||
: null, | ||
element.TryGetProperty("content_type", out var contentTypeElement) | ||
? contentTypeElement.GetString() | ||
: null, | ||
element.TryGetProperty("value", out var valueElement) | ||
? valueElement.GetString() | ||
: null, | ||
element.TryGetProperty("tags", out var tagsElement) | ||
? tagsElement.EnumerateObject().ToDictionary( | ||
property => property.Name, | ||
property => property.Value.GetString()!) | ||
: null); | ||
} | ||
} while (link is { Next: not null }); | ||
} | ||
|
||
public async IAsyncEnumerable<string> GetKeys( | ||
[EnumeratorCancellation] CancellationToken cancellationToken = default) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ConfigurationClient)}.{nameof(GetKeys)}"); | ||
|
||
LinkHeaderValue? link = null; | ||
|
||
do | ||
{ | ||
var uri = link?.Next?.SingleOrDefault() ?? | ||
new Uri("/keys?api-version=1.0", UriKind.Relative); | ||
|
||
using var request = new HttpRequestMessage(HttpMethod.Get, uri); | ||
using var response = await httpClient.SendAsync(request, cancellationToken); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
link = response.Headers.TryGetValues("Link", out var values) | ||
? LinkHeaderValue.Parse(string.Join(", ", values)) | ||
: null; | ||
|
||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); | ||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); | ||
|
||
foreach (var element in document.RootElement.GetProperty("items").EnumerateArray()) | ||
{ | ||
yield return element.GetProperty("name").GetString()!; | ||
} | ||
} while (link is { Next: not null }); | ||
} | ||
|
||
public async IAsyncEnumerable<string?> GetLabels( | ||
[EnumeratorCancellation] CancellationToken cancellationToken = default) | ||
{ | ||
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ConfigurationClient)}.{nameof(GetLabels)}"); | ||
|
||
LinkHeaderValue? link = null; | ||
|
||
do | ||
{ | ||
var uri = link?.Next?.SingleOrDefault() ?? | ||
new Uri("/labels?api-version=1.0", UriKind.Relative); | ||
|
||
using var request = new HttpRequestMessage(HttpMethod.Get, uri); | ||
using var response = await httpClient.SendAsync(request, cancellationToken); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
link = response.Headers.TryGetValues("Link", out var values) | ||
? LinkHeaderValue.Parse(string.Join(", ", values)) | ||
: null; | ||
|
||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); | ||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); | ||
|
||
foreach (var element in document.RootElement.GetProperty("items").EnumerateArray()) | ||
{ | ||
yield return element.GetProperty("name").GetString(); | ||
} | ||
} while (link is { Next: not null }); | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/AzureAppConfigurationEmulator.Common/Headers/LinkHeaderValue.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace AzureAppConfigurationEmulator.Common.Headers; | ||
|
||
public partial class LinkHeaderValue | ||
{ | ||
private readonly IDictionary<string, ICollection<Uri>> _values; | ||
|
||
private LinkHeaderValue(IDictionary<string, ICollection<Uri>> values) | ||
{ | ||
_values = values; | ||
} | ||
|
||
public IEnumerable<Uri>? Next => _values.TryGetValue("next", out var uris) ? uris : null; | ||
|
||
public IEnumerable<Uri>? Prev => _values.TryGetValue("prev", out var uris) ? uris : null; | ||
|
||
public static LinkHeaderValue Parse(string input) | ||
{ | ||
var values = new Dictionary<string, ICollection<Uri>>(); | ||
|
||
if (!string.IsNullOrEmpty(input)) | ||
{ | ||
if (input.Split(',') is { Length: > 0 } links) | ||
{ | ||
foreach (var link in links) | ||
{ | ||
if (RelRegex().Match(link) is { Success: true } rel && | ||
UriRegex().Match(link) is { Success: true } uri) | ||
{ | ||
if (values.TryGetValue(rel.Value, out var uris)) | ||
{ | ||
uris.Add(new Uri(uri.Value, UriKind.RelativeOrAbsolute)); | ||
} | ||
else | ||
{ | ||
values.Add(rel.Value, [new Uri(uri.Value, UriKind.RelativeOrAbsolute)]); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return new LinkHeaderValue(values); | ||
} | ||
|
||
public override string ToString() | ||
{ | ||
var builder = new StringBuilder(); | ||
|
||
foreach (var (rel, uris) in _values) | ||
{ | ||
builder.AppendJoin(", ", uris.Select(uri => $"<{uri}>; rel=\"{rel}\"")); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
|
||
[GeneratedRegex("(?<=rel=\").+?(?=\")")] | ||
private static partial Regex RelRegex(); | ||
|
||
[GeneratedRegex("(?<=<).+?(?=>)")] | ||
private static partial Regex UriRegex(); | ||
} |
14 changes: 14 additions & 0 deletions
14
src/AzureAppConfigurationEmulator/Components/AzureInputDate.razor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
@typeparam TValue | ||
@using System.Linq.Expressions | ||
|
||
<InputDate @attributes="@AdditionalAttributes" class="px-2 pt-1 pb-1.5 w-full bg-white rounded-sm border border-storm-dust h-[24px] placeholder:text-storm-dust invalid:border-alizarin-crimson dark:bg-cod-grey dark:border-star-dust dark:placeholder:text-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" Value="@Value" ValueChanged="@ValueChanged" ValueExpression="@ValueExpression"/> | ||
|
||
@code { | ||
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; } | ||
|
||
[Parameter] public TValue? Value { get; set; } | ||
|
||
[Parameter] public EventCallback<TValue?> ValueChanged { get; set; } | ||
|
||
[Parameter] public Expression<Func<TValue?>>? ValueExpression { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
src/AzureAppConfigurationEmulator/Components/ImportExportContentTypeInputSelect.razor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
@using System.Linq.Expressions | ||
@using AzureAppConfigurationEmulator.Common.Constants | ||
|
||
<AzureInputSelect AdditionalAttributes="@AdditionalAttributes" TValue="@string" Value="@Value" ValueChanged="@HandleValueChanged" ValueExpression="@ValueExpression"> | ||
<option checked="@(Value is null)" value="">(No content type)</option> | ||
<option checked="@(Value is MediaType.SecretReference)" value="@MediaType.SecretReference">Key Vault Reference (@MediaType.SecretReference)</option> | ||
<option checked="@(Value is MediaType.Json)" value="@MediaType.Json">JSON (@MediaType.Json)</option> | ||
</AzureInputSelect> | ||
|
||
@code { | ||
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; } | ||
|
||
[Parameter] public string? Value { get; set; } | ||
|
||
[Parameter] public EventCallback<string?> ValueChanged { get; set; } | ||
|
||
[Parameter] public Expression<Func<string?>>? ValueExpression { get; set; } | ||
|
||
private async Task HandleValueChanged(string? value) | ||
{ | ||
await ValueChanged.InvokeAsync(!string.IsNullOrEmpty(value) ? value : null); | ||
} | ||
|
||
} |
Oops, something went wrong.