topdown: Add new regex.is_valid built-in function

The existing regex functions error if the pattern is invalid. Until
OPA exposes a way to treat built-in function errors as undefined, it
is useful to be able to check for validity. Also, some use cases may
simply want to verify a regex pattern from the outside.

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
This commit is contained in:
Torin Sandall
2020-07-16 11:44:56 -04:00
parent 1ca81b3f90
commit 91a85f79bf
4 changed files with 61 additions and 2 deletions
+12
View File
@@ -87,6 +87,7 @@ var DefaultBuiltins = [...]*Builtin{
CastArray,
// Regular Expressions
RegexIsValid,
RegexMatch,
RegexSplit,
GlobsMatch,
@@ -678,6 +679,17 @@ var RegexMatch = &Builtin{
),
}
// RegexIsValid returns true if the regex pattern string is valid, otherwise false.
var RegexIsValid = &Builtin{
Name: "regex.is_valid",
Decl: types.NewFunction(
types.Args(
types.S,
),
types.B,
),
}
// RegexFindAllStringSubmatch returns an array of all successive matches of the expression.
// It takes two strings and a number, the pattern, the value and number of matches to
// return, -1 means all matches.
+1
View File
@@ -362,6 +362,7 @@ complex types.
| Built-in | Description |
| ------- |-------------|
| <span class="opa-keep-it-together">``re_match(pattern, value)``</span> | true if the ``value`` matches the regex ``pattern`` |
| <span class="opa-keep-it-together">``output := regex.is_valid(pattern)``</span> | ``output`` is a ``boolean`` that indicates if ``pattern` is a valid regex pattern. The detailed syntax for regex patterns is defined by https://github.com/google/re2/wiki/Syntax. |
| <span class="opa-keep-it-together">``output := regex.split(pattern, string)``</span> | ``output`` is ``array[string]`` representing elements of ``string`` separated by ``pattern`` |
| <span class="opa-keep-it-together">``regex.globs_match(glob1, glob2)``</span> | true if the intersection of regex-style globs ``glob1`` and ``glob2`` matches a non-empty set of non-empty strings. The set of regex symbols is limited for this builtin: only ``.``, ``*``, ``+``, ``[``, ``-``, ``]`` and ``\`` are treated as special symbols. |
| <span class="opa-keep-it-normal">``output := regex.template_match(pattern, string, delimiter_start, delimiter_end)``</span> | ``output`` is true if ``string`` matches ``pattern``. ``pattern`` is a string containing ``0..n`` regular expressions delimited by ``delimiter_start`` and ``delimiter_end``. Example ``regex.template_match("urn:foo:{.*}", "urn:foo:bar:baz", "{", "}")`` returns ``true``. |
+17 -1
View File
@@ -9,7 +9,7 @@ import (
"regexp"
"sync"
"github.com/yashtewari/glob-intersection"
gintersect "github.com/yashtewari/glob-intersection"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/topdown/builtins"
@@ -18,6 +18,21 @@ import (
var regexpCacheLock = sync.Mutex{}
var regexpCache map[string]*regexp.Regexp
func builtinRegexIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
s, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return iter(ast.BooleanTerm(false))
}
_, err = regexp.Compile(string(s))
if err != nil {
return iter(ast.BooleanTerm(false))
}
return iter(ast.BooleanTerm(true))
}
func builtinRegexMatch(a, b ast.Value) (ast.Value, error) {
s1, err := builtins.StringOperand(a, 1)
if err != nil {
@@ -192,6 +207,7 @@ func builtinRegexFindAllStringSubmatch(a, b, c ast.Value) (ast.Value, error) {
func init() {
regexpCache = map[string]*regexp.Regexp{}
RegisterBuiltinFunc(ast.RegexIsValid.Name, builtinRegexIsValid)
RegisterFunctionalBuiltin2(ast.RegexMatch.Name, builtinRegexMatch)
RegisterFunctionalBuiltin2(ast.RegexSplit.Name, builtinRegexSplit)
RegisterFunctionalBuiltin2(ast.GlobsMatch.Name, builtinGlobsMatch)
+31 -1
View File
@@ -1,6 +1,36 @@
package topdown
import "testing"
import (
"encoding/json"
"testing"
)
func TestRegexIsValid(t *testing.T) {
tests := []struct {
note string
rules []string
expected interface{}
}{
{
note: "bad operand type",
rules: []string{"p = x { regex.is_valid(data.num, x) }"},
expected: "false",
},
{
note: "bad pattern",
rules: []string{"p = x { regex.is_valid(`++`, x) }"},
expected: "false",
},
{
note: "good pattern",
rules: []string{"p = x { regex.is_valid(`.+`, x) }"},
expected: "true",
},
}
for _, tc := range tests {
runTopDownTestCase(t, map[string]interface{}{"num": json.Number("10")}, tc.note, tc.rules, tc.expected)
}
}
func TestRegexMatchTemplate(t *testing.T) {
tests := []struct {