From baf11a1275bbddc19765135902417d808740e8ed Mon Sep 17 00:00:00 2001 From: Chris <362037+ChrisAnn@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:42:20 +0100 Subject: [PATCH] Adds the ability to specify an appsettings file for user-jwts Addresses https://github.com/dotnet/aspnetcore/issues/56169 --- .../src/Commands/ClearCommand.cs | 36 ++++-- .../src/Commands/CreateCommand.cs | 43 ++++++-- .../src/Commands/RemoveCommand.cs | 38 +++++-- src/Tools/dotnet-user-jwts/src/Resources.resx | 18 +++ .../test/UserJwtsTestFixture.cs | 4 + .../dotnet-user-jwts/test/UserJwtsTests.cs | 104 ++++++++++++++++++ 6 files changed, 222 insertions(+), 21 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index 2b977e3f6313..97d91cf74a4f 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -19,21 +19,43 @@ public static void Register(ProjectCommandLineApplication app) Resources.ClearCommand_ForceOption_Description, CommandOptionType.NoValue); + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), forceOption.HasValue()); + if (!DevJwtCliHelpers.GetProjectAndSecretsId(cmd.ProjectOption.Value(), cmd.Reporter, out var project, out var userSecretsId)) + { + return 1; + } + + var appsettingsFile = "appsettings.Development.json"; + if (appsettingsFileOption.HasValue()) + { + appsettingsFile = appsettingsFileOption.Value(); + if (!appsettingsFile.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + cmd.Reporter.Error(Resources.RemoveCommand_InvalidAppsettingsFile_Error); + return 1; + } + else if (!File.Exists(Path.Combine(Path.GetDirectoryName(project), appsettingsFile))) + { + cmd.Reporter.Error(Resources.FormatRemoveCommand_AppsettingsFileNotFound_Error(Path.GetDirectoryName(project))); + return 1; + } + } + + return Execute(cmd.Reporter, project, userSecretsId, forceOption.HasValue(), appsettingsFile); }); }); } - private static int Execute(IReporter reporter, string projectPath, bool force) + private static int Execute(IReporter reporter, string project, string userSecretsId, bool force, string appsettingsFile) { - if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) - { - return 1; - } var jwtStore = new JwtStore(userSecretsId); var count = jwtStore.Jwts.Count; @@ -54,7 +76,7 @@ private static int Execute(IReporter reporter, string projectPath, bool force) } } - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); foreach (var jwt in jwtStore.Jwts) { JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Value.Scheme); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index ef3a3b833655..1baac76fa888 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -77,24 +77,29 @@ public static void Register(ProjectCommandLineApplication app, Program program) Resources.CreateCommand_ValidForOption_Description, CommandOptionType.SingleValue); + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - var (options, isValid, optionsString) = ValidateArguments( - cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + var (options, isValid, optionsString, appsettingsFile) = ValidateArguments( + cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption, appsettingsFileOption); if (!isValid) { return 1; } - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, cmd.OutputOption.Value(), program); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, cmd.OutputOption.Value(), appsettingsFile, program); }); }); } - private static (JwtCreatorOptions, bool, string) ValidateArguments( + private static (JwtCreatorOptions, bool, string, string) ValidateArguments( IReporter reporter, CommandOption projectOption, CommandOption schemeNameOption, @@ -106,7 +111,8 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( CommandOption validForOption, CommandOption rolesOption, CommandOption scopesOption, - CommandOption claimsOption) + CommandOption claimsOption, + CommandOption appsettingsFileOption) { var isValid = true; var finder = new MsBuildProjectFinder(Directory.GetCurrentDirectory()); @@ -121,6 +127,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( return ( null, isValid, + string.Empty, string.Empty ); } @@ -209,10 +216,31 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(", ", claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}"; } + var appsettingsFile = "appsettings.Development.json"; + if (appsettingsFileOption.HasValue()) + { + appsettingsFile = appsettingsFileOption.Value(); + if (!appsettingsFile.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + reporter.Error(Resources.CreateCommand_InvalidAppsettingsFile_Error); + isValid = false; + } + else if (!File.Exists(Path.Combine(Path.GetDirectoryName(project), appsettingsFile))) + { + reporter.Error(Resources.FormatCreateCommand_AppsettingsFileNotFound_Error(Path.GetDirectoryName(project))); + isValid = false; + } + else + { + optionsString += appsettingsFileOption.HasValue() ? $"{Resources.JwtPrint_appsettingsFile}: {appsettingsFile}{Environment.NewLine}" : string.Empty; + } + } + return ( new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims), isValid, - optionsString); + optionsString, + appsettingsFile); static bool ParseDate(string datetime, out DateTime parsedDateTime) => DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime); @@ -224,6 +252,7 @@ private static int Execute( JwtCreatorOptions options, string optionsString, string outputFormat, + string appsettingsFile, Program program) { if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) @@ -244,7 +273,7 @@ private static int Execute( jwtStore.Jwts.Add(jwtToken.Id, jwt); jwtStore.Save(); - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Audiences, options.Issuer); settingsToWrite.Save(appsettingsFilePath); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs index 0c3a69eaf40e..27c2fdad1a42 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs @@ -15,6 +15,12 @@ public static void Register(ProjectCommandLineApplication app) cmd.Description = Resources.RemoveCommand_Description; var idArgument = cmd.Argument("[id]", Resources.RemoveCommand_IdArgument_Description); + + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => @@ -24,17 +30,35 @@ public static void Register(ProjectCommandLineApplication app) cmd.ShowHelp(); return 0; } - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value); + + if (!DevJwtCliHelpers.GetProjectAndSecretsId(cmd.ProjectOption.Value(), cmd.Reporter, out var project, out var userSecretsId)) + { + return 1; + } + + var appsettingsFile = "appsettings.Development.json"; + if (appsettingsFileOption.HasValue()) + { + appsettingsFile = appsettingsFileOption.Value(); + if (!appsettingsFile.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + cmd.Reporter.Error(Resources.RemoveCommand_InvalidAppsettingsFile_Error); + return 1; + } + else if (!File.Exists(Path.Combine(Path.GetDirectoryName(project), appsettingsFile))) + { + cmd.Reporter.Error(Resources.FormatRemoveCommand_AppsettingsFileNotFound_Error(Path.GetDirectoryName(project))); + return 1; + } + } + + return Execute(cmd.Reporter, project, userSecretsId, idArgument.Value, appsettingsFile); }); }); } - private static int Execute(IReporter reporter, string projectPath, string id) + private static int Execute(IReporter reporter, string project, string userSecretsId, string id, string appsettingsFile) { - if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) - { - return 1; - } var jwtStore = new JwtStore(userSecretsId); if (!jwtStore.Jwts.TryGetValue(id, out var jwt)) @@ -43,7 +67,7 @@ private static int Execute(IReporter reporter, string projectPath, string id) return 1; } - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Scheme); jwtStore.Jwts.Remove(id); jwtStore.Save(); diff --git a/src/Tools/dotnet-user-jwts/src/Resources.resx b/src/Tools/dotnet-user-jwts/src/Resources.resx index 094a066418d9..64b475f88309 100644 --- a/src/Tools/dotnet-user-jwts/src/Resources.resx +++ b/src/Tools/dotnet-user-jwts/src/Resources.resx @@ -150,6 +150,12 @@ The UTC date & time the JWT should expire in the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'. Defaults to 3 months after the --not-before date. Do not use this option in conjunction with the --valid-for option. + + Invalid Appsettings file extension. Ensure file extension is .json. + + + Could not find Appsettings file in '{0}'. Check the filename and that the file exists. + Malformed claims supplied. Ensure each claim is in the format "name=value". @@ -189,6 +195,9 @@ The period the JWT should expire after. Specify using a number followed by a duration type like 'd' for days, 'h' for hours, 'm' for minutes, and 's' for seconds, e.g. '365d'. Do not use this option in conjunction with the --expires-on option. + + The appSettings configuration file to add the test scheme to. + Audience(s) @@ -225,6 +234,9 @@ Scopes + + Appsettings File + Token @@ -312,6 +324,12 @@ No JWT with ID '{0}' found. + + Invalid Appsettings file extension. Ensure file extension is .json. + + + Could not find Appsettings file in '{0}'. Check the filename and that the file exists. + The issuer associated with the signing key to be reset or displayed. Defaults to 'dotnet-user-jwts'. diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs index 946530df7c6a..b812a017255c 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs @@ -81,6 +81,10 @@ public string CreateProject(bool hasSecret = true) Path.Combine(projectPath.FullName, "appsettings.Development.json"), "{}"); + File.WriteAllText( + Path.Combine(projectPath.FullName, "appsettings.Local.json"), + "{}"); + if (hasSecret) { _disposables.Push(() => diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index 71cc2cdb7d10..b37355d79408 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -79,6 +79,23 @@ public void Create_CanModifyExistingScheme() Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); } + [Fact] + public void Create_CanModifyExistingSchemeInGivenAppSettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + Assert.Contains("New JWT saved", _console.GetOutput()); + + var appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("dotnet-user-jwts", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + app.Run(["create", "--project", project, "--issuer", "new-issuer", "--appsettings-file", "appsettings.Local.json"]); + appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + } + [Fact] public void Print_ReturnsNothingForMissingToken() { @@ -154,6 +171,24 @@ public void Remove_RemovesGeneratedToken() Assert.Contains("Scheme2", appsettingsContent); } + [Fact] + public void Remove_RemovesGeneratedTokenInGivenAppsettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json", "--scheme", "Scheme2" }); + + app.Run(new[] { "remove", id, "--project", project, "--appsettings-file", "appsettings.Local.json" }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); + Assert.Contains("Scheme2", appsettingsContent); + } + [Fact] public void Clear_RemovesGeneratedTokens() { @@ -172,6 +207,24 @@ public void Clear_RemovesGeneratedTokens() Assert.DoesNotContain("Scheme2", appsettingsContent); } + [Fact] + public void Clear_RemovesGeneratedTokensInGivenAppsettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json", "--scheme", "Scheme2" }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + app.Run(new[] { "clear", "--project", project, "--appsettings-file", "appsettings.Local.json", "--force" }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); + Assert.DoesNotContain("Scheme2", appsettingsContent); + } + [Fact] public void Key_CanResetSigningKey() { @@ -625,6 +678,18 @@ public void Create_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.DoesNotContain(Resources.CreateCommand_NoAudience_Error, _console.GetOutput()); } + [Fact] + public void Create_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + + var app = new Program(_console); + app.Run(["create", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file in '{Directory.GetCurrentDirectory()}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -637,6 +702,18 @@ public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } + [Fact] + public void Delete_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + + var app = new Program(_console); + app.Run(["remove", "some-id", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file in '{Directory.GetCurrentDirectory()}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -649,6 +726,18 @@ public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } + [Fact] + public void Clear_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + + var app = new Program(_console); + app.Run(["clear", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file in '{Directory.GetCurrentDirectory()}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void List_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -704,6 +793,21 @@ public void Create_CanHandleRelativePathAsOption() Assert.Contains("New JWT saved", _console.GetOutput()); } + [Fact] + public void Create_CanHandleRelativePathAsOptionForAppsettingsOption() + { + var projectPath = fixture.CreateProject(); + var tempPath = Path.GetTempPath(); + var targetPath = Path.GetRelativePath(tempPath, projectPath); + Directory.SetCurrentDirectory(tempPath); + + var app = new Program(_console); + app.Run(new[] { "create", "--project", targetPath, "--appsettings-file", "appsettings.Local.json" }); + + Assert.DoesNotContain($"Could not find Appsettings file in '{projectPath}'. Check the filename and that the file exists.", _console.GetOutput()); + Assert.Contains("New JWT saved", _console.GetOutput()); + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] public void Create_CreatesFileWithUserOnlyUnixFileMode()