Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions MULTILINE_MATCHER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Multi-line Matcher Support

Casbin now supports multi-line matchers with block-style syntax, allowing you to write more complex and readable matcher expressions.

## Features

- **Let statements**: Define intermediate variables to break down complex expressions
- **Early returns**: Use `if` statements with `return` for conditional logic
- **Block syntax**: Write matchers within `{}` braces with multiple lines

## Syntax

### Basic Block Syntax

```ini
[matchers]
m = { \
return r.sub == p.sub && r.obj == p.obj && r.act == p.act \
}
```

### With Let Statements

```ini
[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
let obj_match = r.obj == p.obj \
let act_match = r.act == p.act \
return role_match && obj_match && act_match \
}
```

### With Nested Variables

```ini
[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
let obj_direct_match = r.obj == p.obj \
let obj_inherit_match = g2(r.obj, p.obj) \
let obj_match = obj_direct_match || obj_inherit_match \
let act_match = r.act == p.act \
return role_match && obj_match && act_match \
}
```

### With Early Returns

```ini
[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
if !role_match { \
return false \
} \
if r.act != p.act { \
return false \
} \
if r.obj == p.obj { \
return true \
} \
if g2(r.obj, p.obj) { \
return true \
} \
return false \
}
```

## How It Works

The multi-line matcher syntax is automatically transformed into a single-line expression that can be evaluated by the underlying govaluate engine. This transformation:

1. **Extracts let statements**: Variable definitions are identified and their expressions are stored
2. **Substitutes variables**: All variable references are replaced with their actual expressions
3. **Converts early returns**: `if` statements with returns are transformed into conditional logic using boolean operators

For example, the matcher:
```
{
let role_match = g(r.sub, p.sub)
let obj_match = r.obj == p.obj
return role_match && obj_match
}
```

Is transformed into:
```
(g(r.sub, p.sub)) && (r.obj == p.obj)
```

## Important Notes

1. **Semicolons**: Do NOT use semicolons (`;`) at the end of statements. The config parser treats semicolons as comment markers and will strip them out.

2. **Line continuation**: Use backslash (`\`) at the end of each line to continue the matcher across multiple lines in the config file.

3. **Backward compatibility**: Traditional single-line matchers continue to work without any changes.

4. **In-memory models**: You can use multi-line matchers in code when creating models programmatically:
```go
m := model.NewModel()
m.AddDef("m", "m", `{
let role_match = g(r.sub, p.sub)
let obj_match = r.obj == p.obj
return role_match && obj_match
}`)
```

## Examples

See the `examples/` directory for complete working examples:
- `rbac_with_hierarchy_multiline_model.conf` - Multi-line matcher with let statements
- `rbac_with_early_return_model.conf` - Multi-line matcher with early returns

## Testing

Run the multi-line matcher tests:
```bash
go test -v -run TestMultiLineMatcher
```
22 changes: 22 additions & 0 deletions examples/issue_example_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
let obj_direct_match = r.obj == p.obj \
let obj_inherit_match = g2(r.obj, p.obj) \
let obj_match = obj_direct_match || obj_inherit_match \
let act_match = r.act == p.act \
return role_match && obj_match && act_match \
}
30 changes: 30 additions & 0 deletions examples/rbac_with_early_return_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
if !role_match { \
return false \
} \
if r.act != p.act { \
return false \
} \
if r.obj == p.obj { \
return true \
} \
if g2(r.obj, p.obj) { \
return true \
} \
return false \
}
22 changes: 22 additions & 0 deletions examples/rbac_with_hierarchy_multiline_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = { \
let role_match = g(r.sub, p.sub) \
let obj_direct_match = r.obj == p.obj \
let obj_inherit_match = g2(r.obj, p.obj) \
let obj_match = obj_direct_match || obj_inherit_match \
let act_match = r.act == p.act \
return role_match && obj_match && act_match \
}
7 changes: 7 additions & 0 deletions examples/rbac_with_hierarchy_multiline_policy.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
p, alice, data1, read
p, bob, data2, write
p, data_group_admin, data_group, write

g, alice, data_group_admin
g2, data1, data_group
g2, data2, data_group
3 changes: 3 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ func (model Model) AddDef(sec string, key string, value string) bool {
}

if sec == "m" {
// Transform block-style matchers to single-line expressions
ast.Value = util.TransformBlockMatcher(ast.Value)

// Escape backslashes in string literals to match CSV parsing behavior
ast.Value = util.EscapeStringLiterals(ast.Value)

Expand Down
134 changes: 134 additions & 0 deletions multiline_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2017 The casbin Authors. All Rights Reserved.
//
// 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.

package casbin

import (
"testing"

"github.com/casbin/casbin/v3/model"
)

func TestMultiLineMatcherWithLetStatements(t *testing.T) {
e, err := NewEnforcer("examples/rbac_with_hierarchy_multiline_model.conf", "examples/rbac_with_hierarchy_multiline_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}

// alice has direct permission on data1 for read
testEnforce(t, e, "alice", "data1", "read", true)

// alice doesn't have direct permission on data1 for write, but has via role and resource hierarchy
testEnforce(t, e, "alice", "data1", "write", true)

// bob has direct permission on data2 for write
testEnforce(t, e, "bob", "data2", "write", true)

// bob doesn't have direct permission on data1
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)

// Test with inherited permissions through data_group
testEnforce(t, e, "alice", "data2", "write", true)
}

func TestMultiLineMatcherWithEarlyReturn(t *testing.T) {
e, err := NewEnforcer("examples/rbac_with_early_return_model.conf", "examples/rbac_with_hierarchy_multiline_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}

// alice has direct permission on data1 for read
testEnforce(t, e, "alice", "data1", "read", true)

// alice doesn't have direct permission on data1 for write, but has via role
testEnforce(t, e, "alice", "data1", "write", true)

// bob has direct permission on data2 for write
testEnforce(t, e, "bob", "data2", "write", true)

// bob doesn't have permission on data1
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)

// alice can write to data2 through role and resource hierarchy
testEnforce(t, e, "alice", "data2", "write", true)
}

func TestMultiLineMatcherInMemory(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("g", "g", "_, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", `{
let role_match = g(r.sub, p.sub)
let obj_match = r.obj == p.obj
let act_match = r.act == p.act
return role_match && obj_match && act_match
}`)

e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}

// Add policies
_, _ = e.AddPolicy("alice", "data1", "read")
_, _ = e.AddPolicy("data_admin", "data2", "write")
_, _ = e.AddGroupingPolicy("bob", "data_admin")

// Test enforcement
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "write", true)
testEnforce(t, e, "bob", "data1", "read", false)
}

func TestSimpleBlockMatcher(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", `{
return r.sub == p.sub && r.obj == p.obj && r.act == p.act
}`)

e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}

_, _ = e.AddPolicy("alice", "data1", "read")
_, _ = e.AddPolicy("bob", "data2", "write")

testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "write", true)
testEnforce(t, e, "bob", "data1", "read", false)
}

func TestIssueExampleMatcher(t *testing.T) {
// This test demonstrates the exact use case from the issue
e, err := NewEnforcer("examples/issue_example_model.conf", "examples/rbac_with_hierarchy_multiline_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}

// Verify the multi-line matcher with let statements works correctly
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", true) // via role + resource hierarchy
testEnforce(t, e, "bob", "data2", "write", true)
testEnforce(t, e, "alice", "data2", "write", true) // via role + resource hierarchy
}
Loading
Loading