-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathResolveSecretAttribute.cs
More file actions
130 lines (113 loc) · 4.82 KB
/
ResolveSecretAttribute.cs
File metadata and controls
130 lines (113 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
// TODO: consider supporting multiple calls to Key Vault at the same time
// TODO: consider supporting types other than string
namespace NetBricks;
/// <summary>
/// Attribute that marks a string property for resolving a Key Vault secret reference.
/// This attribute is applied to string properties that contain Azure Key Vault URLs.
/// During configuration setup, these URLs will be replaced with the actual secret values.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ResolveSecretAttribute : ValidationAttribute
{
private static readonly Dictionary<string, string> ErrorMessages = [];
internal static void SetError(Type type, string? propertyName, string errorMessage)
{
string key = $"{type.FullName}.{propertyName}";
ErrorMessages[key] = errorMessage;
}
internal static string? GetError(Type type, string? propertyName)
{
string key = $"{type.FullName}.{propertyName}";
return ErrorMessages.TryGetValue(key, out var message) ? message : null;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is not string posurl)
{
return new ValidationResult("ResolveSecret can only be applied to Strings.");
}
string? error = GetError(validationContext.ObjectType, validationContext.MemberName);
if (!string.IsNullOrEmpty(error))
{
return new ValidationResult(error);
}
return ValidationResult.Success;
}
}
internal static class ResolveSecret
{
private class KeyVaultItem
{
public string? value = null;
}
internal static async Task ApplyAsync<T>(
T instance,
IHttpClientFactory? httpClientFactory = null,
DefaultAzureCredential? defaultAzureCredential = null,
CancellationToken cancellationToken = default)
where T : class
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));
// look for properties with the ResolveSecret attribute
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
// check if the property has the ResolveSecret attribute
var attribute = Attribute.GetCustomAttribute(property, typeof(ResolveSecretAttribute)) as ResolveSecretAttribute;
if (attribute is null)
continue;
// only process strings
var value = property.GetValue(instance);
if (value is not string posurl)
continue;
// shortcut if the URL is empty or not a key vault URL
if (string.IsNullOrEmpty(posurl) ||
!posurl.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
!posurl.Contains(".vault.azure.net/", StringComparison.OrdinalIgnoreCase))
{
continue;
}
// check the requirements
if (httpClientFactory is null || defaultAzureCredential is null)
{
ResolveSecretAttribute.SetError(typeof(T), property.Name, "HttpClientFactory and DefaultAzureCredential must be provided.");
continue;
}
// get an access token
var tokenRequestContext = new TokenRequestContext([$"https://vault.azure.net/.default"]);
var tokenResponse = await defaultAzureCredential!.GetTokenAsync(tokenRequestContext, cancellationToken);
var accessToken = tokenResponse.Token;
// create the HTTP client
using var httpClient = httpClientFactory!.CreateClient("netbricks");
// get from the Key Vault
using (var request = new HttpRequestMessage()
{
RequestUri = new Uri($"{posurl}?api-version=7.0"),
Method = HttpMethod.Get
})
{
request.Headers.Add("Authorization", $"Bearer {accessToken}");
using var response = await httpClient.SendAsync(request, cancellationToken);
var raw = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
ResolveSecretAttribute.SetError(typeof(T), property.Name, $"Key vault request failed: {response.StatusCode} - {raw}");
continue;
}
var item = JsonConvert.DeserializeObject<KeyVaultItem>(raw);
property.SetValue(instance, item?.value);
}
}
}
}