diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml index 8c156863..798d0664 100644 --- a/.github/workflows/nightlybuild.yml +++ b/.github/workflows/nightlybuild.yml @@ -145,148 +145,148 @@ jobs: labels: nightlybuild body: Url is ${{env.action_url}} - buildchromeui: - name: "Nightly Build - Chrome UI" - env: - ASPNETCORE_ENVIRONMENT: "Production" + # buildchromeui: + # name: "Nightly Build - Chrome UI" + # env: + # ASPNETCORE_ENVIRONMENT: "Production" - runs-on: ubuntu-latest + # runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 + # steps: + # - uses: actions/checkout@v2.3.4 - - name: Set Up Variables - run: echo "action_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV + # - name: Set Up Variables + # run: echo "action_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV - - name: Trust Root Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" - password="password" - - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt - sudo update-ca-certificates - - - name: Trust Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" - password="password" - - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt - sudo update-ca-certificates + # - name: Trust Root Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" + # password="password" + + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt + # sudo update-ca-certificates + + # - name: Trust Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" + # password="password" + + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt + # sudo update-ca-certificates - - name: Install NET 9 - uses: actions/setup-dotnet@v4.0.1 - with: - dotnet-version: '9.0.x' + # - name: Install NET 9 + # uses: actions/setup-dotnet@v4.0.1 + # with: + # dotnet-version: '9.0.x' - - name: Restore Nuget Packages - run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} + # - name: Restore Nuget Packages + # run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} - - name: Build Code - run: dotnet build SecurityService.sln --configuration Release + # - name: Build Code + # run: dotnet build SecurityService.sln --configuration Release - - name: Run Unit Tests - run: | - echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}" - dotnet test "SecurityService.UnitTests\SecurityService.UnitTests.csproj" /p:ExcludeByFile="\SecurityService\Views\**\*.cshtml" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" + # - name: Run Unit Tests + # run: | + # echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}" + # dotnet test "SecurityService.UnitTests\SecurityService.UnitTests.csproj" /p:ExcludeByFile="\SecurityService\Views\**\*.cshtml" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" - - name: Build Docker Images - run: | - docker build . --file SecurityService/Dockerfile --tag securityservice:latest - docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest - - - name: Run Integration Tests (UI Chrome) - env: - Browser: Chrome - run: | - dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest - - - uses: actions/upload-artifact@v4.4.0 - if: ${{ failure() }} - with: - name: chromelogs - path: /home/txnproc/trace/ + # - name: Build Docker Images + # run: | + # docker build . --file SecurityService/Dockerfile --tag securityservice:latest + # docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest + + # - name: Run Integration Tests (UI Chrome) + # env: + # Browser: Chrome + # run: | + # dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest + + # - uses: actions/upload-artifact@v4.4.0 + # if: ${{ failure() }} + # with: + # name: chromelogs + # path: /home/txnproc/trace/ - - uses: dacbd/create-issue-action@main - if: ${{ failure() }} - name: Create an issue on build failure - with: - title: Investigate Nightly Build Failure - Chrome UI - token: ${{secrets.GITHUB_TOKEN}} - labels: nightlybuild - body: Url is ${{env.action_url}} - - buildedgeui: - name: "Nightly Build - Edge UI" - env: - ASPNETCORE_ENVIRONMENT: "Production" - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2.3.4 - - - name: Set Up Variables - run: echo "action_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV + # - uses: dacbd/create-issue-action@main + # if: ${{ failure() }} + # name: Create an issue on build failure + # with: + # title: Investigate Nightly Build Failure - Chrome UI + # token: ${{secrets.GITHUB_TOKEN}} + # labels: nightlybuild + # body: Url is ${{env.action_url}} + + # buildedgeui: + # name: "Nightly Build - Edge UI" + # env: + # ASPNETCORE_ENVIRONMENT: "Production" + + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v2.3.4 + + # - name: Set Up Variables + # run: echo "action_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV - - name: Trust Root Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" - password="password" - - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt - sudo update-ca-certificates - - - name: Trust Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" - password="password" - - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt - sudo update-ca-certificates + # - name: Trust Root Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" + # password="password" + + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt + # sudo update-ca-certificates + + # - name: Trust Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" + # password="password" + + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt + # sudo update-ca-certificates - - name: Install NET 9 - uses: actions/setup-dotnet@v4.0.1 - with: - dotnet-version: '9.0.x' + # - name: Install NET 9 + # uses: actions/setup-dotnet@v4.0.1 + # with: + # dotnet-version: '9.0.x' - - name: Restore Nuget Packages - run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} + # - name: Restore Nuget Packages + # run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} - - name: Build Code - run: dotnet build SecurityService.sln --configuration Release + # - name: Build Code + # run: dotnet build SecurityService.sln --configuration Release - - name: Build Docker Images - run: | - docker build . --file SecurityService/Dockerfile --tag securityservice:latest - docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest - - - name: Run Integration Tests (UI Edge) - env: - Browser: Edge - DriverPath: C:\\SeleniumWebDrivers\\EdgeDriver\\ - DriverExe: msedgedriver.exe - run: | - dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest - - - uses: actions/upload-artifact@v4.4.0 - if: ${{ failure() }} - with: - name: edgelogs - path: /home/txnproc/trace/ - - - uses: dacbd/create-issue-action@main - if: ${{ failure() }} - name: Create an issue on build failure - with: - title: Investigate Nightly Build Failure - Edge UI - token: ${{secrets.GITHUB_TOKEN}} - labels: nightlybuild - body: Url is ${{env.action_url}} + # - name: Build Docker Images + # run: | + # docker build . --file SecurityService/Dockerfile --tag securityservice:latest + # docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest + + # - name: Run Integration Tests (UI Edge) + # env: + # Browser: Edge + # DriverPath: C:\\SeleniumWebDrivers\\EdgeDriver\\ + # DriverExe: msedgedriver.exe + # run: | + # dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest + + # - uses: actions/upload-artifact@v4.4.0 + # if: ${{ failure() }} + # with: + # name: edgelogs + # path: /home/txnproc/trace/ + + # - uses: dacbd/create-issue-action@main + # if: ${{ failure() }} + # name: Create an issue on build failure + # with: + # title: Investigate Nightly Build Failure - Edge UI + # token: ${{secrets.GITHUB_TOKEN}} + # labels: nightlybuild + # body: Url is ${{env.action_url}} codecoverage: name: "Nightly Build - Code Coverage" diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index c07fb823..317d6521 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -122,117 +122,117 @@ jobs: path: /home/txnproc/trace/ - buildchromeui: - name: "Build and Unit Test Pull Requests - Chrome UI" - env: - ASPNETCORE_ENVIRONMENT: "Production" + # buildchromeui: + # name: "Build and Unit Test Pull Requests - Chrome UI" + # env: + # ASPNETCORE_ENVIRONMENT: "Production" - runs-on: ubuntu-latest + # runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 + # steps: + # - uses: actions/checkout@v2.3.4 - - name: Trust Root Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" - password="password" + # - name: Trust Root Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" + # password="password" - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt - sudo update-ca-certificates + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt + # sudo update-ca-certificates - - name: Trust Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" - password="password" + # - name: Trust Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" + # password="password" - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt - sudo update-ca-certificates + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt + # sudo update-ca-certificates - - name: Install NET 9 - uses: actions/setup-dotnet@v4.0.1 - with: - dotnet-version: '9.0.x' + # - name: Install NET 9 + # uses: actions/setup-dotnet@v4.0.1 + # with: + # dotnet-version: '9.0.x' - - name: Restore Nuget Packages - run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} + # - name: Restore Nuget Packages + # run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} - - name: Build Code - run: dotnet build SecurityService.sln --configuration Release + # - name: Build Code + # run: dotnet build SecurityService.sln --configuration Release - - name: Build Docker Images - run: | - docker build . --file SecurityService/Dockerfile --tag securityservice:latest - docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest + # - name: Build Docker Images + # run: | + # docker build . --file SecurityService/Dockerfile --tag securityservice:latest + # docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest - - name: Run Integration Tests (UI Chrome) - env: - Browser: Chrome - run: | - dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest + # - name: Run Integration Tests (UI Chrome) + # env: + # Browser: Chrome + # run: | + # dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest - - uses: actions/upload-artifact@v4.4.0 - if: ${{ failure() }} - with: - name: chromelogs - path: /home/txnproc/trace/ + # - uses: actions/upload-artifact@v4.4.0 + # if: ${{ failure() }} + # with: + # name: chromelogs + # path: /home/txnproc/trace/ - buildedgeui: - name: "Build and Unit Test Pull Requests - Edge UI" - env: - ASPNETCORE_ENVIRONMENT: "Production" + # buildedgeui: + # name: "Build and Unit Test Pull Requests - Edge UI" + # env: + # ASPNETCORE_ENVIRONMENT: "Production" - runs-on: ubuntu-latest + # runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 + # steps: + # - uses: actions/checkout@v2.3.4 - - name: Trust Root Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" - password="password" + # - name: Trust Root Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx" + # password="password" - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt - sudo update-ca-certificates + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-root-cert.crt + # sudo update-ca-certificates - - name: Trust Certificate - run: | - certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" - password="password" + # - name: Trust Certificate + # run: | + # certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-web-api.pfx" + # password="password" - openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" - sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt - sudo update-ca-certificates + # openssl pkcs12 -in "$certPath" -out temp.pem -nodes -password "pass:$password" + # sudo cp temp.pem /usr/local/share/ca-certificates/aspnetapp-web-api.crt + # sudo update-ca-certificates - - name: Install NET 9 - uses: actions/setup-dotnet@v4.0.1 - with: - dotnet-version: '9.0.x' + # - name: Install NET 9 + # uses: actions/setup-dotnet@v4.0.1 + # with: + # dotnet-version: '9.0.x' - - name: Restore Nuget Packages - run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} + # - name: Restore Nuget Packages + # run: dotnet restore SecurityService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} - - name: Build Code - run: dotnet build SecurityService.sln --configuration Release + # - name: Build Code + # run: dotnet build SecurityService.sln --configuration Release - - name: Build Docker Images - run: | - docker build . --file SecurityService/Dockerfile --tag securityservice:latest - docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest - - - name: Run Integration Tests (UI Edge) - env: - Browser: Edge - DriverPath: C:\\SeleniumWebDrivers\\EdgeDriver\\ - DriverExe: msedgedriver.exe - run: | - dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest - - - uses: actions/upload-artifact@v4.4.0 - if: ${{ failure() }} - with: - name: edgelogs - path: /home/txnproc/trace/ + # - name: Build Docker Images + # run: | + # docker build . --file SecurityService/Dockerfile --tag securityservice:latest + # docker build . --file SecurityServiceTestUI/Dockerfile --tag securityservicetestui:latest + + # - name: Run Integration Tests (UI Edge) + # env: + # Browser: Edge + # DriverPath: C:\\SeleniumWebDrivers\\EdgeDriver\\ + # DriverExe: msedgedriver.exe + # run: | + # dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest + + # - uses: actions/upload-artifact@v4.4.0 + # if: ${{ failure() }} + # with: + # name: edgelogs + # path: /home/txnproc/trace/ diff --git a/SecurityService.BusinessLogic/AssemblyInfo.cs b/SecurityService.BusinessLogic/AssemblyInfo.cs new file mode 100644 index 00000000..88f02294 --- /dev/null +++ b/SecurityService.BusinessLogic/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SecurityService.UnitTests")] \ No newline at end of file diff --git a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs index 3e162352..b2018d23 100644 --- a/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/UserRequestHandler.cs @@ -2,14 +2,6 @@ using Shared.Results; namespace SecurityService.BusinessLogic.RequestHandlers{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Security.Claims; - using System.Text; - using System.Text.Encodings.Web; - using System.Threading; - using System.Threading.Tasks; using DataTransferObjects.Responses; using Duende.IdentityServer; using Duende.IdentityServer.EntityFramework.DbContexts; @@ -23,6 +15,15 @@ namespace SecurityService.BusinessLogic.RequestHandlers{ using SecurityService.Models; using Shared.Logger; using SimpleResults; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Claims; + using System.Security.Cryptography; + using System.Text; + using System.Text.Encodings.Web; + using System.Threading; + using System.Threading.Tasks; using UserDetails = Models.UserDetails; public class UserRequestHandler : IRequestHandler, @@ -85,10 +86,13 @@ public async Task Handle(SecurityServiceCommands.CreateUserCommand comma RegistrationDateTime = DateTime.Now }; - String passwordValue = String.IsNullOrEmpty(command.Password) ? UserRequestHandler.GenerateRandomPassword(this.UserManager.Options.Password) : command.Password; - + Result passwordValueResult = String.IsNullOrEmpty(command.Password) ? PasswordGenerator.GenerateRandomPassword(this.UserManager.Options.Password) : command.Password; + + if (passwordValueResult.IsFailed) + return ResultHelpers.CreateFailure(passwordValueResult); + // Hash the default password - newIdentityUser.PasswordHash = this.PasswordHasher.HashPassword(newIdentityUser, passwordValue); + newIdentityUser.PasswordHash = this.PasswordHasher.HashPassword(newIdentityUser, passwordValueResult.Data); if (String.IsNullOrEmpty(newIdentityUser.PasswordHash)) { return Result.Failure("Error generating password hash value, hash was null or empty"); @@ -376,13 +380,15 @@ public async Task Handle(SecurityServiceCommands.ProcessPasswordResetReq public async Task Handle(SecurityServiceCommands.SendWelcomeEmailCommand command, CancellationToken cancellationToken){ ApplicationUser i = await this.UserManager.FindByNameAsync(command.Username); await this.UserManager.RemovePasswordAsync(i); - String generatedPassword = UserRequestHandler.GenerateRandomPassword(this.UserManager.Options.Password); - await this.UserManager.AddPasswordAsync(i, generatedPassword); + Result generatedPasswordResult = PasswordGenerator.GenerateRandomPassword(this.UserManager.Options.Password); + if (generatedPasswordResult.IsFailed) + return ResultHelpers.CreateFailure(generatedPasswordResult); + await this.UserManager.AddPasswordAsync(i, generatedPasswordResult.Data); // Send Email TokenResponse token = await this.GetToken(cancellationToken); - SendEmailRequest emailRequest = this.BuildWelcomeEmail(i.Email, generatedPassword); - var result = await this.MessagingServiceClient.SendEmail(token.AccessToken, emailRequest, cancellationToken); + SendEmailRequest emailRequest = this.BuildWelcomeEmail(i.Email, generatedPasswordResult.Data); + Result result = await this.MessagingServiceClient.SendEmail(token.AccessToken, emailRequest, cancellationToken); if (result.IsFailed) return ResultHelpers.CreateFailure(result); @@ -489,48 +495,7 @@ private async Task> ConvertUsersRoles(ApplicationUser identityUser) IList roles = await this.UserManager.GetRolesAsync(identityUser); return roles.ToList(); } - - private static String GenerateRandomPassword(PasswordOptions opts = null){ - if (opts == null) - opts = new PasswordOptions{ - RequiredLength = 8, - RequiredUniqueChars = 4, - RequireDigit = true, - RequireLowercase = true, - RequireNonAlphanumeric = true, - RequireUppercase = true - }; - - String[] randomChars ={ - "ABCDEFGHJKLMNOPQRSTUVWXYZ", // uppercase - "abcdefghijkmnopqrstuvwxyz", // lowercase - "0123456789", // digits - "!@$?_-" // non-alphanumeric - }; - - Random rand = new Random(Environment.TickCount); - List chars = new List(); - - if (opts.RequireUppercase) - chars.Insert(rand.Next(0, chars.Count), randomChars[0][rand.Next(0, randomChars[0].Length)]); - - if (opts.RequireLowercase) - chars.Insert(rand.Next(0, chars.Count), randomChars[1][rand.Next(0, randomChars[1].Length)]); - - if (opts.RequireDigit) - chars.Insert(rand.Next(0, chars.Count), randomChars[2][rand.Next(0, randomChars[2].Length)]); - - if (opts.RequireNonAlphanumeric) - chars.Insert(rand.Next(0, chars.Count), randomChars[3][rand.Next(0, randomChars[3].Length)]); - - for (Int32 i = chars.Count; i < opts.RequiredLength || chars.Distinct().Count() < opts.RequiredUniqueChars; i++){ - String rcs = randomChars[rand.Next(0, randomChars.Length)]; - chars.Insert(rand.Next(0, chars.Count), rcs[rand.Next(0, rcs.Length)]); - } - - return new String(chars.ToArray()); - } - + private async Task GetToken(CancellationToken cancellationToken){ // Get a token to talk to the estate service String clientId = this.ServiceOptions.ClientId; @@ -557,4 +522,82 @@ private async Task GetToken(CancellationToken cancellationToken){ #endregion } -} \ No newline at end of file + + +public static class PasswordGenerator + { + public static Result GenerateRandomPassword(PasswordOptions? opts = null) + { + opts ??= DefaultOptions(); + + var categories = BuildCategories(opts); + var result = ValidateUniqueCharRequirement(opts, categories); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + var chars = new List(); + + AddRequiredCategoryChars(chars, categories); + FillRemainingChars(chars, opts, categories); + SecureShuffle(chars); + + return Result.Success(new string(chars.ToArray())); + } + + private static PasswordOptions DefaultOptions() => new() + { + RequiredLength = 8, + RequiredUniqueChars = 4, + RequireDigit = true, + RequireLowercase = true, + RequireNonAlphanumeric = true, + RequireUppercase = true + }; + + private static List BuildCategories(PasswordOptions opts) + { + var list = new List(); + if (opts.RequireUppercase) list.Add("ABCDEFGHJKLMNOPQRSTUVWXYZ"); + if (opts.RequireLowercase) list.Add("abcdefghijkmnopqrstuvwxyz"); + if (opts.RequireDigit) list.Add("0123456789"); + if (opts.RequireNonAlphanumeric) list.Add("!@$?_-"); + if (!list.Any()) list.Add("abcdefghijkmnopqrstuvwxyz0123456789"); + return list; + } + + private static Result ValidateUniqueCharRequirement(PasswordOptions opts, List categories) + { + var all = string.Concat(categories).Distinct().Count(); + if (opts.RequiredUniqueChars > all) + return Result.Failure($"RequiredUniqueChars ({opts.RequiredUniqueChars}) exceeds available unique characters ({all})."); + + return Result.Success(); + } + + private static void AddRequiredCategoryChars(List chars, List categories) + { + foreach (var cat in categories) + chars.Add(cat[RandomNumberGenerator.GetInt32(cat.Length)]); + } + + private static void FillRemainingChars(List chars, PasswordOptions opts, List categories) + { + while (chars.Count < opts.RequiredLength || chars.Distinct().Count() < opts.RequiredUniqueChars) + { + var set = categories[RandomNumberGenerator.GetInt32(categories.Count)]; + chars.Add(set[RandomNumberGenerator.GetInt32(set.Length)]); + } + } + + private static void SecureShuffle(List chars) + { + for (int i = chars.Count - 1; i > 0; i--) + { + int j = RandomNumberGenerator.GetInt32(i + 1); + (chars[i], chars[j]) = (chars[j], chars[i]); + } + } + } + +} + diff --git a/SecurityService.UnitTests/UserRequestHandlerTests.cs b/SecurityService.UnitTests/UserRequestHandlerTests.cs new file mode 100644 index 00000000..a9907ded --- /dev/null +++ b/SecurityService.UnitTests/UserRequestHandlerTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using SecurityService.BusinessLogic.RequestHandlers; +using Shouldly; +using SimpleResults; +using Xunit; + +namespace SecurityService.UnitTests; + +public class PasswordGeneratorTests +{ + [Fact] + public void GenerateRandomPassword_WithDefaultOptions_ReturnsValidPassword() + { + // Act + Result result = PasswordGenerator.GenerateRandomPassword(); + + // Assert + result.IsSuccess.ShouldBeTrue(); + String password = result.Data; + password.ShouldNotBeNull(); + password.Length.ShouldBeGreaterThanOrEqualTo(8); + password.Count(char.IsUpper).ShouldBeGreaterThan(0); + password.Count(char.IsLower).ShouldBeGreaterThan(0); + password.Count(char.IsDigit).ShouldBeGreaterThan(0); + password.Count(c => "!@$?_-".Contains(c)).ShouldBeGreaterThan(0); + password.Distinct().Count().ShouldBeGreaterThanOrEqualTo(4); + } + + [Fact] + public void GenerateRandomPassword_WithCustomOptions_ReturnsValidPassword() + { + PasswordOptions options = new PasswordOptions + { + RequiredLength = 12, + RequiredUniqueChars = 6, + RequireDigit = true, + RequireLowercase = true, + RequireNonAlphanumeric = true, + RequireUppercase = true + }; + + Result result = PasswordGenerator.GenerateRandomPassword(options); + + result.IsSuccess.ShouldBeTrue(); + String password = result.Data; + password.ShouldNotBeNull(); + password.Length.ShouldBeGreaterThanOrEqualTo(12); + password.Distinct().Count().ShouldBeGreaterThanOrEqualTo(6); + password.Count(char.IsUpper).ShouldBeGreaterThan(0); + password.Count(char.IsLower).ShouldBeGreaterThan(0); + password.Count(char.IsDigit).ShouldBeGreaterThan(0); + password.Count(c => "!@$?_-".Contains(c)).ShouldBeGreaterThan(0); + } + + [Fact] + public void GenerateRandomPassword_TooManyUniqueChars_ReturnsFailure() + { + PasswordOptions options = new PasswordOptions + { + RequiredLength = 10, + RequiredUniqueChars = 100, // Exceeds available unique chars + RequireDigit = true, + RequireLowercase = true, + RequireNonAlphanumeric = true, + RequireUppercase = true + }; + + Result result = PasswordGenerator.GenerateRandomPassword(options); + + result.IsFailed.ShouldBeTrue(); + result.Data.ShouldBeNull(); + result.Message.ShouldNotBeEmpty(); + result.Message.ShouldContain("exceeds available unique characters"); + } + + [Fact] + public void GenerateRandomPassword_NoCategories_FallbackToLowerAndDigits() + { + PasswordOptions options = new PasswordOptions + { + RequiredLength = 8, + RequiredUniqueChars = 4, + RequireDigit = false, + RequireLowercase = false, + RequireNonAlphanumeric = false, + RequireUppercase = false + }; + + Result result = PasswordGenerator.GenerateRandomPassword(options); + + result.IsSuccess.ShouldBeTrue(); + String password = result.Data; + password.ShouldNotBeNull(); + password.Length.ShouldBeGreaterThanOrEqualTo(8); + password.All(c => "abcdefghijkmnopqrstuvwxyz0123456789".Contains(c)).ShouldBeTrue(); + } +} \ No newline at end of file