// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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. package casbin import ( "testing" "github.com/casbin/casbin/v3/model" fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" ) // TestBackslashHandlingConsistency tests that backslashes in string literals // within matcher expressions are handled consistently with CSV-parsed values. // This addresses the issue where govaluate interprets escape sequences in // string literals, but CSV parsing treats backslashes as literal characters. func TestBackslashHandlingConsistency(t *testing.T) { // Test case 1: Literal string in matcher should match CSV-parsed request t.Run("LiteralInMatcher", func(t *testing.T) { m := model.NewModel() m.AddDef("r", "r", "sub, obj, act") m.AddDef("p", "p", "sub, obj, act") m.AddDef("e", "e", "some(where (p.eft == allow))") // User writes '\1\2' in matcher - should be treated as literal backslashes m.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)") e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) if err != nil { t.Fatal(err) } // Add a policy with a regex pattern containing backslashes // CSV format: "\\[0-9]+\\" means literal string with 4 backslashes _, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read") if err != nil { t.Fatal(err) } // This should match because '\1\2' after escaping becomes \1\2, // and the pattern \\[0-9]+\\ matches strings like \1\ (which is a substring of \1\2) result, err := e.Enforce("filename", "dummy", "read") if err != nil { t.Fatal(err) } if !result { t.Errorf("Expected true, got false - literal '\\1\\2' should match after escape processing") } }) // Test case 2: Request parameter should match policy with same backslash content t.Run("RequestParameterVsPolicy", func(t *testing.T) { m := model.NewModel() m.AddDef("r", "r", "sub, obj, act") m.AddDef("p", "p", "sub, obj, act") m.AddDef("e", "e", "some(where (p.eft == allow))") m.AddDef("m", "m", "regexMatch(r.obj, p.obj)") e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) if err != nil { t.Fatal(err) } // Add policy with regex pattern _, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read") if err != nil { t.Fatal(err) } // Request with backslashes - simulating CSV input "\1\2" which becomes \1\2 result, err := e.Enforce("filename", "\\1\\2", "read") if err != nil { t.Fatal(err) } if !result { t.Errorf("Expected true, got false - request \\1\\2 should match regex pattern") } }) // Test case 3: Both approaches should give the same result t.Run("ConsistencyBetweenLiteralAndParameter", func(t *testing.T) { // Create two enforcers with different matchers m1 := model.NewModel() m1.AddDef("r", "r", "sub, obj, act") m1.AddDef("p", "p", "sub, obj, act") m1.AddDef("e", "e", "some(where (p.eft == allow))") m1.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)") m2 := model.NewModel() m2.AddDef("r", "r", "sub, obj, act") m2.AddDef("p", "p", "sub, obj, act") m2.AddDef("e", "e", "some(where (p.eft == allow))") m2.AddDef("m", "m", "regexMatch(r.obj, p.obj)") e1, err := NewEnforcer(m1, fileadapter.NewAdapter("examples/basic_policy.csv")) if err != nil { t.Fatal(err) } e2, err := NewEnforcer(m2, fileadapter.NewAdapter("examples/basic_policy.csv")) if err != nil { t.Fatal(err) } // Add same policy to both pattern := "\\\\[0-9]+\\\\" _, err = e1.AddPolicy("filename", pattern, "read") if err != nil { t.Fatal(err) } _, err = e2.AddPolicy("filename", pattern, "read") if err != nil { t.Fatal(err) } // Test with the same request result1, err := e1.Enforce("filename", "dummy", "read") if err != nil { t.Fatal(err) } result2, err := e2.Enforce("filename", "\\1\\2", "read") if err != nil { t.Fatal(err) } if result1 != result2 { t.Errorf("Inconsistent results: literal in matcher gave %v, parameter gave %v", result1, result2) } }) // Test case 4: Simple equality check with backslashes t.Run("SimpleEqualityWithBackslashes", func(t *testing.T) { m := model.NewModel() m.AddDef("r", "r", "sub, obj, act") m.AddDef("p", "p", "sub, obj, act") m.AddDef("e", "e", "some(where (p.eft == allow))") // In Go source, '\test' (one backslash in the actual string) represents // what would be typed in a web form. After escape processing, it will match // the CSV-parsed value "\test" (one backslash). // Note: In Go source, we write "r.obj == '\\test'" which is a Go string // containing the text: r.obj == '\test' (with ONE backslash in the string content) m.AddDef("m", "m", "r.obj == '\\test' && p.sub == r.sub") e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) if err != nil { t.Fatal(err) } _, err = e.AddPolicy("alice", "any", "read") if err != nil { t.Fatal(err) } // Request with literal backslash from CSV would be "\test" // In Go source, we write "\\test" which represents the string \test (one backslash) result, err := e.Enforce("alice", "\\test", "read") if err != nil { t.Fatal(err) } if !result { t.Errorf("Expected true - literal '\\test' should equal request parameter \\test") } }) }