From cd3050d1d2b493b49fe2168369506a9666084e0f Mon Sep 17 00:00:00 2001 From: Steven Kuhn Date: Tue, 6 Sep 2022 21:32:29 -0500 Subject: [PATCH] Add initial project, build, and test files --- .nuke/build.schema.json | 122 +++ .nuke/parameters.json | 4 + NOTICE | 13 + Sknet.Openiddict.LiteDB.sln | 53 ++ Sknet.Openiddict.LiteDB.v3.ncrunchsolution | 6 + build.cmd | 7 + build.ps1 | 69 ++ build.sh | 62 ++ build/.editorconfig | 11 + build/Build.cs | 89 ++ build/Build.csproj | 21 + build/Build.csproj.DotSettings | 26 + build/Configuration.cs | 11 + build/Directory.Build.props | 8 + build/Directory.Build.targets | 8 + build/Usings.cs | 26 + icon.png | Bin 0 -> 13023 bytes .../OpenIddictLiteDBApplication.cs | 105 +++ .../OpenIddictLiteDBAuthorization.cs | 77 ++ .../OpenIddictLiteDBScope.cs | 79 ++ .../OpenIddictLiteDBToken.cs | 106 +++ .../Sknet.OpenIddict.LiteDB.Models.csproj | 46 + src/Sknet.OpenIddict.LiteDB.Models/Usings.cs | 4 + .../IOpenIddictLiteDBContext.cs | 31 + .../OpenIddictLiteDBBuilder.cs | 189 ++++ .../OpenIddictLiteDBContext.cs | 54 ++ .../OpenIddictLiteDBExtensions.cs | 89 ++ .../OpenIddictLiteDBOptions.cs | 48 ++ ...penIddictLiteDBApplicationStoreResolver.cs | 55 ++ ...nIddictLiteDBAuthorizationStoreResolver.cs | 55 ++ .../OpenIddictLiteDBScopeStoreResolver.cs | 55 ++ .../OpenIddictLiteDBTokenStoreResolver.cs | 55 ++ .../Sknet.OpenIddict.LiteDB.csproj | 45 + .../OpenIddictLiteDBApplicationStore.cs | 651 ++++++++++++++ .../OpenIddictLiteDBAuthorizationStore.cs | 718 ++++++++++++++++ .../Stores/OpenIddictLiteDBScopeStore.cs | 517 +++++++++++ .../Stores/OpenIddictLiteDBTokenStore.cs | 809 ++++++++++++++++++ src/Sknet.OpenIddict.LiteDB/Usings.cs | 15 + .../OpenIddictLiteDBBuilderTests.cs | 281 ++++++ .../OpenIddictLiteDBContextTests.cs | 110 +++ .../OpenIddictLiteDBExtensionsTests.cs | 117 +++ ...dictLiteDBApplicationStoreResolverTests.cs | 72 ++ ...ctLiteDBAuthorizationStoreResolverTests.cs | 72 ++ ...OpenIddictLiteDBScopeStoreResolverTests.cs | 72 ++ ...OpenIddictLiteDBTokenStoreResolverTests.cs | 72 ++ .../Sknet.OpenIddict.LiteDB.Tests.csproj | 31 + test/Sknet.OpenIddict.LiteDB.Tests/Usings.cs | 8 + 47 files changed, 5174 insertions(+) create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json create mode 100644 NOTICE create mode 100644 Sknet.Openiddict.LiteDB.sln create mode 100644 Sknet.Openiddict.LiteDB.v3.ncrunchsolution create mode 100644 build.cmd create mode 100644 build.ps1 create mode 100644 build.sh create mode 100644 build/.editorconfig create mode 100644 build/Build.cs create mode 100644 build/Build.csproj create mode 100644 build/Build.csproj.DotSettings create mode 100644 build/Configuration.cs create mode 100644 build/Directory.Build.props create mode 100644 build/Directory.Build.targets create mode 100644 build/Usings.cs create mode 100644 icon.png create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj create mode 100644 src/Sknet.OpenIddict.LiteDB.Models/Usings.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj create mode 100644 src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBTokenStore.cs create mode 100644 src/Sknet.OpenIddict.LiteDB/Usings.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBBuilderTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBContextTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBExtensionsTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBApplicationStoreResolverTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBAuthorizationStoreResolverTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBScopeStoreResolverTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBTokenStoreResolverTests.cs create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Sknet.OpenIddict.LiteDB.Tests.csproj create mode 100644 test/Sknet.OpenIddict.LiteDB.Tests/Usings.cs diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..f3ff562 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "GitHubAccessToken": { + "type": "string", + "description": "GitHub access token used for creating a new or updating an existing release" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "NuGetApiKey": { + "type": "string", + "description": "NuGet API key used to pushing the Sdk NuGet package" + }, + "NuGetSource": { + "type": "string", + "description": "NuGet source used for pushing the Sdk NuGet package. Default is NuGet.org" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Restore" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Restore" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..becac29 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Sknet.Openiddict.LiteDB.sln" +} \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..30ed802 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +======================================================================= +Sknet.OpenIddict.LiteDB +Copyright (c) 2022 Steven Kuhn and contributors. All rights reserved. +======================================================================= + +This product is based on the OpenIddict (openiddict/openiddict-core) project developed by Kévin Chalet and its contributors at https://github.com/openiddict/openiddict-core. + +======================================================================= +OpenIddict +© Kévin Chalet. All rights reserved. +======================================================================= + +Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0). See https://github.com/openiddict/openiddict-core for more information concerning the license and the contributors participating to this project. \ No newline at end of file diff --git a/Sknet.Openiddict.LiteDB.sln b/Sknet.Openiddict.LiteDB.sln new file mode 100644 index 0000000..8c02c46 --- /dev/null +++ b/Sknet.Openiddict.LiteDB.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32825.248 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CB8DF41A-EFAE-4CDD-A8CB-558CF2F50E5F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sknet.OpenIddict.LiteDB", "src\Sknet.OpenIddict.LiteDB\Sknet.OpenIddict.LiteDB.csproj", "{02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sknet.OpenIddict.LiteDB.Models", "src\Sknet.OpenIddict.LiteDB.Models\Sknet.OpenIddict.LiteDB.Models.csproj", "{198BFA54-A239-4149-9A02-0259761A2127}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{217F1279-2978-4C25-B60F-0B65BA2E3B00}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Build", "build\Build.csproj", "{B2746FD1-1277-4BFC-954A-84753B12A028}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sknet.OpenIddict.LiteDB.Tests", "test\Sknet.OpenIddict.LiteDB.Tests\Sknet.OpenIddict.LiteDB.Tests.csproj", "{4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Release|Any CPU.Build.0 = Release|Any CPU + {198BFA54-A239-4149-9A02-0259761A2127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {198BFA54-A239-4149-9A02-0259761A2127}.Debug|Any CPU.Build.0 = Debug|Any CPU + {198BFA54-A239-4149-9A02-0259761A2127}.Release|Any CPU.ActiveCfg = Release|Any CPU + {198BFA54-A239-4149-9A02-0259761A2127}.Release|Any CPU.Build.0 = Release|Any CPU + {B2746FD1-1277-4BFC-954A-84753B12A028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2746FD1-1277-4BFC-954A-84753B12A028}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED} = {649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B} + {198BFA54-A239-4149-9A02-0259761A2127} = {649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B} + {B2746FD1-1277-4BFC-954A-84753B12A028} = {217F1279-2978-4C25-B60F-0B65BA2E3B00} + {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D} = {CB8DF41A-EFAE-4CDD-A8CB-558CF2F50E5F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6ADAF88E-F6DC-44EF-957A-79D15C271767} + EndGlobalSection +EndGlobal diff --git a/Sknet.Openiddict.LiteDB.v3.ncrunchsolution b/Sknet.Openiddict.LiteDB.v3.ncrunchsolution new file mode 100644 index 0000000..10420ac --- /dev/null +++ b/Sknet.Openiddict.LiteDB.v3.ncrunchsolution @@ -0,0 +1,6 @@ + + + True + True + + \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..8833a53 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,69 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\Build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "Current" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..10c6ced --- /dev/null +++ b/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/Build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..31e43dc --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..07c635a --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using Nuke.Common; +using Nuke.Common.CI; +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main () => Execute(x => x.Compile); + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Parameter("GitHub access token used for creating a new or updating an existing release.")] + readonly string GitHubAccessToken; + + [Parameter("NuGet source used for pushing the Sdk NuGet package. Default is NuGet.org.")] + readonly string NuGetSource = "https://api.nuget.org/v3/index.json"; + + [Parameter("NuGet API key used to pushing the Sdk NuGet package.")] + readonly string NuGetApiKey; + + [Solution] readonly Solution Solution; + [GitRepository] readonly GitRepository GitRepository; + [GitVersion(Framework = "net6.0", NoFetch = true)] readonly GitVersion GitVersion; + + static AbsolutePath SourceDirectory => RootDirectory / "src"; + static AbsolutePath TestsDirectory => RootDirectory / "test"; + static AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + + const string GitHubRepositoryName = "InRuleGitStorage"; + const string GitHubRepositoryOwner = "openiddict-litedb"; + + readonly string[] NuGetRestoreSources = new[] { + "https://api.nuget.org/v3/index.json" + }; + + protected override void OnBuildInitialized() + { + Log.Information($"GitVersion settings:\n{JsonConvert.SerializeObject(GitVersion, Formatting.Indented)}"); + } + + Target Clean => _ => _ + .Before(Restore) + .Executes(() => + { + Log.Debug("Deleting all bin/obj directories..."); + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + + Log.Debug("Cleaning artifacts directory..."); + EnsureCleanDirectory(ArtifactsDirectory); + + Log.Debug("Deleting test results directories..."); + TestsDirectory.GlobDirectories("**/TestResults").ForEach(DeleteDirectory); + }); + + Target Restore => _ => _ + .Executes(() => + { + Log.Debug("Restoring NuGet packages for solution..."); + DotNetRestore(s => s + .SetProjectFile(Solution) + .SetSources(NuGetRestoreSources)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + Log.Debug("Compiling solution..."); + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetVersion(GitVersion.FullSemVer) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + .SetProperty("RepositoryBranch", GitVersion.BranchName) + .SetProperty("RepositoryCommit", GitVersion.Sha) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + +} diff --git a/build/Build.csproj b/build/Build.csproj new file mode 100644 index 0000000..560b64f --- /dev/null +++ b/build/Build.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + CS0649;CS0169 + .. + .. + 1 + + + + + + + + + + + diff --git a/build/Build.csproj.DotSettings b/build/Build.csproj.DotSettings new file mode 100644 index 0000000..c8947fc --- /dev/null +++ b/build/Build.csproj.DotSettings @@ -0,0 +1,26 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 0000000..ad4e5ca --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,11 @@ +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/Directory.Build.props b/build/Directory.Build.props new file mode 100644 index 0000000..e147d63 --- /dev/null +++ b/build/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets new file mode 100644 index 0000000..2532609 --- /dev/null +++ b/build/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Usings.cs b/build/Usings.cs new file mode 100644 index 0000000..eb88c1b --- /dev/null +++ b/build/Usings.cs @@ -0,0 +1,26 @@ +global using System; +global using System.ComponentModel; +global using System.IO; +global using System.Linq; + +global using Newtonsoft.Json; +global using Serilog; +global using Nuke.Common; +global using Nuke.Common.CI; +global using Nuke.Common.Execution; +global using Nuke.Common.Git; +global using Nuke.Common.IO; +global using Nuke.Common.ProjectModel; +global using Nuke.Common.Tooling; +global using Nuke.Common.Tools.DotNet; +global using Nuke.Common.Tools.GitVersion; +global using Nuke.Common.Tools.MSBuild; +global using Nuke.Common.Tools.NuGet; +global using Nuke.Common.Utilities.Collections; + +global using static Nuke.Common.EnvironmentInfo; +global using static Nuke.Common.IO.CompressionTasks; +global using static Nuke.Common.IO.FileSystemTasks; +global using static Nuke.Common.IO.PathConstruction; +global using static Nuke.Common.Tools.DotNet.DotNetTasks; +global using static Nuke.Common.Tools.NuGet.NuGetTasks; \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..94fdf507b34edae2dd86d51bfcad12b028b3b4f6 GIT binary patch literal 13023 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 literal 0 HcmV?d00001 diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs new file mode 100644 index 0000000..d61a228 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Models; + +/// +/// Represents an OpenIddict application. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; Type = {Type,nq}")] +public class OpenIddictLiteDBApplication +{ + /// + /// Gets or sets the client identifier associated with the current application. + /// + [BsonField("client_id")] + public virtual string? ClientId { get; set; } + + /// + /// Gets or sets the client secret associated with the current application. + /// Note: depending on the application manager used to create this instance, + /// this property may be hashed or encrypted for security reasons. + /// + [BsonField("client_secret")] + public virtual string? ClientSecret { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + [BsonField("concurrency_token")] + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the consent type associated with the current application. + /// + [BsonField("consent_type")] + public virtual string? ConsentType { get; set; } + + /// + /// Gets or sets the display name associated with the current application. + /// + [BsonField("display_name")] + public virtual string? DisplayName { get; set; } + + /// + /// Gets or sets the localized display names associated with the current application. + /// + [BsonField("display_names")] + public virtual IReadOnlyDictionary? DisplayNames { get; set; } + = ImmutableDictionary.Create(); + + /// + /// Gets or sets the unique identifier associated with the current application. + /// + [BsonId] + public virtual ObjectId Id { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the permissions associated with the current application. + /// + [BsonField("permissions")] + public virtual IReadOnlyList? Permissions { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the logout callback URLs associated with the current application. + /// + [BsonField("post_logout_redirect_uris")] + public virtual IReadOnlyList? PostLogoutRedirectUris { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the additional properties associated with the current application. + /// + [BsonField("properties")] + public virtual IReadOnlyDictionary? Properties { get; set; } + + /// + /// Gets or sets the callback URLs associated with the current application. + /// + [BsonField("redirect_uris")] + public virtual IReadOnlyList? RedirectUris { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the requirements associated with the current application. + /// + [BsonField("requirements")] + public virtual IReadOnlyList? Requirements { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the application type + /// associated with the current application. + /// + [BsonField("type")] + public virtual string? Type { get; set; } +} diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs new file mode 100644 index 0000000..82c13d6 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Models; + +/// +/// Represents an OpenIddict authorization. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] +public class OpenIddictLiteDBAuthorization +{ + /// + /// Gets or sets the identifier of the application associated with the current authorization. + /// + [BsonField("application_id")] + public virtual ObjectId ApplicationId { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the concurrency token. + /// + [BsonField("concurrency_token")] + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the UTC creation date of the current authorization. + /// + [BsonField("creation_date")] + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the unique identifier associated with the current authorization. + /// + [BsonId] + public virtual ObjectId Id { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the additional properties associated with the current authorization. + /// + [BsonField("properties")] + public virtual IReadOnlyDictionary? Properties { get; set; } + + /// + /// Gets or sets the scopes associated with the current authorization. + /// + [BsonField("scopes")] + public virtual IReadOnlyList? Scopes { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the status of the current authorization. + /// + [BsonField("status")] + public virtual string? Status { get; set; } + + /// + /// Gets or sets the subject associated with the current authorization. + /// + [BsonField("subject")] + public virtual string? Subject { get; set; } + + /// + /// Gets or sets the type of the current authorization. + /// + [BsonField("type")] + public virtual string? Type { get; set; } +} diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs new file mode 100644 index 0000000..8f76a80 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Models; + +/// +/// Represents an OpenIddict scope. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")] +public class OpenIddictLiteDBScope +{ + /// + /// Gets or sets the concurrency token. + /// + [BsonField("concurrency_token")] + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the public description associated with the current scope. + /// + [BsonField("description")] + public virtual string? Description { get; set; } + + /// + /// Gets or sets the localized public descriptions associated with the current scope. + /// + [BsonField("descriptions")] + public virtual IReadOnlyDictionary? Descriptions { get; set; } + = ImmutableDictionary.Create(); + + /// + /// Gets or sets the display name associated with the current scope. + /// + [BsonField("display_name")] + public virtual string? DisplayName { get; set; } + + /// + /// Gets or sets the localized display names associated with the current scope. + /// + [BsonField("display_names")] + public virtual IReadOnlyDictionary? DisplayNames { get; set; } + = ImmutableDictionary.Create(); + + /// + /// Gets or sets the unique identifier associated with the current scope. + /// + [BsonId] + public virtual ObjectId Id { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the unique name associated with the current scope. + /// + [BsonField("name")] + public virtual string? Name { get; set; } + + /// + /// Gets or sets the additional properties associated with the current scope. + /// + [BsonField("properties")] + public virtual IReadOnlyDictionary? Properties { get; set; } + + /// + /// Gets or sets the resources associated with the current scope. + /// + [BsonField("resources")] + public virtual IReadOnlyList? Resources { get; set; } = ImmutableList.Create(); +} diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs new file mode 100644 index 0000000..859b9e3 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Models; + +/// +/// Represents an OpenIddict token. +/// +[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")] +public class OpenIddictLiteDBToken +{ + /// + /// Gets or sets the identifier of the application associated with the current token. + /// + [BsonField("application_id")] + public virtual ObjectId ApplicationId { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the identifier of the authorization associated with the current token. + /// + [BsonField("authorization_id")] + public virtual ObjectId AuthorizationId { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the concurrency token. + /// + [BsonField("concurrency_token")] + public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the UTC creation date of the current token. + /// + [BsonField("creation_date")] + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the UTC expiration date of the current token. + /// + [BsonField("expiration_date")] + public virtual DateTime? ExpirationDate { get; set; } + + /// + /// Gets or sets the unique identifier associated with the current token. + /// + [BsonId] + public virtual ObjectId Id { get; set; } = ObjectId.Empty; + + /// + /// Gets or sets the payload of the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be encrypted for security reasons. + /// + [BsonField("payload")] + public virtual string? Payload { get; set; } + + /// + /// Gets or sets the additional properties associated with the current token. + /// + [BsonField("properties")] + public virtual IReadOnlyDictionary? Properties { get; set; } + + /// + /// Gets or sets the UTC redemption date of the current token. + /// + [BsonField("redemption_date")] + public virtual DateTime? RedemptionDate { get; set; } + + /// + /// Gets or sets the reference identifier associated + /// with the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be hashed or encrypted for security reasons. + /// + [BsonField("reference_id")] + public virtual string? ReferenceId { get; set; } + + /// + /// Gets or sets the status of the current token. + /// + [BsonField("status")] + public virtual string? Status { get; set; } + + /// + /// Gets or sets the subject associated with the current token. + /// + [BsonField("subject")] + public virtual string? Subject { get; set; } + + /// + /// Gets or sets the type of the current token. + /// + [BsonField("type")] + public virtual string? Type { get; set; } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj b/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj new file mode 100644 index 0000000..8c0f9ab --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj @@ -0,0 +1,46 @@ + + + + net461;netstandard2.0;net6.0 + enable + enable + 10.0 + + Steven Kuhn + Copyright (c) 2022 Steven Kuhn and contributors + Document-oriented entities for the OpenIddict LiteDB stores. + true + True + True + icon.png + MIT + https://github.com/stevenkuhn/openiddict-litedb + README.md + $(PackageTags);litedb;models + git + https://github.com/stevenkuhn/openiddict-litedb.git + snupkg + + + + + + + + + True + \ + + + True + \ + + + + + + + + + + diff --git a/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs b/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs new file mode 100644 index 0000000..954fe3d --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs @@ -0,0 +1,4 @@ +global using LiteDB; +global using System.Collections.Immutable; +global using System.Diagnostics; +global using System.Text.Json; \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs b/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs new file mode 100644 index 0000000..68a872f --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Exposes the LiteDB database used by the OpenIddict stores. +/// +public interface IOpenIddictLiteDBContext +{ + /// + /// Gets the . + /// + /// + /// A that can be used to monitor the + /// asynchronous operation, whose result returns the LiteDB database. + /// + ValueTask GetDatabaseAsync(CancellationToken cancellationToken); +} diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs new file mode 100644 index 0000000..14e08ca --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes the necessary methods required to configure the OpenIddict LiteDB services. +/// +public class OpenIddictLiteDBBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictLiteDBBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict LiteDB configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictLiteDBBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default application entity. + /// + /// The . + public OpenIddictLiteDBBuilder ReplaceDefaultApplicationEntity() + where TApplication : OpenIddictLiteDBApplication + { + Services.Configure(options => options.DefaultApplicationType = typeof(TApplication)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default authorization entity. + /// + /// The . + public OpenIddictLiteDBBuilder ReplaceDefaultAuthorizationEntity() + where TAuthorization : OpenIddictLiteDBAuthorization + { + Services.Configure(options => options.DefaultAuthorizationType = typeof(TAuthorization)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default scope entity. + /// + /// The . + public OpenIddictLiteDBBuilder ReplaceDefaultScopeEntity() + where TScope : OpenIddictLiteDBScope + { + Services.Configure(options => options.DefaultScopeType = typeof(TScope)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default token entity. + /// + /// The . + public OpenIddictLiteDBBuilder ReplaceDefaultTokenEntity() + where TToken : OpenIddictLiteDBToken + { + Services.Configure(options => options.DefaultTokenType = typeof(TToken)); + + return this; + } + + /// + /// Replaces the default applications collection name (by default, openiddict.applications). + /// + /// The collection name + /// The . + public OpenIddictLiteDBBuilder SetApplicationsCollectionName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.ApplicationsCollectionName = name); + } + + /// + /// Replaces the default authorizations collection name (by default, openiddict.authorizations). + /// + /// The collection name + /// The . + public OpenIddictLiteDBBuilder SetAuthorizationsCollectionName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.AuthorizationsCollectionName = name); + } + + /// + /// Replaces the default scopes collection name (by default, openiddict.scopes). + /// + /// The collection name + /// The . + public OpenIddictLiteDBBuilder SetScopesCollectionName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.ScopesCollectionName = name); + } + + /// + /// Replaces the default tokens collection name (by default, openiddict.tokens). + /// + /// The collection name + /// The . + public OpenIddictLiteDBBuilder SetTokensCollectionName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.TokensCollectionName = name); + } + + /// + /// Configures the LiteDB stores to use the specified database + /// instead of retrieving it from the dependency injection container. + /// + /// The . + /// The . + public OpenIddictLiteDBBuilder UseDatabase(ILiteDatabase database) + { + if (database is null) + { + throw new ArgumentNullException(nameof(database)); + } + + return Configure(options => options.Database = database); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => base.Equals(obj); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() => base.ToString(); +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs new file mode 100644 index 0000000..0f707bf --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +public class OpenIddictLiteDBContext : IOpenIddictLiteDBContext +{ + private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; + + public OpenIddictLiteDBContext( + IOptionsMonitor options, + IServiceProvider provider) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + public ValueTask GetDatabaseAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return new(Task.FromCanceled(cancellationToken)); + } + + var database = _options.CurrentValue.Database; + if (database is null) + { + database = _provider.GetService(); + } + + if (database is null) + { + return new(Task.FromException( + new InvalidOperationException("No suitable LiteDB database service can be found.\r\nTo configure the OpenIddict LiteDB stores to use a specific database, use 'services.AddOpenIddict().AddCore().UseLiteDB().UseDatabase()' or register an 'ILiteDatabase' in the dependency injection container in 'ConfigureServices()'."))); + } + + return new(database); + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs new file mode 100644 index 0000000..05b6f5f --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Microsoft.Extensions.DependencyInjection; + + +/// +/// Exposes extensions allowing to register the OpenIddict LiteDB services. +/// +public static class OpenIddictLiteDBExtensions +{ + /// + /// Registers the LiteDB stores services in the DI container and + /// configures OpenIddict to use the LiteDB entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictLiteDBBuilder UseLiteDB(this OpenIddictCoreBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + // Note: Mongo uses simple binary comparison checks by default so the additional + // query filtering applied by the default OpenIddict managers can be safely disabled. + // builder.DisableAdditionalFiltering(); + + builder.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + + // Note: the LiteDB stores/resolvers don't depend on scoped/transient services and thus + // can be safely registered as singleton services and shared/reused across requests. + builder.ReplaceApplicationStoreResolver(ServiceLifetime.Singleton) + .ReplaceAuthorizationStoreResolver(ServiceLifetime.Singleton) + .ReplaceScopeStoreResolver(ServiceLifetime.Singleton) + .ReplaceTokenStoreResolver(ServiceLifetime.Singleton); + + builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBApplicationStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBAuthorizationStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBScopeStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBTokenStore<>)); + + builder.Services.TryAddSingleton(); + + return new OpenIddictLiteDBBuilder(builder.Services); + } + + /// + /// Registers the LiteDB stores services in the DI container and + /// configures OpenIddict to use the LiteDB entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the LiteDB services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictCoreBuilder UseLiteDB( + this OpenIddictCoreBuilder builder, Action configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseLiteDB()); + + return builder; + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs new file mode 100644 index 0000000..1c2dbb3 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Provides various settings needed to configure the OpenIddict LiteDB integration. +/// +public class OpenIddictLiteDBOptions +{ + /// + /// Gets or sets the name of the applications collection (by default, openiddict.applications). + /// + public string ApplicationsCollectionName { get; set; } = "openiddict.applications"; + + /// + /// Gets or sets the name of the authorizations collection (by default, openiddict.authorizations). + /// + public string AuthorizationsCollectionName { get; set; } = "openiddict.authorizations"; + + /// + /// Gets or sets the used by the OpenIddict stores. + /// If no value is explicitly set, the database is resolved from the DI container. + /// + public ILiteDatabase? Database { get; set; } + + /// + /// Gets or sets the name of the scopes collection (by default, openiddict.scopes). + /// + public string ScopesCollectionName { get; set; } = "openiddict.scopes"; + + /// + /// Gets or sets the name of the tokens collection (by default, openiddict.tokens). + /// + public string TokensCollectionName { get; set; } = "openiddict.tokens"; +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs new file mode 100644 index 0000000..33712d3 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Exposes a method allowing to resolve an application store. +/// +public class OpenIddictLiteDBApplicationStoreResolver : IOpenIddictApplicationStoreResolver +{ + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IServiceProvider _provider; + + public OpenIddictLiteDBApplicationStoreResolver(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + /// Returns an application store compatible with the specified application type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Application entity. + /// An . + public IOpenIddictApplicationStore Get() where TApplication : class + { + var store = _provider.GetService>(); + if (store is not null) + { + return store; + } + + var type = _cache.GetOrAdd(typeof(TApplication), key => + { + if (!typeof(OpenIddictLiteDBApplication).IsAssignableFrom(key)) + { + throw new InvalidOperationException("The specified application type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBApplication' entity or a custom entity that inherits from the 'OpenIddictLiteDBApplication' entity."); + } + + return typeof(OpenIddictLiteDBApplicationStore<>).MakeGenericType(key); + }); + + return (IOpenIddictApplicationStore)_provider.GetRequiredService(type); + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs new file mode 100644 index 0000000..5f0938e --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Exposes a method allowing to resolve an authorization store. +/// +public class OpenIddictLiteDBAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver +{ + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IServiceProvider _provider; + + public OpenIddictLiteDBAuthorizationStoreResolver(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + /// Returns an authorization store compatible with the specified authorization type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Authorization entity. + /// An . + public IOpenIddictAuthorizationStore Get() where TAuthorization : class + { + var store = _provider.GetService>(); + if (store is not null) + { + return store; + } + + var type = _cache.GetOrAdd(typeof(TAuthorization), key => + { + if (!typeof(OpenIddictLiteDBAuthorization).IsAssignableFrom(key)) + { + throw new InvalidOperationException("The specified authorization type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBAuthorization' entity or a custom entity that inherits from the 'OpenIddictLiteDBAuthorization' entity."); + } + + return typeof(OpenIddictLiteDBAuthorizationStore<>).MakeGenericType(key); + }); + + return (IOpenIddictAuthorizationStore)_provider.GetRequiredService(type); + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs new file mode 100644 index 0000000..f97b749 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Exposes a method allowing to resolve a scope store. +/// +public class OpenIddictLiteDBScopeStoreResolver : IOpenIddictScopeStoreResolver +{ + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IServiceProvider _provider; + + public OpenIddictLiteDBScopeStoreResolver(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + /// Returns a scope store compatible with the specified scope type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Scope entity. + /// An . + public IOpenIddictScopeStore Get() where TScope : class + { + var store = _provider.GetService>(); + if (store is not null) + { + return store; + } + + var type = _cache.GetOrAdd(typeof(TScope), key => + { + if (!typeof(OpenIddictLiteDBScope).IsAssignableFrom(key)) + { + throw new InvalidOperationException("The specified scope type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBScope' entity or a custom entity that inherits from the 'OpenIddictLiteDBScope' entity."); + } + + return typeof(OpenIddictLiteDBScopeStore<>).MakeGenericType(key); + }); + + return (IOpenIddictScopeStore)_provider.GetRequiredService(type); + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs new file mode 100644 index 0000000..dd7a791 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Exposes a method allowing to resolve a token store. +/// +public class OpenIddictLiteDBTokenStoreResolver : IOpenIddictTokenStoreResolver +{ + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IServiceProvider _provider; + + public OpenIddictLiteDBTokenStoreResolver(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + /// Returns a token store compatible with the specified token type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Token entity. + /// An . + public IOpenIddictTokenStore Get() where TToken : class + { + var store = _provider.GetService>(); + if (store is not null) + { + return store; + } + + var type = _cache.GetOrAdd(typeof(TToken), key => + { + if (!typeof(OpenIddictLiteDBToken).IsAssignableFrom(key)) + { + throw new InvalidOperationException("The specified token type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBToken' entity or a custom entity that inherits from the 'OpenIddictLiteDBToken' entity."); + } + + return typeof(OpenIddictLiteDBTokenStore<>).MakeGenericType(key); + }); + + return (IOpenIddictTokenStore)_provider.GetRequiredService(type); + } +} \ No newline at end of file diff --git a/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj b/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj new file mode 100644 index 0000000..8f098f1 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj @@ -0,0 +1,45 @@ + + + + net461;net472;net48;netstandard2.0;netstandard2.1;net6.0 + enable + enable + 10.0 + + Steven Kuhn + Copyright (c) 2022 Steven Kuhn and contributors + LiteDB stores for OpenIddict. + true + True + True + icon.png + MIT + https://github.com/stevenkuhn/openiddict-litedb + README.md + $(PackageTags);litedb + git + https://github.com/stevenkuhn/openiddict-litedb.git + snupkg + + + + + True + \ + + + True + \ + + + + + + + + + + + + + diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs new file mode 100644 index 0000000..5c088d2 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs @@ -0,0 +1,651 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Provides methods allowing to manage the applications stored in a database. +/// +/// The type of the Application entity. +public class OpenIddictLiteDBApplicationStore : IOpenIddictApplicationStore + where TApplication : OpenIddictLiteDBApplication +{ + public OpenIddictLiteDBApplicationStore( + IOpenIddictLiteDBContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictLiteDBContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return collection.LongCount(); + } + + /// + public virtual async ValueTask CountAsync( + Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return query(collection.FindAll().AsQueryable()).LongCount(); + } + + /// + public virtual async ValueTask CreateAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + collection.Insert(application); + } + + /// + public virtual async ValueTask DeleteAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + if (collection.DeleteMany(entity => + entity.Id == application.Id && + entity.ConcurrencyToken == application.ConcurrencyToken) == 0) + { + throw new OpenIddictExceptions.ConcurrencyException("The application was concurrently updated and cannot be persisted in its current state.\r\nReload the application from the database and retry the operation."); + } + + // Delete the authorizations associated with the application. + database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName) + .DeleteMany(authorization => authorization.ApplicationId == application.Id); + + // Delete the tokens associated with the application. + database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteMany(token => token.ApplicationId == application.Id); + } + + /// + public virtual async ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return collection.Find(application => application.ClientId == identifier).FirstOrDefault(); + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return collection.FindById(identifier); + } + + /// + public virtual IAsyncEnumerable FindByPostLogoutRedirectUriAsync( + string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + var applications = collection.Query() + .Where(entity => entity.PostLogoutRedirectUris.Contains(address)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var application in applications) + { + yield return application; + } + } + } + + /// + public virtual IAsyncEnumerable FindByRedirectUriAsync( + string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + var applications = collection.Query() + .Where(entity => entity.RedirectUris.Contains(address)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var application in applications) + { + yield return application; + } + } + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return query(collection.FindAll().AsQueryable(), state).FirstOrDefault(); + } + + /// + public virtual ValueTask GetClientIdAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.ClientId); + } + + /// + public virtual ValueTask GetClientSecretAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.ClientSecret); + } + + /// + public virtual ValueTask GetClientTypeAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.Type); + } + + /// + public virtual ValueTask GetConsentTypeAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.ConsentType); + } + + /// + public virtual ValueTask GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.DisplayName); + } + + /// + public virtual ValueTask> GetDisplayNamesAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.DisplayNames is not { Count: > 0 }) + { + return new(ImmutableDictionary.Create()); + } + + return new(application.DisplayNames.ToImmutableDictionary( + pair => CultureInfo.GetCultureInfo(pair.Key), + pair => pair.Value)); + } + + /// + public virtual ValueTask GetIdAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new(application.Id.ToString()); + } + + /// + public virtual ValueTask> GetPermissionsAsync( + TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Permissions is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(application.Permissions.ToImmutableArray()); + } + + /// + public virtual ValueTask> GetPostLogoutRedirectUrisAsync( + TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.PostLogoutRedirectUris is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(application.PostLogoutRedirectUris.ToImmutableArray()); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Properties is null || application.Properties.Count == 0) + { + return new(ImmutableDictionary.Create()); + } + + return new(application.Properties.ToImmutableDictionary()); + } + + /// + public virtual ValueTask> GetRedirectUrisAsync( + TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.RedirectUris is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(application.RedirectUris.ToImmutableArray()); + } + + /// + public virtual ValueTask> GetRequirementsAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Requirements is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(application.Requirements.ToImmutableArray()); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException("An error occurred while trying to create a new application instance.\r\nMake sure that the application entity is not abstract and has a public parameterless constructor or create a custom application store that overrides 'InstantiateAsync()' to use a custom factory.", exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync( + int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + var applications = collection + .Find(Query.All(), offset ?? 0, count ?? int.MaxValue) + .ToAsyncEnumerable(); + + await foreach (var application in applications) + { + yield return application; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + var applications = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable(); + + await foreach (var application in applications) + { + yield return application; + } + } + } + + /// + public virtual ValueTask SetClientIdAsync(TApplication application, + string? identifier, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientId = identifier; + + return default; + } + + /// + public virtual ValueTask SetClientSecretAsync(TApplication application, + string? secret, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientSecret = secret; + + return default; + } + + /// + public virtual ValueTask SetClientTypeAsync(TApplication application, + string? type, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.Type = type; + + return default; + } + + /// + public virtual ValueTask SetConsentTypeAsync(TApplication application, + string? type, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ConsentType = type; + + return default; + } + + /// + public virtual ValueTask SetDisplayNameAsync(TApplication application, + string? name, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.DisplayName = name; + + return default; + } + + /// + public virtual ValueTask SetDisplayNamesAsync(TApplication application, + ImmutableDictionary names, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (names is not { Count: > 0 }) + { + application.DisplayNames = null; + + return default; + } + + application.DisplayNames = names.ToImmutableDictionary( + pair => pair.Key.Name, + pair => pair.Value); + + return default; + } + + /// + public virtual ValueTask SetPermissionsAsync(TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (permissions.IsDefaultOrEmpty) + { + application.Permissions = null; + + return default; + } + + application.Permissions = permissions.ToImmutableList(); + + return default; + } + + /// + public virtual ValueTask SetPostLogoutRedirectUrisAsync(TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.PostLogoutRedirectUris = null; + + return default; + } + + application.PostLogoutRedirectUris = addresses.ToImmutableList(); + + return default; + } + + /// + public virtual ValueTask SetPropertiesAsync(TApplication application, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (properties is not { Count: > 0 }) + { + application.Properties = null; + + return default; + } + + application.Properties = properties; + + return default; + } + + /// + public virtual ValueTask SetRedirectUrisAsync(TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.RedirectUris = null; + + return default; + } + + application.RedirectUris = addresses.ToImmutableList(); + + return default; + } + + /// + public virtual ValueTask SetRequirementsAsync(TApplication application, + ImmutableArray requirements, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (requirements.IsDefaultOrEmpty) + { + application.Requirements = null; + + return default; + } + + application.Requirements = requirements.ToImmutableList(); + + return default; + } + + /// + public virtual async ValueTask UpdateAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + // Generate a new concurrency token and attach it + // to the application before persisting the changes. + var timestamp = application.ConcurrencyToken; + application.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + if (collection.Count(entity => entity.Id == application.Id && entity.ConcurrencyToken == timestamp) == 0) + { + throw new ConcurrencyException("The application was concurrently updated and cannot be persisted in its current state.\r\nReload the application from the database and retry the operation."); + } + + collection.Update(application); + } +} diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs new file mode 100644 index 0000000..cee4548 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs @@ -0,0 +1,718 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Provides methods allowing to manage the authorizations stored in a database. +/// +/// The type of the Authorization entity. +public class OpenIddictLiteDBAuthorizationStore : IOpenIddictAuthorizationStore + where TAuthorization : OpenIddictLiteDBAuthorization +{ + public OpenIddictLiteDBAuthorizationStore( + IOpenIddictLiteDBContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictLiteDBContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return collection.LongCount(); + } + + /// + public virtual async ValueTask CountAsync( + Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return query(collection.FindAll().AsQueryable()).LongCount(); + } + + /// + public virtual async ValueTask CreateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + collection.Insert(authorization); + } + + /// + public virtual async ValueTask DeleteAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + if (collection.DeleteMany(entity => + entity.Id == authorization.Id && + entity.ConcurrencyToken == authorization.ConcurrencyToken) == 0) + { + throw new OpenIddictExceptions.ConcurrencyException("The authorization was concurrently updated and cannot be persisted in its current state.\r\nReload the authorization from the database and retry the operation."); + } + + // Delete the tokens associated with the authorization. + database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteMany(token => token.AuthorizationId == authorization.Id); + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => + entity.Subject == subject && + entity.ApplicationId == new ObjectId(client)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, + string status, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => + entity.Subject == subject && + entity.ApplicationId == new ObjectId(client) && + entity.Status == status) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, + string status, string type, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => + entity.Subject == subject && + entity.ApplicationId == new ObjectId(client) && + entity.Status == status && + entity.Type == type) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, + string status, string type, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => + entity.Subject == subject && + entity.ApplicationId == new ObjectId(client) && + entity.Status == status && + entity.Type == type && + Enumerable.All(scopes, scope => entity.Scopes.Contains(scope))) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual IAsyncEnumerable FindByApplicationIdAsync( + string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => entity.ApplicationId == new ObjectId(identifier)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return collection.FindById(identifier); + } + + /// + public virtual IAsyncEnumerable FindBySubjectAsync( + string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection.Query() + .Where(entity => entity.Subject == subject) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual ValueTask GetApplicationIdAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.ApplicationId == ObjectId.Empty) + { + return new(result: null); + } + + return new(authorization.ApplicationId.ToString()); + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return query(collection.FindAll().AsQueryable(), state).FirstOrDefault(); + } + + /// + public virtual ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.CreationDate is null) + { + return new(result: null); + } + + return new(DateTime.SpecifyKind(authorization.CreationDate.Value, DateTimeKind.Utc)); + } + + /// + public virtual ValueTask GetIdAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new(authorization.Id.ToString()); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.Properties is null || authorization.Properties.Count == 0) + { + return new(ImmutableDictionary.Create()); + } + + return new(authorization.Properties.ToImmutableDictionary()); + } + + /// + public virtual ValueTask> GetScopesAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.Scopes is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(authorization.Scopes.ToImmutableArray()); + } + + /// + public virtual ValueTask GetStatusAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new(authorization.Status); + } + + /// + public virtual ValueTask GetSubjectAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new(authorization.Subject); + } + + /// + public virtual ValueTask GetTypeAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new(authorization.Type); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException("An error occurred while trying to create a new authorization instance.\r\nMake sure that the authorization entity is not abstract and has a public parameterless constructor or create a custom authorization store that overrides 'InstantiateAsync()' to use a custom factory.", exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync( + int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = collection + .Find(Query.All(), offset ?? 0, count ?? int.MaxValue) + .ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var authorizations = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable(); + + await foreach (var authorization in authorizations) + { + yield return authorization; + } + } + } + + /// + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + + //var database = await Context.GetDatabaseAsync(cancellationToken); + //var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + // Note: directly deleting the resulting set of an aggregate query is not supported by MongoDb + // To work around this limitation, the authorization identifiers are stored in an intermediate + // list and delete requests are sent to remove the documents corresponding to these identifiers. + + //var identifiers = + // await (from authorization in collection.AsQueryable() + // join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable() + // on authorization.Id equals token.AuthorizationId into tokens + // where authorization.CreationDate < threshold.UtcDateTime + // where authorization.Status != Statuses.Valid || + // (authorization.Type == AuthorizationTypes.AdHoc && !tokens.Any()) + // select authorization.Id).ToListAsync(cancellationToken); + + // Note: to avoid generating delete requests with very large filters, a buffer is used here and the + // maximum number of elements that can be removed by a single call to PruneAsync() is deliberately limited. + //foreach (var buffer in Buffer(identifiers.Take(1_000_000), 1_000)) + //{ + // await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id), cancellationToken); + //} + + //static IEnumerable> Buffer(IEnumerable source, int count) + //{ + // List? buffer = null; + + // foreach (var element in source) + // { + // buffer ??= new List(capacity: 1); + // buffer.Add(element); + + // if (buffer.Count == count) + // { + // yield return buffer; + + // buffer = null; + // } + // } + + // if (buffer is not null) + // { + // yield return buffer; + // } + //} + } + + /// + public virtual ValueTask SetApplicationIdAsync(TAuthorization authorization, + string? identifier, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + authorization.ApplicationId = new ObjectId(identifier); + } + + else + { + authorization.ApplicationId = ObjectId.Empty; + } + + return default; + } + + /// + public virtual ValueTask SetCreationDateAsync(TAuthorization authorization, + DateTimeOffset? date, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.CreationDate = date?.UtcDateTime; + + return default; + } + + /// + public virtual ValueTask SetPropertiesAsync(TAuthorization authorization, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (properties is not { Count: > 0 }) + { + authorization.Properties = null; + + return default; + } + + authorization.Properties = properties; + + return default; + } + + /// + public virtual ValueTask SetScopesAsync(TAuthorization authorization, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (scopes.IsDefaultOrEmpty) + { + authorization.Scopes = null; + + return default; + } + + authorization.Scopes = scopes.ToImmutableList(); + + return default; + } + + /// + public virtual ValueTask SetStatusAsync(TAuthorization authorization, string? status, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Status = status; + + return default; + } + + /// + public virtual ValueTask SetSubjectAsync(TAuthorization authorization, string? subject, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Subject = subject; + + return default; + } + + /// + public virtual ValueTask SetTypeAsync(TAuthorization authorization, string? type, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Type = type; + + return default; + } + + /// + public virtual async ValueTask UpdateAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization is null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + // Generate a new concurrency token and attach it + // to the authorization before persisting the changes. + var timestamp = authorization.ConcurrencyToken; + authorization.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + if (collection.Count(entity => entity.Id == authorization.Id && entity.ConcurrencyToken == timestamp) == 0) + { + throw new ConcurrencyException("The authorization was concurrently updated and cannot be persisted in its current state.\r\nReload the authorization from the database and retry the operation."); + } + + collection.Update(authorization); + } +} + diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs new file mode 100644 index 0000000..c86cd81 --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Provides methods allowing to manage the scopes stored in a database. +/// +/// The type of the Scope entity. +public class OpenIddictLiteDBScopeStore : IOpenIddictScopeStore + where TScope : OpenIddictLiteDBScope +{ + public OpenIddictLiteDBScopeStore( + IOpenIddictLiteDBContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictLiteDBContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return collection.LongCount(); + } + + /// + public virtual async ValueTask CountAsync( + Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return query(collection.FindAll().AsQueryable()).LongCount(); + } + + /// + public virtual async ValueTask CreateAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + collection.Insert(scope); + } + + /// + public virtual async ValueTask DeleteAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + if (collection.DeleteMany(entity => + entity.Id == scope.Id && + entity.ConcurrencyToken == scope.ConcurrencyToken) == 0) + { + throw new OpenIddictExceptions.ConcurrencyException("The scope was concurrently updated and cannot be persisted in its current state.\r\nReload the scope from the database and retry the operation."); + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return collection.FindById(identifier); + } + + /// + public virtual async ValueTask FindByNameAsync(string name, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return collection.Query() + .Where(entity => entity.Name == name) + .FirstOrDefault(); + } + + /// + public virtual IAsyncEnumerable FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken) + { + if (names.Any(name => string.IsNullOrEmpty(name))) + { + throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + var scopes = collection.Query() + .Where(entity => Enumerable.Contains(names, entity.Name)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var scope in scopes) + { + yield return scope; + } + } + } + + /// + public virtual IAsyncEnumerable FindByResourceAsync(string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + var scopes = collection.Query() + .Where(entity => entity.Resources.Contains(resource)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var scope in scopes) + { + yield return scope; + } + } + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return query(collection.FindAll().AsQueryable(), state).FirstOrDefault(); + } + + /// + public virtual ValueTask GetDescriptionAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new(scope.Description); + } + + /// + public virtual ValueTask> GetDescriptionsAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Descriptions is not { Count: > 0 }) + { + return new(ImmutableDictionary.Create()); + } + + return new(scope.Descriptions.ToImmutableDictionary( + pair => CultureInfo.GetCultureInfo(pair.Key), + pair => pair.Value)); + } + + /// + public virtual ValueTask GetDisplayNameAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new(scope.DisplayName); + } + + /// + public virtual ValueTask> GetDisplayNamesAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.DisplayNames is not { Count: > 0 }) + { + return new(ImmutableDictionary.Create()); + } + + return new(scope.DisplayNames.ToImmutableDictionary( + pair => CultureInfo.GetCultureInfo(pair.Key), + pair => pair.Value)); + } + + /// + public virtual ValueTask GetIdAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new(scope.Id.ToString()); + } + + /// + public virtual ValueTask GetNameAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new(scope.Name); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Properties is null || scope.Properties.Count == 0) + { + return new(ImmutableDictionary.Create()); + } + + return new(scope.Properties.ToImmutableDictionary()); + } + + /// + public virtual ValueTask> GetResourcesAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Resources is not { Count: > 0 }) + { + return new(ImmutableArray.Create()); + } + + return new(scope.Resources.ToImmutableArray()); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException("An error occurred while trying to create a new scope instance.\r\nMake sure that the scope entity is not abstract and has a public parameterless constructor or create a custom scope store that overrides 'InstantiateAsync()' to use a custom factory.", exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync( + int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + var scopes = collection + .Find(Query.All(), offset ?? 0, count ?? int.MaxValue) + .ToAsyncEnumerable(); + + await foreach (var scope in scopes) + { + yield return scope; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + var scopes = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable(); + + await foreach (var scope in scopes) + { + yield return scope; + } + } + } + + /// + public virtual ValueTask SetDescriptionAsync(TScope scope, string? description, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Description = description; + + return default; + } + + /// + public virtual ValueTask SetDescriptionsAsync(TScope scope, + ImmutableDictionary descriptions, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (descriptions is not { Count: > 0 }) + { + scope.Descriptions = null; + + return default; + } + + scope.Descriptions = descriptions.ToImmutableDictionary( + pair => pair.Key.Name, + pair => pair.Value); + + return default; + } + + /// + public virtual ValueTask SetDisplayNamesAsync(TScope scope, + ImmutableDictionary names, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (names is not { Count: > 0 }) + { + scope.DisplayNames = null; + + return default; + } + + scope.DisplayNames = names.ToImmutableDictionary( + pair => pair.Key.Name, + pair => pair.Value); + + return default; + } + + /// + public virtual ValueTask SetDisplayNameAsync(TScope scope, string? name, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.DisplayName = name; + + return default; + } + + /// + public virtual ValueTask SetNameAsync(TScope scope, string? name, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Name = name; + + return default; + } + + /// + public virtual ValueTask SetPropertiesAsync(TScope scope, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (properties is not { Count: > 0 }) + { + scope.Properties = null; + + return default; + } + + scope.Properties = properties; + + return default; + } + + /// + public virtual ValueTask SetResourcesAsync(TScope scope, ImmutableArray resources, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (resources.IsDefaultOrEmpty) + { + scope.Resources = null; + + return default; + } + + scope.Resources = resources.ToImmutableList(); + + return default; + } + + /// + public virtual async ValueTask UpdateAsync(TScope scope, CancellationToken cancellationToken) + { + if (scope is null) + { + throw new ArgumentNullException(nameof(scope)); + } + + // Generate a new concurrency token and attach it + // to the scope before persisting the changes. + var timestamp = scope.ConcurrencyToken; + scope.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + if (collection.Count(entity => entity.Id == scope.Id && entity.ConcurrencyToken == timestamp) == 0) + { + throw new ConcurrencyException("The scope was concurrently updated and cannot be persisted in its current state.\r\nReload the scope from the database and retry the operation."); + } + + collection.Update(scope); + } +} diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBTokenStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBTokenStore.cs new file mode 100644 index 0000000..5410c2c --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBTokenStore.cs @@ -0,0 +1,809 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB; + +/// +/// Provides methods allowing to manage the tokens stored in a database. +/// +/// The type of the Token entity. +public class OpenIddictLiteDBTokenStore : IOpenIddictTokenStore + where TToken : OpenIddictLiteDBToken +{ + public OpenIddictLiteDBTokenStore( + IOpenIddictLiteDBContext context, + IOptionsMonitor options) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictLiteDBContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + public virtual async ValueTask CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return collection.LongCount(); + } + + /// + public virtual async ValueTask CountAsync( + Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return query(collection.FindAll().AsQueryable()).LongCount(); + } + + /// + public virtual async ValueTask CreateAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + collection.Insert(token); + } + + /// + public virtual async ValueTask DeleteAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + if (collection.DeleteMany(entity => + entity.Id == token.Id && + entity.ConcurrencyToken == token.ConcurrencyToken) == 0) + { + throw new ConcurrencyException("The token was concurrently updated and cannot be persisted in its current state.\r\nReload the token from the database and retry the operation."); + } + } + + /// + public virtual IAsyncEnumerable FindAsync(string subject, + string client, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => + entity.ApplicationId == new ObjectId(client) && + entity.Subject == subject) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, + string status, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => + entity.ApplicationId == new ObjectId(client) && + entity.Subject == subject && + entity.Status == status) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual IAsyncEnumerable FindAsync( + string subject, string client, + string status, string type, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => + entity.ApplicationId == new ObjectId(client) && + entity.Subject == subject && + entity.Status == status && + entity.Type == type) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual IAsyncEnumerable FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => entity.ApplicationId == new ObjectId(identifier)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual IAsyncEnumerable FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => entity.AuthorizationId == new ObjectId(identifier)) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return collection.FindById(identifier); + } + + /// + public virtual async ValueTask FindByReferenceIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return collection.Query() + .Where(entity => entity.ReferenceId == identifier) + .FirstOrDefault(); + } + + /// + public virtual IAsyncEnumerable FindBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection.Query() + .Where(entity => entity.Subject == subject) + .ToEnumerable().ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual ValueTask GetApplicationIdAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.ApplicationId == ObjectId.Empty) + { + return new(result: null); + } + + return new(token.ApplicationId.ToString()); + } + + /// + public virtual async ValueTask GetAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return query(collection.FindAll().AsQueryable(), state).FirstOrDefault(); + } + + /// + public virtual ValueTask GetAuthorizationIdAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.AuthorizationId == ObjectId.Empty) + { + return new(result: null); + } + + return new(token.AuthorizationId.ToString()); + } + + /// + public virtual ValueTask GetCreationDateAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.CreationDate is null) + { + return new(result: null); + } + + return new(DateTime.SpecifyKind(token.CreationDate.Value, DateTimeKind.Utc)); + } + + /// + public virtual ValueTask GetExpirationDateAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.ExpirationDate is null) + { + return new(result: null); + } + + return new(DateTime.SpecifyKind(token.ExpirationDate.Value, DateTimeKind.Utc)); + } + + /// + public virtual ValueTask GetIdAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.Id.ToString()); + } + + /// + public virtual ValueTask GetPayloadAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.Payload); + } + + /// + public virtual ValueTask> GetPropertiesAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.Properties is null || token.Properties.Count == 0) + { + return new(ImmutableDictionary.Create()); + } + + return new(token.Properties.ToImmutableDictionary()); + } + + /// + public virtual ValueTask GetRedemptionDateAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.RedemptionDate is null) + { + return new(result: null); + } + + return new(DateTime.SpecifyKind(token.RedemptionDate.Value, DateTimeKind.Utc)); + } + + /// + public virtual ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.ReferenceId); + } + + /// + public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.Status); + } + + /// + public virtual ValueTask GetSubjectAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.Subject); + } + + /// + public virtual ValueTask GetTypeAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new(token.Type); + } + + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + { + try + { + return new(Activator.CreateInstance()); + } + + catch (MemberAccessException exception) + { + return new(Task.FromException( + new InvalidOperationException("An error occurred while trying to create a new token instance.\r\nMake sure that the token entity is not abstract and has a public parameterless constructor or create a custom token store that overrides 'InstantiateAsync()' to use a custom factory.", exception))); + } + } + + /// + public virtual async IAsyncEnumerable ListAsync( + int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = collection + .Find(Query.All(), offset ?? 0, count ?? int.MaxValue) + .ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + + /// + public virtual IAsyncEnumerable ListAsync( + Func, TState, IQueryable> query, + TState state, CancellationToken cancellationToken) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var tokens = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable(); + + await foreach (var token in tokens) + { + yield return token; + } + } + } + + /// + public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + + //var database = await Context.GetDatabaseAsync(cancellationToken); + //var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + //// Note: directly deleting the resulting set of an aggregate query is not supported by MongoDb. + //// To work around this limitation, the token identifiers are stored in an intermediate list + //// and delete requests are sent to remove the documents corresponding to these identifiers. + + //var identifiers = + // await (from token in collection.AsQueryable() + // join authorization in database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName).AsQueryable() + // on token.AuthorizationId equals authorization.Id into authorizations + // where token.CreationDate < threshold.UtcDateTime + // where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || + // token.ExpirationDate < DateTime.UtcNow || + // authorizations.Any(authorization => authorization.Status != Statuses.Valid) + // select token.Id).ToListAsync(cancellationToken); + + //// Note: to avoid generating delete requests with very large filters, a buffer is used here and the + //// maximum number of elements that can be removed by a single call to PruneAsync() is deliberately limited. + //foreach (var buffer in Buffer(identifiers.Take(1_000_000), 1_000)) + //{ + // await collection.DeleteManyAsync(token => buffer.Contains(token.Id), cancellationToken); + //} + + //static IEnumerable> Buffer(IEnumerable source, int count) + //{ + // List? buffer = null; + + // foreach (var element in source) + // { + // buffer ??= new List(capacity: 1); + // buffer.Add(element); + + // if (buffer.Count == count) + // { + // yield return buffer; + + // buffer = null; + // } + // } + + // if (buffer is not null) + // { + // yield return buffer; + // } + //} + } + + /// + public virtual ValueTask SetApplicationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + token.ApplicationId = new ObjectId(identifier); + } + + else + { + token.ApplicationId = ObjectId.Empty; + } + + return default; + } + + /// + public virtual ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + token.AuthorizationId = new ObjectId(identifier); + } + + else + { + token.AuthorizationId = ObjectId.Empty; + } + + return default; + } + + /// + public virtual ValueTask SetCreationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.CreationDate = date?.UtcDateTime; + + return default; + } + + /// + public virtual ValueTask SetExpirationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ExpirationDate = date?.UtcDateTime; + + return default; + } + + /// + public virtual ValueTask SetPayloadAsync(TToken token, string? payload, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Payload = payload; + + return default; + } + + /// + public virtual ValueTask SetPropertiesAsync(TToken token, + ImmutableDictionary properties, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (properties is not { Count: > 0 }) + { + token.Properties = null; + + return default; + } + + token.Properties = properties; + + return default; + } + + /// + public virtual ValueTask SetRedemptionDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.RedemptionDate = date?.UtcDateTime; + + return default; + } + + /// + public virtual ValueTask SetReferenceIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ReferenceId = identifier; + + return default; + } + + /// + public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Status = status; + + return default; + } + + /// + public virtual ValueTask SetSubjectAsync(TToken token, string? subject, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Subject = subject; + + return default; + } + + /// + public virtual ValueTask SetTypeAsync(TToken token, string? type, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Type = type; + + return default; + } + + /// + public virtual async ValueTask UpdateAsync(TToken token, CancellationToken cancellationToken) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + // Generate a new concurrency token and attach it + // to the token before persisting the changes. + var timestamp = token.ConcurrencyToken; + token.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + if (collection.Count(entity => entity.Id == token.Id && entity.ConcurrencyToken == timestamp) == 0) + { + throw new ConcurrencyException("The token was concurrently updated and cannot be persisted in its current state.\r\nReload the token from the database and retry the operation."); + } + + collection.Update(token); + } +} diff --git a/src/Sknet.OpenIddict.LiteDB/Usings.cs b/src/Sknet.OpenIddict.LiteDB/Usings.cs new file mode 100644 index 0000000..a241bde --- /dev/null +++ b/src/Sknet.OpenIddict.LiteDB/Usings.cs @@ -0,0 +1,15 @@ +global using LiteDB; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Options; +global using OpenIddict.Abstractions; +global using OpenIddict.Core; +global using Sknet.OpenIddict.LiteDB; +global using Sknet.OpenIddict.LiteDB.Models; +global using System.Collections.Concurrent; +global using System.Collections.Immutable; +global using System.ComponentModel; +global using System.Globalization; +global using System.Runtime.CompilerServices; +global using System.Text.Json; +global using static OpenIddict.Abstractions.OpenIddictExceptions; diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBBuilderTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBBuilderTests.cs new file mode 100644 index 0000000..f76aea1 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBBuilderTests.cs @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBBuilderTests +{ + [Fact] + public void Constructor_ThrowsAnExceptionForNullServices() + { + // Arrange + var services = (IServiceCollection)null!; + + // Act and assert + var exception = Assert.Throws(() => new OpenIddictLiteDBBuilder(services)); + + Assert.Equal("services", exception.ParamName); + } + + [Fact] + public void ReplaceDefaultApplicationEntity_EntityIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceDefaultApplicationEntity(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(CustomApplication), options.DefaultApplicationType); + } + + [Fact] + public void ReplaceDefaultAuthorizationEntity_EntityIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceDefaultAuthorizationEntity(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(CustomAuthorization), options.DefaultAuthorizationType); + } + + [Fact] + public void ReplaceDefaultScopeEntity_EntityIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceDefaultScopeEntity(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(CustomScope), options.DefaultScopeType); + } + + [Fact] + public void ReplaceDefaultTokenEntity_EntityIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.ReplaceDefaultTokenEntity(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(CustomToken), options.DefaultTokenType); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void SetApplicationsCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string name) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetApplicationsCollectionName(name)); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The collection name cannot be null or empty.", exception.Message); + } + + [Fact] + public void SetApplicationsCollectionName_CollectionNameIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetApplicationsCollectionName("custom_collection"); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal("custom_collection", options.ApplicationsCollectionName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void SetAuthorizationsCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string name) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetAuthorizationsCollectionName(name)); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The collection name cannot be null or empty.", exception.Message); + } + + [Fact] + public void SetAuthorizationsCollectionName_CollectionNameIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetAuthorizationsCollectionName("custom_collection"); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal("custom_collection", options.AuthorizationsCollectionName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void SetScopesCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string name) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetScopesCollectionName(name)); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The collection name cannot be null or empty.", exception.Message); + } + + [Fact] + public void SetScopesCollectionName_CollectionNameIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetScopesCollectionName("custom_collection"); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal("custom_collection", options.ScopesCollectionName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void SetTokensCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string name) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetTokensCollectionName(name)); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The collection name cannot be null or empty.", exception.Message); + } + + [Fact] + public void SetTokensCollectionName_CollectionNameIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetTokensCollectionName("custom_collection"); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal("custom_collection", options.TokensCollectionName); + } + + [Fact] + public void UseDatabase_ThrowsAnExceptionForNullDatabase() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + return builder.UseDatabase(database: null!); + }); + + Assert.Equal("database", exception.ParamName); + } + + [Fact] + public void UseDatabase_SetsDatabaseInOptions() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + var database = Mock.Of(); + + // Act + builder.UseDatabase(database); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(database, options.Database); + } + + private static OpenIddictLiteDBBuilder CreateBuilder(IServiceCollection services) + => services.AddOpenIddict().AddCore().UseLiteDB(); + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddOptions(); + + return services; + } + + public class CustomApplication : OpenIddictLiteDBApplication { } + public class CustomAuthorization : OpenIddictLiteDBAuthorization { } + public class CustomScope : OpenIddictLiteDBScope { } + public class CustomToken : OpenIddictLiteDBToken { } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBContextTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBContextTests.cs new file mode 100644 index 0000000..a23b919 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBContextTests.cs @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBContextTests +{ + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionForCanceledToken() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var options = Mock.Of>(); + var token = new CancellationToken(canceled: true); + + var context = new OpenIddictLiteDBContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await context.GetDatabaseAsync(token); + }); + + Assert.Equal(token, exception.CancellationToken); + } + + [Fact] + public async Task GetDatabaseAsync_PrefersDatabaseRegisteredInOptionsToDatabaseRegisteredInDependencyInjectionContainer() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of()); + + var provider = services.BuildServiceProvider(); + + var database = Mock.Of(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictLiteDBOptions + { + Database = database + }); + + var context = new OpenIddictLiteDBContext(options, provider); + + // Act and assert + Assert.Same(database, await context.GetDatabaseAsync(CancellationToken.None)); + } + + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionWhenDatabaseCannotBeFound() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictLiteDBOptions + { + Database = null + }); + + var context = new OpenIddictLiteDBContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await context.GetDatabaseAsync(CancellationToken.None); + }); + + Assert.Equal("No suitable LiteDB database service can be found.\r\nTo configure the OpenIddict LiteDB stores to use a specific database, use 'services.AddOpenIddict().AddCore().UseLiteDB().UseDatabase()' or register an 'ILiteDatabase' in the dependency injection container in 'ConfigureServices()'.", exception.Message); + } + + [Fact] + public async Task GetDatabaseAsync_UsesDatabaseRegisteredInDependencyInjectionContainer() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of()); + + var database = Mock.Of(); + services.AddSingleton(database); + + var provider = services.BuildServiceProvider(); + + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictLiteDBOptions + { + Database = null + }); + + var context = new OpenIddictLiteDBContext(options, provider); + + // Act and assert + Assert.Same(database, await context.GetDatabaseAsync(CancellationToken.None)); + } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBExtensionsTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBExtensionsTests.cs new file mode 100644 index 0000000..9131ff9 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/OpenIddictLiteDBExtensionsTests.cs @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBExtensionsTests +{ + [Fact] + public void UseLiteDB_ThrowsAnExceptionForNullBuilder() + { + // Arrange + var builder = (OpenIddictCoreBuilder)null!; + + // Act and assert + var exception = Assert.Throws(builder.UseLiteDB); + + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void UseLiteDB_ThrowsAnExceptionForNullConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictCoreBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.UseLiteDB(configuration: null!)); + + Assert.Equal("configuration", exception.ParamName); + } + + [Fact] + public void UseLiteDB_RegistersDefaultEntities() + { + // Arrange + var services = new ServiceCollection().AddOptions(); + var builder = new OpenIddictCoreBuilder(services); + + // Act + builder.UseLiteDB(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(OpenIddictLiteDBApplication), options.DefaultApplicationType); + Assert.Equal(typeof(OpenIddictLiteDBAuthorization), options.DefaultAuthorizationType); + Assert.Equal(typeof(OpenIddictLiteDBScope), options.DefaultScopeType); + Assert.Equal(typeof(OpenIddictLiteDBToken), options.DefaultTokenType); + } + + [Theory] + [InlineData(typeof(IOpenIddictApplicationStoreResolver), typeof(OpenIddictLiteDBApplicationStoreResolver))] + [InlineData(typeof(IOpenIddictAuthorizationStoreResolver), typeof(OpenIddictLiteDBAuthorizationStoreResolver))] + [InlineData(typeof(IOpenIddictScopeStoreResolver), typeof(OpenIddictLiteDBScopeStoreResolver))] + [InlineData(typeof(IOpenIddictTokenStoreResolver), typeof(OpenIddictLiteDBTokenStoreResolver))] + public void UseLiteDB_RegistersLiteDBStoreResolvers(Type serviceType, Type implementationType) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictCoreBuilder(services); + + // Act + builder.UseLiteDB(); + + // Assert + Assert.Contains(services, service => service.ServiceType == serviceType && + service.ImplementationType == implementationType); + } + + [Theory] + [InlineData(typeof(OpenIddictLiteDBApplicationStore<>))] + [InlineData(typeof(OpenIddictLiteDBAuthorizationStore<>))] + [InlineData(typeof(OpenIddictLiteDBScopeStore<>))] + [InlineData(typeof(OpenIddictLiteDBTokenStore<>))] + public void UseLiteDB_RegistersLiteDBStore(Type type) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictCoreBuilder(services); + + // Act + builder.UseLiteDB(); + + // Assert + Assert.Contains(services, service => service.ServiceType == type && service.ImplementationType == type); + } + + [Fact] + public void UseLiteDB_RegistersLiteDBContext() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictCoreBuilder(services); + + // Act + builder.UseLiteDB(); + + // Assert + Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Singleton && + service.ServiceType == typeof(IOpenIddictLiteDBContext) && + service.ImplementationType == typeof(OpenIddictLiteDBContext)); + } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBApplicationStoreResolverTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBApplicationStoreResolverTests.cs new file mode 100644 index 0000000..10517f9 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBApplicationStoreResolverTests.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBApplicationStoreResolverTests +{ + [Fact] + public void Get_ReturnsCustomStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBApplicationStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + [Fact] + public void Get_ThrowsAnExceptionForInvalidEntityType() + { + // Arrange + var services = new ServiceCollection(); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBApplicationStoreResolver(provider); + + // Act and assert + var exception = Assert.Throws(resolver.Get); + + Assert.Equal("The specified application type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBApplication' entity or a custom entity that inherits from the 'OpenIddictLiteDBApplication' entity.", exception.Message); + } + + [Fact] + public void Get_ReturnsDefaultStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(CreateStore()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBApplicationStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + private static OpenIddictLiteDBApplicationStore CreateStore() + => new Mock>( + Mock.Of(), + Mock.Of>()).Object; + + public class CustomApplication { } + + public class MyApplication : OpenIddictLiteDBApplication { } +} diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBAuthorizationStoreResolverTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBAuthorizationStoreResolverTests.cs new file mode 100644 index 0000000..d182976 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBAuthorizationStoreResolverTests.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBAuthorizationStoreResolverTests +{ + [Fact] + public void Get_ReturnsCustomStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBAuthorizationStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + [Fact] + public void Get_ThrowsAnExceptionForInvalidEntityType() + { + // Arrange + var services = new ServiceCollection(); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBAuthorizationStoreResolver(provider); + + // Act and assert + var exception = Assert.Throws(resolver.Get); + + Assert.Equal("The specified authorization type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBAuthorization' entity or a custom entity that inherits from the 'OpenIddictLiteDBAuthorization' entity.", exception.Message); + } + + [Fact] + public void Get_ReturnsDefaultStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(CreateStore()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBAuthorizationStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + private static OpenIddictLiteDBAuthorizationStore CreateStore() + => new Mock>( + Mock.Of(), + Mock.Of>()).Object; + + public class CustomAuthorization { } + + public class MyAuthorization : OpenIddictLiteDBAuthorization { } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBScopeStoreResolverTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBScopeStoreResolverTests.cs new file mode 100644 index 0000000..892afd9 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBScopeStoreResolverTests.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBScopeStoreResolverTests +{ + [Fact] + public void Get_ReturnsCustomStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBScopeStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + [Fact] + public void Get_ThrowsAnExceptionForInvalidEntityType() + { + // Arrange + var services = new ServiceCollection(); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBScopeStoreResolver(provider); + + // Act and assert + var exception = Assert.Throws(resolver.Get); + + Assert.Equal("The specified scope type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBScope' entity or a custom entity that inherits from the 'OpenIddictLiteDBScope' entity.", exception.Message); + } + + [Fact] + public void Get_ReturnsDefaultStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(CreateStore()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBScopeStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + private static OpenIddictLiteDBScopeStore CreateStore() + => new Mock>( + Mock.Of(), + Mock.Of>()).Object; + + public class CustomScope { } + + public class MyScope : OpenIddictLiteDBScope { } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBTokenStoreResolverTests.cs b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBTokenStoreResolverTests.cs new file mode 100644 index 0000000..c420e1e --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Resolvers/OpenIddictLiteDBTokenStoreResolverTests.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Steven Kuhn and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Sknet.OpenIddict.LiteDB.Tests; + +public class OpenIddictLiteDBTokenStoreResolverTests +{ + [Fact] + public void Get_ReturnsCustomStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBTokenStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + [Fact] + public void Get_ThrowsAnExceptionForInvalidEntityType() + { + // Arrange + var services = new ServiceCollection(); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBTokenStoreResolver(provider); + + // Act and assert + var exception = Assert.Throws(resolver.Get); + + Assert.Equal("The specified token type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBToken' entity or a custom entity that inherits from the 'OpenIddictLiteDBToken' entity.", exception.Message); + } + + [Fact] + public void Get_ReturnsDefaultStoreCorrespondingToTheSpecifiedTypeWhenAvailable() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(CreateStore()); + + var provider = services.BuildServiceProvider(); + var resolver = new OpenIddictLiteDBTokenStoreResolver(provider); + + // Act and assert + Assert.NotNull(resolver.Get()); + } + + private static OpenIddictLiteDBTokenStore CreateStore() + => new Mock>( + Mock.Of(), + Mock.Of>()).Object; + + public class CustomToken { } + + public class MyToken : OpenIddictLiteDBToken { } +} \ No newline at end of file diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Sknet.OpenIddict.LiteDB.Tests.csproj b/test/Sknet.OpenIddict.LiteDB.Tests/Sknet.OpenIddict.LiteDB.Tests.csproj new file mode 100644 index 0000000..a0a75a7 --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Sknet.OpenIddict.LiteDB.Tests.csproj @@ -0,0 +1,31 @@ + + + + net462;net472;net48;netcoreapp3.1;net6.0 + enable + enable + 10.0 + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/Sknet.OpenIddict.LiteDB.Tests/Usings.cs b/test/Sknet.OpenIddict.LiteDB.Tests/Usings.cs new file mode 100644 index 0000000..86ffacd --- /dev/null +++ b/test/Sknet.OpenIddict.LiteDB.Tests/Usings.cs @@ -0,0 +1,8 @@ +global using LiteDB; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Options; +global using Moq; +global using OpenIddict.Abstractions; +global using OpenIddict.Core; +global using Sknet.OpenIddict.LiteDB.Models; +global using Xunit;