diff --git a/src/MusicCatalogue.Api/Controllers/SecretsController.cs b/src/MusicCatalogue.Api/Controllers/SecretsController.cs new file mode 100644 index 0000000..c34a7f2 --- /dev/null +++ b/src/MusicCatalogue.Api/Controllers/SecretsController.cs @@ -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 settings) + { + _settings = settings.Value; + SecretResolver.ResolveAllSecrets(_settings); + } + + /// + /// Return a secret from the configuration file + /// + /// + /// + [HttpGet] + [Route("{name}")] + public ActionResult GetSecret(string name) + { + var secret = _settings.Secrets.FirstOrDefault(x => x.Name == name); + + if (secret == null) + { + return NotFound(); + } + + if (string.IsNullOrEmpty(secret.Value)) + { + return NoContent(); + } + + return secret.Value; + } + } +} diff --git a/src/MusicCatalogue.Api/Program.cs b/src/MusicCatalogue.Api/Program.cs index 03fda86..524b7c7 100644 --- a/src/MusicCatalogue.Api/Program.cs +++ b/src/MusicCatalogue.Api/Program.cs @@ -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; @@ -43,6 +43,7 @@ public static void Main(string[] args) builder.Services.Configure(section); var settings = section.Get(); ApiKeyResolver.ResolveAllApiKeys(settings!); + SecretResolver.ResolveAllSecrets(settings!); // Configure the DB context builder.Services.AddScoped(); diff --git a/src/MusicCatalogue.Api/appsettings.json b/src/MusicCatalogue.Api/appsettings.json index 9fe8954..8aded7b 100644 --- a/src/MusicCatalogue.Api/appsettings.json +++ b/src/MusicCatalogue.Api/appsettings.json @@ -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", diff --git a/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs b/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs index e2da087..697b3ea 100644 --- a/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs +++ b/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs @@ -13,6 +13,7 @@ public class MusicApplicationSettings public MusicCatalogueEnvironment Environment { get; set; } public string CatalogueExportPath { get; set; } = ""; public string ReportsExportPath { get; set; } = ""; + public List Secrets { get; set; } = new List(); public List ApiEndpoints { get; set; } = new List(); public List ApiServiceKeys { get; set; } = new List(); diff --git a/src/MusicCatalogue.Entities/Config/Secret.cs b/src/MusicCatalogue.Entities/Config/Secret.cs new file mode 100644 index 0000000..5a7383c --- /dev/null +++ b/src/MusicCatalogue.Entities/Config/Secret.cs @@ -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; } = ""; + } +} diff --git a/src/MusicCatalogue.Logic/Config/ApiKeyResolver.cs b/src/MusicCatalogue.Logic/Config/ApiKeyResolver.cs index a010e1d..7364c18 100644 --- a/src/MusicCatalogue.Logic/Config/ApiKeyResolver.cs +++ b/src/MusicCatalogue.Logic/Config/ApiKeyResolver.cs @@ -2,45 +2,19 @@ namespace MusicCatalogue.Logic.Config { - public static class ApiKeyResolver + public class ApiKeyResolver : ResolverBase { - /// - /// Resolve an API key given the value from the configuration file - /// - /// - /// - 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; - } - /// /// Resolve all the API key definitions in the supplied application settings /// /// 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); } } } diff --git a/src/MusicCatalogue.Logic/Config/MusicCatalogueConfigReader.cs b/src/MusicCatalogue.Logic/Config/MusicCatalogueConfigReader.cs index 5ba94cc..b010c14 100644 --- a/src/MusicCatalogue.Logic/Config/MusicCatalogueConfigReader.cs +++ b/src/MusicCatalogue.Logic/Config/MusicCatalogueConfigReader.cs @@ -18,6 +18,9 @@ public class MusicCatalogueConfigReader : ConfigReader { // 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; diff --git a/src/MusicCatalogue.Logic/Config/ResolverBase.cs b/src/MusicCatalogue.Logic/Config/ResolverBase.cs new file mode 100644 index 0000000..9eee22c --- /dev/null +++ b/src/MusicCatalogue.Logic/Config/ResolverBase.cs @@ -0,0 +1,31 @@ +namespace MusicCatalogue.Logic.Config +{ + public abstract class ResolverBase + { + /// + /// Resolve a value given the value from the configuration file + /// + /// + /// + 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; + } + } +} diff --git a/src/MusicCatalogue.Logic/Config/SecretResolver.cs b/src/MusicCatalogue.Logic/Config/SecretResolver.cs new file mode 100644 index 0000000..0973e10 --- /dev/null +++ b/src/MusicCatalogue.Logic/Config/SecretResolver.cs @@ -0,0 +1,21 @@ +using MusicCatalogue.Entities.Config; + +namespace MusicCatalogue.Logic.Config +{ + public class SecretResolver : ResolverBase + { + /// + /// Resolve all the API key definitions in the supplied application settings + /// + /// + 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); + } + } + } +} diff --git a/src/MusicCatalogue.LookupTool/appsettings.json b/src/MusicCatalogue.LookupTool/appsettings.json index 5413e61..95a4681 100644 --- a/src/MusicCatalogue.LookupTool/appsettings.json +++ b/src/MusicCatalogue.LookupTool/appsettings.json @@ -2,6 +2,7 @@ "ApplicationSettings": { "LogFile": "C:\\MyApps\\MusicCatalogue\\MusicCatalogue.LookupTool.log", "MinimumLogLevel": "Info", + "Secrets": [], "ApiEndpoints": [ { "EndpointType": "Albums", diff --git a/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj b/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj index 2ebf996..f5508ba 100644 --- a/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj +++ b/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj @@ -16,19 +16,27 @@ + + Always + + Always + Always Always + + Always + diff --git a/src/MusicCatalogue.Tests/MusicCatalogueConfigReaderTest.cs b/src/MusicCatalogue.Tests/MusicCatalogueConfigReaderTest.cs index 626f3c1..06edfd4 100644 --- a/src/MusicCatalogue.Tests/MusicCatalogueConfigReaderTest.cs +++ b/src/MusicCatalogue.Tests/MusicCatalogueConfigReaderTest.cs @@ -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] @@ -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); + } } } diff --git a/src/MusicCatalogue.Tests/appsettings.json b/src/MusicCatalogue.Tests/appsettings.json index 2615b5c..7a69947 100644 --- a/src/MusicCatalogue.Tests/appsettings.json +++ b/src/MusicCatalogue.Tests/appsettings.json @@ -9,6 +9,12 @@ "Url": "https://theaudiodb.p.rapidapi.com/searchalbum.php" } ], + "Secrets": [ + { + "Name": "Maps API Key", + "Value": "my-maps-key" + } + ], "ApiServiceKeys": [ { "Service": "TheAudioDB", diff --git a/src/MusicCatalogue.Tests/secret.txt b/src/MusicCatalogue.Tests/secret.txt new file mode 100644 index 0000000..fcd00c4 --- /dev/null +++ b/src/MusicCatalogue.Tests/secret.txt @@ -0,0 +1 @@ +my-separate-maps-key \ No newline at end of file diff --git a/src/MusicCatalogue.Tests/separatesecretappsettings.json b/src/MusicCatalogue.Tests/separatesecretappsettings.json new file mode 100644 index 0000000..89fdc70 --- /dev/null +++ b/src/MusicCatalogue.Tests/separatesecretappsettings.json @@ -0,0 +1,10 @@ +{ + "ApplicationSettings": { + "Secrets": [ + { + "Name": "Maps API Key", + "Value": "secret.txt" + } + ] + } +}