feat: initial cli implementation
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1303.severity = none;
|
||||
dotnet_diagnostic.CA2007.severity = none;
|
||||
dotnet_diagnostic.CA1822.severity = none;
|
||||
dotnet_diagnostic.CA1812.severity = none;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class DeleteCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "delete";
|
||||
private const string CommandDescription = "Delete the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to remove."
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var isDeleted = await _secureConfig.DeleteAsync(key, ct);
|
||||
|
||||
if (isDeleted)
|
||||
{
|
||||
_terminal.WriteLine($"Successfully removed value for {key}");
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
_terminal.WriteLine($"Unable to remove value for {key}");
|
||||
return ExitCodes.KeyNotRemoved;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal static class ExitCodes
|
||||
{
|
||||
public const int Success = 0;
|
||||
public const int KeyNotFound = 1;
|
||||
public const int KeyNotRemoved = 2;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class GetCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "get";
|
||||
private const string CommandDescription = "Retrieves the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to retrieve."
|
||||
};
|
||||
|
||||
private readonly Option<bool> _prettyOption = new("--pretty", "-p")
|
||||
{
|
||||
Description = "Indicates whether the value - if JSON - should be pretty printed or not"
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
_prettyOption
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var shouldPrintPretty = result.GetValue(_prettyOption);
|
||||
var value = await _secureConfig.GetAsync<string>(key, ct);
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
_terminal.WriteLine($"Unable to retrieve value for {key}");
|
||||
return ExitCodes.KeyNotFound;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(value);
|
||||
|
||||
if (shouldPrintPretty)
|
||||
{
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
var context = new CliJsonContext(options);
|
||||
value = JsonSerializer.Serialize(json.RootElement, context.JsonElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = JsonSerializer.Serialize(json.RootElement, CliJsonContext.Default.JsonElement);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
_terminal.WriteLine(value);
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.CommandLine;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal interface ICommandFactory
|
||||
{
|
||||
Command Create();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal interface ITerminal
|
||||
{
|
||||
public void WriteLine(string value);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class SetCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "set";
|
||||
private const string CommandDescription = "Sets the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to set."
|
||||
};
|
||||
|
||||
private readonly Argument<string> _valueArg = new("value")
|
||||
{
|
||||
Description = "The value that you want to set for the key."
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
_valueArg,
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var value = result.GetRequiredValue(_valueArg);
|
||||
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(value);
|
||||
value = JsonSerializer.Serialize(json.RootElement, CliJsonContext.Default.JsonElement);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
await _secureConfig.SetAsync(key, value, ct);
|
||||
|
||||
_terminal.WriteLine($"Successfully set value of {key}");
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class Terminal : ITerminal
|
||||
{
|
||||
public void WriteLine(string value)
|
||||
{
|
||||
Console.WriteLine(value);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class Base64KeyOptions
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class CryptoProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = CryptoProviderTypes.Aes;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class CryptoProviderTypes
|
||||
{
|
||||
public const string Aes = "aes";
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class JsonStorageOptions
|
||||
{
|
||||
public string DirectoryPath { get; set; } = AppContext.BaseDirectory;
|
||||
public string FileName { get; set; } = "secure_config.json";
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class KeyProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = KeyProviderTypes.MachineId;
|
||||
public Base64KeyOptions Base64 { get; set; } = new();
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class KeyProviderTypes
|
||||
{
|
||||
public const string MachineId = "machineid";
|
||||
public const string Base64 = "base64";
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class SecureConfigBuilderExtensions
|
||||
{
|
||||
public static ISecureConfigBuilder ApplyProfile(
|
||||
this ISecureConfigBuilder builder,
|
||||
SecureConfigProfile profile
|
||||
)
|
||||
{
|
||||
ConfigureStorage(builder, profile.Storage);
|
||||
ConfigureKey(builder, profile.Key);
|
||||
ConfigureCrypto(builder, profile.Crypto);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureStorage(ISecureConfigBuilder builder, StorageProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case StorageProviderTypes.Json:
|
||||
builder.UseJsonFileStorage(opt =>
|
||||
{
|
||||
opt.DirectoryPath = options.Json.DirectoryPath;
|
||||
opt.FileName = options.Json.FileName;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Storage provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureKey(ISecureConfigBuilder builder, KeyProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case KeyProviderTypes.MachineId:
|
||||
builder.WithMachineIdKey();
|
||||
break;
|
||||
case KeyProviderTypes.Base64:
|
||||
builder.WithBase64EncryptionKey(options.Base64.Key);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Key provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureCrypto(ISecureConfigBuilder builder, CryptoProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case CryptoProviderTypes.Aes:
|
||||
builder.WithAesCryptoProvider();
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Crypto provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class SecureConfigCliOptions
|
||||
{
|
||||
public string DefaultProfile { get; set; } = "Default";
|
||||
public Dictionary<string, SecureConfigProfile> Profiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class SecureConfigProfile
|
||||
{
|
||||
public StorageProviderOptions Storage { get; set; } = new();
|
||||
public KeyProviderOptions Key { get; set; } = new();
|
||||
public CryptoProviderOptions Crypto { get; set; } = new();
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class StorageProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = StorageProviderTypes.Json;
|
||||
public JsonStorageOptions Json { get; set; } = new();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class StorageProviderTypes
|
||||
{
|
||||
public const string Json = "json";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class CliJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(static config =>
|
||||
{
|
||||
config.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", false);
|
||||
})
|
||||
.ConfigureLogging(static logging => logging.ClearProviders())
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.AddSingleton(args);
|
||||
|
||||
services.AddSecureConfig(b =>
|
||||
{
|
||||
var cliOptions = new SecureConfigCliOptions();
|
||||
ctx.Configuration.Bind(cliOptions);
|
||||
|
||||
var profileName = ctx.Configuration.GetValue<string>("profile") ?? cliOptions.DefaultProfile;
|
||||
|
||||
if (cliOptions.Profiles.TryGetValue(profileName, out var profile) is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Secure configuration profile '{profileName}' not found.");
|
||||
}
|
||||
|
||||
b.AddJsonAotContext(CliJsonContext.Default).ApplyProfile(profile);
|
||||
});
|
||||
|
||||
services.AddSingleton<ITerminal, Terminal>();
|
||||
services.AddSingleton<ICommandFactory, GetCommandFactory>();
|
||||
services.AddSingleton<ICommandFactory, SetCommandFactory>();
|
||||
services.AddSingleton<ICommandFactory, DeleteCommandFactory>();
|
||||
services.AddHostedService<SecureConfigCli>();
|
||||
})
|
||||
.RunConsoleAsync();
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli;
|
||||
|
||||
internal sealed class SecureConfigCli : IHostedService
|
||||
{
|
||||
private readonly string[] _args;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly RootCommand _rootCommand = new("secure-config is a command line utility used to manage your encrypted configuration")
|
||||
{
|
||||
new Option<string>("--profile")
|
||||
{
|
||||
Description = "Allows specifying which profile to use from the appsettings.json file.",
|
||||
},
|
||||
};
|
||||
|
||||
public SecureConfigCli(
|
||||
string[] args,
|
||||
IHostApplicationLifetime lifetime,
|
||||
IEnumerable<ICommandFactory> commandFactories
|
||||
)
|
||||
{
|
||||
_args = args;
|
||||
_lifetime = lifetime;
|
||||
|
||||
foreach (var factory in commandFactories)
|
||||
{
|
||||
_rootCommand.Add(factory.Create());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var exitCode = await _rootCommand.Parse(_args[1..]).InvokeAsync(cancellationToken: cancellationToken);
|
||||
Environment.ExitCode = exitCode;
|
||||
|
||||
_lifetime.StopApplication();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>secure-config</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(MSBuildProjectName).Tests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StevanFreeborn.Extensions.Configuration.Secure\StevanFreeborn.Extensions.Configuration.Secure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="appsettings.*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user