Skip to content
Merged
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,43 @@ https://casbin.org/docs/adapters

https://casbin.org/docs/role-managers

## Expression Validation and Cross-Platform Compatibility

Starting from version 1.98.1, jCasbin validates expressions to ensure cross-platform compatibility with other Casbin implementations (Go, Node.js, Python, .NET, etc.).

### Restricted Syntax

The following AviatorScript-specific features are **not allowed** in `eval()` expressions and policy rules:

- **Namespace methods**: `seq.list()`, `string.startsWith()`, `string.endsWith()`, `math.sqrt()`, etc.
- **Advanced control structures**: `lambda`, `let`, `fn`, `for`, `while`, `return`, `if-then-else`, `->`

These features are restricted because they are specific to AviatorScript and would make policies incompatible with other Casbin implementations.

### Allowed Syntax

The following standard Casbin syntax is fully supported:

- **Operators**: `&&`, `||`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `+`, `-`, `*`, `/`, `!`, `in`
- **Built-in functions**: `g()`, `keyMatch()`, `keyMatch2-5()`, `regexMatch()`, `ipMatch()`, `globMatch()`, `timeMatch()`, `eval()`
- **Custom functions**: Users can still register custom functions using `enforcer.addFunction()`
- **Variable access**: `r.attr`, `p.attr` (automatically escaped to `r_attr`, `p_attr`)

### Example

```java
// ❌ NOT allowed - AviatorScript-specific syntax
"eval(seq.list('admin', 'editor'))"
"eval(string.startsWith(r.path, '/admin'))"

// ✅ Allowed - Standard Casbin syntax
"eval(r.age > 18 && r.age < 65)"
"r.role in ('admin', 'editor')" // Converted to include(tuple(...), ...)
"g(r.sub, p.sub) && keyMatch(r.path, p.path)"
```

If an expression contains restricted syntax, it will be logged as a warning and return `false`.

## Examples

| Model | Model file | Policy file |
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@ public String getName() {
* @return the result of the eval.
*/
public static boolean eval(String eval, Map<String, Object> env, AviatorEvaluatorInstance aviatorEval) {
// Validate expression to block AviatorScript-specific features
// that break cross-platform compatibility
try {
ExpressionValidator.validateExpression(eval);
} catch (IllegalArgumentException e) {
Util.logPrintfWarn("Expression validation failed: {}", e.getMessage());
return false;
}

boolean res;
if (aviatorEval != null) {
try {
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/org/casbin/jcasbin/util/ExpressionValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 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 org.casbin.jcasbin.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* ExpressionValidator validates expressions to ensure they only use standard Casbin syntax
* and don't expose AviatorScript-specific features that would break cross-platform compatibility.
*/
public class ExpressionValidator {

// Patterns for AviatorScript-specific syntax that should be blocked
private static final Pattern[] DISALLOWED_PATTERNS = {
Pattern.compile("\\bseq\\."), // seq.list(), seq.map(), etc.
Pattern.compile("\\bstring\\."), // string.startsWith(), string.endsWith(), etc.
Pattern.compile("\\bmath\\."), // math.sqrt(), math.pow(), etc.
Pattern.compile("\\blambda\\b"), // lambda expressions
Pattern.compile("\\blet\\b"), // variable binding
Pattern.compile("\\bfn\\b"), // function definitions
Pattern.compile("->"), // lambda arrow
Pattern.compile("=>"), // alternative lambda arrow
Pattern.compile("\\bfor\\b"), // for loops
Pattern.compile("\\bwhile\\b"), // while loops
Pattern.compile("\\breturn\\b"), // return statements
Pattern.compile("\\bif\\b.*\\bthen\\b.*\\belse\\b"), // if-then-else (aviator style)
};

/**
* Validates that an expression only uses standard Casbin syntax.
*
* @param expression the expression to validate
* @throws IllegalArgumentException if the expression contains non-standard syntax
*/
public static void validateExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return;
}

// Check for disallowed AviatorScript-specific patterns
for (Pattern pattern : DISALLOWED_PATTERNS) {
Matcher matcher = pattern.matcher(expression);
if (matcher.find()) {
throw new IllegalArgumentException(
"Expression contains non-standard syntax: '" + matcher.group() +
"'. This AviatorScript-specific feature is not part of Casbin's standard specification."
);
}
}
}
}
231 changes: 231 additions & 0 deletions src/test/java/org/casbin/jcasbin/main/ExpressionValidatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright 2024 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 org.casbin.jcasbin.main;

import org.casbin.jcasbin.util.ExpressionValidator;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class ExpressionValidatorTest {

@Test
public void testValidStandardCasbinExpressions() {
// Standard operators and comparisons should be allowed
ExpressionValidator.validateExpression("r_sub == p_sub");
ExpressionValidator.validateExpression("r_sub == p_sub && r_obj == p_obj");
ExpressionValidator.validateExpression("r_sub == p_sub || r_obj == p_obj");
ExpressionValidator.validateExpression("r_age > 18");
ExpressionValidator.validateExpression("r_age >= 18 && r_age < 65");
ExpressionValidator.validateExpression("r_status != 'banned'");

// Arithmetic should be allowed
ExpressionValidator.validateExpression("r_price * 1.1 > p_threshold");
ExpressionValidator.validateExpression("r_count + p_offset < 100");
ExpressionValidator.validateExpression("r_value - p_discount >= 0");
ExpressionValidator.validateExpression("r_total / r_count > 50");

// Negation should be allowed
ExpressionValidator.validateExpression("!r_disabled");
ExpressionValidator.validateExpression("!(r_sub == p_sub)");
}

@Test
public void testValidCasbinBuiltInFunctions() {
// All standard Casbin functions should be allowed
ExpressionValidator.validateExpression("g(r_sub, p_sub)");
ExpressionValidator.validateExpression("g2(r_sub, p_sub, r_domain)");
ExpressionValidator.validateExpression("keyMatch(r_path, p_path)");
ExpressionValidator.validateExpression("keyMatch2(r_path, p_path)");
ExpressionValidator.validateExpression("keyMatch3(r_path, p_path)");
ExpressionValidator.validateExpression("keyMatch4(r_path, p_path)");
ExpressionValidator.validateExpression("keyMatch5(r_path, p_path)");
ExpressionValidator.validateExpression("keyGet(r_path, p_path)");
ExpressionValidator.validateExpression("keyGet2(r_path, p_path, 'id')");
ExpressionValidator.validateExpression("regexMatch(r_path, p_pattern)");
ExpressionValidator.validateExpression("ipMatch(r_ip, p_cidr)");
ExpressionValidator.validateExpression("globMatch(r_path, p_glob)");
ExpressionValidator.validateExpression("allMatch(r_key, p_key)");
ExpressionValidator.validateExpression("timeMatch(r_time, p_time)");
ExpressionValidator.validateExpression("eval(p_rule)");

// Include and tuple are used for "in" operator conversion
ExpressionValidator.validateExpression("include(r_obj, r_sub)");
ExpressionValidator.validateExpression("include(tuple('admin', 'editor'), r_role)");

// Custom functions should be allowed (users can register them)
ExpressionValidator.validateExpression("customFunc(r_sub, p_sub)");
ExpressionValidator.validateExpression("myFunction(r_value)");
}

@Test
public void testValidComplexExpressions() {
// Complex combinations should be allowed
ExpressionValidator.validateExpression("g(r_sub, p_sub) && r_obj == p_obj && r_act == p_act");
ExpressionValidator.validateExpression("g(r_sub, p_sub) && keyMatch(r_path, p_path)");
ExpressionValidator.validateExpression("eval(p_sub_rule) && r_obj == p_obj");
ExpressionValidator.validateExpression("r_age > 18 && include(tuple('read', 'write'), r_act)");
ExpressionValidator.validateExpression("r_sub.age >= 18 && custom(r_obj)");
}

@Test
public void testDisallowedAviatorScriptSequenceMethods() {
// seq.list() should be disallowed
try {
ExpressionValidator.validateExpression("seq.list('A', 'B')");
fail("Should have thrown IllegalArgumentException for seq.list()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("seq."));
assertTrue(e.getMessage().contains("AviatorScript-specific"));
}

// seq.map() should be disallowed
try {
ExpressionValidator.validateExpression("seq.map(r_items, lambda(x) -> x * 2)");
fail("Should have thrown IllegalArgumentException for seq.map()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("seq."));
}
}

@Test
public void testDisallowedAviatorScriptStringMethods() {
// string.startsWith() should be disallowed
try {
ExpressionValidator.validateExpression("string.startsWith(r_path, '/admin')");
fail("Should have thrown IllegalArgumentException for string.startsWith()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("string."));
assertTrue(e.getMessage().contains("AviatorScript-specific"));
}

// string.endsWith() should be disallowed
try {
ExpressionValidator.validateExpression("string.endsWith(r_path, '.pdf')");
fail("Should have thrown IllegalArgumentException for string.endsWith()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("string."));
}

// string.substring() should be disallowed
try {
ExpressionValidator.validateExpression("string.substring(r_path, 0, 5)");
fail("Should have thrown IllegalArgumentException for string.substring()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("string."));
}
}

@Test
public void testDisallowedAviatorScriptMathMethods() {
// math.sqrt() should be disallowed
try {
ExpressionValidator.validateExpression("math.sqrt(r_value) > 10");
fail("Should have thrown IllegalArgumentException for math.sqrt()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("math."));
assertTrue(e.getMessage().contains("AviatorScript-specific"));
}

// math.pow() should be disallowed
try {
ExpressionValidator.validateExpression("math.pow(r_base, 2)");
fail("Should have thrown IllegalArgumentException for math.pow()");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("math."));
}
}

@Test
public void testDisallowedLambdaExpressions() {
// Lambda with arrow should be disallowed
try {
ExpressionValidator.validateExpression("lambda(x) -> x * 2");
fail("Should have thrown IllegalArgumentException for lambda");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("lambda") || e.getMessage().contains("->"));
}

// Alternative lambda syntax should be disallowed
try {
ExpressionValidator.validateExpression("(x) => x * 2");
fail("Should have thrown IllegalArgumentException for lambda arrow");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("=>"));
}
}

@Test
public void testDisallowedControlStructures() {
// for loops should be disallowed
try {
ExpressionValidator.validateExpression("for x in r_items { x * 2 }");
fail("Should have thrown IllegalArgumentException for 'for'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("for"));
}

// while loops should be disallowed
try {
ExpressionValidator.validateExpression("while x < 10 { x = x + 1 }");
fail("Should have thrown IllegalArgumentException for 'while'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("while"));
}

// if-then-else (Aviator style) should be disallowed
try {
ExpressionValidator.validateExpression("if r_age > 18 then 'adult' else 'minor'");
fail("Should have thrown IllegalArgumentException for 'if-then-else'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("if") || e.getMessage().contains("then") || e.getMessage().contains("else"));
}
}

@Test
public void testDisallowedVariableBindingAndFunctions() {
// let variable binding should be disallowed
try {
ExpressionValidator.validateExpression("let x = 10; x * 2");
fail("Should have thrown IllegalArgumentException for 'let'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("let"));
}

// function definitions should be disallowed
try {
ExpressionValidator.validateExpression("fn add(a, b) { a + b }");
fail("Should have thrown IllegalArgumentException for 'fn'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("fn"));
}

// return statements should be disallowed
try {
ExpressionValidator.validateExpression("return r_value * 2");
fail("Should have thrown IllegalArgumentException for 'return'");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("return"));
}
}

@Test
public void testNullAndEmptyExpressions() {
// Null and empty expressions should be allowed (no validation needed)
ExpressionValidator.validateExpression(null);
ExpressionValidator.validateExpression("");
ExpressionValidator.validateExpression(" ");
}
}
Loading