This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-Casbin.NET.git


The following commit(s) were added to refs/heads/master by this push:
     new c84c486  fix: match Go in in operator, subject priority, eval and 
inline comments
c84c486 is described below

commit c84c4864f06f405a8f1fb57281b892b2b81c84bc
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 01:49:35 2026 +0800

    fix: match Go in in operator, subject priority, eval and inline comments
---
 .github/workflows/build.yml                     |  9 +++
 Casbin.UnitTests/ModelTests/ConformanceTest.cs  | 76 +++++++++++++++++++++++++
 Casbin.UnitTests/UtilTests/StringUtilTest.cs    | 16 +++---
 Casbin/Config/DefaultConfig.cs                  |  2 +-
 Casbin/EnforceView.cs                           |  1 +
 Casbin/Enforcer.Internal.cs                     |  8 ++-
 Casbin/Evaluation/EffiectEvaluation.cs          | 15 +----
 Casbin/Extensions/Enforcer/EnforcerExtension.cs |  2 +
 Casbin/Extensions/Model/ModelExtension.cs       | 33 +++++++++++
 Casbin/Model/DefaultPolicyStore.Node.cs         | 21 +++----
 Casbin/Model/RoleAssertion.cs                   | 27 +++++++--
 Casbin/Util/StringUtil.cs                       | 54 +++++++++++++++++-
 12 files changed, 220 insertions(+), 44 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a273b1b..6f60590 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,6 +26,13 @@ jobs:
       with:
         fetch-depth: 0
 
+    - name: Checkout conformance cases
+      uses: actions/checkout@v4
+      with:
+        repository: apache/casbin
+        path: casbin-conformance
+        sparse-checkout: conformance
+
     - name: Setup .NET SDK
       uses: actions/setup-dotnet@v4
       with:
@@ -48,6 +55,8 @@ jobs:
       run: dotnet build -c Release --no-restore
 
     - name: Test solution
+      env:
+        CASBIN_CONFORMANCE_DIR: ${{ github.workspace 
}}/casbin-conformance/conformance
       run: dotnet test -c Release --no-build --no-restore --results-directory 
test-results --verbosity normal --collect:"XPlat Code Coverage" `
            -- 
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=json,cobertura,lcov,teamcity,opencover
 
diff --git a/Casbin.UnitTests/ModelTests/ConformanceTest.cs 
b/Casbin.UnitTests/ModelTests/ConformanceTest.cs
new file mode 100644
index 0000000..7e3b4a7
--- /dev/null
+++ b/Casbin.UnitTests/ModelTests/ConformanceTest.cs
@@ -0,0 +1,76 @@
+#if !NET452 && !NET461 && !NET462
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Xunit;
+
+namespace Casbin.UnitTests.ModelTests;
+
+public class ConformanceTest
+{
+    private class ConformanceCase
+    {
+        public string model { get; set; }
+        public string policy { get; set; }
+        public List<ConformanceRequest> tests { get; set; }
+    }
+
+    private class ConformanceRequest
+    {
+        public List<string> request { get; set; }
+        public bool allow { get; set; }
+        public List<string> explain { get; set; }
+    }
+
+    [Fact]
+    public void TestConformance()
+    {
+        string dir = 
Environment.GetEnvironmentVariable("CASBIN_CONFORMANCE_DIR");
+        if (string.IsNullOrEmpty(dir))
+        {
+            return;
+        }
+
+        string[] files = Directory.GetFiles(Path.Combine(dir, "cases"), 
"*.json").OrderBy(f => f, StringComparer.Ordinal).ToArray();
+        Assert.NotEmpty(files);
+
+        List<string> failures = new();
+        foreach (string file in files)
+        {
+            string name = Path.GetFileName(file);
+            ConformanceCase c = 
JsonSerializer.Deserialize<ConformanceCase>(File.ReadAllText(file));
+            Enforcer e;
+            try
+            {
+                e = new Enforcer(Path.Combine(dir, "data", c.model), 
Path.Combine(dir, "data", c.policy));
+            }
+            catch (Exception ex)
+            {
+                failures.Add($"{name}: {ex.Message}");
+                continue;
+            }
+
+            foreach (ConformanceRequest test in c.tests)
+            {
+                try
+                {
+                    var (allow, explains) = 
e.EnforceEx(test.request.ToArray());
+                    List<string> explain = explains.FirstOrDefault()?.ToList() 
?? new List<string>();
+                    if (allow != test.allow || 
!explain.SequenceEqual(test.explain))
+                    {
+                        failures.Add($"{name} [{string.Join(", ", 
test.request)}]: got ({allow}, [{string.Join(", ", explain)}]), want 
({test.allow}, [{string.Join(", ", test.explain)}])");
+                    }
+                }
+                catch (Exception ex)
+                {
+                    failures.Add($"{name} [{string.Join(", ", test.request)}]: 
{ex.Message}");
+                }
+            }
+        }
+
+        Assert.True(failures.Count == 0, $"{failures.Count} conformance 
failures:\n{string.Join("\n", failures)}");
+    }
+}
+#endif
diff --git a/Casbin.UnitTests/UtilTests/StringUtilTest.cs 
b/Casbin.UnitTests/UtilTests/StringUtilTest.cs
index dec3aff..bd5daf7 100644
--- a/Casbin.UnitTests/UtilTests/StringUtilTest.cs
+++ b/Casbin.UnitTests/UtilTests/StringUtilTest.cs
@@ -17,10 +17,10 @@ public class StringUtilTest
     public static IEnumerable<object[]> ReplaceEvalTestData =
     [
         [
-            "eval(rule1)", "a == b",
+            "eval(rule1)", "(a == b)",
             new Dictionary<string, string> { ["rule1"] = "a == b" }],
         [
-            "eval(rule1) && c && d", "a == b && c && d",
+            "eval(rule1) && c && d", "(a == b) && c && d",
             new Dictionary<string, string> { ["rule1"] = "a == b" }
         ],
         [
@@ -30,27 +30,27 @@ public class StringUtilTest
             "eval(rule1) && c && d", "eval(rule1) && c && d", null
         ],
         [
-            "eval(rule1) || eval(rule2)", "a == b || a == c",
+            "eval(rule1) || eval(rule2)", "(a == b) || (a == c)",
             new Dictionary<string, string> { ["rule1"] = "a == b", ["rule2"] = 
"a == c" }
         ],
         [
-            "eval(rule1) || eval(rule2) && c && d", "a == b || a == c && c && 
d",
+            "eval(rule1) || eval(rule2) && c && d", "(a == b) || (a == c) && c 
&& d",
             new Dictionary<string, string> { ["rule1"] = "a == b", ["rule2"] = 
"a == c" }
         ],
         [
-            "eval(rule1) || eval(rule2)", "a == b || eval(rule2)",
+            "eval(rule1) || eval(rule2)", "(a == b) || eval(rule2)",
             new Dictionary<string, string> { ["rule1"] = "a == b" }
         ],
         [
-            "eval(rule1) || eval(rule2) && c && d", "a == b || eval(rule2) && 
c && d",
+            "eval(rule1) || eval(rule2) && c && d", "(a == b) || eval(rule2) 
&& c && d",
             new Dictionary<string, string> { ["rule1"] = "a == b" }
         ],
         [
-            "eval(rule1) || eval(rule2)", "eval(rule1) || a == c",
+            "eval(rule1) || eval(rule2)", "eval(rule1) || (a == c)",
             new Dictionary<string, string> { ["rule2"] = "a == c" }
         ],
         [
-            "eval(rule1) || eval(rule2) && c && d", "eval(rule1) || a == c && 
c && d",
+            "eval(rule1) || eval(rule2) && c && d", "eval(rule1) || (a == c) 
&& c && d",
             new Dictionary<string, string> { ["rule2"] = "a == c" }
         ],
         [
diff --git a/Casbin/Config/DefaultConfig.cs b/Casbin/Config/DefaultConfig.cs
index 116b8df..9269fa4 100644
--- a/Casbin/Config/DefaultConfig.cs
+++ b/Casbin/Config/DefaultConfig.cs
@@ -182,7 +182,7 @@ namespace Casbin.Config
             using var streamReader = new StreamReader(stream);
             while ((line = streamReader.ReadLine()) != null)
             {
-                line = line.Split(_defaultComment[0]).First().Trim();
+                line = line.Split(_defaultComment[0], ';').First().Trim();
                 if (line.EndsWith(_defaultFeed))
                 {
                     processedValue += line.Split(_defaultFeed[0]).First();
diff --git a/Casbin/EnforceView.cs b/Casbin/EnforceView.cs
index 0d5bb33..2555107 100644
--- a/Casbin/EnforceView.cs
+++ b/Casbin/EnforceView.cs
@@ -60,6 +60,7 @@ namespace Casbin
             string effectType = PermConstants.DefaultPolicyEffectType)
         {
             matcher = matcher.Replace('\'', '"');
+            matcher = StringUtil.ReplaceInListOperator(matcher);
             IReadOnlyAssertion requestAssertion = 
model.Sections.GetRequestAssertion(requestType);
             PolicyAssertion policyAssertion = 
model.Sections.GetPolicyAssertion(policyType);
             IReadOnlyAssertion effectAssertion = 
model.Sections.GetPolicyEffectAssertion(effectType);
diff --git a/Casbin/Enforcer.Internal.cs b/Casbin/Enforcer.Internal.cs
index 02dad2b..f72ffa5 100644
--- a/Casbin/Enforcer.Internal.cs
+++ b/Casbin/Enforcer.Internal.cs
@@ -302,6 +302,11 @@ public partial class Enforcer
 
         if (context.Explain && effectChain.HitPolicy)
         {
+            if (effectChain.CanChain is false && effectChain.Result is false)
+            {
+                context.Explanations.Clear();
+            }
+
             context.Explanations.Add(policy);
         }
 
@@ -328,7 +333,7 @@ public partial class Enforcer
         Dictionary<string, string> rules = new();
         foreach (KeyValuePair<string, int> rule in context.View.EvalRules)
         {
-            rules[rule.Key] = policyValues[rule.Value];
+            rules[rule.Key] = policyValues[rule.Value].Replace('\'', '"');
         }
 
         expressionString = StringUtil.ReplaceEval(expressionString, rules);
@@ -342,6 +347,7 @@ public partial class Enforcer
             return expressionString;
         }
 
+        expressionString = StringUtil.ReplaceInListOperator(expressionString);
         expressionString = StringUtil.ReplaceInOperator(expressionString);
         return expressionString;
     }
diff --git a/Casbin/Evaluation/EffiectEvaluation.cs 
b/Casbin/Evaluation/EffiectEvaluation.cs
index a9c6d08..993a41b 100644
--- a/Casbin/Evaluation/EffiectEvaluation.cs
+++ b/Casbin/Evaluation/EffiectEvaluation.cs
@@ -51,6 +51,7 @@ namespace Casbin.Evaluation
                     break;
 
                 case EffectExpressionType.Priority:
+                case EffectExpressionType.PriorityAllOverride:
                     switch (effect)
                     {
                         case PolicyEffect.Allow:
@@ -78,20 +79,6 @@ namespace Casbin.Evaluation
                     }
                     break;
 
-                case EffectExpressionType.PriorityAllOverride:
-                    switch (effect)
-                    {
-                        case PolicyEffect.Allow:
-                            result = true;
-                            hitPolicy = true;
-                            return false;
-                        case PolicyEffect.Deny:
-                            result = false;
-                            hitPolicy = true;
-                            return false;
-                    }
-                    break;
-
                 case EffectExpressionType.Custom:
                     // TODO: Support custom policy effect.
                     break;
diff --git a/Casbin/Extensions/Enforcer/EnforcerExtension.cs 
b/Casbin/Extensions/Enforcer/EnforcerExtension.cs
index 2889435..e44c87e 100644
--- a/Casbin/Extensions/Enforcer/EnforcerExtension.cs
+++ b/Casbin/Extensions/Enforcer/EnforcerExtension.cs
@@ -28,6 +28,7 @@ namespace Casbin
             }
 
             enforcer.Model = DefaultModel.CreateFromFile(enforcer.Model.Path);
+            enforcer.Model.RegisterDomainMatchingFunc();
         }
 
         #endregion
@@ -129,6 +130,7 @@ namespace Casbin
         public static IEnforcer SetModel(this IEnforcer enforcer, IModel model)
         {
             enforcer.Model = model;
+            model?.RegisterDomainMatchingFunc();
             enforcer.EnforceCache?.Clear();
 #if !NET452
             enforcer.Logger?.LogInformation("Enforcer Cache, Cleared all 
enforce cache");
diff --git a/Casbin/Extensions/Model/ModelExtension.cs 
b/Casbin/Extensions/Model/ModelExtension.cs
index 224a9e1..b2ee445 100644
--- a/Casbin/Extensions/Model/ModelExtension.cs
+++ b/Casbin/Extensions/Model/ModelExtension.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 using Casbin.Functions;
 using Casbin.Persist;
@@ -262,6 +263,38 @@ namespace Casbin.Model
                 assertion.RoleManager, 
model.GFunctionCachePool.GetCache(roleType)));
         }
 
+        internal static void RegisterDomainMatchingFunc(this IModel model)
+        {
+            if 
(model.Sections.ContainsSection(PermConstants.Section.RoleSection) is false
+                || 
model.Sections.TryGetAssertion(PermConstants.Section.RequestSection,
+                    PermConstants.DefaultRequestType, out RequestAssertion 
requestAssertion) is false
+                || 
model.Sections.TryGetAssertion(PermConstants.Section.PolicySection,
+                    PermConstants.DefaultPolicyType, out PolicyAssertion 
policyAssertion) is false
+                || 
model.Sections.TryGetAssertion(PermConstants.Section.MatcherSection,
+                    PermConstants.DefaultMatcherType, out MatcherAssertion 
matcherAssertion) is false
+                || requestAssertion.Tokens.Count < 2 || 
policyAssertion.Tokens.Count < 2)
+            {
+                return;
+            }
+
+            string requestDomainToken = requestAssertion.Tokens.First(token => 
token.Value is 1).Key;
+            string policyDomainToken = policyAssertion.Tokens.First(token => 
token.Value is 1).Key;
+            string matchFunc =
+                
$"keyMatch({PermConstants.DefaultRequestType}.{requestDomainToken}, 
{PermConstants.DefaultPolicyType}.{policyDomainToken})";
+            if (matcherAssertion.Value.Contains(matchFunc) is false)
+            {
+                return;
+            }
+
+            foreach (KeyValuePair<string, RoleAssertion> pair in 
model.Sections.GetRoleAssertions())
+            {
+                if (pair.Value.GroupPolicyCount > 2)
+                {
+                    
pair.Value.RoleManager.AddDomainMatchingFunc(BuiltInFunctions.KeyMatch);
+                }
+            }
+        }
+
         /// <summary>
         ///     Initializes the roles in RBAC.
         /// </summary>
diff --git a/Casbin/Model/DefaultPolicyStore.Node.cs 
b/Casbin/Model/DefaultPolicyStore.Node.cs
index e44e96d..6030481 100644
--- a/Casbin/Model/DefaultPolicyStore.Node.cs
+++ b/Casbin/Model/DefaultPolicyStore.Node.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 
 namespace Casbin.Model;
@@ -313,25 +314,19 @@ public partial class DefaultPolicyStore
             }
 
 
-            int PolicyComparison(IPolicyValues p1, IPolicyValues p2)
+            int GetSubjectLevel(IPolicyValues policy)
             {
-                string domain1 = "", domain2 = "";
-                if (domainIndex != -1)
-                {
-                    domain1 = p1[domainIndex];
-                    domain2 = p2[domainIndex];
-                }
-
-                string name1 = nameFormatter(domain1, p1[subjectIndex]);
-                string name2 = nameFormatter(domain2, p2[subjectIndex]);
-
-                return subjectHierarchyMap[name1] - subjectHierarchyMap[name2];
+                string domain = domainIndex != -1 ? policy[domainIndex] : "";
+                string name = nameFormatter(domain, policy[subjectIndex]);
+                return subjectHierarchyMap.TryGetValue(name, out int level) ? 
level : 0;
             }
 
             Lock.EnterWriteLock();
             try
             {
-                Policy.Sort(PolicyComparison);
+                List<IPolicyValues> sortedPolicy = 
Policy.OrderByDescending(GetSubjectLevel).ToList();
+                Policy.Clear();
+                Policy.AddRange(sortedPolicy);
             }
             finally
             {
diff --git a/Casbin/Model/RoleAssertion.cs b/Casbin/Model/RoleAssertion.cs
index 1537079..fa87a47 100644
--- a/Casbin/Model/RoleAssertion.cs
+++ b/Casbin/Model/RoleAssertion.cs
@@ -11,9 +11,19 @@ public class RoleAssertion : PolicyAssertion
 
     public IRoleManager RoleManager { get; internal set; }
 
+    internal int GroupPolicyCount
+    {
+        get
+        {
+            int paramsIndex = Value.IndexOf('(');
+            string value = paramsIndex is -1 ? Value : Value.Substring(0, 
paramsIndex);
+            return value.Count(c => c is '_');
+        }
+    }
+
     public void BuildRoleLinks()
     {
-        int count = Value.Count(c => c is '_');
+        int count = GroupPolicyCount;
         if (count < 2)
         {
             throw new InvalidOperationException("the number of \"_\" in role 
definition should be at least 2.");
@@ -27,7 +37,7 @@ public class RoleAssertion : PolicyAssertion
 
     internal void BuildIncrementalRoleLink(PolicyOperation policyOperation, 
IEnumerable<string> rule)
     {
-        int count = Value.Count(c => c is '_');
+        int count = GroupPolicyCount;
         if (count < 2)
         {
             throw new InvalidOperationException("the number of \"_\" in role 
definition should be at least 2.");
@@ -39,7 +49,7 @@ public class RoleAssertion : PolicyAssertion
     internal void BuildIncrementalRoleLink(PolicyOperation policyOperation,
         IEnumerable<string> oldRule, IEnumerable<string> newRule)
     {
-        int count = Value.Count(c => c is '_');
+        int count = GroupPolicyCount;
         if (count < 2)
         {
             throw new InvalidOperationException("the number of \"_\" in role 
definition should be at least 2.");
@@ -50,7 +60,7 @@ public class RoleAssertion : PolicyAssertion
 
     internal void BuildIncrementalRoleLinks(PolicyOperation policyOperation, 
IEnumerable<IEnumerable<string>> rules)
     {
-        int count = Value.Count(c => c is '_');
+        int count = GroupPolicyCount;
         if (count < 2)
         {
             throw new InvalidOperationException("the number of \"_\" in role 
definition should be at least 2.");
@@ -65,7 +75,7 @@ public class RoleAssertion : PolicyAssertion
     internal void BuildIncrementalRoleLinks(PolicyOperation policyOperation,
         IEnumerable<IEnumerable<string>> oldRules, 
IEnumerable<IEnumerable<string>> newRules)
     {
-        int count = Value.Count(c => c is '_');
+        int count = GroupPolicyCount;
         if (count < 2)
         {
             throw new InvalidOperationException("the number of \"_\" in role 
definition should be at least 2.");
@@ -198,7 +208,12 @@ public class RoleAssertion : PolicyAssertion
                 policyChildrenMap[parent] = new List<string>(new[] { child });
             }
 
-            refer[parent] = refer[child] = 0;
+            if (refer.ContainsKey(parent) is false)
+            {
+                refer[parent] = 0;
+            }
+
+            refer[child] = 1;
         }
 
         Queue<string> queue = new();
diff --git a/Casbin/Util/StringUtil.cs b/Casbin/Util/StringUtil.cs
index 1377cb6..c6cbd73 100644
--- a/Casbin/Util/StringUtil.cs
+++ b/Casbin/Util/StringUtil.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text.RegularExpressions;
 
 namespace Casbin.Util
@@ -8,6 +9,7 @@ namespace Casbin.Util
     {
         private static readonly Regex s_evalRegex = 
new(@"\beval\((?<rule>[^)]*)\)");
         private static readonly Regex s_inOperatorRegex = 
new(@"([a-zA-Z0-9_.()""]+) in ([a-zA-Z0-9_.()""]+)");
+        private static readonly Regex s_inListOperatorRegex = 
new(@"([a-zA-Z0-9_.]+)\s+in\s+(?:\(([^()]*)\)|\[([^\[\]]*)\])");
 
         /// <summary>
         /// Removes the comments starting with # in the text.
@@ -86,7 +88,7 @@ namespace Casbin.Util
                 {
                     return match.Value;
                 }
-                return rule;
+                return "(" + rule + ")";
             });
         }
 
@@ -109,5 +111,55 @@ namespace Casbin.Util
             });
             return res;
         }
+
+        /// <summary>
+        /// Replace in operator with a literal list, such as "a in ('b', 
'c')", with "(a == 'b' || a == 'c')".
+        /// </summary>
+        /// <param name="expressString"></param>
+        /// <returns></returns>
+        internal static string ReplaceInListOperator(string expressString)
+        {
+            return s_inListOperatorRegex.Replace(expressString, match =>
+            {
+                string item = match.Groups[1].Value;
+                string list = match.Groups[2].Success ? match.Groups[2].Value 
: match.Groups[3].Value;
+                List<string> values = SplitListValues(list);
+                if (values.Count < 2 && (values.Count is 0 || 
values[0].StartsWith("\"") is false))
+                {
+                    return match.Value;
+                }
+
+                return "(" + string.Join(" || ", values.Select(value => item + 
" == " + value)) + ")";
+            });
+        }
+
+        private static List<string> SplitListValues(string list)
+        {
+            List<string> values = new();
+            int start = 0;
+            bool inQuotes = false;
+            for (int i = 0; i <= list.Length; i++)
+            {
+                if (i < list.Length && list[i] is '"')
+                {
+                    inQuotes = !inQuotes;
+                }
+
+                if (i < list.Length && (inQuotes || list[i] is not ','))
+                {
+                    continue;
+                }
+
+                string value = list.Substring(start, i - start).Trim();
+                if (value.Length is not 0)
+                {
+                    values.Add(value);
+                }
+
+                start = i + 1;
+            }
+
+            return values;
+        }
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to