mirror of
https://github.com/alibaba/higress.git
synced 2026-05-11 14:27:27 +08:00
feat: support ext_auth wasmplugin (#1103)
This commit is contained in:
201
plugins/wasm-go/extensions/ext-auth/expr/matcher.go
Normal file
201
plugins/wasm-go/extensions/ext-auth/expr/matcher.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
matchPatternExact string = "exact"
|
||||
matchPatternPrefix string = "prefix"
|
||||
matchPatternSuffix string = "suffix"
|
||||
matchPatternContains string = "contains"
|
||||
matchPatternRegex string = "regex"
|
||||
|
||||
matchIgnoreCase string = "ignore_case"
|
||||
)
|
||||
|
||||
type Matcher interface {
|
||||
Match(s string) bool
|
||||
}
|
||||
|
||||
type stringExactMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringExactMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.ToLower(s) == m.target
|
||||
}
|
||||
return s == m.target
|
||||
}
|
||||
|
||||
type stringPrefixMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringPrefixMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.HasPrefix(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.HasPrefix(s, m.target)
|
||||
}
|
||||
|
||||
type stringSuffixMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringSuffixMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.HasSuffix(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.HasSuffix(s, m.target)
|
||||
}
|
||||
|
||||
type stringContainsMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringContainsMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.Contains(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.Contains(s, m.target)
|
||||
}
|
||||
|
||||
type stringRegexMatcher struct {
|
||||
regex *regexp.Regexp
|
||||
}
|
||||
|
||||
func (m *stringRegexMatcher) Match(s string) bool {
|
||||
return m.regex.MatchString(s)
|
||||
}
|
||||
|
||||
type repeatedStringMatcher struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (rsm *repeatedStringMatcher) Match(s string) bool {
|
||||
for _, m := range rsm.matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
|
||||
builtMatchers := make([]Matcher, len(matchers))
|
||||
|
||||
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, matcherType MatcherConstructor) (Matcher, error) {
|
||||
result := json.Get(targetKey)
|
||||
if result.Exists() && result.String() != "" {
|
||||
target := result.String()
|
||||
return matcherType(target, ignoreCase)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, item := range matchers {
|
||||
var matcher Matcher
|
||||
var err error
|
||||
|
||||
// If allIgnoreCase is true, it takes precedence over any user configuration,
|
||||
// forcing case-insensitive matching regardless of individual item settings.
|
||||
ignoreCase := allIgnoreCase
|
||||
if !allIgnoreCase {
|
||||
ignoreCaseResult := item.Get(matchIgnoreCase)
|
||||
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
|
||||
ignoreCase = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, matcherType := range []struct {
|
||||
key string
|
||||
creator MatcherConstructor
|
||||
}{
|
||||
{matchPatternExact, newStringExactMatcher},
|
||||
{matchPatternPrefix, newStringPrefixMatcher},
|
||||
{matchPatternSuffix, newStringSuffixMatcher},
|
||||
{matchPatternContains, newStringContainsMatcher},
|
||||
{matchPatternRegex, newStringRegexMatcher},
|
||||
} {
|
||||
if matcher, err = createMatcher(item, matcherType.key, ignoreCase, matcherType.creator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matcher == nil {
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
builtMatchers[i] = matcher
|
||||
|
||||
}
|
||||
|
||||
return &repeatedStringMatcher{
|
||||
matchers: builtMatchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MatcherConstructor func(string, bool) (Matcher, error)
|
||||
|
||||
func newStringExactMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringExactMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringPrefixMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringPrefixMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringSuffixMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringSuffixMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringContainsMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringContainsMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringRegexMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase && !strings.HasPrefix(target, "(?i)") {
|
||||
target = "(?i)" + target
|
||||
}
|
||||
re, err := regexp.Compile(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stringRegexMatcher{regex: re}, nil
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, true)
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, false)
|
||||
}
|
||||
|
||||
func BuildStringMatcher(matcher gjson.Result) (Matcher, error) {
|
||||
return BuildRepeatedStringMatcher([]gjson.Result{matcher})
|
||||
}
|
||||
121
plugins/wasm-go/extensions/ext-auth/expr/matcher_test.go
Normal file
121
plugins/wasm-go/extensions/ext-auth/expr/matcher_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg string
|
||||
matched []string
|
||||
mismatched []string
|
||||
}{
|
||||
{
|
||||
name: "exact",
|
||||
cfg: `{"exact": "foo"}`,
|
||||
matched: []string{"foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "exact, ignore_case",
|
||||
cfg: `{"exact": "foo", "ignore_case": true}`,
|
||||
matched: []string{"Foo", "foo"},
|
||||
},
|
||||
{
|
||||
name: "prefix",
|
||||
cfg: `{"prefix": "/p"}`,
|
||||
matched: []string{"/p", "/pa"},
|
||||
mismatched: []string{"/P"},
|
||||
},
|
||||
{
|
||||
name: "prefix, ignore_case",
|
||||
cfg: `{"prefix": "/p", "ignore_case": true}`,
|
||||
matched: []string{"/P", "/p", "/pa", "/Pa"},
|
||||
mismatched: []string{"/"},
|
||||
},
|
||||
{
|
||||
name: "suffix",
|
||||
cfg: `{"suffix": "foo"}`,
|
||||
matched: []string{"foo", "0foo"},
|
||||
mismatched: []string{"fo", "fooo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "suffix, ignore_case",
|
||||
cfg: `{"suffix": "foo", "ignore_case": true}`,
|
||||
matched: []string{"aFoo", "foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "contains",
|
||||
cfg: `{"contains": "foo"}`,
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"fo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "contains, ignore_case",
|
||||
cfg: `{"contains": "foo", "ignore_case": true}`,
|
||||
matched: []string{"aFoo", "foo", "FoO"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex",
|
||||
cfg: `{"regex": "fo{2}"}`,
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"aFoo", "fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case",
|
||||
cfg: `{"regex": "fo{2}", "ignore_case": true}`,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case & case insensitive specified in regex",
|
||||
cfg: `{"regex": "(?i)fo{2}", "ignore_case": true}`,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
built, _ := BuildStringMatcher(gjson.Parse(tt.cfg))
|
||||
for _, s := range tt.matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range tt.mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
|
||||
cfgs := []string{
|
||||
`{"exact":"foo"}`,
|
||||
`{"prefix":"pre"}`,
|
||||
`{"regex":"^Cache"}`,
|
||||
}
|
||||
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
|
||||
mismatched := []string{"afoo", "fo"}
|
||||
ms := []gjson.Result{}
|
||||
for _, cfg := range cfgs {
|
||||
ms = append(ms, gjson.Parse(cfg))
|
||||
}
|
||||
built, _ := BuildRepeatedStringMatcherIgnoreCase(ms)
|
||||
for _, s := range matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassOutRegexCompileErr(t *testing.T) {
|
||||
cfg := `{"regex":"(?!)aa"}`
|
||||
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user