From d5925a6476af4f7edd92876fa4148cbb5e48b235 Mon Sep 17 00:00:00 2001 From: Sergio Matias Urquin Date: Tue, 29 Apr 2025 18:57:20 -0600 Subject: [PATCH] Add project files. --- .gitignore | 362 ++++++++++++++++++ .pipelines/GitVersion.yml | 59 +++ .pipelines/templated-pipeline.yml | 46 +++ Core.Cerberos.Adapters.sln | 25 ++ .../Adapters/Base/BaseAdapterResponse.cs | 33 ++ .../Adapters/ModuleAdapter.cs | 118 ++++++ .../Adapters/PermissionAdapter.cs | 95 +++++ .../Adapters/RoleAdapter.cs | 107 ++++++ .../Adapters/TokenAdapter.cs | 18 + .../Adapters/UserAdapter.cs | 171 +++++++++ .../Attributes/Permission.cs | 9 + .../Attributes/PermissionAttribute.cs | 72 ++++ .../Common/Constants/AccessLevelEnum.cs | 32 ++ .../Common/Constants/AzureAd.cs | 43 +++ .../Common/Constants/Claims.cs | 73 ++++ .../Common/Constants/CollectionNames.cs | 25 ++ .../Common/Constants/EnvironmentVariables.cs | 21 + .../Common/Constants/KeyVaultConfiguration.cs | 33 ++ .../Common/Constants/MimeTypes.cs | 153 ++++++++ .../Common/Constants/Routes.cs | 114 ++++++ .../Common/Constants/Schemes.cs | 18 + .../Common/Constants/Secrets.cs | 59 +++ .../Common/Enums/ApplicationsEnum.cs | 42 ++ .../Common/Enums/StatusEnum.cs | 32 ++ .../SchemaFilters/EnumArrayJsonConverter.cs | 17 + .../Common/SchemaFilters/EnumSchemaFilter.cs | 31 ++ .../Contracts/ITokenProvider.cs | 19 + .../Contracts/ITokenService.cs | 32 ++ .../Core.Cerberos.Adapters.csproj | 30 ++ .../Extensions/AuthenticationExtension.cs | 97 +++++ .../Extensions/SwaggerExtensions.cs | 193 ++++++++++ .../Extensions/TelemetryExtensions.cs | 22 ++ .../Extensions/TrackingMechanismExtension.cs | 23 ++ .../PermissionsAuthorizationAdapter.cs | 13 + .../AuthenticatedHttpClientHandler.cs | 29 ++ .../PermissionsAuthorizationHandler.cs | 18 + Core.Cerberos.Adapters/Helpers/AuthHelper.cs | 52 +++ Core.Cerberos.Adapters/Helpers/RsaHelper.cs | 94 +++++ .../Options/JwtIssuerOptions.cs | 60 +++ .../Services/TokenService.cs | 146 +++++++ .../Settings/AuthSettings.cs | 25 ++ .../TokenProvider/HttpContextTokenProvider.cs | 32 ++ .../UserExistenceAdapter.cs | 22 ++ README.md | 1 + 44 files changed, 2716 insertions(+) create mode 100644 .gitignore create mode 100644 .pipelines/GitVersion.yml create mode 100644 .pipelines/templated-pipeline.yml create mode 100644 Core.Cerberos.Adapters.sln create mode 100644 Core.Cerberos.Adapters/Adapters/Base/BaseAdapterResponse.cs create mode 100644 Core.Cerberos.Adapters/Adapters/ModuleAdapter.cs create mode 100644 Core.Cerberos.Adapters/Adapters/PermissionAdapter.cs create mode 100644 Core.Cerberos.Adapters/Adapters/RoleAdapter.cs create mode 100644 Core.Cerberos.Adapters/Adapters/TokenAdapter.cs create mode 100644 Core.Cerberos.Adapters/Adapters/UserAdapter.cs create mode 100644 Core.Cerberos.Adapters/Attributes/Permission.cs create mode 100644 Core.Cerberos.Adapters/Attributes/PermissionAttribute.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/AccessLevelEnum.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/AzureAd.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/Claims.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/CollectionNames.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/EnvironmentVariables.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/KeyVaultConfiguration.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/MimeTypes.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/Routes.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/Schemes.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/Secrets.cs create mode 100644 Core.Cerberos.Adapters/Common/Enums/ApplicationsEnum.cs create mode 100644 Core.Cerberos.Adapters/Common/Enums/StatusEnum.cs create mode 100644 Core.Cerberos.Adapters/Common/SchemaFilters/EnumArrayJsonConverter.cs create mode 100644 Core.Cerberos.Adapters/Common/SchemaFilters/EnumSchemaFilter.cs create mode 100644 Core.Cerberos.Adapters/Contracts/ITokenProvider.cs create mode 100644 Core.Cerberos.Adapters/Contracts/ITokenService.cs create mode 100644 Core.Cerberos.Adapters/Core.Cerberos.Adapters.csproj create mode 100644 Core.Cerberos.Adapters/Extensions/AuthenticationExtension.cs create mode 100644 Core.Cerberos.Adapters/Extensions/SwaggerExtensions.cs create mode 100644 Core.Cerberos.Adapters/Extensions/TelemetryExtensions.cs create mode 100644 Core.Cerberos.Adapters/Extensions/TrackingMechanismExtension.cs create mode 100644 Core.Cerberos.Adapters/Handlers/Adapters/PermissionsAuthorizationAdapter.cs create mode 100644 Core.Cerberos.Adapters/Handlers/AuthenticatedHttpClientHandler.cs create mode 100644 Core.Cerberos.Adapters/Handlers/PermissionsAuthorizationHandler.cs create mode 100644 Core.Cerberos.Adapters/Helpers/AuthHelper.cs create mode 100644 Core.Cerberos.Adapters/Helpers/RsaHelper.cs create mode 100644 Core.Cerberos.Adapters/Options/JwtIssuerOptions.cs create mode 100644 Core.Cerberos.Adapters/Services/TokenService.cs create mode 100644 Core.Cerberos.Adapters/Settings/AuthSettings.cs create mode 100644 Core.Cerberos.Adapters/TokenProvider/HttpContextTokenProvider.cs create mode 100644 Core.Cerberos.Adapters/UserExistenceAdapter.cs create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee5385 --- /dev/null +++ b/.gitignore @@ -0,0 +1,362 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd diff --git a/.pipelines/GitVersion.yml b/.pipelines/GitVersion.yml new file mode 100644 index 0000000..f5867a9 --- /dev/null +++ b/.pipelines/GitVersion.yml @@ -0,0 +1,59 @@ +mode: mainline +assembly-versioning-scheme: MajorMinorPatch +tag-prefix: '[vV]' +major-version-bump-message: '\+semver:\s?(breaking|major)' +minor-version-bump-message: '\+semver:\s?(feature|minor)' +patch-version-bump-message: '\+semver:\s?(fix|patch)' +no-bump-message: '\+semver:\s?(none|skip)' +legacy-semver-padding: 4 +build-metadata-padding: 4 +commits-since-version-source-padding: 4 +commit-message-incrementing: Enabled +branches: + main: + regex: ^master$|^main$ + tag: '' + increment: Patch + prevent-increment-of-merged-branch-version: true + track-merge-target: false + source-branches: [ 'develop', 'release' ] + tracks-release-branches: false + is-release-branch: false + is-mainline: true + pre-release-weight: 55000 + feature: + regex: ^features?[/-] + tag: useBranchName + increment: Inherit + prevent-increment-of-merged-branch-version: false + track-merge-target: false + source-branches: [ 'develop', 'main', 'release', 'feature', 'support', 'hotfix' ] + tracks-release-branches: false + is-release-branch: false + is-mainline: false + pre-release-weight: 30000 + release: + regex: ^releases?[/-] + tag: beta + increment: None + is-mainline: true + regex: ^releases?[/-] + prevent-increment-of-merged-branch-version: true + track-merge-target: false + source-branches: [ 'develop', 'main', 'support', 'release' ] + tracks-release-branches: false + is-release-branch: true + is-mainline: false + pre-release-weight: 30000 + hotfix: + regex: ^hotfix(es)?[/-] + increment: Patch + prevent-increment-of-merged-branch-version: false + track-merge-target: false + source-branches: [ 'develop', 'main', 'support' ] + tracks-release-branches: false + is-release-branch: false + is-mainline: false + pre-release-weight: 30000 +ignore: + sha: [] \ No newline at end of file diff --git a/.pipelines/templated-pipeline.yml b/.pipelines/templated-pipeline.yml new file mode 100644 index 0000000..805a9d7 --- /dev/null +++ b/.pipelines/templated-pipeline.yml @@ -0,0 +1,46 @@ +pool: + vmImage: 'ubuntu-latest' + +variables: + project: 'Core.Cerberos.Adapters/Core.Cerberos.Adapters.csproj' + solution: 'Core.Cerberos.Adapters.sln' + buildConfiguration: 'Release' + snykConnectionEndpoint: 'SnykConnection' + feed: '1b3770f1-17db-4bf2-a43d-49f305aa7a22' + artifactName: 'Core.Cerberos' + projectFileName: 'Core.Cerberos.Adapters.csproj' + projectPath: 'Core.Cerberos.Adapters/' + +resources: + repositories: + - repository: templates + name: "Template.DevOps.Pipelines" + type: "git" + +jobs: +- job: CI + steps: + - template: templates/dotnet/v1/step1_setup.yml@templates + - template: templates/dotnet/v1/step2_versioning.yml@templates + parameters: + projectFileName: '$(projectFileName)' + path: '$(projectPath)' + - template: templates/dotnet/v1/step3_restore_and_build.yml@templates + parameters: + project: '$(project)' + solution: '$(solution)' + buildConfiguration: '$(buildConfiguration)' + projectNameOnSonar: 'Core.Cerberos' + projectKeyOnSonar: 'heathpbu_Core.Cerberos' + feed: '$(feed)' + - template: templates/dotnet/v1/step4_sonar_analysis.yml@templates + + - template: templates/dotnet/v1/step5_snyk_analysis.yml@templates + parameters: + snykConnectionEndpoint: '$(snykConnectionEndpoint)' + solutionToScan: '$(solution)' + + - template: templates/dotnet/v1/step6_release_nuget.yml@templates + parameters: + artifactName: '$(artifactName)' + project: '$(project)' diff --git a/Core.Cerberos.Adapters.sln b/Core.Cerberos.Adapters.sln new file mode 100644 index 0000000..34a1919 --- /dev/null +++ b/Core.Cerberos.Adapters.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34928.147 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Cerberos.Adapters", "Core.Cerberos.Adapters\Core.Cerberos.Adapters.csproj", "{C902AB37-E6D1-4CE7-B271-0E3969C989F3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C902AB37-E6D1-4CE7-B271-0E3969C989F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C902AB37-E6D1-4CE7-B271-0E3969C989F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C902AB37-E6D1-4CE7-B271-0E3969C989F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C902AB37-E6D1-4CE7-B271-0E3969C989F3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5BADECD6-CE7F-4167-A29F-AC3D7E4FE4D0} + EndGlobalSection +EndGlobal diff --git a/Core.Cerberos.Adapters/Adapters/Base/BaseAdapterResponse.cs b/Core.Cerberos.Adapters/Adapters/Base/BaseAdapterResponse.cs new file mode 100644 index 0000000..5a67d10 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/Base/BaseAdapterResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Core.Cerberos.Adapters +{ + public class BaseAdapterResponse + { + public bool HasError { get; set; } = false; + public bool IsSuccess { get; private set; } = false; + public string Message { get; set; } + + public void SetResult(string message) + { + HasError = false; + IsSuccess = true; + Message = message; + } + public string SetErrorMessage(string message) + { + var _message = new + { + Content = JsonSerializer.Serialize(message), + HasError = true + }; + + return JsonSerializer.Serialize(_message); + } + } +} diff --git a/Core.Cerberos.Adapters/Adapters/ModuleAdapter.cs b/Core.Cerberos.Adapters/Adapters/ModuleAdapter.cs new file mode 100644 index 0000000..717bc20 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/ModuleAdapter.cs @@ -0,0 +1,118 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Common.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters +{ + /// + /// Adapter for representing a module. + /// + public class ModuleAdapter + { + /// + /// Gets or sets the ID of the module. + /// + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("id")] + public string Id { get; set; } = null!; + + /// + /// Gets or sets the name of the module. + /// + [BsonElement("name")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + /// + /// Gets or sets the description of the module. + /// + [BsonElement("description")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the description of the module. + /// + [BsonElement("icon")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("icon")] + public string? Icon { get; set; } + + /// + /// Gets or sets the description of the module. + /// + [BsonElement("route")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("route")] + public string Route { get; set; } = null!; + + /// + /// Gets or sets the order of the module. + /// + [BsonElement("order")] + [BsonRepresentation(BsonType.Int32)] + [JsonPropertyName("order")] + public int? Order { get; set; } + + /// + /// Gets or sets the application of the module. + /// + [BsonElement("application")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("application")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ApplicationsEnum? Application { get; set; } = null!; + + /// + /// Gets or sets the date and time when the module was created. + /// + [BsonElement("createdAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the user who created the module. + /// + [BsonElement("createdBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the date and time when the module was last updated. + /// + [BsonElement("updatedAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } = null; + + /// + /// Gets or sets the user who last updated the module. + /// + [BsonElement("updatedBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } = null; + + /// + /// Gets or sets the status of the module. + /// + [BsonElement("status")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum Status { get; set; } = StatusEnum.Active; + } +} diff --git a/Core.Cerberos.Adapters/Adapters/PermissionAdapter.cs b/Core.Cerberos.Adapters/Adapters/PermissionAdapter.cs new file mode 100644 index 0000000..7499119 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/PermissionAdapter.cs @@ -0,0 +1,95 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Common.Constants; +using Core.Cerberos.Adapters.Common.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters +{ + /// + /// Adapter for representing a permission. + /// + public class PermissionAdapter + { + /// + /// Gets or sets the ID of the entity. + /// + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("id")] + public string Id { get; set; } = null!; + + /// + /// Gets or sets the name of the entity. + /// + [BsonElement("name")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + /// + /// Gets or sets the description of the entity. + /// + [BsonElement("description")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the status of the entity object. + /// + [BsonElement("accessLevel")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("accessLevel")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public AccessLevelEnum? AccessLevel { get; set; } = null!; + + /// + /// Gets or sets the date and time when the entity was created. + /// + [BsonElement("createdAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the user who created the entity. + /// + [BsonElement("createdBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the date and time when the entity was last updated. + /// + [BsonElement("updatedAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } = null; + + /// + /// Gets or sets the user who last updated the entity. + /// + [BsonElement("updatedBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } = null; + + /// + /// Gets or sets the status of the entity. + /// + [BsonElement("status")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum Status { get; set; } = StatusEnum.Active; + } +} diff --git a/Core.Cerberos.Adapters/Adapters/RoleAdapter.cs b/Core.Cerberos.Adapters/Adapters/RoleAdapter.cs new file mode 100644 index 0000000..d2c03c7 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/RoleAdapter.cs @@ -0,0 +1,107 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Common.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters +{ + /// + /// Adapter representing a role. + /// + public class RoleAdapter + { + /// + /// Gets or sets the unique identifier of the role. + /// + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("id")] + public string Id { get; set; } = null!; + + /// + /// Gets or sets the name of the role. + /// + [BsonElement("name")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + /// + /// Gets or sets the description of the role. + /// + [BsonElement("description")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the status of the entity. + /// + [BsonElement("applications")] + [JsonPropertyName("applications")] + [JsonConverter(typeof(EnumArrayJsonConverter))] + public ApplicationsEnum[]? Applications { get; set; } + + /// + /// Gets or sets the modules of the role. + /// + [BsonElement("modules")] + [JsonPropertyName("modules")] + public string[] Modules { get; set; } = null!; + + /// + /// Gets or sets the permissions of the role. + /// + [BsonElement("permissions")] + [JsonPropertyName("permissions")] + public string[] Permissions { get; set; } = null!; + + /// + /// Gets or sets the date and time when the entity was created. + /// + [BsonElement("createdAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the user who created the entity. + /// + [BsonElement("createdBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the date and time when the entity was last updated. + /// + [BsonElement("updatedAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } = null; + + /// + /// Gets or sets the user who last updated the entity. + /// + [BsonElement("updatedBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } = null; + + /// + /// Gets or sets the status of the entity. + /// + [BsonElement("status")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum Status { get; set; } = StatusEnum.Active; + } +} diff --git a/Core.Cerberos.Adapters/Adapters/TokenAdapter.cs b/Core.Cerberos.Adapters/Adapters/TokenAdapter.cs new file mode 100644 index 0000000..b7f5b51 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/TokenAdapter.cs @@ -0,0 +1,18 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Cerberos.Adapters +{ + public class TokenAdapter + { + public UserAdapter? User { get; set; } + + public RoleAdapter? Role { get; set; } + + public IEnumerable? Permissions { get; set; } + public IEnumerable? Modules { get; set; } + } +} diff --git a/Core.Cerberos.Adapters/Adapters/UserAdapter.cs b/Core.Cerberos.Adapters/Adapters/UserAdapter.cs new file mode 100644 index 0000000..9afda00 --- /dev/null +++ b/Core.Cerberos.Adapters/Adapters/UserAdapter.cs @@ -0,0 +1,171 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +using Core.Cerberos.Adapters.Common.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters +{ + /// + /// Adapter representing a user. + /// + public class UserAdapter : BaseAdapterResponse + { + /// + /// Gets or sets the unique identifier of the user. + /// + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("id")] + public string Id { get; set; } = null!; + + /// + /// Gets or sets the guid of the user. + /// + [BsonElement("guid")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("guid")] + public string? Guid { get; set; } + + /// + /// Gets or sets the email address of the user. + /// + [BsonElement("email")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("email")] + public string Email { get; set; } = null!; + + /// + /// Gets or sets the name of the user. + /// + [BsonElement("name")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + /// + /// Gets or sets the middlename of the user. + /// + [BsonElement("middleName")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("middleName")] + public string? MiddleName { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + [BsonElement("lastName")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("lastName")] + public string LastName { get; set; } = null!; + + /// + /// Gets or sets the name of the user. + /// + [BsonElement("displayName")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// + /// Gets or sets the role ID of the user. + /// + [BsonElement("roleId")] + [BsonRepresentation(BsonType.ObjectId)] + [JsonPropertyName("roleId")] + public string RoleId { get; set; } = null!; + + /// + /// Gets or sets the array of companies associated with the user. + /// + [BsonElement("companies")] + [JsonPropertyName("companies")] + public string[] Companies { get; set; } = null!; + + /// + /// Gets or sets the array of projects associated with the user. + /// + [BsonElement("projects")] + [JsonPropertyName("projects")] + public string[]? Projects { get; set; } + + /// + /// Gets or sets the boolean of the consent form accepted by the user. + /// + [BsonElement("consentFormAccepted")] + [JsonPropertyName("consentFormAccepted")] + [BsonIgnoreIfNull] + public bool ConsentFormAccepted { get; set; } + + /// + /// Gets or sets the timestamp of the last login of the user. + /// + [BsonElement("lastLogIn")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("lastLogIn")] + public DateTime? LastLogIn { get; set; } + + /// + /// Gets or sets the timestamp of the last logout of the user. + /// + [BsonElement("lastLogOut")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("lastLogOut")] + public DateTime? LastLogOut { get; set; } + + + /// + /// Gets or sets the token associated with the user. + /// + [BsonElement("token")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("token")] + public string? Token { get; set; } = null; + + /// + /// Gets or sets the date and time when the entity was created. + /// + [BsonElement("createdAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the user who created the entity. + /// + [BsonElement("createdBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("createdBy")] + public string? CreatedBy { get; set; } + + /// + /// Gets or sets the date and time when the entity was last updated. + /// + [BsonElement("updatedAt")] + [BsonRepresentation(BsonType.DateTime)] + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } = null; + + /// + /// Gets or sets the user who last updated the entity. + /// + [BsonElement("updatedBy")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("updatedBy")] + public string? UpdatedBy { get; set; } = null; + + /// + /// Gets or sets the status of the entity. + /// + [BsonElement("status")] + [BsonRepresentation(BsonType.String)] + [JsonPropertyName("status")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public StatusEnum Status { get; set; } = StatusEnum.Active; + } +} diff --git a/Core.Cerberos.Adapters/Attributes/Permission.cs b/Core.Cerberos.Adapters/Attributes/Permission.cs new file mode 100644 index 0000000..a531b52 --- /dev/null +++ b/Core.Cerberos.Adapters/Attributes/Permission.cs @@ -0,0 +1,9 @@ +namespace Core.Cerberos.Adapters +{ + public class Permission + { + public string Name { get; set; } + + public string AccessLevel { get; set; } + } +} diff --git a/Core.Cerberos.Adapters/Attributes/PermissionAttribute.cs b/Core.Cerberos.Adapters/Attributes/PermissionAttribute.cs new file mode 100644 index 0000000..7db7cc5 --- /dev/null +++ b/Core.Cerberos.Adapters/Attributes/PermissionAttribute.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Core.Cerberos.Adapters.Attributes +{ + /// + /// Custom authorization attribute that checks if the user has any of the required permissions. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class PermissionAttribute : AuthorizeAttribute, IAuthorizationFilter + { + private readonly string _requiredPermissions; + + /// + /// Initializes a new instance of the class. + /// + /// The array of permissions required to access the resource. + public PermissionAttribute(string requiredPermissions) + { + _requiredPermissions = requiredPermissions; + } + + /// + /// Called during the authorization process to determine if the user has any of the required permissions. + /// + /// The context in which the authorization filter operates. + public void OnAuthorization(AuthorizationFilterContext context) + { + try + { + var hasPermission = false; + + var servicePermissionsList = _requiredPermissions.Replace(" ", "").Split(',').ToList(); + + var servicePermissions = servicePermissionsList.Select(s => new Permission + { + Name = s.Substring(0, s.IndexOf('.')), + AccessLevel = s.Substring(s.IndexOf('.') + 1), + }); + + var userPermissionsList = context.HttpContext.User.Claims + .Where(c => c.Type == "permissions") + .Select(c => c.Value) + .ToList(); + + var userPermissions = userPermissionsList.Select(s => new Permission + { + Name = s.Substring(0, s.IndexOf('.')), + AccessLevel = s.Substring(s.IndexOf('.') + 1), + }); + + foreach (var servicePermission in servicePermissions) + { + hasPermission = userPermissions + .Where(up => up.Name == servicePermission.Name && up.AccessLevel == "All" + || up.Name == servicePermission.Name && up.AccessLevel == servicePermission.AccessLevel) + .Count() > 0 ? true : false; + + if (hasPermission) break; + } + + if (!hasPermission) + context.Result = new UnauthorizedResult(); + } + catch (Exception ex) + { + context.Result = new UnauthorizedResult(); + } + } + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/AccessLevelEnum.cs b/Core.Cerberos.Adapters/Common/Constants/AccessLevelEnum.cs new file mode 100644 index 0000000..da9745e --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/AccessLevelEnum.cs @@ -0,0 +1,32 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Specifies different access level for a permission. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum AccessLevelEnum + { + /// + /// The object is accessible for reading. + /// + Read = 0, + + /// + /// The object is accessible for writing. + /// + Write = 1, + + /// + /// The object is accessible for all operations. + /// + All = 2 + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/AzureAd.cs b/Core.Cerberos.Adapters/Common/Constants/AzureAd.cs new file mode 100644 index 0000000..9e739b6 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/AzureAd.cs @@ -0,0 +1,43 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for Azure Active Directory. + /// + public class AzureAd + { + /// + /// The ClientId parameter. + /// + public const string ClientId = "AzureAdB2C:ClientId"; + + /// + /// The TenantId parameter. + /// + public const string TenantId = "AzureAdB2C:TenantId"; + + /// + /// The ClientSecret parameter. + /// + public const string ClientSecret = "AzureAdB2C:ClientSecret"; + + /// + /// The MicrosoftOnlineUri parameter. + /// + public const string MicrosoftOnlineUri = "https://login.microsoftonline.com/"; + + /// + /// The GraphUri parameter. + /// + public const string GraphUri = "https://graph.microsoft.com/.default"; + + /// + /// The Instance parameter. + /// + public const string Instance = "AzureAdB2C:Instance"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/Claims.cs b/Core.Cerberos.Adapters/Common/Constants/Claims.cs new file mode 100644 index 0000000..e1977aa --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Claims.cs @@ -0,0 +1,73 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for claims used in JWT tokens. + /// + public class Claims + { + /// + /// Claim name for user's name. + /// + public const string Name = "name"; + + /// + /// Claim name for user's guid. + /// + public const string GUID = "guid"; + + /// + /// Claim name for user's ID. + /// + public const string Id = "id"; + + /// + /// Claim name for user's role ID. + /// + public const string Role = "role"; + + /// + /// Claim name for user's role Iidentifier. + /// + public const string RoleId = "roleId"; + + /// + /// Claim name for user's companies. + /// + public const string Companies = "companies"; + + /// + /// Claim name for user's projects. + /// + public const string Projects = "projects"; + + /// + /// Claim name for user's applications. + /// + public const string Applications = "applications"; + + /// + /// Claim name for application's modules. + /// + public const string Modules = "modules"; + + /// + /// Claim name for user's permissions. + /// + public const string Permissions = "permissions"; + + /// + /// Claim name for user's ID. + /// + public const string Email = "email"; + + /// + /// Claim name for user's role. + /// + public const string LSARoleId = "LSARoleId"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/CollectionNames.cs b/Core.Cerberos.Adapters/Common/Constants/CollectionNames.cs new file mode 100644 index 0000000..f611d40 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/CollectionNames.cs @@ -0,0 +1,25 @@ +namespace Core.Cerberos.Adapters.Common.Constants +{ + public static class CollectionNames + { + /// + /// The User collection name. + /// + public const string User = "Users"; + + /// + /// The Role collection name. + /// + public const string Role = "Roles"; + + /// + /// The Permission collection name. + /// + public const string Permission = "Permissions"; + + /// + /// The Module collection name. + /// + public const string Module = "Modules"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/EnvironmentVariables.cs b/Core.Cerberos.Adapters/Common/Constants/EnvironmentVariables.cs new file mode 100644 index 0000000..d58e337 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/EnvironmentVariables.cs @@ -0,0 +1,21 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants of the environment variables for this service. + /// + public static class EnvironmentVariables + { + /// + /// The stage environment vriable. + /// + public const string Stage = "ASPNETCORE_ENVIRONMENT"; + } +} + + diff --git a/Core.Cerberos.Adapters/Common/Constants/KeyVaultConfiguration.cs b/Core.Cerberos.Adapters/Common/Constants/KeyVaultConfiguration.cs new file mode 100644 index 0000000..c09ec85 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/KeyVaultConfiguration.cs @@ -0,0 +1,33 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for Key Vault configuration. + /// + public class KeyVaultConfiguration + { + /// + /// The KeyVaultUrl parameter. + /// + public const string KeyVaultUrl = "KeyVaultConfiguration:KeyVaultUrl"; + + /// + /// The TenantId parameter. + /// + public const string TenantId = "KeyVaultConfiguration:TenantId"; + + /// + /// The ClientId parameter. + /// + public const string ClientId = "KeyVaultConfiguration:ClientId"; + + /// + /// The ClientSecretId parameter. + /// + public const string ClientSecretId = "KeyVaultConfiguration:ClientSecretId"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/MimeTypes.cs b/Core.Cerberos.Adapters/Common/Constants/MimeTypes.cs new file mode 100644 index 0000000..d9bffc7 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/MimeTypes.cs @@ -0,0 +1,153 @@ +// *********************************************************************** +// +// Axen IT +// +// *********************************************************************** + +using System.Globalization; + +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for the mime types. + /// + public static class MimeTypes + { + /// + /// The application version. + /// + public const string ApplicationVersion = "1.0"; + + /// + /// The service application/json mime type. + /// + public const string ApplicationJson = "application/json"; + + /// + /// The application/pdf mime type. + /// + public const string ApplicationPdf = "application/pdf"; + + /// + /// The end index. + /// + public const int EndIndex = 5; + + /// + /// The JPEG extension. + /// + public const string ExtensionGif = "gif"; + + /// + /// The JPEG extension. + /// + public const string ExtensionJpeg = "jpeg"; + + /// + /// The PNG extension. + /// + public const string ExtensionPng = "png"; + + /// + /// The SVG extension. + /// + public const string ExtensionSvg = "svg"; + + /// + /// The image/gif mime type. + /// + public const string ImageGif = "image/gif"; + + /// + /// The image/jpeg mime type. + /// + public const string ImageJpeg = "image/jpeg"; + + /// + /// The image/png mime type. + /// + public const string ImagePng = "image/png"; + + /// + /// The image/svg+xml mime type. + /// + public const string ImageSvg = "image/svg+xml"; + + /// + /// The identifier GIF. + /// + public const string IdentifierGif = "R0LGO"; + + /// + /// The identifier PNG. + /// + public const string IdentifierJpeg = "/9J/4"; + + /// + /// The identifier PDF. + /// + public const string IdentifierPdf = "JVBER"; + + /// + /// The identifier PNG. + /// + public const string IdentifierPng = "IVBOR"; + + /// + /// The identifier SVG. + /// + public const string IdentifierSvg = "PHN2Z"; + + /// + /// The parameter name. + /// + public const string ParameterName = "MimeType"; + + /// + /// The start index. + /// + public const int StartIndex = 0; + + /// + /// The mime type dictionary. + /// + public static readonly Dictionary Dictionary = new Dictionary + { + { IdentifierJpeg, ImageJpeg }, + { IdentifierPng, ImagePng }, + { IdentifierGif, ImageGif }, + { IdentifierSvg, ImageSvg }, + }; + + /// + /// The mime type dictionary. + /// + public static readonly Dictionary DictionaryExtension = new Dictionary + { + { IdentifierJpeg, ExtensionJpeg }, + { IdentifierPng, ExtensionPng }, + { IdentifierGif, ExtensionGif }, + { IdentifierSvg, ExtensionSvg }, + }; + + /// + /// Gets the mime type. + /// + /// The content with mime type identifier, substring 0, 5 from content. + /// A representing the value. + public static string GetMimeType(this string content) + { + return Dictionary.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value; + } + + /// + /// Gets the extension. + /// + /// The mime type identifier, substring 0, 5 from content. + /// A representing the value. + public static string GetExtension(this string content) + { + return DictionaryExtension.FirstOrDefault(_ => _.Key == content[..EndIndex].ToUpper(CultureInfo.InvariantCulture)).Value; + } + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/Routes.cs b/Core.Cerberos.Adapters/Common/Constants/Routes.cs new file mode 100644 index 0000000..440aac9 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Routes.cs @@ -0,0 +1,114 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants of the routes of this service. + /// + public static class Routes + { + /// + /// The User route. + /// + public const string User = "users"; + + /// + /// The Register User route. + /// + public const string Register = "{sendInvitation}/send-invitation/register"; + + /// + /// The identifier route. + /// + public const string Id = "{id}"; + + /// + /// The Authentication route. + /// + public const string Authentication = "api/v1/authentication"; + + /// + /// The LogIn route. + /// + public const string LogIn = "{email}/login"; + + /// + /// The LogOut route. + /// + public const string LogOut = "{email}/logout"; + + /// + /// The Generate Token route. + /// + public const string GenerateToken = "GenerateToken"; + + /// + /// The refresh token route. + /// + public const string RefreshToken = "RefreshToken"; + + /// + /// The InviteUser route. + /// + public const string InviteUser = "invite-user"; + + /// + /// The role identifier route. + /// + public const string RoleId = "role/{roleId}"; + + /// + /// The GetPermissionList route. + /// + public const string GetPermissionList = "GetPermissionList"; + + /// + /// The GetModuleList route. + /// + public const string GetModuleList = "GetModuleList"; + + /// + /// The ChangeStatus route. + /// + public const string ChangeStatus = "{id}/{newStatus}/ChangeStatus"; + + /// + /// The AddCompany route. + /// + public const string AddCompany = "{userId}/Companies/{companyId}/Add"; + + /// + /// The RemoveCompany route. + /// + public const string RemoveCompany = "{userId}/Companies/{companyId}/Remove"; + + /// + /// The AddProject route. + /// + public const string AddProject = "{userId}/Projects/{projectId}/Add"; + + /// + /// The RemoveProject route. + /// + public const string RemoveProject = "{userId}/Projects/{projectId}/Remove"; + + /// + /// The AddApplication route. + /// + public const string AddApplication = "{roleId}/{application}/AddApplication"; + + /// + /// The RemoveApplication route. + /// + public const string RemoveApplication = "{roleId}/{application}/RemoveApplication"; + + /// + /// The email route. + /// + public const string Email = "{email}/GetByEmail"; + } +} \ No newline at end of file diff --git a/Core.Cerberos.Adapters/Common/Constants/Schemes.cs b/Core.Cerberos.Adapters/Common/Constants/Schemes.cs new file mode 100644 index 0000000..67e77e5 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Schemes.cs @@ -0,0 +1,18 @@ +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for schemes. + /// + public class Schemes + { + /// + /// The heath scheme. + /// + public const string HeathScheme = "HeathScheme"; + + /// + /// The azure scheme. + /// + public const string AzureScheme = "AzureScheme"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/Secrets.cs b/Core.Cerberos.Adapters/Common/Constants/Secrets.cs new file mode 100644 index 0000000..a3fc7c7 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Secrets.cs @@ -0,0 +1,59 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +namespace Core.Cerberos.Adapters.Common.Constants +{ + /// + /// Constants for secrets in azure key vault. + /// + public class Secrets + { + /// + /// The MongoDBName parameter. + /// + public const string MongoDBName = "MongoDBName"; + + /// + /// The MongoDBConnection parameter. + /// + public const string MongoDBConnection = "MongoDBConnection"; + + /// + /// The Issuer parameter for JWT settings. + /// + public const string Issuer = "Issuer"; + + /// + /// The Audience parameter for JWT settings. + /// + public const string Audience = "Audience"; + + /// + /// The TokenExpirationInMinutes parameter for JWT settings. + /// + public const string TokenExpirationInMinutes = "TokenExpirationInMinutes"; + + /// + /// The TokenExpirationInHours parameter for JWT settings. + /// + public const string TokenExpirationInHours = "TokenExpirationInHours"; + + /// + /// The IssuerSigningKey parameter for JWT settings. + /// + public const string IssuerSigningKey = "IssuerSigningKey"; + + public const string AzureADInstance = "B2C:InstanceUri"; + public const string AzureADTenantId = "B2C:TenantId"; + public const string AzureADClientId = "B2C:ClientId"; + public const string AzureADClientSecret = "B2C:ClientSecret"; + public const string HeathCerberosAppAuthorizationUrl = "Swagger:AuthorizationUri"; + public const string HeathCerberosAppTokenUrl = "Swagger:TokenUri"; + public const string HeathCerberosAppClientId = "Swagger:ClientId"; + public const string HeathCerberosAppScope = "Swagger:Scope"; + public const string PrivateKey = "B2C:JwtIssuerOptions:TokenPrivateKey"; + public const string PublicKey = "B2C:JwtIssuerOptions:TokenPublicKey"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Enums/ApplicationsEnum.cs b/Core.Cerberos.Adapters/Common/Enums/ApplicationsEnum.cs new file mode 100644 index 0000000..c51bf6f --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Enums/ApplicationsEnum.cs @@ -0,0 +1,42 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters.Common.Enums +{ + /// + /// Defines the applications associated with the role. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ApplicationsEnum + { + /// + /// LSA Web Portal application. + /// + LSAWebPortal = 0, + + /// + /// Customer DashBoard application. + /// + CustomerDashboard = 1, + + /// + /// Discover application. + /// + Discover = 2, + + /// + /// LSA Mobile application. + /// + LSAMobile = 3, + + /// + /// BluePrint application. + /// + BluePrint = 4, + } +} diff --git a/Core.Cerberos.Adapters/Common/Enums/StatusEnum.cs b/Core.Cerberos.Adapters/Common/Enums/StatusEnum.cs new file mode 100644 index 0000000..213cad4 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Enums/StatusEnum.cs @@ -0,0 +1,32 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters.Common.Enums +{ + /// + /// Defines the status of an entity. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum StatusEnum + { + /// + /// Indicates the entity is active. + /// + Active = 0, + + /// + /// Indicates the entity is inactive. + /// + Inactive = 1, + + /// + /// Indicates the entity is deleted. + /// + Deleted = 2 + } +} diff --git a/Core.Cerberos.Adapters/Common/SchemaFilters/EnumArrayJsonConverter.cs b/Core.Cerberos.Adapters/Common/SchemaFilters/EnumArrayJsonConverter.cs new file mode 100644 index 0000000..686f088 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/SchemaFilters/EnumArrayJsonConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +public class EnumArrayJsonConverter : JsonConverter where T : Enum +{ + public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = JsonSerializer.Deserialize(ref reader, options); + return Array.ConvertAll(values, value => (T)Enum.Parse(typeof(T), value)); + } + + public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) + { + var stringValues = Array.ConvertAll(value, v => v.ToString()); + JsonSerializer.Serialize(writer, stringValues, options); + } +} diff --git a/Core.Cerberos.Adapters/Common/SchemaFilters/EnumSchemaFilter.cs b/Core.Cerberos.Adapters/Common/SchemaFilters/EnumSchemaFilter.cs new file mode 100644 index 0000000..cdc9480 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/SchemaFilters/EnumSchemaFilter.cs @@ -0,0 +1,31 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +/// +/// Applies enumeration schema to OpenAPI schema definitions. +/// +public class EnumSchemaFilter : ISchemaFilter +{ + /// + /// Applies the schema filter to an OpenAPI schema. + /// + /// The OpenAPI schema to apply the filter to. + /// The context information for the schema filter. + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type.IsEnum) + { + schema.Enum.Clear(); + Enum.GetNames(context.Type) + .ToList() + .ForEach(name => schema.Enum.Add(new OpenApiString(name))); + } + } +} diff --git a/Core.Cerberos.Adapters/Contracts/ITokenProvider.cs b/Core.Cerberos.Adapters/Contracts/ITokenProvider.cs new file mode 100644 index 0000000..2eaf533 --- /dev/null +++ b/Core.Cerberos.Adapters/Contracts/ITokenProvider.cs @@ -0,0 +1,19 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +namespace Core.Cerberos.Adapters.Contracts +{ + /// + /// Interface for token provider. + /// + public interface ITokenProvider + { + /// + /// Get token from headers. + /// + string GetToken(); + } +} diff --git a/Core.Cerberos.Adapters/Contracts/ITokenService.cs b/Core.Cerberos.Adapters/Contracts/ITokenService.cs new file mode 100644 index 0000000..b68609a --- /dev/null +++ b/Core.Cerberos.Adapters/Contracts/ITokenService.cs @@ -0,0 +1,32 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Core.Cerberos.Adapters.Contracts +{ + /// + /// Interface for authenticacion service. + /// + public interface ITokenService + { + /// + /// Refreshes the access token. + /// + string GenerateAccessToken(TokenAdapter adapter); + + /// + /// Refreshes the access token. + /// + IActionResult RefreshAccessToken(HttpContext context, TokenAdapter adapter); + + /// + /// Extracts the user email claim from the http context. + /// + string GetEmailClaim(HttpContext httpContext); + } +} diff --git a/Core.Cerberos.Adapters/Core.Cerberos.Adapters.csproj b/Core.Cerberos.Adapters/Core.Cerberos.Adapters.csproj new file mode 100644 index 0000000..5bcf6c6 --- /dev/null +++ b/Core.Cerberos.Adapters/Core.Cerberos.Adapters.csproj @@ -0,0 +1,30 @@ + + + + + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/Core.Cerberos.Adapters/Extensions/AuthenticationExtension.cs b/Core.Cerberos.Adapters/Extensions/AuthenticationExtension.cs new file mode 100644 index 0000000..315c205 --- /dev/null +++ b/Core.Cerberos.Adapters/Extensions/AuthenticationExtension.cs @@ -0,0 +1,97 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Common.Constants; +using Core.Cerberos.Adapters.Contracts; +using Core.Cerberos.Adapters.Handlers; +using Core.Cerberos.Adapters.Options; +using Core.Cerberos.Adapters.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web; +using Microsoft.IdentityModel.Tokens; +using System.Security.Cryptography; + +namespace Core.Cerberos.Adapters.Extensions +{ + /// + /// Extension methods for configuring authentication with various Azure AD setups. + /// + public static class AuthenticationExtension + { + /// + /// Configures authentication using Azure AD for an API that requires downstream API access. + /// + /// The to add the services to. + /// The containing Azure AD configuration settings. + public static void ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration, AuthSettings authSettings) + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; + + var azureAdInMemorySettings = new Dictionary + { + { "AzureAdB2C:Instance", authSettings.AzureADInstance ?? string.Empty }, + { "AzureAdB2C:TenantId", authSettings.AzureADTenantId ?? string.Empty }, + { "AzureAdB2C:ClientId", authSettings.AzureADClientId ?? string.Empty }, + { "AzureAdB2C:ClientSecret", authSettings.AzureADClientSecret ?? string.Empty } + }; + + var configurationBuilder = new ConfigurationBuilder() + .AddConfiguration(configuration) + .AddInMemoryCollection(azureAdInMemorySettings); + + var combinedConfiguration = configurationBuilder.Build(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(combinedConfiguration.GetSection("AzureAdB2C"), Schemes.AzureScheme) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddMicrosoftGraph(configuration.GetSection("MicrosoftGraph")) + .AddInMemoryTokenCaches(); + + var rsa = RSA.Create(); + rsa.ImportFromPem(authSettings.PrivateKey?.ToCharArray()); + var rsaPrivateKey = new RsaSecurityKey(rsa); + + var rsaPublic = RSA.Create(); + rsaPublic.ImportFromPem(authSettings.PublicKey?.ToCharArray()); + var rsaPublicKey = new RsaSecurityKey(rsaPublic); + + var jwtAppSettingOptions = configuration.GetSection("B2C:JwtIssuerOptions"); + var jwtIssuerOptions = jwtAppSettingOptions.Get(); + + if (string.IsNullOrEmpty(jwtIssuerOptions?.Issuer) || string.IsNullOrEmpty(jwtIssuerOptions.Audience)) + throw new InvalidOperationException("JwtIssuerOptions are not configured correctly."); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(Schemes.HeathScheme, x => + { + x.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = jwtIssuerOptions?.Issuer, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidAudience = jwtIssuerOptions?.Audience, + IssuerSigningKey = rsaPublicKey + }; + }); + + services.Configure(options => + { + options.Issuer = jwtIssuerOptions?.Issuer; + options.Audience = jwtIssuerOptions?.Audience; + options.SigningCredentials = new SigningCredentials(rsaPrivateKey, SecurityAlgorithms.RsaSha256); + }); + + services.AddSingleton(jwtAppSettingOptions); + services.AddTransient(); + services.AddTransient(); + } + } +} diff --git a/Core.Cerberos.Adapters/Extensions/SwaggerExtensions.cs b/Core.Cerberos.Adapters/Extensions/SwaggerExtensions.cs new file mode 100644 index 0000000..ec311bb --- /dev/null +++ b/Core.Cerberos.Adapters/Extensions/SwaggerExtensions.cs @@ -0,0 +1,193 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Asp.Versioning.ApiExplorer; +using Core.Cerberos.Adapters.Common.Constants; +using Core.Cerberos.Adapters.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace Core.Cerberos.Adapters.Extensions +{ + /// + /// Extension methods for configuring Swagger documentation and UI. + /// + public static class SwaggerExtensions + { + private static readonly string? environment = Environment.GetEnvironmentVariable(EnvironmentVariables.Stage); + /// + /// Adds Swagger services to the specified . + /// + /// The to add the services to. + public static void AddSwagger(this IServiceCollection services, IConfiguration configuration, string DocumentationFile, AuthSettings authSettings) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(configuration, DocumentationFile, authSettings); + services.AddTransient, ConfigureSwaggerOptions>(); + } + + /// + /// Configures Swagger generation with OAuth2 security and XML comments. + /// + /// The to add the services to. + /// The containing Swagger and OAuth2 configuration settings. + public static void AddSwaggerGen(this IServiceCollection services, IConfiguration configuration, string DocumentationFile, AuthSettings authSettings) + { + services.AddSwaggerGen(c => + { + c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Description = "OAuth2.0 Authorization Code flow", + Name = "oauth2.0", + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(authSettings.HeathCerberosAppAuthorizationUrl ?? string.Empty), + TokenUrl = new Uri(authSettings.HeathCerberosAppTokenUrl ?? string.Empty), + Scopes = new Dictionary + { + { authSettings.HeathCerberosAppScope ?? string.Empty, "Access API as User" } + } + } + } + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }, + new[] { authSettings.HeathCerberosAppScope } + } + }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + + var filePath = Path.Combine(AppContext.BaseDirectory, DocumentationFile); + c.IncludeXmlComments(filePath); + c.SchemaFilter(); + }); + } + + /// + /// Configures Swagger and Swagger UI for the application. + /// + /// The instance. + /// The containing Swagger configuration settings. + public static void ConfigureSwagger(this WebApplication app, IConfiguration configuration) + { + app.UseSwagger(); + app.UseSwaggerUI(options => + { + foreach (var version in app.DescribeApiVersions().Select(version => version.GroupName)) + options.SwaggerEndpoint($"/swagger/{version}/swagger.json", version); + + options.DisplayRequestDuration(); + options.EnableTryItOutByDefault(); + options.DocExpansion(DocExpansion.None); + }); + } + + /// + /// Configures Swagger UI for the application with OAuth2 settings. + /// + /// The instance. + /// The containing Swagger UI and OAuth2 configuration settings. + public static void UseSwaggerUI(this WebApplication app, IConfiguration configuration, AuthSettings authSettings) + { + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Custom Auth API with Azure AD v1"); + options.OAuthClientId(authSettings.HeathCerberosAppClientId); + options.OAuthUsePkce(); + options.OAuthScopeSeparator(" "); + }); + } + + /// + /// Adds API versioning and API explorer to the application. + /// + /// The to add the services to. + /// The modified instance. + public static IServiceCollection AddVersioning(this IServiceCollection services, IConfiguration configuration) + { + services.AddApiVersioning(options => options.ReportApiVersions = true) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + return services; + } + } + + /// + /// Configures Swagger generation options. + /// + public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions + { + /// + /// Configures SwaggerGen options. + /// + /// The SwaggerGen options to configure. + public void Configure(SwaggerGenOptions options) + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, new OpenApiInfo + { + Title = AppDomain.CurrentDomain.FriendlyName, + Version = description.ApiVersion.ToString() + }); + } + + // Map DateOnly type to Swagger schema + options.MapType(() => new OpenApiSchema + { + Type = "string", + Format = "date", + Example = new OpenApiString(DateOnly.MinValue.ToString()) + }); + + // Customize schema IDs for Swagger + options.CustomSchemaIds(type => type.ToString().Replace("+", ".")); + } + } +} diff --git a/Core.Cerberos.Adapters/Extensions/TelemetryExtensions.cs b/Core.Cerberos.Adapters/Extensions/TelemetryExtensions.cs new file mode 100644 index 0000000..816fa0c --- /dev/null +++ b/Core.Cerberos.Adapters/Extensions/TelemetryExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace Core.Cerberos.Adapters.Extensions +{ + public static class TelemetryExtensions + { + public static void AddTelemetry(this IServiceCollection services) + { + // Add OpenTelemetry Tracing + services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("lsa.dashboard.bff.api")) + .WithTracing(tracing => tracing.AddAspNetCoreInstrumentation().AddConsoleExporter()) + .WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation().AddConsoleExporter()). + WithLogging(logs => logs.AddConsoleExporter()); + + } + } +} diff --git a/Core.Cerberos.Adapters/Extensions/TrackingMechanismExtension.cs b/Core.Cerberos.Adapters/Extensions/TrackingMechanismExtension.cs new file mode 100644 index 0000000..9dc847f --- /dev/null +++ b/Core.Cerberos.Adapters/Extensions/TrackingMechanismExtension.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; + +namespace Core.Cerberos.Adapters.Extensions +{ + public sealed class TrackingMechanismExtension : DelegatingHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public TrackingMechanismExtension(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_httpContextAccessor.HttpContext.Items.TryGetValue("TrackingId", out var trackingId)) + { + request.Headers.Add("TrackingId", trackingId.ToString()); + } + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Core.Cerberos.Adapters/Handlers/Adapters/PermissionsAuthorizationAdapter.cs b/Core.Cerberos.Adapters/Handlers/Adapters/PermissionsAuthorizationAdapter.cs new file mode 100644 index 0000000..82ea008 --- /dev/null +++ b/Core.Cerberos.Adapters/Handlers/Adapters/PermissionsAuthorizationAdapter.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Core.Cerberos.Adapters.Handlers.Adapters +{ + public class PermissionsAuthorizationAdapter : IAuthorizationRequirement + { + public PermissionsAuthorizationAdapter(string[] permission) + { + Permission = permission; + } + public string[] Permission { get; set; } + } +} diff --git a/Core.Cerberos.Adapters/Handlers/AuthenticatedHttpClientHandler.cs b/Core.Cerberos.Adapters/Handlers/AuthenticatedHttpClientHandler.cs new file mode 100644 index 0000000..dcf7231 --- /dev/null +++ b/Core.Cerberos.Adapters/Handlers/AuthenticatedHttpClientHandler.cs @@ -0,0 +1,29 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Contracts; + +namespace Core.Cerberos.Adapters.Handlers +{ + /// + /// Class to inject the token in all requests. + /// + public class AuthenticatedHttpClientHandler(ITokenProvider tokenProvider) : DelegatingHandler + { + private readonly ITokenProvider _tokenProvider = tokenProvider; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var token = _tokenProvider.GetToken(); + if (!string.IsNullOrEmpty(token)) + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + return await base.SendAsync(request, cancellationToken); + } + } +} diff --git a/Core.Cerberos.Adapters/Handlers/PermissionsAuthorizationHandler.cs b/Core.Cerberos.Adapters/Handlers/PermissionsAuthorizationHandler.cs new file mode 100644 index 0000000..cf15512 --- /dev/null +++ b/Core.Cerberos.Adapters/Handlers/PermissionsAuthorizationHandler.cs @@ -0,0 +1,18 @@ +using Core.Cerberos.Adapters.Handlers.Adapters; +using Microsoft.AspNetCore.Authorization; + +namespace Core.Cerberos.Adapters.Handlers +{ + public class PermissionsAuthorizationHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionsAuthorizationAdapter requirement) + { + if (context.User.Claims.Any(x => x.Type == "LSARoleId" && requirement.Permission.Contains(x.Value))) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Core.Cerberos.Adapters/Helpers/AuthHelper.cs b/Core.Cerberos.Adapters/Helpers/AuthHelper.cs new file mode 100644 index 0000000..f6ca9fd --- /dev/null +++ b/Core.Cerberos.Adapters/Helpers/AuthHelper.cs @@ -0,0 +1,52 @@ +using Azure.Identity; +using Core.Cerberos.Adapters.Common.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.Extensions.Logging; + +namespace Core.Cerberos.Adapters.Helpers +{ + public static class AuthHelper + { + private static readonly ILogger logger = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }).CreateLogger("AuthHelper"); + + + public static AuthSettings GetAuthSettings(WebApplicationBuilder builder, string appConfigLabel) + { + builder.Configuration.AddAzureAppConfiguration(options => + { + var endpoint = builder.Configuration.GetSection("Endpoints:AppConfigurationURI").Value; + + if (string.IsNullOrEmpty(endpoint)) + throw new ArgumentException("The app configuration is missing"); + + options.Connect(new Uri(endpoint), new DefaultAzureCredential()) + .Select(KeyFilter.Any, "cerberos_common") + .Select(KeyFilter.Any, appConfigLabel); + + options.ConfigureKeyVault(keyVaultOptions => + { + keyVaultOptions.SetCredential(new DefaultAzureCredential()); + }); + }); + + return new AuthSettings + { + AzureADInstance = builder.Configuration.GetSection(Secrets.AzureADInstance).Value, + AzureADTenantId = builder.Configuration.GetSection(Secrets.AzureADTenantId).Value, + AzureADClientId = builder.Configuration.GetSection(Secrets.AzureADClientId).Value, + AzureADClientSecret = builder.Configuration.GetSection(Secrets.AzureADClientSecret).Value, + HeathCerberosAppAuthorizationUrl = builder.Configuration.GetSection(Secrets.HeathCerberosAppAuthorizationUrl).Value, + HeathCerberosAppTokenUrl = builder.Configuration.GetSection(Secrets.HeathCerberosAppTokenUrl).Value, + HeathCerberosAppClientId = builder.Configuration.GetSection(Secrets.HeathCerberosAppClientId).Value, + HeathCerberosAppScope = builder.Configuration.GetSection(Secrets.HeathCerberosAppScope).Value, + PrivateKey = builder.Configuration.GetSection(Secrets.PrivateKey).Value, + PublicKey = builder.Configuration.GetSection(Secrets.PublicKey).Value, + }; + } + } +} diff --git a/Core.Cerberos.Adapters/Helpers/RsaHelper.cs b/Core.Cerberos.Adapters/Helpers/RsaHelper.cs new file mode 100644 index 0000000..1cd296c --- /dev/null +++ b/Core.Cerberos.Adapters/Helpers/RsaHelper.cs @@ -0,0 +1,94 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using System.Security.Cryptography; +using System.Text; + +namespace Core.Cerberos.Adapters.Helpers +{ + /// + /// Handles all methods related to RSA encryption"/>. + /// + public class RsaHelper + { + private readonly RSACryptoServiceProvider _privateKey; + private readonly RSACryptoServiceProvider _publicKey; + private readonly string keysFolder = "Keys\\"; + private readonly string exeDirectory = AppContext.BaseDirectory; + + /// + /// Initializes a new instance of . + /// + public RsaHelper() + { + exeDirectory = exeDirectory + keysFolder; + + _publicKey = GetPublicKeyFromPemFile(); + _privateKey = GetPrivateKeyFromPemFile(); + } + + /// + /// Encrypts a text using RSA algorithm. + /// + /// The text to be encrypted. + /// The encrypted text. + public string Encrypt(string text) + { + byte[] dataBytes = Encoding.UTF8.GetBytes(text); + var encryptedBytes = _publicKey.Encrypt(Encoding.UTF8.GetBytes(text), true); + return Convert.ToBase64String(encryptedBytes); + } + + /// + /// Decrypts a text using RSA algorithm. + /// + /// The encrypted text to be decrypted. + /// The decrypted text. + public string Decrypt(string encrypted) + { + var decryptedBytes = _privateKey.Decrypt(Convert.FromBase64String(encrypted), true); + return Encoding.UTF8.GetString(decryptedBytes, 0, decryptedBytes.Length); + } + + /// + ///Obtains the private key from a file. + /// + /// The private key. + private RSACryptoServiceProvider GetPrivateKeyFromPemFile() + { + using (TextReader privateKeyTextReader = new StringReader(File.ReadAllText(Path.Combine(exeDirectory, "HeathPrivateKey.pem")))) + { + AsymmetricCipherKeyPair readKeyPair = (AsymmetricCipherKeyPair)new PemReader(privateKeyTextReader).ReadObject(); + + RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)readKeyPair.Private); + RSACryptoServiceProvider csp = new RSACryptoServiceProvider(); + csp.ImportParameters(rsaParams); + return csp; + } + } + + /// + ///Obtains the public key from a file. + /// + /// The public key. + public RSACryptoServiceProvider GetPublicKeyFromPemFile() + { + using (TextReader publicKeyTextReader = new StringReader(File.ReadAllText(Path.Combine(exeDirectory, "HeathPublicKey.pem")))) + { + RsaKeyParameters publicKeyParam = (RsaKeyParameters)new PemReader(publicKeyTextReader).ReadObject(); + + RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKeyParam); + + RSACryptoServiceProvider csp = new RSACryptoServiceProvider(); + csp.ImportParameters(rsaParams); + return csp; + } + } + } +} diff --git a/Core.Cerberos.Adapters/Options/JwtIssuerOptions.cs b/Core.Cerberos.Adapters/Options/JwtIssuerOptions.cs new file mode 100644 index 0000000..d103d15 --- /dev/null +++ b/Core.Cerberos.Adapters/Options/JwtIssuerOptions.cs @@ -0,0 +1,60 @@ +using Microsoft.IdentityModel.Tokens; + +namespace Core.Cerberos.Adapters.Options +{ + /// + /// JWT token Issuer options (used for JWT Factory) + /// + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class JwtIssuerOptions + { + /// + /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. + /// + public string? Issuer { get; set; } + + /// + /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. + /// + public string? Subject { get; set; } + + /// + /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. + /// + public string? Audience { get; set; } + + /// + /// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. + /// + public DateTime Expiration => IssuedAt.Add(ValidFor); + + /// + /// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. + /// + public DateTime NotBefore => DateTime.UtcNow; + + /// + /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. + /// + public DateTime IssuedAt => DateTime.UtcNow; + + /// + /// Set the timespan the token will be valid for (default is 120 min) + /// + public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(525601); + + + + /// + /// "jti" (JWT ID) Claim (default ID is a GUID) + /// + public Func> JtiGenerator => + () => Task.FromResult(Guid.NewGuid().ToString()); + + /// + /// The signing key to use when generating tokens. + /// + public SigningCredentials? SigningCredentials { get; set; } + } +} diff --git a/Core.Cerberos.Adapters/Services/TokenService.cs b/Core.Cerberos.Adapters/Services/TokenService.cs new file mode 100644 index 0000000..937fb4c --- /dev/null +++ b/Core.Cerberos.Adapters/Services/TokenService.cs @@ -0,0 +1,146 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** +using Core.Cerberos.Adapters.Common.Constants; +using Core.Cerberos.Adapters.Contracts; +using Core.Cerberos.Adapters.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.Data; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text.Json; + +namespace Core.Cerberos.Adapters.Services +{ + /// + /// Service responsible for manage authenticacion. + /// + public class TokenService : ITokenService + { + private readonly JwtSecurityTokenHandler tokenHandler; + private readonly IConfiguration configuration; + private readonly JwtIssuerOptions jwtOptions; + private readonly JsonSerializerOptions jsonOptions; + + /// + /// Initializes a new instance of the class. + /// + public TokenService( + IConfiguration configuration, + IOptions jwtOptions + ) + { + tokenHandler = new JwtSecurityTokenHandler(); + this.configuration = configuration; + this.jwtOptions = jwtOptions.Value; + jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + } + + /// + /// Refreshes the token. + /// + public IActionResult RefreshAccessToken(HttpContext httpContext, TokenAdapter tokenAdapter) + { + var tokenString = httpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + + if (tokenString is not null) + { + var oldToken = tokenHandler.ReadJwtToken(tokenString); + + var tokenExpiration = oldToken.Claims.FirstOrDefault(c => c.Type == "exp")?.Value; + + var difference = ValidateTokenExpiration(tokenExpiration ?? ""); + + if (difference.Value.TotalMinutes <= 5) + + return new OkObjectResult(GenerateAccessToken(tokenAdapter)); + } + + return new BadRequestObjectResult("The token could not be refreshed"); + } + + /// + /// Generates a JWT token for the provided user data. + /// + /// The user data. + /// The user DTO with the generated token. + public string GenerateAccessToken(TokenAdapter adapter) + { + + + var hours = 1; + var minutes = 0; + var expires = DateTime.UtcNow + .AddHours(hours) + .AddMinutes(minutes); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new Claim[] + { + + new Claim(Claims.Name, adapter?.User?.DisplayName ?? string.Empty), + new Claim(Claims.GUID, adapter?.User?.Guid ?? string.Empty), + new Claim(Claims.Email, adapter?.User?.Email ?? string.Empty), + new Claim(Claims.Role, adapter?.Role?.Name ?? string.Empty), + new Claim(Claims.RoleId, adapter?.Role?.Id ?? string.Empty), + new Claim(Claims.Applications, JsonSerializer.Serialize(adapter?.Role?.Applications), JsonClaimValueTypes.JsonArray), + new Claim(Claims.Modules, JsonSerializer.Serialize(adapter?.Modules?.Select(m => new { m.Name, m.Application, m.Route, m.Icon, m.Order }), jsonOptions), JsonClaimValueTypes.JsonArray), + new Claim(Claims.Companies, JsonSerializer.Serialize(adapter?.User?.Companies), JsonClaimValueTypes.JsonArray), + new Claim(Claims.Projects, JsonSerializer.Serialize(adapter?.User?.Projects), JsonClaimValueTypes.JsonArray), + new Claim(Claims.Permissions, JsonSerializer.Serialize(adapter?.Permissions?.Select(p => $"{p.Name}.{p.AccessLevel}".Replace(" ", "")).ToArray()), JsonClaimValueTypes.JsonArray), + }), + + Expires = expires, + Issuer = jwtOptions.Issuer, + Audience = jwtOptions.Audience, + SigningCredentials = jwtOptions.SigningCredentials + }; + + var token = tokenHandler.CreateEncodedJwt(tokenDescriptor); + + return token; + } + + public ActionResult ValidateTokenExpiration(string tokenExpiration) + { + long unixTimestamp = long.Parse(tokenExpiration ?? "0"); + DateTimeOffset dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(unixTimestamp); + DateTime dateTimeExpiration = dateTimeOffset.UtcDateTime; + + var difference = dateTimeExpiration - DateTime.UtcNow; + + if (difference.TotalMinutes <= 0) + + return new BadRequestObjectResult("Expired token"); + + else return difference; + } + + /// + /// Extracts the user email claim from the http context. + /// + public string GetEmailClaim(HttpContext httpContext) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var tokenString = httpContext.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last(); + var token = tokenHandler.ReadJwtToken(tokenString); + var email = !string.IsNullOrEmpty(token.Claims.FirstOrDefault(c => c.Type == "email")?.Value) + ? token.Claims.FirstOrDefault(c => c.Type == "email")?.Value + : token.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; + + return (email is not null) ? email : ""; + } + } +} \ No newline at end of file diff --git a/Core.Cerberos.Adapters/Settings/AuthSettings.cs b/Core.Cerberos.Adapters/Settings/AuthSettings.cs new file mode 100644 index 0000000..5becd38 --- /dev/null +++ b/Core.Cerberos.Adapters/Settings/AuthSettings.cs @@ -0,0 +1,25 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +public class AuthSettings +{ + // Azure AD Settings + public string? AzureADInstance { get; set; } + public string? AzureADTenantId { get; set; } + public string? AzureADClientId { get; set; } + public string? AzureADClientSecret { get; set; } + + // Heath Cerberos App Settings + public string? HeathCerberosAppAuthorizationUrl { get; set; } + public string? HeathCerberosAppTokenUrl { get; set; } + public string? HeathCerberosAppClientId { get; set; } + public string? HeathCerberosAppScope { get; set; } + + // Token Keys + public string? PrivateKey { get; set; } + public string? PublicKey { get; set; } +} + diff --git a/Core.Cerberos.Adapters/TokenProvider/HttpContextTokenProvider.cs b/Core.Cerberos.Adapters/TokenProvider/HttpContextTokenProvider.cs new file mode 100644 index 0000000..1ccece5 --- /dev/null +++ b/Core.Cerberos.Adapters/TokenProvider/HttpContextTokenProvider.cs @@ -0,0 +1,32 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using Core.Cerberos.Adapters.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Core.Cerberos.Adapters.TokenProvider +{ + /// + /// Class to return the access token to controllers. + /// + public class HttpContextTokenProvider : ITokenProvider + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextTokenProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Get token from headers. + /// + public string GetToken() + { + return _httpContextAccessor.HttpContext?.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + } + } +} diff --git a/Core.Cerberos.Adapters/UserExistenceAdapter.cs b/Core.Cerberos.Adapters/UserExistenceAdapter.cs new file mode 100644 index 0000000..32d1098 --- /dev/null +++ b/Core.Cerberos.Adapters/UserExistenceAdapter.cs @@ -0,0 +1,22 @@ +// *********************************************************************** +// +// Heath +// +// *********************************************************************** + +using System.Text.Json.Serialization; + +namespace Core.Cerberos.Adapters +{ + /// + /// Adapter representing a user. + /// + public class UserExistenceAdapter + { + /// + /// user existence. + /// + [JsonPropertyName("existence")] + public bool Existence { get; set; } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8917575 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +SharedLibs \ No newline at end of file