diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0205ec1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,52 @@
+# EditorConfig: https://EditorConfig.org
+
+root = true
+
+# All Files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# XML Configuration Files
+[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct,refactorlog,runsettings}]
+indent_size = 2
+
+# JSON Files
+[*.{json,json5,webmanifest}]
+indent_size = 2
+
+# Project Files
+[*.{csproj,sqlproj}]
+indent_size = 2
+
+# YAML Files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown Files
+[*.md]
+trim_trailing_whitespace = false
+
+# Web Files
+[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,pcss,svg,vue}]
+indent_size = 2
+
+# Batch Files
+[*.{cmd,bat}]
+end_of_line = crlf
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+[*.{cs,vb}]
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = true
+dotnet_style_namespace_match_folder = true
+
+[*.cs]
+csharp_using_directive_placement = outside_namespace
+csharp_style_namespace_declarations = file_scoped:warning
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5387cb2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "11:00"
+ open-pull-requests-limit: 10
+
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..015aff9
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,99 @@
+name: Build
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ DOTNET_ENVIRONMENT: github
+ ASPNETCORE_ENVIRONMENT: github
+ BUILD_PATH: '${{github.workspace}}/artifacts'
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ tags:
+ - 'v*'
+ pull_request:
+ branches:
+ - main
+ - develop
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+
+ - name: Restore Dependencies
+ run: dotnet restore
+
+ - name: Build Solution
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Run Test
+ run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --settings coverlet.runsettings
+
+ - name: Report Coverage
+ if: success()
+ uses: coverallsapp/github-action@v2
+ with:
+ file: "${{github.workspace}}/test/*/TestResults/*/coverage.info"
+ format: lcov
+
+ - name: Create Packages
+ if: success() && github.event_name != 'pull_request'
+ run: dotnet pack --configuration Release --include-symbols --include-source --no-build --output "${{env.BUILD_PATH}}"
+
+ - name: Upload Packages
+ if: success() && github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v3
+ with:
+ name: packages
+ path: "${{env.BUILD_PATH}}"
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ if: success() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
+
+ steps:
+ - name: Download Artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: packages
+
+ - name: Publish Packages GitHub
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://nuget.pkg.github.com/loresoft/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
+ done
+
+ - name: Publish Packages feedz
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://f.feedz.io/loresoft/open/nuget/index.json --api-key ${{ secrets.FEEDDZ_KEY }} --skip-duplicate
+ done
+
+ - name: Publish Packages Nuget
+ if: startsWith(github.ref, 'refs/tags/v')
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_KEY }} --skip-duplicate
+ done
+
diff --git a/Authorizone.sln b/Authorizone.sln
new file mode 100644
index 0000000..b3913a2
--- /dev/null
+++ b/Authorizone.sln
@@ -0,0 +1,38 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorizone", "src\Authorizone\Authorizone.csproj", "{9CE78075-9509-4113-B645-45A082C68839}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorizone.Tests", "test\Authorizone.Tests\Authorizone.Tests.csproj", "{DA3CCF24-533A-436A-9929-28AF83D29B32}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{4F71B12A-605F-48FE-94F5-FF3B776FEA6D}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9CE78075-9509-4113-B645-45A082C68839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CE78075-9509-4113-B645-45A082C68839}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CE78075-9509-4113-B645-45A082C68839}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CE78075-9509-4113-B645-45A082C68839}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA3CCF24-533A-436A-9929-28AF83D29B32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA3CCF24-533A-436A-9929-28AF83D29B32}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA3CCF24-533A-436A-9929-28AF83D29B32}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA3CCF24-533A-436A-9929-28AF83D29B32}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E07DA442-1D4F-4528-BB2E-29E884BFFA66}
+ EndGlobalSection
+EndGlobal
diff --git a/coverlet.runsettings b/coverlet.runsettings
new file mode 100644
index 0000000..4eac22c
--- /dev/null
+++ b/coverlet.runsettings
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ lcov
+ Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,TestSDKAutoGeneratedCode
+ true
+
+
+
+
+
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..6482efc
Binary files /dev/null and b/logo.png differ
diff --git a/src/Authorizone/AuthorizationActions.cs b/src/Authorizone/AuthorizationActions.cs
new file mode 100644
index 0000000..c6cca46
--- /dev/null
+++ b/src/Authorizone/AuthorizationActions.cs
@@ -0,0 +1,6 @@
+namespace Authorizone;
+
+public static class AuthorizationActions
+{
+ public const string All = "all";
+}
diff --git a/src/Authorizone/AuthorizationBuilder.cs b/src/Authorizone/AuthorizationBuilder.cs
new file mode 100644
index 0000000..80a3e65
--- /dev/null
+++ b/src/Authorizone/AuthorizationBuilder.cs
@@ -0,0 +1,39 @@
+namespace Authorizone;
+
+public class AuthorizationBuilder
+{
+ private readonly List _rules = [];
+
+ public AuthorizationBuilder Allow(string action, string subject, IEnumerable? fields = null)
+ {
+ if (string.IsNullOrWhiteSpace(action))
+ throw new ArgumentException("Action cannot be null or whitespace.", nameof(action));
+
+ if (string.IsNullOrWhiteSpace(subject))
+ throw new ArgumentException("Subject cannot be null or whitespace.", nameof(subject));
+
+ var rule = new AuthorizationRule(action, subject, fields?.ToList());
+ _rules.Add(rule);
+
+ return this;
+ }
+
+ public AuthorizationBuilder Forbid(string action, string subject, IEnumerable? fields = null)
+ {
+ if (string.IsNullOrWhiteSpace(action))
+ throw new ArgumentException("Action cannot be null or whitespace.", nameof(action));
+
+ if (string.IsNullOrWhiteSpace(subject))
+ throw new ArgumentException("Subject cannot be null or whitespace.", nameof(subject));
+
+ var rule = new AuthorizationRule(action, subject, fields?.ToList(), true);
+ _rules.Add(rule);
+
+ return this;
+ }
+
+ public AuthorizationContext Build()
+ {
+ return new AuthorizationContext(_rules);
+ }
+}
diff --git a/src/Authorizone/AuthorizationBuilderExtensions.cs b/src/Authorizone/AuthorizationBuilderExtensions.cs
new file mode 100644
index 0000000..e01ea9d
--- /dev/null
+++ b/src/Authorizone/AuthorizationBuilderExtensions.cs
@@ -0,0 +1,134 @@
+namespace Authorizone;
+
+public static class AuthorizationBuilderExtensions
+{
+ public static AuthorizationBuilder Allow(this AuthorizationBuilder builder, IEnumerable actions, IEnumerable subjects, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (actions == null)
+ throw new ArgumentNullException(nameof(actions));
+
+ if (subjects == null)
+ throw new ArgumentNullException(nameof(subjects));
+
+ var subjectList = subjects.ToList();
+
+ foreach (var action in actions)
+ {
+ foreach (var subject in subjectList)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Allow(action, subject, fields);
+ }
+ }
+
+ return builder;
+ }
+
+ public static AuthorizationBuilder Allow(this AuthorizationBuilder builder, IEnumerable actions, string subject, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (actions == null)
+ throw new ArgumentNullException(nameof(actions));
+
+ if (string.IsNullOrWhiteSpace(subject))
+ throw new ArgumentException("Subject cannot be null or whitespace.", nameof(subject));
+
+ foreach (var action in actions)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Allow(action, subject, fields);
+ }
+
+ return builder;
+ }
+
+ public static AuthorizationBuilder Allow(this AuthorizationBuilder builder, string action, IEnumerable subjects, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (subjects == null)
+ throw new ArgumentNullException(nameof(subjects));
+
+ if (string.IsNullOrWhiteSpace(action))
+ throw new ArgumentException("Action cannot be null or whitespace.", nameof(action));
+
+ foreach (var subject in subjects)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Allow(action, subject, fields);
+ }
+
+ return builder;
+ }
+
+ public static AuthorizationBuilder Forbid(this AuthorizationBuilder builder, IEnumerable actions, IEnumerable subjects, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (actions == null)
+ throw new ArgumentNullException(nameof(actions));
+
+ if (subjects == null)
+ throw new ArgumentNullException(nameof(subjects));
+
+ var subjectList = subjects.ToList();
+
+ foreach (var action in actions)
+ {
+ foreach (var subject in subjectList)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Forbid(action, subject, fields);
+ }
+ }
+
+ return builder;
+ }
+
+ public static AuthorizationBuilder Forbid(this AuthorizationBuilder builder, IEnumerable actions, string subject, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (actions == null)
+ throw new ArgumentNullException(nameof(actions));
+
+ if (string.IsNullOrWhiteSpace(subject))
+ throw new ArgumentException("Subject cannot be null or whitespace.", nameof(subject));
+
+ foreach (var action in actions)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Forbid(action, subject, fields);
+ }
+
+ return builder;
+ }
+
+ public static AuthorizationBuilder Forbid(this AuthorizationBuilder builder, string action, IEnumerable subjects, IEnumerable? fields = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ if (subjects == null)
+ throw new ArgumentNullException(nameof(subjects));
+
+ if (string.IsNullOrWhiteSpace(action))
+ throw new ArgumentException("Action cannot be null or whitespace.", nameof(action));
+
+ foreach (var subject in subjects)
+ {
+ // ReSharper disable once PossibleMultipleEnumeration
+ builder.Forbid(action, subject, fields);
+ }
+
+ return builder;
+ }
+}
diff --git a/src/Authorizone/AuthorizationContext.cs b/src/Authorizone/AuthorizationContext.cs
new file mode 100644
index 0000000..620a795
--- /dev/null
+++ b/src/Authorizone/AuthorizationContext.cs
@@ -0,0 +1,70 @@
+namespace Authorizone;
+
+public class AuthorizationContext(IReadOnlyCollection rules, StringComparer? stringComparer = null)
+{
+ public IReadOnlyCollection Rules { get; } = rules ?? throw new ArgumentNullException(nameof(rules));
+
+ public StringComparer StringComparer { get; } = stringComparer ?? StringComparer.InvariantCultureIgnoreCase;
+
+
+ public bool Authorized(string? action, string? subject, string? field = null)
+ {
+ if (action is null || subject is null)
+ return false;
+
+ var matchedRules = MatchRules(action, subject, field);
+ bool? state = null;
+
+ foreach (var matchedRule in matchedRules)
+ {
+ if (matchedRule.Denied == true)
+ state = state != null && (state.Value && false);
+ else
+ state = state == null || (state.Value || true);
+ }
+
+ return state ?? false;
+ }
+
+ public bool Unauthorized(string? action, string? subject, string? field = null) => !Authorized(action, subject, field);
+
+ public IEnumerable MatchRules(string? action, string? subject, string? field = null)
+ {
+ if (action is null || subject is null)
+ return Enumerable.Empty();
+
+ return Rules.Where(r => RuleMatcher(r, action, subject, field));
+ }
+
+
+ private bool RuleMatcher(AuthorizationRule rule, string action, string subject, string? field = null)
+ {
+ return SubjectMather(rule, subject)
+ && ActionMather(rule, action)
+ && FieldMatcher(rule, field);
+ }
+
+ private bool SubjectMather(AuthorizationRule rule, string subject)
+ {
+ // can match global all or requested subject
+ return StringComparer.Equals(rule.Subject, subject)
+ || StringComparer.Equals(rule.Subject, AuthorizationSubjects.All);
+ }
+
+ private bool ActionMather(AuthorizationRule rule, string action)
+ {
+ // can match global manage action or requested action
+ return StringComparer.Equals(rule.Action, action)
+ || StringComparer.Equals(rule.Action, AuthorizationActions.All);
+ }
+
+ private bool FieldMatcher(AuthorizationRule rule, string? field)
+ {
+ // if rule doesn't have fields, all allowed
+ if (field == null || rule.Fields == null || rule.Fields.Count == 0)
+ return true;
+
+ // ensure rule has field
+ return rule.Fields.Contains(field, StringComparer);
+ }
+}
diff --git a/src/Authorizone/AuthorizationRule.cs b/src/Authorizone/AuthorizationRule.cs
new file mode 100644
index 0000000..4803dc0
--- /dev/null
+++ b/src/Authorizone/AuthorizationRule.cs
@@ -0,0 +1,7 @@
+namespace Authorizone;
+
+public record AuthorizationRule(
+ string Action,
+ string Subject,
+ List? Fields = null,
+ bool? Denied = null);
diff --git a/src/Authorizone/AuthorizationSubjects.cs b/src/Authorizone/AuthorizationSubjects.cs
new file mode 100644
index 0000000..b389cd1
--- /dev/null
+++ b/src/Authorizone/AuthorizationSubjects.cs
@@ -0,0 +1,6 @@
+namespace Authorizone;
+
+public static class AuthorizationSubjects
+{
+ public const string All = "all";
+}
diff --git a/src/Authorizone/Authorizone.csproj b/src/Authorizone/Authorizone.csproj
new file mode 100644
index 0000000..a410a7b
--- /dev/null
+++ b/src/Authorizone/Authorizone.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ enable
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..42c48a9
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,55 @@
+
+
+
+ An isomorphic authorization library for restricting resources by action, subjct and fields
+ Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft
+ LoreSoft
+ en-US
+ true
+ authorization;auth
+ https://github.com/loresoft/Authorizone
+ MIT
+ logo.png
+ README.md
+ git
+ https://github.com/loresoft/Authorizone
+ true
+
+
+
+ portable
+ true
+ true
+ snupkg
+
+
+
+ latest
+ enable
+ 1591
+
+
+
+ v
+
+
+
+
+
+
+
+
+
+
+ true
+ \
+ false
+
+
+ true
+ \
+ false
+
+
+
+
diff --git a/test/Authorizone.Tests/AuthorizationContextTests.cs b/test/Authorizone.Tests/AuthorizationContextTests.cs
new file mode 100644
index 0000000..27743da
--- /dev/null
+++ b/test/Authorizone.Tests/AuthorizationContextTests.cs
@@ -0,0 +1,81 @@
+namespace Authorizone.Tests;
+
+public class AuthorizationContextTests
+{
+ [Fact]
+ public void AllowByDefault()
+ {
+ var context = new AuthorizationBuilder()
+ .Allow("test", AuthorizationSubjects.All)
+ .Allow(AuthorizationActions.All, "Post")
+ .Forbid("publish", "Post")
+ .Build();
+
+ Assert.True(context.Authorized("read", "Post"));
+ Assert.True(context.Authorized("update", "Post"));
+ Assert.True(context.Authorized("archive", "Post"));
+ Assert.False(context.Authorized(null, "Post"));
+ Assert.False(context.Authorized("archive", null));
+ Assert.False(context.Authorized("read", "User"));
+ Assert.True(context.Authorized("delete", "Post"));
+ Assert.False(context.Authorized("publish", "Post"));
+ Assert.True(context.Authorized("test", "User"));
+ Assert.True(context.Authorized("test", "Post"));
+ }
+
+ [Fact]
+ public void AllowConstructRules()
+ {
+ var context = new AuthorizationBuilder()
+ .Allow("read", "Article")
+ .Allow("update", "Article")
+ .Build();
+
+ Assert.True(context.Authorized("read", "Article"));
+ Assert.True(context.Authorized("update", "Article"));
+ Assert.False(context.Authorized("delete", "Article"));
+ }
+
+ [Fact]
+ public void AllowSpecifyMultipleActions()
+ {
+ var context = new AuthorizationBuilder()
+ .Allow(["read", "update"], "Post")
+ .Build();
+
+ Assert.True(context.Authorized("read", "Post"));
+ Assert.True(context.Authorized("update", "Post"));
+ Assert.False(context.Authorized("delete", "Post"));
+ }
+
+ [Fact]
+ public void AllowSpecifyMultipleSubjects()
+ {
+ var context = new AuthorizationBuilder()
+ .Allow("read", ["Post", "User"])
+ .Build();
+
+ Assert.True(context.Authorized("read", "Post"));
+ Assert.True(context.Authorized("read", "User"));
+ Assert.False(context.Authorized("read", "Article"));
+ }
+
+ [Fact]
+ public void AllowRulesWithFields()
+ {
+ var context = new AuthorizationBuilder()
+ .Allow("read", "Post", ["title", "id"])
+ .Allow("read", "User")
+ .Build();
+
+ Assert.True(context.Authorized("read", "Post"));
+ Assert.True(context.Authorized("read", "Post", "id"));
+ Assert.True(context.Authorized("read", "Post", "title"));
+ Assert.False(context.Authorized("read", "Post", "ssn"));
+
+ Assert.True(context.Authorized("read", "User"));
+ Assert.True(context.Authorized("read", "User", "id"));
+ }
+
+
+}
diff --git a/test/Authorizone.Tests/Authorizone.Tests.csproj b/test/Authorizone.Tests/Authorizone.Tests.csproj
new file mode 100644
index 0000000..9545ba2
--- /dev/null
+++ b/test/Authorizone.Tests/Authorizone.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net6.0;net8.0
+ enable
+ enable
+ latest
+
+ false
+ true
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/test/Authorizone.Tests/GlobalUsings.cs b/test/Authorizone.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/test/Authorizone.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file