Skip to content

Commit

Permalink
Store secrets in separate config and retrieve via API
Browse files Browse the repository at this point in the history
  • Loading branch information
davewalker5 committed Nov 20, 2023
1 parent bea1239 commit 19151c7
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 29 deletions.
47 changes: 47 additions & 0 deletions src/MusicCatalogue.Api/Controllers/SecretsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MusicCatalogue.Entities.Config;
using MusicCatalogue.Logic.Config;

namespace MusicCatalogue.Api.Controllers
{
[Authorize]
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("[controller]")]
public class SecretsController : Controller
{
private readonly MusicApplicationSettings _settings;

public SecretsController(IOptions<MusicApplicationSettings> settings)
{
_settings = settings.Value;
SecretResolver.ResolveAllSecrets(_settings);
}

/// <summary>
/// Return a secret from the configuration file
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[HttpGet]
[Route("{name}")]
public ActionResult<string?> GetSecret(string name)
{
var secret = _settings.Secrets.FirstOrDefault(x => x.Name == name);

Check warning on line 32 in src/MusicCatalogue.Api/Controllers/SecretsController.cs

View workflow job for this annotation

GitHub Actions / build

"Find" method should be used instead of the "FirstOrDefault" extension method.

if (secret == null)
{
return NotFound();
}

if (string.IsNullOrEmpty(secret.Value))
{
return NoContent();
}

return secret.Value;
}
}
}
3 changes: 2 additions & 1 deletion src/MusicCatalogue.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MusicCatalogue.Api.Entities;
Expand Down Expand Up @@ -43,6 +43,7 @@ public static void Main(string[] args)
builder.Services.Configure<MusicApplicationSettings>(section);
var settings = section.Get<MusicApplicationSettings>();
ApiKeyResolver.ResolveAllApiKeys(settings!);
SecretResolver.ResolveAllSecrets(settings!);

// Configure the DB context
builder.Services.AddScoped<MusicCatalogueDbContext>();
Expand Down
6 changes: 6 additions & 0 deletions src/MusicCatalogue.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"Environment": "Development",
"CatalogueExportPath": "C:\\MyApps\\MusicCatalogue\\Export",
"ReportsExportPath": "C:\\MyApps\\MusicCatalogue\\Export\\Reports",
"Secrets": [
{
"Name": "Maps API Key",
"Value": "C:\\MyApps\\MusicCatalogue\\mapsapi.key"
}
],
"ApiEndpoints": [
{
"EndpointType": "Albums",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class MusicApplicationSettings
public MusicCatalogueEnvironment Environment { get; set; }
public string CatalogueExportPath { get; set; } = "";
public string ReportsExportPath { get; set; } = "";
public List<Secret> Secrets { get; set; } = new List<Secret>();
public List<ApiEndpoint> ApiEndpoints { get; set; } = new List<ApiEndpoint>();
public List<ApiServiceKey> ApiServiceKeys { get; set; } = new List<ApiServiceKey>();

Expand Down
11 changes: 11 additions & 0 deletions src/MusicCatalogue.Entities/Config/Secret.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Diagnostics.CodeAnalysis;

namespace MusicCatalogue.Entities.Config
{
[ExcludeFromCodeCoverage]
public class Secret
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
}
}
30 changes: 2 additions & 28 deletions src/MusicCatalogue.Logic/Config/ApiKeyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,19 @@

namespace MusicCatalogue.Logic.Config
{
public static class ApiKeyResolver
public class ApiKeyResolver : ResolverBase
{
/// <summary>
/// Resolve an API key given the value from the configuration file
/// </summary>
/// <param name="configValue"></param>
/// <returns></returns>
public static string ResolveApiKey(string configValue)
{
string apiKey;

// If the value from the configuration file is a valid file path, the keys are
// stored separately. This separation allows the API keys not to be published
// as part of the API Docker container image but read from a volume mount
if (File.Exists(configValue))
{
apiKey = File.ReadAllText(configValue);
}
else
{
// Not a path to a file, so just return the configuration value as the key
apiKey = configValue;
}

return apiKey;
}

/// <summary>
/// Resolve all the API key definitions in the supplied application settings
/// </summary>
/// <param name="settings"></param>
public static void ResolveAllApiKeys(MusicApplicationSettings settings)
{

// Iterate over the service API key definitions
foreach (var service in settings.ApiServiceKeys)
{
// Resolve the key for the current service
service.Key = ResolveApiKey(service.Key);
service.Key = ResolveValue(service.Key);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/MusicCatalogue.Logic/Config/MusicCatalogueConfigReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class MusicCatalogueConfigReader : ConfigReader<MusicApplicationSettings>
{
// Resolve all the API keys for services where the key is held in a separate file
ApiKeyResolver.ResolveAllApiKeys(settings);

// Repeat for the secrets
SecretResolver.ResolveAllSecrets(settings!);
}

return settings;
Expand Down
31 changes: 31 additions & 0 deletions src/MusicCatalogue.Logic/Config/ResolverBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace MusicCatalogue.Logic.Config
{
public abstract class ResolverBase
{
/// <summary>
/// Resolve a value given the value from the configuration file
/// </summary>
/// <param name="configValue"></param>
/// <returns></returns>
public static string ResolveValue(string configValue)
{
string resolvedValue;

// If the value from the configuration file is a valid file path, the actual value
// is stored separately in the file indicated. This separation allows secrets not to
// be published as part of the API or UI Docker container images but read from volume
// mounts
if (File.Exists(configValue))
{
resolvedValue = File.ReadAllText(configValue);
}
else
{
// Not a path to a file, so just return the configuration value
resolvedValue = configValue;
}

return resolvedValue;
}
}
}
21 changes: 21 additions & 0 deletions src/MusicCatalogue.Logic/Config/SecretResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using MusicCatalogue.Entities.Config;

namespace MusicCatalogue.Logic.Config
{
public class SecretResolver : ResolverBase
{
/// <summary>
/// Resolve all the API key definitions in the supplied application settings
/// </summary>
/// <param name="settings"></param>
public static void ResolveAllSecrets(MusicApplicationSettings settings)
{
// Iterate over the secret definitions
foreach (var secret in settings.Secrets)
{
// Resolve the value for the current secret
secret.Value = ResolveValue(secret.Value);
}
}
}
}
1 change: 1 addition & 0 deletions src/MusicCatalogue.LookupTool/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"ApplicationSettings": {
"LogFile": "C:\\MyApps\\MusicCatalogue\\MusicCatalogue.LookupTool.log",
"MinimumLogLevel": "Info",
"Secrets": [],
"ApiEndpoints": [
{
"EndpointType": "Albums",
Expand Down
8 changes: 8 additions & 0 deletions src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,27 @@
<ItemGroup>
<None Remove="apikey.txt" />
<None Remove="appsettings.json" />
<None Remove="secret.txt" />
<None Remove="separateapikeyappsettings.json" />
<None Remove="separatesecretappsettings.json" />
</ItemGroup>

<ItemGroup>
<Content Include="apikey.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="secret.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="separateapikeyappsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="separatesecretappsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 16 additions & 0 deletions src/MusicCatalogue.Tests/MusicCatalogueConfigReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public void ReadAppSettingsTest()
Assert.AreEqual(1, settings?.ApiServiceKeys.Count);
Assert.AreEqual(ApiServiceType.TheAudioDB, settings?.ApiServiceKeys.First().Service);
Assert.AreEqual("my-key", settings?.ApiServiceKeys.First().Key);

Assert.IsNotNull(settings?.Secrets);
Assert.AreEqual(1, settings?.Secrets.Count);
Assert.AreEqual("Maps API Key", settings?.Secrets.First().Name);
Assert.AreEqual("my-maps-key", settings?.Secrets.First().Value);
}

[TestMethod]
Expand All @@ -38,5 +43,16 @@ public void SeparateApiKeyFileTest()
Assert.AreEqual(ApiServiceType.TheAudioDB, settings?.ApiServiceKeys.First().Service);
Assert.AreEqual("my-separate-key", settings?.ApiServiceKeys.First().Key);
}

[TestMethod]
public void SeparateSecretsFileTest()
{
var settings = new MusicCatalogueConfigReader().Read("separatesecretappsettings.json");

Assert.IsNotNull(settings?.Secrets);
Assert.AreEqual(1, settings?.Secrets.Count);
Assert.AreEqual("Maps API Key", settings?.Secrets.First().Name);
Assert.AreEqual("my-separate-maps-key", settings?.Secrets.First().Value);
}
}
}
6 changes: 6 additions & 0 deletions src/MusicCatalogue.Tests/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
"Url": "https://theaudiodb.p.rapidapi.com/searchalbum.php"
}
],
"Secrets": [
{
"Name": "Maps API Key",
"Value": "my-maps-key"
}
],
"ApiServiceKeys": [
{
"Service": "TheAudioDB",
Expand Down
1 change: 1 addition & 0 deletions src/MusicCatalogue.Tests/secret.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
my-separate-maps-key
10 changes: 10 additions & 0 deletions src/MusicCatalogue.Tests/separatesecretappsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"ApplicationSettings": {
"Secrets": [
{
"Name": "Maps API Key",
"Value": "secret.txt"
}
]
}
}

0 comments on commit 19151c7

Please sign in to comment.