diff --git a/.editorconfig b/.editorconfig index 29359c2..ff3bc44 100644 --- a/.editorconfig +++ b/.editorconfig @@ -145,7 +145,7 @@ csharp_style_unused_value_expression_statement_preference = discard_variable:sil csharp_style_var_elsewhere = true:suggestion csharp_style_var_for_built_in_types = true:warning csharp_style_var_when_type_is_apparent = true:error -csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_prefer_primary_constructors = false:hint dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent diff --git a/Ellosoft.AwsCredentialsManager.sln.DotSettings b/Ellosoft.AwsCredentialsManager.sln.DotSettings index 8554af4..fecc67f 100644 --- a/Ellosoft.AwsCredentialsManager.sln.DotSettings +++ b/Ellosoft.AwsCredentialsManager.sln.DotSettings @@ -1,5 +1,7 @@  + DO_NOT_SHOW HINT + DO_NOT_SHOW True DO_NOT_SHOW True diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Config/ConfigBranch.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Config/ConfigBranch.cs new file mode 100644 index 0000000..3826dff --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Config/ConfigBranch.cs @@ -0,0 +1,9 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Commands.Config; + +[Name("config")] +[Description("Open config file")] +public class ConfigBranch +{ +} diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs new file mode 100644 index 0000000..c7b07dc --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Services; +using Ellosoft.AwsCredentialsManager.Services.IO; + +namespace Ellosoft.AwsCredentialsManager.Commands.Config; + +[Name("aws")] +[Description("Open AWS credentials file")] +[Examples("aws")] +public class OpenAwsConfig(IFileManager fileManager) : Command +{ + public override int Execute(CommandContext context) + { + var awsCredentialsPath = Path.Combine(AppDataDirectory.UserProfileDirectory, ".aws", "credentials"); + + if (!File.Exists(awsCredentialsPath)) + throw new CommandException($"The file {awsCredentialsPath} does not exist"); + + fileManager.OpenFile(awsCredentialsPath); + + return 0; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs new file mode 100644 index 0000000..9447a81 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Services.Configuration; +using Ellosoft.AwsCredentialsManager.Services.IO; + +namespace Ellosoft.AwsCredentialsManager.Commands.Config; + +[Name("user")] +[Description("Open credentials manager user config file (default)")] +[Examples("user")] +public class OpenConfig(IConfigManager configManager, IFileManager fileManager) : Command +{ + public override int Execute(CommandContext context) + { + if (!File.Exists(configManager.AppConfigPath)) + configManager.SaveConfig(); + + fileManager.OpenFile(configManager.AppConfigPath); + + return 0; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs index 004d3fd..ba5bcce 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs @@ -64,7 +64,12 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var oktaAppUrl = settings.OktaAppUrl ?? await GetAwsAppUrl(settings.OktaUserProfile); var awsRole = settings.AwsRoleArn ?? await GetAwsRoleArn(settings.OktaUserProfile, oktaAppUrl); - _credentialsManager.CreateCredential(settings.Name, settings.AwsProfile!, awsRole, oktaAppUrl, settings.OktaUserProfile); + _credentialsManager.CreateCredential( + name: settings.Name, + awsProfile: settings.AwsProfile!, + awsRole: awsRole, + oktaAppUrl: oktaAppUrl, + oktaProfile: settings.OktaUserProfile); AnsiConsole.MarkupLine($"[bold green]'{settings.Name}' credentials created[/]"); diff --git a/src/Ellosoft.AwsCredentialsManager/Program.cs b/src/Ellosoft.AwsCredentialsManager/Program.cs index 28c1a0e..b9b2b29 100644 --- a/src/Ellosoft.AwsCredentialsManager/Program.cs +++ b/src/Ellosoft.AwsCredentialsManager/Program.cs @@ -1,19 +1,15 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. using System.Diagnostics; +using Ellosoft.AwsCredentialsManager; using Ellosoft.AwsCredentialsManager.Commands; +using Ellosoft.AwsCredentialsManager.Commands.Config; using Ellosoft.AwsCredentialsManager.Commands.Credentials; using Ellosoft.AwsCredentialsManager.Commands.Okta; using Ellosoft.AwsCredentialsManager.Commands.RDS; using Ellosoft.AwsCredentialsManager.Infrastructure.Cli; using Ellosoft.AwsCredentialsManager.Infrastructure.Logging; using Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade; -using Ellosoft.AwsCredentialsManager.Services.AWS; -using Ellosoft.AwsCredentialsManager.Services.AWS.Interactive; -using Ellosoft.AwsCredentialsManager.Services.Configuration; -using Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; -using Ellosoft.AwsCredentialsManager.Services.Okta; -using Ellosoft.AwsCredentialsManager.Services.Okta.Interactive; using Microsoft.Extensions.DependencyInjection; using Serilog.Events; @@ -24,26 +20,7 @@ var services = new ServiceCollection() .SetupLogging(logger) - .AddSingleton() - .AddSingleton() - .AddSingleton(); - -// okta related services -services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - -// aws related services -services - .AddSingleton() - .AddSingleton(); - -services - .AddKeyedSingleton(nameof(OktaHttpClientFactory), OktaHttpClientFactory.CreateHttpClient()); + .RegisterAppServices(); var registrar = new TypeRegistrar(services); var app = new CommandApp(registrar); @@ -68,6 +45,12 @@ { rds.AddCommand(); rds.AddCommand(); + }) + .AddBranch(cfg => + { + cfg.SetDefaultCommand(); + cfg.AddCommand(); + cfg.AddCommand(); }); config.PropagateExceptions(); diff --git a/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs b/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs new file mode 100644 index 0000000..61716f7 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Services.AWS; +using Ellosoft.AwsCredentialsManager.Services.AWS.Interactive; +using Ellosoft.AwsCredentialsManager.Services.Configuration; +using Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; +using Ellosoft.AwsCredentialsManager.Services.IO; +using Ellosoft.AwsCredentialsManager.Services.Okta; +using Ellosoft.AwsCredentialsManager.Services.Okta.Interactive; +using Microsoft.Extensions.DependencyInjection; + +namespace Ellosoft.AwsCredentialsManager; + +public static class ServiceRegistration +{ + public static IServiceCollection RegisterAppServices(this IServiceCollection services) + { + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // okta related services + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // aws related services + services + .AddSingleton() + .AddSingleton(); + + services + .AddKeyedSingleton(nameof(OktaHttpClientFactory), OktaHttpClientFactory.CreateHttpClient()); + + return services; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs index 2579373..8d25b5b 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs @@ -27,13 +27,17 @@ internal sealed record ProfileMetadata(string RoleArn, string AccessKey, DateTim /// Thrown when the SAML authentication assertion fails to meet the requirements /// or AWS STS is unable to assume the specified role due to invalid parameters. /// + /// + /// The AssumeRoleWithSAMLAsync issues an HTTP POST request to https://sts.amazonaws.com, which does not require a region, + /// however the region is still required as part of the AmazonSecurityTokenServiceClient constructor validation, therefore USEast2 is being used. + /// public async Task GetAwsCredentials( string samlAssertion, string roleArn, string idp, int expirationInMinutes = 120) { - using var stsClient = new AmazonSecurityTokenServiceClient(new AnonymousAWSCredentials(), (RegionEndpoint?)null); + using var stsClient = new AmazonSecurityTokenServiceClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast2); var request = new AssumeRoleWithSAMLRequest { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs b/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs index 3103632..8ed30c2 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs @@ -24,7 +24,7 @@ public static class AppDataDirectory /// /// The name of the file to get the path for. /// The full path to the specified file in the application's data directory. - public static string GetPath(string fileName) => IOPath.Combine(GetOrCreateAppDataDirectory(), fileName); + public static string GetPath(string fileName) => IOPath.Combine(Path, fileName); private static string GetOrCreateAppDataDirectory() { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs index 35998ba..8ed75d9 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs @@ -8,8 +8,10 @@ public interface IConfigManager { AppConfig AppConfig { get; } + string AppConfigPath { get; } + /// - /// Persist the state into a file + /// Persist the state into a file, an empty file will be create if this method is called on a "empty" state /// void SaveConfig(); } @@ -17,12 +19,13 @@ public interface IConfigManager public class ConfigManager : IConfigManager { private const string APP_CONFIG_FILE = "aws_cred_mgr.yml"; - - private static readonly string AppConfigPath = Path.Combine(AppDataDirectory.UserProfileDirectory, APP_CONFIG_FILE); + private static readonly string InternalAppConfigPath = Path.Combine(AppDataDirectory.UserProfileDirectory, APP_CONFIG_FILE); private readonly ConfigReader _configReader = new(); private readonly ConfigWriter _configWriter = new(); + public string AppConfigPath => InternalAppConfigPath; + public ConfigManager() => AppConfig = GetConfiguration(); public AppConfig AppConfig { get; } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs index 1c65a8d..02af5cc 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs @@ -164,5 +164,5 @@ private static IDeserializer CreateDeserializer() => .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); - public static string GetYamlName(string value) => UnderscoredNamingConvention.Instance.Apply(value); + private static string GetYamlName(string value) => UnderscoredNamingConvention.Instance.Apply(value); } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs index 3f47b90..44b6e44 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs @@ -6,15 +6,11 @@ namespace Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; -public class CredentialsManager +public class CredentialsManager(IConfigManager configManager) { - private readonly IConfigManager _configManager; - - public CredentialsManager(IConfigManager configManager) => _configManager = configManager; - public string GetCredential() { - var appConfig = _configManager.AppConfig; + var appConfig = configManager.AppConfig; if (appConfig.Credentials.Count == 0) throw new CommandException("No AWS credentials found, please use [green]'aws-cred-mgr cred new'[/] to create a new profile"); @@ -34,7 +30,7 @@ public string GetCredential() public bool TryGetCredential(string credentialProfile, [NotNullWhen(true)] out CredentialsConfiguration? credentialsConfig) { - if (_configManager.AppConfig.Credentials.TryGetValue(credentialProfile, out credentialsConfig)) + if (configManager.AppConfig.Credentials.TryGetValue(credentialProfile, out credentialsConfig)) { if (credentialsConfig is { OktaProfile: not null, OktaAppUrl: not null }) return true; @@ -57,7 +53,7 @@ public void CreateCredential(string name, string awsProfile, string awsRole, str OktaProfile = oktaProfile }; - _configManager.AppConfig.Credentials[name] = credential; - _configManager.SaveConfig(); + configManager.AppConfig.Credentials[name] = credential; + configManager.SaveConfig(); } } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs new file mode 100644 index 0000000..17914f6 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ellosoft.AwsCredentialsManager.Services.IO; + +public interface IFileManager +{ + void OpenFile(string filePath); +} + +public class FileManager : IFileManager +{ + public void OpenFile(string filePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start("explorer", $"\"{filePath}\""); + + return; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", $"\"{filePath}\""); + + return; + } + + throw new InvalidOperationException("Unsupported operating system."); + } +}