From 7dcf843b722f83288677a232db8150dc4bdc34cd Mon Sep 17 00:00:00 2001 From: Ismael Leon Date: Thu, 23 Jul 2026 19:58:15 -0600 Subject: [PATCH] Restructure project to src layout; add CI/Release Replace legacy MiddleWare solution with a new multi-project layout under src and tests. Adds Authorization.sln and new projects (Authorization.Abstractions, Common, DataAccess, Business, Middleware) plus unit tests and helper libraries. Remove many old MiddleWare project files and source code. Introduce modern GitHub Actions workflows: ci.yml for build/test and release.yml for packing/publishing; delete the previous Abstract.yml and Auth.yml workflows. Update .gitignore to ignore local AI assistant files and expand README with badges and overview. --- .github/workflows/Abstract.yml | 52 ------- .github/workflows/Auth.yml | 53 ------- .github/workflows/ci.yml | 42 +++++ .github/workflows/release.yml | 56 +++++++ .gitignore | 7 + Authorization.sln | 114 ++++++++++++++ .../Autorizacion.Abstracciones.csproj | 14 -- .../BW/IAutorizacionBW.cs | 15 -- .../DA/IRepositorioDapper.cs | 14 -- .../DA/ISeguridadDA.cs | 16 -- .../Entidades/Perfil.cs | 14 -- .../Entidades/Usuario.cs | 16 -- .../Modelos/Perfil.cs | 14 -- .../Modelos/Usuario.cs | 16 -- .../Autorizacion.Middleware.csproj | 18 --- .../Autorizacion.Middleware/ClaimsPerfil.cs | 88 ----------- MiddleWare/BW/Autorizacion.BW.csproj | 12 -- MiddleWare/BW/AutorizacionBW.cs | 30 ---- MiddleWare/DA/Autorizacion.DA.csproj | 19 --- MiddleWare/DA/Repos/RepositorioDapper.cs | 28 ---- MiddleWare/DA/SeguridadDA.cs | 33 ---- MiddleWare/Helpers/Convertidor.cs | 25 --- MiddleWare/Helpers/Helpers.csproj | 12 -- MiddleWare/Helpers/Mapeador.cs | 54 ------- MiddleWare/MiddleWare.sln | 49 ------ README.md | 143 ++++++++++++++---- .../Authorization.Abstractions.csproj | 8 + .../Business/IAuthorizationManager.cs | 26 ++++ .../DataAccess/IDbConnectionFactory.cs | 20 +++ .../DataAccess/ISecurityRepository.cs | 15 ++ .../Entities/Profile.cs | 13 ++ .../Entities/User.cs | 19 +++ .../Models/Profile.cs | 13 ++ src/Authorization.Abstractions/Models/User.cs | 21 +++ .../Options/ClaimsEnrichmentOptions.cs | 34 +++++ .../Authorization.Business.csproj | 12 ++ .../AuthorizationManager.cs | 29 ++++ .../Authorization.Common.csproj | 8 + src/Authorization.Common/Converter.cs | 46 ++++++ src/Authorization.Common/Mapper.cs | 87 +++++++++++ .../Authorization.DataAccess.csproj | 20 +++ .../SecurityRepository.cs | 63 ++++++++ .../SqlConnectionFactory.cs | 36 +++++ .../Authorization.Middleware.csproj | 23 +++ .../AuthorizationMiddlewareExtensions.cs | 19 +++ .../ClaimsEnrichmentMiddleware.cs | 132 ++++++++++++++++ .../ServiceCollectionExtensions.cs | 42 +++++ src/Directory.Build.props | 41 +++++ .../Authorization.UnitTests.csproj | 33 ++++ .../AuthorizationManagerTests.cs | 48 ++++++ .../ClaimsEnrichmentMiddlewareTests.cs | 122 +++++++++++++++ .../Authorization.UnitTests/ConverterTests.cs | 50 ++++++ tests/Authorization.UnitTests/GlobalUsings.cs | 1 + tests/Authorization.UnitTests/MapperTests.cs | 64 ++++++++ 54 files changed, 1379 insertions(+), 620 deletions(-) delete mode 100644 .github/workflows/Abstract.yml delete mode 100644 .github/workflows/Auth.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 Authorization.sln delete mode 100644 MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj delete mode 100644 MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs delete mode 100644 MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs delete mode 100644 MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj delete mode 100644 MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs delete mode 100644 MiddleWare/BW/Autorizacion.BW.csproj delete mode 100644 MiddleWare/BW/AutorizacionBW.cs delete mode 100644 MiddleWare/DA/Autorizacion.DA.csproj delete mode 100644 MiddleWare/DA/Repos/RepositorioDapper.cs delete mode 100644 MiddleWare/DA/SeguridadDA.cs delete mode 100644 MiddleWare/Helpers/Convertidor.cs delete mode 100644 MiddleWare/Helpers/Helpers.csproj delete mode 100644 MiddleWare/Helpers/Mapeador.cs delete mode 100644 MiddleWare/MiddleWare.sln create mode 100644 src/Authorization.Abstractions/Authorization.Abstractions.csproj create mode 100644 src/Authorization.Abstractions/Business/IAuthorizationManager.cs create mode 100644 src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs create mode 100644 src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs create mode 100644 src/Authorization.Abstractions/Entities/Profile.cs create mode 100644 src/Authorization.Abstractions/Entities/User.cs create mode 100644 src/Authorization.Abstractions/Models/Profile.cs create mode 100644 src/Authorization.Abstractions/Models/User.cs create mode 100644 src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs create mode 100644 src/Authorization.Business/Authorization.Business.csproj create mode 100644 src/Authorization.Business/AuthorizationManager.cs create mode 100644 src/Authorization.Common/Authorization.Common.csproj create mode 100644 src/Authorization.Common/Converter.cs create mode 100644 src/Authorization.Common/Mapper.cs create mode 100644 src/Authorization.DataAccess/Authorization.DataAccess.csproj create mode 100644 src/Authorization.DataAccess/SecurityRepository.cs create mode 100644 src/Authorization.DataAccess/SqlConnectionFactory.cs create mode 100644 src/Authorization.Middleware/Authorization.Middleware.csproj create mode 100644 src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs create mode 100644 src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs create mode 100644 src/Authorization.Middleware/ServiceCollectionExtensions.cs create mode 100644 src/Directory.Build.props create mode 100644 tests/Authorization.UnitTests/Authorization.UnitTests.csproj create mode 100644 tests/Authorization.UnitTests/AuthorizationManagerTests.cs create mode 100644 tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs create mode 100644 tests/Authorization.UnitTests/ConverterTests.cs create mode 100644 tests/Authorization.UnitTests/GlobalUsings.cs create mode 100644 tests/Authorization.UnitTests/MapperTests.cs diff --git a/.github/workflows/Abstract.yml b/.github/workflows/Abstract.yml deleted file mode 100644 index 77c8ded..0000000 --- a/.github/workflows/Abstract.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: .NET - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore dependencies - run: dotnet restore - working-directory: ./MiddleWare/Autorizacion.Abstracciones - - name: Build - run: dotnet build --no-restore --configuration Release - working-directory: ./MiddleWare/Autorizacion.Abstracciones - - name: Pack - run: dotnet pack --configuration Release - working-directory: ./MiddleWare/Autorizacion.Abstracciones - - uses: actions/upload-artifact@v4 - with: - name: Autorizacion.Abstracciones - if-no-files-found: error - retention-days: 7 - path: ./MiddleWare/Autorizacion.Abstracciones/bin/Release/*.nupkg - - release: - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Downlod Artifacts - uses: actions/download-artifact@v4 - - - name: List Files - run: ls -R - - - name: Prep packages - run: dotnet nuget add source --username Isma-L154 --password ${{ secrets.NUGET_TOKEN }} --store-password-in-clear-text --name Package "https://nuget.pkg.github.com/Isma-L154/index.json" - - name: Push package to GitHub packages - run: dotnet nuget push Autorizacion.Abstracciones/*.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source "Package" --skip-duplicate diff --git a/.github/workflows/Auth.yml b/.github/workflows/Auth.yml deleted file mode 100644 index 64caac9..0000000 --- a/.github/workflows/Auth.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: .NET - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore dependencies - run: dotnet restore - working-directory: ./MiddleWare - - name: Build - run: dotnet build --no-restore --configuration Release - working-directory: ./MiddleWare - - name: Pack - run: dotnet pack --configuration Release --no-build --output ./nupkgs - working-directory: ./MiddleWare - - - uses: actions/upload-artifact@v4 - with: - name: Autorizacion.Middleware - if-no-files-found: error - retention-days: 7 - path: /home/runner/work/MiddleWare/MiddleWare/MiddleWare/*/*/*/*.nupkg - - release: - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Downlod Artifacts - uses: actions/download-artifact@v4 - - - name: List Files - run: ls -R - - - name: Prep packages - run: dotnet nuget add source --username Isma-L154 --password ${{ secrets.NUGET_TOKEN }} --store-password-in-clear-text --name Package "https://nuget.pkg.github.com/Isma-L154/index.json" - - name: Push package to GitHub packages - run: dotnet nuget push Autorizacion.Middleware/*/*/*/*.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source "Package" --skip-duplicate diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9f320cc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches-ignore: [] # build every branch push + pull_request: + branches: [ "main" ] + workflow_dispatch: + +# Cancel superseded runs on the same ref to save minutes. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }} + restore-keys: nuget-${{ runner.os }}- + + - name: Restore + run: dotnet restore Authorization.sln + + - name: Build + run: dotnet build Authorization.sln --no-restore --configuration Release + + - name: Test + run: dotnet test Authorization.sln --no-build --configuration Release --verbosity normal diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e372b78 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + push: + branches: [ "main" ] + workflow_dispatch: + +# Use the built-in GITHUB_TOKEN to publish to GitHub Packages — +# no personal access token to manage or rotate. +permissions: + contents: read + packages: write + +concurrency: + group: release-main + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore + run: dotnet restore Authorization.sln + + - name: Build + run: dotnet build Authorization.sln --no-restore --configuration Release + + - name: Test + run: dotnet test Authorization.sln --no-build --configuration Release + + - name: Pack + run: dotnet pack Authorization.sln --no-build --configuration Release --output "${{ github.workspace }}/artifacts" + + - name: Upload packages artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + if-no-files-found: error + retention-days: 7 + path: ${{ github.workspace }}/artifacts/*.nupkg + + - name: Push to GitHub Packages + run: > + dotnet nuget push "${{ github.workspace }}/artifacts/*.nupkg" + --api-key ${{ secrets.GITHUB_TOKEN }} + --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" + --skip-duplicate diff --git a/.gitignore b/.gitignore index a4fe18b..01e3c21 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,10 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# Claude / AI assistant local files (never commit) +CLAUDE.md +.claude/ +.claude*/ +CLAUDE.local.md +**/CLAUDE.md diff --git a/Authorization.sln b/Authorization.sln new file mode 100644 index 0000000..4e844bf --- /dev/null +++ b/Authorization.sln @@ -0,0 +1,114 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Abstractions", "src\Authorization.Abstractions\Authorization.Abstractions.csproj", "{60BA81F8-B279-47E3-8786-6FAE86C81FD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Common", "src\Authorization.Common\Authorization.Common.csproj", "{E8AF7B80-0A65-45E5-8F73-357A0FFFF699}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.DataAccess", "src\Authorization.DataAccess\Authorization.DataAccess.csproj", "{03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Business", "src\Authorization.Business\Authorization.Business.csproj", "{8B921C4E-CCBD-49DE-91F8-0955695BC2D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Middleware", "src\Authorization.Middleware\Authorization.Middleware.csproj", "{C3F978CB-98CB-46B9-82FB-D795B1D76DA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.UnitTests", "tests\Authorization.UnitTests\Authorization.UnitTests.csproj", "{E3F53F4A-E25E-4221-877B-81917F7C6C5A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x64.Build.0 = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x86.Build.0 = Debug|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|Any CPU.Build.0 = Release|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x64.ActiveCfg = Release|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x64.Build.0 = Release|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x86.ActiveCfg = Release|Any CPU + {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x86.Build.0 = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x64.Build.0 = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x86.Build.0 = Debug|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|Any CPU.Build.0 = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x64.ActiveCfg = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x64.Build.0 = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x86.ActiveCfg = Release|Any CPU + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x86.Build.0 = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x64.Build.0 = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x86.Build.0 = Debug|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|Any CPU.Build.0 = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x64.ActiveCfg = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x64.Build.0 = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x86.ActiveCfg = Release|Any CPU + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x86.Build.0 = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x64.Build.0 = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x86.Build.0 = Debug|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|Any CPU.Build.0 = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x64.ActiveCfg = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x64.Build.0 = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x86.ActiveCfg = Release|Any CPU + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x86.Build.0 = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x64.Build.0 = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x86.Build.0 = Debug|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|Any CPU.Build.0 = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x64.ActiveCfg = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x64.Build.0 = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x86.ActiveCfg = Release|Any CPU + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x86.Build.0 = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x64.Build.0 = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x86.Build.0 = Debug|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|Any CPU.Build.0 = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x64.ActiveCfg = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x64.Build.0 = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x86.ActiveCfg = Release|Any CPU + {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {60BA81F8-B279-47E3-8786-6FAE86C81FD3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E8AF7B80-0A65-45E5-8F73-357A0FFFF699} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {8B921C4E-CCBD-49DE-91F8-0955695BC2D7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C3F978CB-98CB-46B9-82FB-D795B1D76DA4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E3F53F4A-E25E-4221-877B-81917F7C6C5A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj b/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj deleted file mode 100644 index 2b3967c..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net8.0 - enable - enable - 1.2.0 - True - - - - - - diff --git a/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs b/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs deleted file mode 100644 index 4e4e1de..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Autorizacion.Abstracciones.Modelos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.BW -{ - public interface IAutorizacionBW - { - Task ObtenerUsuario(Usuario usuario); - Task> ObtenerPerfilesxUsuario(Usuario usuario); - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs b/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs deleted file mode 100644 index f222673..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.DA -{ - public interface IRepositorioDapper - { - SqlConnection ObtenerRepositorioDapper(); - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs b/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs deleted file mode 100644 index cc539a0..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Autorizacion.Abstracciones.Modelos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.DA -{ - public interface ISeguridadDA - { - Task ObtenerUsuario(Usuario usuario); - Task> ObtenerPerfilesxUsuario(Usuario usuario); - - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs b/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs deleted file mode 100644 index 0c43090..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.Entidades -{ - public class Perfil - { - public int Id { get; set; } - public string Nombre { get; set; } - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs b/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs deleted file mode 100644 index e4e5a17..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.Entidades -{ - public class Usuario - { - public Guid Id { get; set; } - public string NombreUsuario { get; set; } - public string PasswordHash { get; set; } - public string CorreoElectronico { get; set; } - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs b/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs deleted file mode 100644 index cd69c0f..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.Modelos -{ - public class Perfil - { - public int Id { get; set; } - public string Nombre { get; set; } - } -} diff --git a/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs b/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs deleted file mode 100644 index bf074df..0000000 --- a/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.Abstracciones.Modelos -{ - public class Usuario - { - public Guid Id { get; set; } - public string NombreUsuario { get; set; } - public string PasswordHash { get; set; } - public string CorreoElectronico { get; set; } - } -} diff --git a/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj b/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj deleted file mode 100644 index b3304f0..0000000 --- a/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - enable - 1.1.0 - True - - - - - - - - - - diff --git a/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs b/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs deleted file mode 100644 index d0976be..0000000 --- a/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Autorizacion.Abstracciones.BW; -using Autorizacion.Abstracciones.Modelos; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using System.Security.Claims; - -namespace Autorizacion.Middleware -{ - public class ClaimsPerfil - { - private readonly RequestDelegate _next; - private readonly IConfiguration _configuration; - private IAutorizacionBW _autorizacionBW; - - public ClaimsPerfil(RequestDelegate next, IConfiguration configuration) - { - _next = next; - _configuration = configuration; - } - //Capturamos lo que hay e intercepta el flujo - public async Task InvokeAsync(HttpContext httpContext, IAutorizacionBW autorizacionBW) - { - _autorizacionBW = autorizacionBW; - ClaimsIdentity appIdentity = await ValidarAutorizacion(httpContext); - httpContext.User.AddIdentity(appIdentity); //Se inyecta un Claim al Usuario - await _next(httpContext); // Se lo mandamos a la aplicacion para que continue - } - - private async Task ValidarAutorizacion(HttpContext httpContext) - { - var claims = new List(); - if (httpContext.User != null && httpContext.User.Identity.IsAuthenticated) //Si el usuario esta autenticado, obtenemos la info del usuario - { - await ObtenerUsuario(httpContext, claims); - await ObtenerPerfiles(httpContext, claims); - } - var appIdentity = new ClaimsIdentity(claims); //Le mandamos la info previamente recolectada - return appIdentity; - } - - //Perfiles ---- - private async Task ObtenerPerfiles(HttpContext httpContext, List claims) - { - var perfiles = await obtenerInformacionPerfiles(httpContext); - if (perfiles != null && perfiles.Any()) - { - foreach (var perfil in perfiles) - { - //Realizamos un Claim del ID/Rol que tiene - claims.Add(new Claim(ClaimTypes.Role, perfil.Id.ToString())); - } - } - } - - private async Task> obtenerInformacionPerfiles(HttpContext httpContext) - { - //Obtenemos la info del perfil por medio del flujo del BW - return await _autorizacionBW.ObtenerPerfilesxUsuario(new Abstracciones.Modelos.Usuario { NombreUsuario = httpContext.User.Claims.Where(c => c.Type == "usuario").FirstOrDefault().Value }); - } - - //Usuarios ---- - private async Task ObtenerUsuario(HttpContext httpContext, List claims) - { - var usuario = await obtenerInformacionUsuario(httpContext); - if (usuario is not null && !string.IsNullOrEmpty(usuario.Id.ToString()) && !string.IsNullOrEmpty(usuario.NombreUsuario.ToString()) && !string.IsNullOrEmpty(usuario.CorreoElectronico.ToString())) - { - //Si todo es correcto, y ninguna info del user es null, añadimos un claim por cada atributo que sea necesario - claims.Add(new Claim(ClaimTypes.Email, usuario.CorreoElectronico)); - claims.Add(new Claim(ClaimTypes.Name, usuario.NombreUsuario)); - claims.Add(new Claim("IdUsuario", usuario.Id.ToString())); - } - } - - private async Task obtenerInformacionUsuario(HttpContext httpContext) - { - //Obtenemos la info del usuario por medio del flujo del BW - return await _autorizacionBW.ObtenerUsuario(new Abstracciones.Modelos.Usuario { NombreUsuario = httpContext.User.Claims.Where(c => c.Type == "usuario").FirstOrDefault().Value }); - } - } - public static class ClaimsUsuarioMiddlewareExtensions - { - public static IApplicationBuilder AutorizacionClaims(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} diff --git a/MiddleWare/BW/Autorizacion.BW.csproj b/MiddleWare/BW/Autorizacion.BW.csproj deleted file mode 100644 index 32d7320..0000000 --- a/MiddleWare/BW/Autorizacion.BW.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net8.0 - enable - enable - 1.1.0 - - - - - diff --git a/MiddleWare/BW/AutorizacionBW.cs b/MiddleWare/BW/AutorizacionBW.cs deleted file mode 100644 index a171d8b..0000000 --- a/MiddleWare/BW/AutorizacionBW.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Autorizacion.Abstracciones.BW; -using Autorizacion.Abstracciones.DA; -using Autorizacion.Abstracciones.Modelos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.BW -{ - public class AutorizacionBW : IAutorizacionBW - { - private ISeguridadDA _seguridadDA; - - public AutorizacionBW(ISeguridadDA seguridadDA) - { - _seguridadDA = seguridadDA; - } - public async Task> ObtenerPerfilesxUsuario(Usuario usuario) - { - return await _seguridadDA.ObtenerPerfilesxUsuario(usuario); - } - - public async Task ObtenerUsuario(Usuario usuario) - { - return await _seguridadDA.ObtenerUsuario(usuario); - } - } -} diff --git a/MiddleWare/DA/Autorizacion.DA.csproj b/MiddleWare/DA/Autorizacion.DA.csproj deleted file mode 100644 index e725811..0000000 --- a/MiddleWare/DA/Autorizacion.DA.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - enable - 1.3.0 - - - - - - - - - - - - diff --git a/MiddleWare/DA/Repos/RepositorioDapper.cs b/MiddleWare/DA/Repos/RepositorioDapper.cs deleted file mode 100644 index 50cf6f1..0000000 --- a/MiddleWare/DA/Repos/RepositorioDapper.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Autorizacion.Abstracciones.DA; -using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Autorizacion.DA.Repos -{ - public class RepositorioDapper : IRepositorioDapper - { - private readonly IConfiguration _configutarion; - private SqlConnection _connection; - - public RepositorioDapper(IConfiguration configutarion) - { - _configutarion = configutarion; - _connection = new SqlConnection(_configutarion.GetConnectionString("BDSeguridad")); - } - - public SqlConnection ObtenerRepositorioDapper() - { - return _connection; - } - } -} diff --git a/MiddleWare/DA/SeguridadDA.cs b/MiddleWare/DA/SeguridadDA.cs deleted file mode 100644 index 50bc1f8..0000000 --- a/MiddleWare/DA/SeguridadDA.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Autorizacion.Abstracciones.DA; -using Autorizacion.Abstracciones.Modelos; -using Dapper; -using Helpers; -using System.Data.SqlClient; - -namespace Autorizacion.DA -{ - public class SeguridadDA : ISeguridadDA - { - IRepositorioDapper _repositorioDapper; - private SqlConnection _sqlConnection; - - public SeguridadDA(IRepositorioDapper repositorioDapper) - { - _repositorioDapper = repositorioDapper; - _sqlConnection = _repositorioDapper.ObtenerRepositorioDapper(); - } - public async Task> ObtenerPerfilesxUsuario(Usuario usuario) - { - string sql = @"[ObtenerPerfilesxUsuario]"; - var consulta = await _sqlConnection.QueryAsync(sql, new { CorreoElectronico = usuario.CorreoElectronico, NombreUsuario = usuario.NombreUsuario }); - return Convertidor.ConvertirLista(consulta); - } - - public async Task ObtenerUsuario(Usuario usuario) - { - string sql = @"[ObtenerUsuario]"; - var consulta = await _sqlConnection.QueryAsync(sql, new { CorreoElectronico = usuario.CorreoElectronico, NombreUsuario = usuario.NombreUsuario }); - return Convertidor.Convertir(consulta.FirstOrDefault()); - } - } -} diff --git a/MiddleWare/Helpers/Convertidor.cs b/MiddleWare/Helpers/Convertidor.cs deleted file mode 100644 index d028925..0000000 --- a/MiddleWare/Helpers/Convertidor.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Helpers -{ - public static class Convertidor - { - public static TModeloEntrada Clonar(TModeloEntrada elemento) where TModeloEntrada : class, new() => Convertidor.Convertir(elemento); - - public static TModeloSalida Convertir( - TModeloEntrada elementoBase, - Action reglaTransformacion = null) - where TModeloEntrada : class - where TModeloSalida : new() - { - return Mapeador.MapearObjetos(elementoBase, reglaTransformacion); - } - - public static IEnumerable ConvertirLista( - IEnumerable elementos, - Action reglaTransformacion = null) - where TModeloEntrada : class - where TModeloSalida : new() - { - return (IEnumerable)elementos.Select((Func)(elementoBase => Mapeador.MapearObjetos(elementoBase, reglaTransformacion))).ToList(); - } - } -} diff --git a/MiddleWare/Helpers/Helpers.csproj b/MiddleWare/Helpers/Helpers.csproj deleted file mode 100644 index 6928042..0000000 --- a/MiddleWare/Helpers/Helpers.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net8.0 - enable - enable - 1.1.0 - - - - - diff --git a/MiddleWare/Helpers/Mapeador.cs b/MiddleWare/Helpers/Mapeador.cs deleted file mode 100644 index c9413fa..0000000 --- a/MiddleWare/Helpers/Mapeador.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace Helpers -{ - public static class Mapeador - { - public static TModeloSalida MapearObjetos( - TModeloEntrada objOrigen, - Action ReglaTransformacion = null) - where TModeloEntrada : class - where TModeloSalida : new() - { - Type type1 = typeof(TModeloEntrada); - Type type2 = typeof(TModeloSalida); - TModeloSalida destino = default(TModeloSalida); - if ((object)objOrigen != null) - { - destino = (TModeloSalida)Activator.CreateInstance(type2); - PropertyInfo[] properties1 = type1.GetProperties(); - PropertyInfo[] properties2 = type2.GetProperties(); - Mapeador.SincronizarObjetos(objOrigen, destino, properties1, properties2); - if (ReglaTransformacion != null) - ReglaTransformacion(objOrigen, destino); - } - return destino; - } - - private static void SincronizarObjetos( - TModeloEntrada origen, - TModeloSalida destino, - PropertyInfo[] propiedadesOrigen, - PropertyInfo[] propiedadesDestino) - { - foreach (PropertyInfo propertyInfo1 in propiedadesOrigen) - { - string nombrePropiedad = propertyInfo1.Name; - PropertyInfo propertyInfo2 = ((IEnumerable)propiedadesDestino).FirstOrDefault((Func)(x => x.Name == nombrePropiedad)); - if (propertyInfo2 != (PropertyInfo)null && propertyInfo2.CanWrite && propertyInfo2.GetIndexParameters().Length == 0 && propertyInfo1.PropertyType.Name == propertyInfo2.PropertyType.Name) - { - if ((propertyInfo2.PropertyType.IsClass ? 1 : (propertyInfo2.PropertyType.IsInterface ? 1 : 0)) == 0 | propertyInfo2.PropertyType.Name.Equals("String") | propertyInfo2.PropertyType.Name.EndsWith("[]")) - { - object obj = propertyInfo1.GetValue((object)origen, (object[])null); - propertyInfo2.SetValue((object)destino, obj, (object[])null); - } - } - } - } - } -} diff --git a/MiddleWare/MiddleWare.sln b/MiddleWare/MiddleWare.sln deleted file mode 100644 index 6fbc6be..0000000 --- a/MiddleWare/MiddleWare.sln +++ /dev/null @@ -1,49 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34525.116 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.Abstracciones", "Autorizacion.Abstracciones\Autorizacion.Abstracciones.csproj", "{F84E1C30-E36A-4809-989C-3BD1268EFD93}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Helpers", "Helpers\Helpers.csproj", "{270F74FB-FB5B-4472-9FC1-A554BBA780BD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.DA", "DA\Autorizacion.DA.csproj", "{FDA5784D-8BC8-448E-A04E-D4E1C7716240}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.BW", "BW\Autorizacion.BW.csproj", "{A624FB44-349D-4E11-AA7F-B3EB69161494}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.Middleware", "Autorizacion.Middleware\Autorizacion.Middleware.csproj", "{1F0C08C4-F365-452E-A180-947C28C18DEA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Release|Any CPU.Build.0 = Release|Any CPU - {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Release|Any CPU.Build.0 = Release|Any CPU - {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Release|Any CPU.Build.0 = Release|Any CPU - {A624FB44-349D-4E11-AA7F-B3EB69161494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A624FB44-349D-4E11-AA7F-B3EB69161494}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A624FB44-349D-4E11-AA7F-B3EB69161494}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A624FB44-349D-4E11-AA7F-B3EB69161494}.Release|Any CPU.Build.0 = Release|Any CPU - {1F0C08C4-F365-452E-A180-947C28C18DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F0C08C4-F365-452E-A180-947C28C18DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F0C08C4-F365-452E-A180-947C28C18DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F0C08C4-F365-452E-A180-947C28C18DEA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {6FA99B5F-1890-48FB-9F37-068EFA98EDDE} - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md index 0cc0e12..7b52be0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,139 @@ -# 🔐 JWT Authentication & Authorization Middleware +# 🔐 JWT Authorization Claims Middleware -This side project is a simple yet effective **middleware** component designed to handle **user authentication and authorization using JSON Web Tokens (JWT)**. It is designed to be easily integrated into other projects by deploying it as a **NuGet package**. +[![CI](https://github.com/Isma-L154/MiddleWare/actions/workflows/ci.yml/badge.svg)](https://github.com/Isma-L154/MiddleWare/actions/workflows/ci.yml) +[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4)](https://dotnet.microsoft.com/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Middleware plays a vital role in web applications by acting as a filter between the HTTP request and the core application logic. This middleware ensures that only authenticated and authorized users can access protected routes or resources. +An ASP.NET Core middleware that **enriches an already-authenticated request principal** with identity claims (the user's id, name and email) and one **role claim per profile**, resolved from a SQL Server security store. It's shipped as a small set of **NuGet packages** so it can be dropped into any .NET 8 web app. + +> This is a personal side project. It sits between the HTTP request and your application logic, turning a bare authenticated token into a fully-populated `ClaimsPrincipal` your authorization policies can rely on. + +--- + +## ✨ What it does + +Once a request has been authenticated (by JWT bearer auth, for example), the middleware: + +1. Reads the configured user-name claim from the incoming principal. +2. Looks up the matching user in the security database (via a stored procedure). +3. Adds `Email`, `Name` and `IdUsuario` claims. +4. Looks up the user's profiles and adds a `Role` claim for each one. + +If anything goes wrong resolving that data (missing claim, unknown user, database outage), the request **degrades gracefully**: it continues unenriched instead of crashing the pipeline. + +--- + +## 🧱 Architecture + +The solution is layered so each concern is isolated and independently testable: + +| Project | Responsibility | +| --- | --- | +| `Authorization.Abstractions` | Contracts: entities, models, options and interfaces. No external dependencies. | +| `Authorization.Common` | Cached, reflection-based object mapper used to map entities → models. | +| `Authorization.DataAccess` | Dapper + `Microsoft.Data.SqlClient` access to stored procedures. | +| `Authorization.Business` | Thin business layer orchestrating identity resolution. | +| `Authorization.Middleware` | The ASP.NET Core middleware plus DI and pipeline extensions. | + +``` +Request ─▶ Authentication ─▶ ClaimsEnrichmentMiddleware ─▶ your app + │ + IAuthorizationManager (Business) + │ + ISecurityRepository (DataAccess) + │ + IDbConnectionFactory ─▶ SQL Server +``` --- -## 🧩 What is Middleware? +## 📦 Installation -A **middleware** is a function or component that processes HTTP requests and/or responses within the application pipeline. It's typically used to: +The packages are published to **GitHub Packages**. Add the feed and install the entry-point package (it pulls the rest in transitively): -- Authenticate and authorize users -- Log incoming and outgoing traffic -- Handle errors or exceptions -- Modify requests or responses -- Manage headers, sessions, or CORS +```bash +dotnet nuget add source "https://nuget.pkg.github.com/Isma-L154/index.json" \ + --name github --username --password + +dotnet add package Authorization.Middleware +``` --- -## ✅ Purpose of This Middleware +## 🚀 Usage + +**1. Register the services** (wires the connection factory, repository and business manager): + +```csharp +using Authorization.Middleware; + +builder.Services.AddAuthorizationClaims(); +``` -The main purpose of this middleware is to provide **basic but secure access control** through JWT-based authentication. Specifically, it: +**2. Add the middleware to the pipeline**, after authentication: -- 🔐 **Authenticates**: Verifies the validity of a JWT sent in the request (usually in the Authorization header). -- 🔓 **Authorizes**: Checks whether the user has the required role or permissions to access a specific route or resource. +```csharp +app.UseAuthentication(); +app.UseAuthorizationClaims(); // enrich the principal +app.UseAuthorization(); +``` + +**3. Configure the connection string** in `appsettings.json`: + +```json +{ + "ConnectionStrings": { + "SecurityDb": "Server=...;Database=...;Trusted_Connection=True;Encrypt=True;" + } +} +``` --- -## 🔧 How It Works +## ⚙️ Configuration + +Everything that used to be hard-coded is now configurable through `ClaimsEnrichmentOptions`: + +```csharp +builder.Services.AddAuthorizationClaims(options => +{ + options.ConnectionStringName = "SecurityDb"; // ConnectionStrings key + options.UserNameClaimType = "usuario"; // inbound JWT claim to read + options.GetUserProcedure = "ObtenerUsuario"; // stored procedure names + options.GetProfilesProcedure = "ObtenerPerfilesxUsuario"; +}); +``` + +| Option | Default | Description | +| --- | --- | --- | +| `ConnectionStringName` | `SecurityDb` | Key under `ConnectionStrings` for the security DB. | +| `UserNameClaimType` | `usuario` | Inbound claim type carrying the user name. | +| `GetUserProcedure` | `ObtenerUsuario` | Stored procedure returning a user by name/email. | +| `GetProfilesProcedure` | `ObtenerPerfilesxUsuario` | Stored procedure returning a user's profiles. | + +--- + +## 🧪 Building & testing + +```bash +dotnet build Authorization.sln -c Release +dotnet test Authorization.sln -c Release +``` -1. The client sends a request with a JWT in the `Authorization` header (e.g., `Bearer `). -2. The middleware: - - Verifies the token's validity and signature. - - Decodes the token to extract user data (e.g., ID, roles). - - Checks if the user has permission to access the endpoint. -3. If the token is invalid or permissions are insufficient, the request is rejected with the appropriate HTTP status (`401 Unauthorized` or `403 Forbidden`). +CI runs on every push and pull request; packages are published from the `main` branch. --- -## 🛠️ Technologies Used +## 🛠️ Technologies -- **JSON Web Tokens (JWT)** for token generation and validation -- Written in **[C# , .NET 8.0]** -- Lightweight, reusable, and easily integrated into any route-based app -- Deployed as a **NuGet package** for easy integration into other projects +- **.NET 8.0**, C# latest +- **Dapper** for micro-ORM data access +- **Microsoft.Data.SqlClient** (the maintained SQL Server driver) +- **xUnit** + **Moq** for unit tests +- Distributed as **NuGet packages** via GitHub Packages --- -## 📦 Deployment via NuGet +## 📄 License -This middleware is packaged and deployed as a **NuGet package**, making it easy to integrate into any **C# .NET Core** project. To install the middleware package in your project: +Released under the [MIT License](LICENSE). diff --git a/src/Authorization.Abstractions/Authorization.Abstractions.csproj b/src/Authorization.Abstractions/Authorization.Abstractions.csproj new file mode 100644 index 0000000..acdd200 --- /dev/null +++ b/src/Authorization.Abstractions/Authorization.Abstractions.csproj @@ -0,0 +1,8 @@ + + + + Authorization.Abstractions + Contracts (entities, models, options and interfaces) for the JWT authorization middleware. Has no external dependencies. + + + diff --git a/src/Authorization.Abstractions/Business/IAuthorizationManager.cs b/src/Authorization.Abstractions/Business/IAuthorizationManager.cs new file mode 100644 index 0000000..1f6751a --- /dev/null +++ b/src/Authorization.Abstractions/Business/IAuthorizationManager.cs @@ -0,0 +1,26 @@ +using Authorization.Abstractions.Models; + +namespace Authorization.Abstractions.Business; + +/// +/// Business-layer entry point for resolving the identity data used to enrich +/// the request principal (the current user and the profiles they hold). +/// +public interface IAuthorizationManager +{ + /// + /// Resolves the full user record for the supplied lookup criteria. + /// + /// Partial user carrying at least a lookup key (user name or email). + /// Token used to cancel the underlying I/O. + /// The resolved user, or null when no match exists. + Task GetUserAsync(User user, CancellationToken cancellationToken = default); + + /// + /// Resolves every profile (role) granted to the supplied user. + /// + /// User whose profiles are requested. + /// Token used to cancel the underlying I/O. + /// The profiles for the user; an empty sequence when none exist. + Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default); +} diff --git a/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs b/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs new file mode 100644 index 0000000..4594dff --- /dev/null +++ b/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs @@ -0,0 +1,20 @@ +using System.Data.Common; + +namespace Authorization.Abstractions.DataAccess; + +/// +/// Creates database connections on demand. +/// +/// +/// Every call returns a brand-new, unopened connection. This is deliberate: +/// a single is not thread-safe, so sharing one +/// instance across concurrent requests corrupts state. Handing out fresh +/// connections lets the underlying ADO.NET connection pool do its job. +/// The abstract return type keeps this contract +/// free of any concrete database-provider dependency. +/// +public interface IDbConnectionFactory +{ + /// Creates a new, unopened database connection. + DbConnection CreateConnection(); +} diff --git a/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs b/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs new file mode 100644 index 0000000..1801b3f --- /dev/null +++ b/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs @@ -0,0 +1,15 @@ +using Authorization.Abstractions.Models; + +namespace Authorization.Abstractions.DataAccess; + +/// +/// Data-access contract for the security store that backs authorization. +/// +public interface ISecurityRepository +{ + /// Reads a single user matching the supplied lookup criteria. + Task GetUserAsync(User user, CancellationToken cancellationToken = default); + + /// Reads every profile (role) granted to the supplied user. + Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default); +} diff --git a/src/Authorization.Abstractions/Entities/Profile.cs b/src/Authorization.Abstractions/Entities/Profile.cs new file mode 100644 index 0000000..b966ead --- /dev/null +++ b/src/Authorization.Abstractions/Entities/Profile.cs @@ -0,0 +1,13 @@ +namespace Authorization.Abstractions.Entities; + +/// +/// Database-facing representation of a security profile (role) a user can hold. +/// +public sealed class Profile +{ + /// Unique identifier of the profile. + public int Id { get; set; } + + /// Display name of the profile. + public string? Name { get; set; } +} diff --git a/src/Authorization.Abstractions/Entities/User.cs b/src/Authorization.Abstractions/Entities/User.cs new file mode 100644 index 0000000..d6ab7f9 --- /dev/null +++ b/src/Authorization.Abstractions/Entities/User.cs @@ -0,0 +1,19 @@ +namespace Authorization.Abstractions.Entities; + +/// +/// Database-facing representation of a user, as returned by the security data store. +/// +public sealed class User +{ + /// Unique identifier of the user. + public Guid Id { get; set; } + + /// Login name of the user. + public string? UserName { get; set; } + + /// Hashed password. Never expose this outside the data layer. + public string? PasswordHash { get; set; } + + /// Email address of the user. + public string? Email { get; set; } +} diff --git a/src/Authorization.Abstractions/Models/Profile.cs b/src/Authorization.Abstractions/Models/Profile.cs new file mode 100644 index 0000000..3629a98 --- /dev/null +++ b/src/Authorization.Abstractions/Models/Profile.cs @@ -0,0 +1,13 @@ +namespace Authorization.Abstractions.Models; + +/// +/// Domain model of a security profile (role) held by a user. +/// +public sealed class Profile +{ + /// Unique identifier of the profile. + public int Id { get; set; } + + /// Display name of the profile. + public string? Name { get; set; } +} diff --git a/src/Authorization.Abstractions/Models/User.cs b/src/Authorization.Abstractions/Models/User.cs new file mode 100644 index 0000000..4cb82ea --- /dev/null +++ b/src/Authorization.Abstractions/Models/User.cs @@ -0,0 +1,21 @@ +namespace Authorization.Abstractions.Models; + +/// +/// Domain model of a user, used by the business and middleware layers. +/// Kept separate from the database entity so persistence changes never leak +/// into the pipeline contract. +/// +public sealed class User +{ + /// Unique identifier of the user. + public Guid Id { get; set; } + + /// Login name of the user. + public string? UserName { get; set; } + + /// Hashed password. Never surfaced in claims, logs or responses. + public string? PasswordHash { get; set; } + + /// Email address of the user. + public string? Email { get; set; } +} diff --git a/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs b/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs new file mode 100644 index 0000000..a8f2515 --- /dev/null +++ b/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs @@ -0,0 +1,34 @@ +namespace Authorization.Abstractions.Options; + +/// +/// Configuration for the claims-enrichment middleware and the security store. +/// Everything that used to be hard-coded (the inbound claim to read the user +/// name from, and the stored-procedure names) is configurable here so the +/// package can be reused without recompiling. +/// +public sealed class ClaimsEnrichmentOptions +{ + /// + /// Name of the connection string (in ConnectionStrings) pointing at + /// the security database. Defaults to SecurityDb. + /// + public string ConnectionStringName { get; set; } = "SecurityDb"; + + /// + /// The inbound JWT claim type that carries the user name used to look the + /// user up. Defaults to usuario to preserve the historical contract. + /// + public string UserNameClaimType { get; set; } = "usuario"; + + /// + /// Stored procedure that returns a single user by user name / email. + /// Defaults to ObtenerUsuario (the existing database object name). + /// + public string GetUserProcedure { get; set; } = "ObtenerUsuario"; + + /// + /// Stored procedure that returns the profiles for a user. + /// Defaults to ObtenerPerfilesxUsuario (the existing database object name). + /// + public string GetProfilesProcedure { get; set; } = "ObtenerPerfilesxUsuario"; +} diff --git a/src/Authorization.Business/Authorization.Business.csproj b/src/Authorization.Business/Authorization.Business.csproj new file mode 100644 index 0000000..90e68a7 --- /dev/null +++ b/src/Authorization.Business/Authorization.Business.csproj @@ -0,0 +1,12 @@ + + + + Authorization.Business + Business layer for the JWT authorization middleware; orchestrates identity resolution over the data-access layer. + + + + + + + diff --git a/src/Authorization.Business/AuthorizationManager.cs b/src/Authorization.Business/AuthorizationManager.cs new file mode 100644 index 0000000..9036357 --- /dev/null +++ b/src/Authorization.Business/AuthorizationManager.cs @@ -0,0 +1,29 @@ +using Authorization.Abstractions.Business; +using Authorization.Abstractions.DataAccess; +using Authorization.Abstractions.Models; + +namespace Authorization.Business; + +/// +/// Default that delegates identity +/// resolution to the security data-access layer. It is intentionally thin: +/// its role is to keep the middleware decoupled from persistence so business +/// rules can grow here without touching the pipeline. +/// +public sealed class AuthorizationManager : IAuthorizationManager +{ + private readonly ISecurityRepository _securityRepository; + + public AuthorizationManager(ISecurityRepository securityRepository) + { + _securityRepository = securityRepository ?? throw new ArgumentNullException(nameof(securityRepository)); + } + + /// + public Task GetUserAsync(User user, CancellationToken cancellationToken = default) + => _securityRepository.GetUserAsync(user, cancellationToken); + + /// + public Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default) + => _securityRepository.GetProfilesForUserAsync(user, cancellationToken); +} diff --git a/src/Authorization.Common/Authorization.Common.csproj b/src/Authorization.Common/Authorization.Common.csproj new file mode 100644 index 0000000..fc3d216 --- /dev/null +++ b/src/Authorization.Common/Authorization.Common.csproj @@ -0,0 +1,8 @@ + + + + Authorization.Common + Shared helpers for the JWT authorization middleware, including a cached reflection-based object mapper. + + + diff --git a/src/Authorization.Common/Converter.cs b/src/Authorization.Common/Converter.cs new file mode 100644 index 0000000..2435761 --- /dev/null +++ b/src/Authorization.Common/Converter.cs @@ -0,0 +1,46 @@ +namespace Authorization.Common; + +/// +/// Convenience facade over for single objects and sequences. +/// +public static class Converter +{ + /// Creates a shallow copy of . + public static TModel? Clone(TModel? source) + where TModel : class, new() + => Mapper.Map(source); + + /// Converts a single object to . + public static TDestination? Convert( + TSource? source, + Action? transform = null) + where TSource : class + where TDestination : new() + => Mapper.Map(source, transform); + + /// + /// Converts a sequence, skipping any elements that map to null. + /// Materialised to a list so the (already-executed) source query is only + /// enumerated once. + /// + public static IReadOnlyList ConvertList( + IEnumerable source, + Action? transform = null) + where TSource : class + where TDestination : new() + { + ArgumentNullException.ThrowIfNull(source); + + var result = new List(); + foreach (var element in source) + { + var mapped = Mapper.Map(element, transform); + if (mapped is not null) + { + result.Add(mapped); + } + } + + return result; + } +} diff --git a/src/Authorization.Common/Mapper.cs b/src/Authorization.Common/Mapper.cs new file mode 100644 index 0000000..0784255 --- /dev/null +++ b/src/Authorization.Common/Mapper.cs @@ -0,0 +1,87 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Authorization.Common; + +/// +/// Lightweight convention-based object mapper: copies matching public +/// properties (by name and type) from a source object to a new destination +/// instance. +/// +/// +/// This runs on the request hot path, so the reflection work (discovering +/// which source/destination properties line up) is computed once per +/// type-pair and cached. Only value types, strings and arrays are copied, +/// mirroring the original shallow-copy behaviour so nested reference graphs +/// are never shared by accident. +/// +public static class Mapper +{ + private static readonly ConcurrentDictionary<(Type Source, Type Destination), PropertyPair[]> PropertyMapCache = new(); + + private readonly record struct PropertyPair(PropertyInfo Source, PropertyInfo Destination); + + /// + /// Maps onto a new instance of + /// . + /// + /// Object to read values from. May be null. + /// Optional hook to apply custom rules after the automatic copy. + /// The populated destination, or the type default when is null. + public static TDestination? Map( + TSource? source, + Action? transform = null) + where TSource : class + where TDestination : new() + { + if (source is null) + { + return default; + } + + var destination = new TDestination(); + foreach (var pair in GetPropertyMap(typeof(TSource), typeof(TDestination))) + { + pair.Destination.SetValue(destination, pair.Source.GetValue(source)); + } + + transform?.Invoke(source, destination); + return destination; + } + + private static PropertyPair[] GetPropertyMap(Type source, Type destination) => + PropertyMapCache.GetOrAdd((source, destination), static key => BuildPropertyMap(key.Source, key.Destination)); + + private static PropertyPair[] BuildPropertyMap(Type source, Type destination) + { + var destinationProperties = destination + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name, StringComparer.Ordinal); + + var pairs = new List(); + foreach (var sourceProperty in source.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!destinationProperties.TryGetValue(sourceProperty.Name, out var destinationProperty)) + { + continue; + } + + if (!destinationProperty.CanWrite || + destinationProperty.GetIndexParameters().Length != 0 || + destinationProperty.PropertyType != sourceProperty.PropertyType || + !IsCopyable(destinationProperty.PropertyType)) + { + continue; + } + + pairs.Add(new PropertyPair(sourceProperty, destinationProperty)); + } + + return pairs.ToArray(); + } + + // Copy value types, strings and arrays only; skip complex reference types + // to avoid sharing mutable nested objects between source and destination. + private static bool IsCopyable(Type type) => + !type.IsClass || type == typeof(string) || type.IsArray; +} diff --git a/src/Authorization.DataAccess/Authorization.DataAccess.csproj b/src/Authorization.DataAccess/Authorization.DataAccess.csproj new file mode 100644 index 0000000..b3485c2 --- /dev/null +++ b/src/Authorization.DataAccess/Authorization.DataAccess.csproj @@ -0,0 +1,20 @@ + + + + Authorization.DataAccess + Dapper-based data access for the JWT authorization middleware, backed by SQL Server stored procedures. + + + + + + + + + + + + + + + diff --git a/src/Authorization.DataAccess/SecurityRepository.cs b/src/Authorization.DataAccess/SecurityRepository.cs new file mode 100644 index 0000000..a624d76 --- /dev/null +++ b/src/Authorization.DataAccess/SecurityRepository.cs @@ -0,0 +1,63 @@ +using System.Data; +using Authorization.Abstractions.DataAccess; +using Authorization.Abstractions.Options; +using Authorization.Common; +using Dapper; +using Microsoft.Extensions.Options; +using Entities = Authorization.Abstractions.Entities; +using Models = Authorization.Abstractions.Models; + +namespace Authorization.DataAccess; + +/// +/// Reads users and their profiles from the security database through stored +/// procedures, using Dapper. +/// +public sealed class SecurityRepository : ISecurityRepository +{ + private readonly IDbConnectionFactory _connectionFactory; + private readonly ClaimsEnrichmentOptions _options; + + public SecurityRepository(IDbConnectionFactory connectionFactory, IOptions options) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _options = (options ?? throw new ArgumentNullException(nameof(options))).Value; + } + + /// + public async Task GetUserAsync(Models.User user, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(user); + + // A fresh connection per call: SqlConnection is not thread-safe and + // must not be shared across concurrent requests. `await using` guarantees + // it is returned to the pool even if the query throws. + await using var connection = _connectionFactory.CreateConnection(); + + var command = new CommandDefinition( + _options.GetUserProcedure, + new { user.Email, user.UserName }, + commandType: CommandType.StoredProcedure, + cancellationToken: cancellationToken); + + var entity = await connection.QueryFirstOrDefaultAsync(command); + return Converter.Convert(entity); + } + + /// + public async Task> GetProfilesForUserAsync(Models.User user, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(user); + + await using var connection = _connectionFactory.CreateConnection(); + + var command = new CommandDefinition( + _options.GetProfilesProcedure, + new { user.Email, user.UserName }, + commandType: CommandType.StoredProcedure, + cancellationToken: cancellationToken); + + var entities = await connection.QueryAsync(command); + return Converter.ConvertList(entities); + } +} diff --git a/src/Authorization.DataAccess/SqlConnectionFactory.cs b/src/Authorization.DataAccess/SqlConnectionFactory.cs new file mode 100644 index 0000000..763c442 --- /dev/null +++ b/src/Authorization.DataAccess/SqlConnectionFactory.cs @@ -0,0 +1,36 @@ +using System.Data.Common; +using Authorization.Abstractions.DataAccess; +using Authorization.Abstractions.Options; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Authorization.DataAccess; + +/// +/// SQL Server implementation of . +/// +/// +/// The connection string is read once and cached; each call returns a fresh +/// so callers can safely open, use and dispose it +/// without any cross-request sharing. ADO.NET pools the physical connections +/// underneath, so this is both correct and cheap. +/// +public sealed class SqlConnectionFactory : IDbConnectionFactory +{ + private readonly string _connectionString; + + public SqlConnectionFactory(IConfiguration configuration, IOptions options) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(options); + + var name = options.Value.ConnectionStringName; + _connectionString = configuration.GetConnectionString(name) + ?? throw new InvalidOperationException( + $"Connection string '{name}' was not found. Configure it under \"ConnectionStrings\"."); + } + + /// + public DbConnection CreateConnection() => new SqlConnection(_connectionString); +} diff --git a/src/Authorization.Middleware/Authorization.Middleware.csproj b/src/Authorization.Middleware/Authorization.Middleware.csproj new file mode 100644 index 0000000..e744ddd --- /dev/null +++ b/src/Authorization.Middleware/Authorization.Middleware.csproj @@ -0,0 +1,23 @@ + + + + Authorization.Middleware + ASP.NET Core middleware that enriches the authenticated principal with user and profile (role) claims resolved from a security store. + + + + + + + + + + + + + + + + + + diff --git a/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs b/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs new file mode 100644 index 0000000..beede5e --- /dev/null +++ b/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; + +namespace Authorization.Middleware; + +/// +/// Pipeline registration for . +/// +public static class AuthorizationMiddlewareExtensions +{ + /// + /// Adds the claims-enrichment middleware to the request pipeline. Place it + /// after authentication so the principal is already established. + /// + public static IApplicationBuilder UseAuthorizationClaims(this IApplicationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.UseMiddleware(); + } +} diff --git a/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs b/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs new file mode 100644 index 0000000..cb42eef --- /dev/null +++ b/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs @@ -0,0 +1,132 @@ +using System.Security.Claims; +using Authorization.Abstractions.Business; +using Authorization.Abstractions.Models; +using Authorization.Abstractions.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Authorization.Middleware; + +/// +/// Enriches the authenticated principal with claims resolved from the security +/// store: the user's identifier, name and email, plus a role claim per profile. +/// +/// +/// Middleware is instantiated once (singleton), so no per-request state is kept +/// in fields — the scoped is received through +/// instead. Any failure while resolving identity data +/// is logged and swallowed so a transient store outage degrades gracefully +/// (the request continues unenriched) rather than crashing the pipeline. +/// +public sealed partial class ClaimsEnrichmentMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly ClaimsEnrichmentOptions _options; + + public ClaimsEnrichmentMiddleware( + RequestDelegate next, + ILogger logger, + IOptions options) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = (options ?? throw new ArgumentNullException(nameof(options))).Value; + } + + /// Intercepts the request, enriches the principal, then forwards it. + public async Task InvokeAsync(HttpContext context, IAuthorizationManager authorizationManager) + { + var identity = await BuildClaimsIdentityAsync(context, authorizationManager); + if (identity is not null) + { + context.User.AddIdentity(identity); + } + + await _next(context); + } + + private async Task BuildClaimsIdentityAsync(HttpContext context, IAuthorizationManager authorizationManager) + { + // Only enrich real, authenticated principals; anonymous traffic flows through untouched. + if (context.User.Identity is not { IsAuthenticated: true }) + { + return null; + } + + var userName = context.User.FindFirst(_options.UserNameClaimType)?.Value; + if (string.IsNullOrWhiteSpace(userName)) + { + LogMissingUserNameClaim(_options.UserNameClaimType, context.TraceIdentifier); + return null; + } + + try + { + var claims = new List(); + var user = await authorizationManager.GetUserAsync(new User { UserName = userName }, context.RequestAborted); + if (user is null) + { + LogUserNotFound(context.TraceIdentifier); + return null; + } + + AddUserClaims(claims, user); + await AddProfileClaimsAsync(claims, user, authorizationManager, context.RequestAborted); + + return new ClaimsIdentity(claims); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + { + // Client went away; nothing to log at error level. + return null; + } + catch (Exception ex) + { + // Graceful degradation: never let an identity-store failure crash the request. + LogEnrichmentFailed(ex, context.TraceIdentifier); + return null; + } + } + + private static void AddUserClaims(ICollection claims, User user) + { + if (!string.IsNullOrEmpty(user.Email)) + { + claims.Add(new Claim(ClaimTypes.Email, user.Email)); + } + + if (!string.IsNullOrEmpty(user.UserName)) + { + claims.Add(new Claim(ClaimTypes.Name, user.UserName)); + } + + claims.Add(new Claim("IdUsuario", user.Id.ToString())); + } + + private async Task AddProfileClaimsAsync( + ICollection claims, + User user, + IAuthorizationManager authorizationManager, + CancellationToken cancellationToken) + { + var profiles = await authorizationManager.GetProfilesForUserAsync(user, cancellationToken); + foreach (var profile in profiles) + { + claims.Add(new Claim(ClaimTypes.Role, profile.Id.ToString())); + } + } + + [LoggerMessage(Level = LogLevel.Debug, + Message = "Skipping claims enrichment: inbound principal has no '{ClaimType}' claim. TraceId={TraceId}")] + private partial void LogMissingUserNameClaim(string claimType, string traceId); + + [LoggerMessage(Level = LogLevel.Debug, + Message = "Skipping claims enrichment: no user matched the inbound claim. TraceId={TraceId}")] + private partial void LogUserNotFound(string traceId); + + [LoggerMessage(Level = LogLevel.Error, + Message = "Claims enrichment failed; continuing without enriched claims. TraceId={TraceId}")] + private partial void LogEnrichmentFailed(Exception exception, string traceId); +} diff --git a/src/Authorization.Middleware/ServiceCollectionExtensions.cs b/src/Authorization.Middleware/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..042c11e --- /dev/null +++ b/src/Authorization.Middleware/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using Authorization.Abstractions.Business; +using Authorization.Abstractions.DataAccess; +using Authorization.Abstractions.Options; +using Authorization.Business; +using Authorization.DataAccess; +using Microsoft.Extensions.DependencyInjection; + +namespace Authorization.Middleware; + +/// +/// Dependency-injection registration for the authorization stack. A single +/// call wires the connection factory, repository and business manager so +/// consumers no longer have to assemble the graph by hand. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers everything the claims-enrichment middleware needs. + /// + /// The service collection. + /// Optional hook to override connection-string name, claim type or stored-procedure names. + public static IServiceCollection AddAuthorizationClaims( + this IServiceCollection services, + Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + var optionsBuilder = services.AddOptions(); + if (configure is not null) + { + optionsBuilder.Configure(configure); + } + + // The factory only caches an immutable connection string, so it is safe + // as a singleton; it still hands out a fresh connection per call. + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..8d0c512 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,41 @@ + + + + + net8.0 + latest + enable + enable + true + true + + $(NoWarn);CS1591 + + + + + 2.0.0 + Isma-L154 + Isma-L154 + JWT Authorization Middleware + MIT + https://github.com/Isma-L154/MiddleWare + https://github.com/Isma-L154/MiddleWare + git + middleware;jwt;authentication;authorization;aspnetcore;claims + README.md + true + snupkg + + false + + + + + + + diff --git a/tests/Authorization.UnitTests/Authorization.UnitTests.csproj b/tests/Authorization.UnitTests/Authorization.UnitTests.csproj new file mode 100644 index 0000000..dc14bcf --- /dev/null +++ b/tests/Authorization.UnitTests/Authorization.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/tests/Authorization.UnitTests/AuthorizationManagerTests.cs b/tests/Authorization.UnitTests/AuthorizationManagerTests.cs new file mode 100644 index 0000000..dd7fd74 --- /dev/null +++ b/tests/Authorization.UnitTests/AuthorizationManagerTests.cs @@ -0,0 +1,48 @@ +using Authorization.Abstractions.DataAccess; +using Authorization.Abstractions.Models; +using Authorization.Business; +using Moq; + +namespace Authorization.UnitTests; + +public class AuthorizationManagerTests +{ + [Fact] + public async Task GetUserAsync_DelegatesToRepository() + { + var expected = new User { Id = Guid.NewGuid(), UserName = "jdoe" }; + var repository = new Mock(); + repository + .Setup(r => r.GetUserAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + var manager = new AuthorizationManager(repository.Object); + + var result = await manager.GetUserAsync(new User { UserName = "jdoe" }); + + Assert.Same(expected, result); + repository.Verify(r => r.GetUserAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetProfilesForUserAsync_DelegatesToRepository() + { + var expected = new[] { new Profile { Id = 1, Name = "admin" } }; + var repository = new Mock(); + repository + .Setup(r => r.GetProfilesForUserAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + var manager = new AuthorizationManager(repository.Object); + + var result = await manager.GetProfilesForUserAsync(new User { UserName = "jdoe" }); + + Assert.Same(expected, result); + } + + [Fact] + public void Constructor_NullRepository_Throws() + { + Assert.Throws(() => new AuthorizationManager(null!)); + } +} diff --git a/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs b/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs new file mode 100644 index 0000000..61e1afc --- /dev/null +++ b/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs @@ -0,0 +1,122 @@ +using System.Security.Claims; +using Authorization.Abstractions.Business; +using Authorization.Abstractions.Models; +using Authorization.Abstractions.Options; +using Authorization.Middleware; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; + +namespace Authorization.UnitTests; + +public class ClaimsEnrichmentMiddlewareTests +{ + private static readonly IOptions Options = + Microsoft.Extensions.Options.Options.Create(new ClaimsEnrichmentOptions()); + + [Fact] + public async Task Invoke_AnonymousUser_CallsNextWithoutEnrichment() + { + var manager = new Mock(MockBehavior.Strict); + var context = new DefaultHttpContext(); + var nextCalled = false; + + var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options); + + await middleware.InvokeAsync(context, manager.Object); + + Assert.True(nextCalled); + Assert.DoesNotContain(context.User.Identities, i => i.HasClaim(c => c.Type == ClaimTypes.Name)); + manager.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Invoke_AuthenticatedUser_AddsUserAndRoleClaims() + { + var userId = Guid.NewGuid(); + var manager = new Mock(); + manager + .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new User { Id = userId, UserName = "jdoe", Email = "jdoe@example.com" }); + manager + .Setup(m => m.GetProfilesForUserAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new[] { new Profile { Id = 10, Name = "admin" }, new Profile { Id = 20, Name = "user" } }); + + var context = BuildAuthenticatedContext("jdoe"); + var nextCalled = false; + var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options); + + await middleware.InvokeAsync(context, manager.Object); + + Assert.True(nextCalled); + Assert.Equal("jdoe@example.com", context.User.FindFirst(ClaimTypes.Email)?.Value); + Assert.Equal("jdoe", context.User.FindFirst(ClaimTypes.Name)?.Value); + Assert.Equal(userId.ToString(), context.User.FindFirst("IdUsuario")?.Value); + var roles = context.User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray(); + Assert.Equal(new[] { "10", "20" }, roles); + } + + [Fact] + public async Task Invoke_MissingUserNameClaim_DoesNotEnrich() + { + var manager = new Mock(MockBehavior.Strict); + var context = BuildAuthenticatedContext(userName: null); + var middleware = new ClaimsEnrichmentMiddleware(_ => Task.CompletedTask, NullLogger.Instance, Options); + + await middleware.InvokeAsync(context, manager.Object); + + manager.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Invoke_UserNotFound_DoesNotAddClaims() + { + var manager = new Mock(); + manager + .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((User?)null); + + var context = BuildAuthenticatedContext("ghost"); + var middleware = new ClaimsEnrichmentMiddleware(_ => Task.CompletedTask, NullLogger.Instance, Options); + + await middleware.InvokeAsync(context, manager.Object); + + Assert.Null(context.User.FindFirst("IdUsuario")); + manager.Verify(m => m.GetProfilesForUserAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Invoke_StoreThrows_DegradesGracefullyAndCallsNext() + { + // The core resilience guarantee: an identity-store failure must never + // crash the pipeline. + var manager = new Mock(); + manager + .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("database is down")); + + var context = BuildAuthenticatedContext("jdoe"); + var nextCalled = false; + var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options); + + var exception = await Record.ExceptionAsync(() => middleware.InvokeAsync(context, manager.Object)); + + Assert.Null(exception); + Assert.True(nextCalled); + Assert.Null(context.User.FindFirst("IdUsuario")); + } + + private static DefaultHttpContext BuildAuthenticatedContext(string? userName) + { + var claims = new List(); + if (userName is not null) + { + claims.Add(new Claim("usuario", userName)); + } + + // A non-null authentication type makes the identity report IsAuthenticated == true. + var identity = new ClaimsIdentity(claims, authenticationType: "TestAuth"); + return new DefaultHttpContext { User = new ClaimsPrincipal(identity) }; + } +} diff --git a/tests/Authorization.UnitTests/ConverterTests.cs b/tests/Authorization.UnitTests/ConverterTests.cs new file mode 100644 index 0000000..f3554e6 --- /dev/null +++ b/tests/Authorization.UnitTests/ConverterTests.cs @@ -0,0 +1,50 @@ +using Authorization.Common; +using Entities = Authorization.Abstractions.Entities; +using Models = Authorization.Abstractions.Models; + +namespace Authorization.UnitTests; + +public class ConverterTests +{ + [Fact] + public void ConvertList_MapsEveryElement() + { + var source = new[] + { + new Entities.Profile { Id = 1, Name = "admin" }, + new Entities.Profile { Id = 2, Name = "user" }, + }; + + var result = Converter.ConvertList(source); + + Assert.Equal(2, result.Count); + Assert.Equal("admin", result[0].Name); + Assert.Equal("user", result[1].Name); + } + + [Fact] + public void ConvertList_EmptySource_ReturnsEmpty() + { + var result = Converter.ConvertList(Array.Empty()); + Assert.Empty(result); + } + + [Fact] + public void ConvertList_NullSource_Throws() + { + Assert.Throws( + () => Converter.ConvertList(null!)); + } + + [Fact] + public void Clone_ProducesIndependentCopy() + { + var original = new Models.User { Id = Guid.NewGuid(), UserName = "jdoe" }; + + var clone = Converter.Clone(original); + + Assert.NotNull(clone); + Assert.NotSame(original, clone); + Assert.Equal(original.UserName, clone!.UserName); + } +} diff --git a/tests/Authorization.UnitTests/GlobalUsings.cs b/tests/Authorization.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/Authorization.UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/tests/Authorization.UnitTests/MapperTests.cs b/tests/Authorization.UnitTests/MapperTests.cs new file mode 100644 index 0000000..2736756 --- /dev/null +++ b/tests/Authorization.UnitTests/MapperTests.cs @@ -0,0 +1,64 @@ +using Authorization.Common; +using Entities = Authorization.Abstractions.Entities; +using Models = Authorization.Abstractions.Models; + +namespace Authorization.UnitTests; + +public class MapperTests +{ + [Fact] + public void Map_CopiesMatchingProperties() + { + var id = Guid.NewGuid(); + var source = new Entities.User + { + Id = id, + UserName = "jdoe", + Email = "jdoe@example.com", + PasswordHash = "hash", + }; + + var result = Mapper.Map(source); + + Assert.NotNull(result); + Assert.Equal(id, result!.Id); + Assert.Equal("jdoe", result.UserName); + Assert.Equal("jdoe@example.com", result.Email); + Assert.Equal("hash", result.PasswordHash); + } + + [Fact] + public void Map_NullSource_ReturnsDefault() + { + var result = Mapper.Map(null); + Assert.Null(result); + } + + [Fact] + public void Map_AppliesTransformAfterCopy() + { + var source = new Entities.Profile { Id = 7, Name = "admin" }; + + var result = Mapper.Map( + source, + (src, dest) => dest.Name = src.Name!.ToUpperInvariant()); + + Assert.NotNull(result); + Assert.Equal(7, result!.Id); + Assert.Equal("ADMIN", result.Name); + } + + [Fact] + public void Map_IsConsistentAcrossCachedCalls() + { + // Exercises the per-type-pair property-map cache: a second call must + // produce the same result as the first. + var first = Mapper.Map(new Entities.Profile { Id = 1, Name = "a" }); + var second = Mapper.Map(new Entities.Profile { Id = 2, Name = "b" }); + + Assert.Equal(1, first!.Id); + Assert.Equal("a", first.Name); + Assert.Equal(2, second!.Id); + Assert.Equal("b", second.Name); + } +}