mirror of
https://github.com/alibaba/higress.git
synced 2026-03-07 18:10:54 +08:00
ut: add ext-auth unit tests (#1710)
This commit is contained in:
@@ -65,7 +65,7 @@ func ParseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) erro
|
||||
return err
|
||||
}
|
||||
|
||||
if err := parseMatchRules(json, config, log); err != nil {
|
||||
if err := parseMatchRules(json, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpServic
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMatchRules(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
func parseMatchRules(json gjson.Result, config *ExtAuthConfig) error {
|
||||
matchListConfig := json.Get("match_list")
|
||||
if !matchListConfig.Exists() {
|
||||
config.MatchRules = expr.MatchRulesDefaults()
|
||||
@@ -260,10 +260,15 @@ func parseMatchRules(json gjson.Result, config *ExtAuthConfig, log wrapper.Log)
|
||||
var err error
|
||||
|
||||
matchListConfig.ForEach(func(key, value gjson.Result) bool {
|
||||
pathMatcher, err := expr.BuildStringMatcher(
|
||||
pathMatcher, buildErr := expr.BuildStringMatcher(
|
||||
value.Get("match_rule_type").Str,
|
||||
value.Get("match_rule_path").Str, false)
|
||||
if err != nil {
|
||||
if buildErr != nil {
|
||||
err = fmt.Errorf("failed to build string matcher for rule with domain %q, path %q, type %q: %w",
|
||||
value.Get("match_rule_domain").Str,
|
||||
value.Get("match_rule_path").Str,
|
||||
value.Get("match_rule_type").Str,
|
||||
buildErr)
|
||||
return false // stop iterating
|
||||
}
|
||||
ruleList = append(ruleList, expr.Rule{
|
||||
@@ -274,7 +279,7 @@ func parseMatchRules(json gjson.Result, config *ExtAuthConfig, log wrapper.Log)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build string matcher for rule %v: %w", matchListConfig, err)
|
||||
return err
|
||||
}
|
||||
|
||||
config.MatchRules = expr.MatchRules{
|
||||
|
||||
@@ -1,136 +1,368 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
"ext-auth/expr"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
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
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expected ExtAuthConfig
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Valid Config with Default Values",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: ExtAuthConfig{
|
||||
HttpService: HttpService{
|
||||
EndpointMode: "envoy",
|
||||
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: "example.com",
|
||||
Port: 80,
|
||||
Host: "",
|
||||
}),
|
||||
PathPrefix: "/auth",
|
||||
Timeout: 1000,
|
||||
},
|
||||
MatchRules: expr.MatchRulesDefaults(),
|
||||
FailureModeAllow: false,
|
||||
FailureModeAllowHeaderAdd: false,
|
||||
StatusOnError: 403,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid Config with Custom Values",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "forward_auth",
|
||||
"endpoint": {
|
||||
"service_name": "auth.example.com",
|
||||
"service_port": 8080,
|
||||
"service_host": "auth.example.com",
|
||||
"request_method": "POST",
|
||||
"path": "/auth"
|
||||
},
|
||||
"timeout": 2000,
|
||||
"authorization_request": {
|
||||
"headers_to_add": {
|
||||
"X-Auth-Source": "wasm"
|
||||
},
|
||||
"with_request_body": true,
|
||||
"max_request_body_bytes": 1048576
|
||||
}
|
||||
},
|
||||
"skipped_path_prefixes": ["/health", "/metrics"],
|
||||
"failure_mode_allow": true,
|
||||
"failure_mode_allow_header_add": true,
|
||||
"status_on_error": 500
|
||||
}`,
|
||||
expected: ExtAuthConfig{
|
||||
HttpService: HttpService{
|
||||
EndpointMode: "forward_auth",
|
||||
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: "auth.example.com",
|
||||
Port: 8080,
|
||||
Host: "auth.example.com",
|
||||
}),
|
||||
RequestMethod: "POST",
|
||||
Path: "/auth",
|
||||
Timeout: 2000,
|
||||
AuthorizationRequest: AuthorizationRequest{
|
||||
HeadersToAdd: map[string]string{
|
||||
"X-Auth-Source": "wasm",
|
||||
},
|
||||
WithRequestBody: true,
|
||||
MaxRequestBodyBytes: 1048576,
|
||||
},
|
||||
},
|
||||
MatchRules: expr.MatchRulesDefaults(),
|
||||
FailureModeAllow: true,
|
||||
FailureModeAllowHeaderAdd: true,
|
||||
StatusOnError: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Missing HttpService Configuration",
|
||||
json: `{}`,
|
||||
expectedErr: "missing http_service in config",
|
||||
},
|
||||
{
|
||||
name: "Invalid Endpoint Mode",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "invalid_mode",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedErr: "endpoint_mode invalid_mode is not supported",
|
||||
},
|
||||
{
|
||||
name: "Missing Endpoint Configuration",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy"
|
||||
}
|
||||
}`,
|
||||
expectedErr: "missing endpoint in config",
|
||||
},
|
||||
{
|
||||
name: "Empty Service Name",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "",
|
||||
"service_port": 80
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedErr: "endpoint service name must not be empty",
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Method with Request Body",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "forward_auth",
|
||||
"endpoint": {
|
||||
"service_name": "auth.example.com",
|
||||
"service_port": 8080,
|
||||
"request_method": "GET",
|
||||
"path": "/auth"
|
||||
},
|
||||
"authorization_request": {
|
||||
"with_request_body": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedErr: "requestMethod GET does not support with_request_body set to true",
|
||||
},
|
||||
{
|
||||
name: "Missing Path for Forward Auth",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "forward_auth",
|
||||
"endpoint": {
|
||||
"service_name": "auth.example.com",
|
||||
"service_port": 8080,
|
||||
"service_host": "auth.example.com",
|
||||
"request_method": "POST"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedErr: "when endpoint_mode is forward_auth, endpoint path must not be empty",
|
||||
},
|
||||
{
|
||||
name: "Missing Path Prefix for Envoy",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedErr: "when endpoint_mode is envoy, endpoint path_prefix must not be empty",
|
||||
},
|
||||
{
|
||||
name: "Valid Match Rules with Blacklist",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
},
|
||||
"match_type": "blacklist",
|
||||
"match_list": [
|
||||
{
|
||||
"match_rule_domain": "*.bar.com",
|
||||
"match_rule_path": "/headers",
|
||||
"match_rule_type": "prefix"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expected: ExtAuthConfig{
|
||||
HttpService: HttpService{
|
||||
EndpointMode: "envoy",
|
||||
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: "example.com",
|
||||
Port: 80,
|
||||
Host: "",
|
||||
}),
|
||||
PathPrefix: "/auth",
|
||||
Timeout: 1000,
|
||||
},
|
||||
MatchRules: expr.MatchRules{
|
||||
Mode: "blacklist",
|
||||
RuleList: []expr.Rule{
|
||||
{
|
||||
Domain: "*.bar.com",
|
||||
Path: func() expr.Matcher {
|
||||
pathMatcher, err := expr.BuildStringMatcher(expr.MatchPatternPrefix, "/headers", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
FailureModeAllow: false,
|
||||
FailureModeAllowHeaderAdd: false,
|
||||
StatusOnError: 403,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid Match Rules with Whitelist",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
},
|
||||
"match_type": "whitelist",
|
||||
"match_list": [
|
||||
{
|
||||
"match_rule_domain": "*.foo.com",
|
||||
"match_rule_path": "/api",
|
||||
"match_rule_type": "exact"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expected: ExtAuthConfig{
|
||||
HttpService: HttpService{
|
||||
EndpointMode: "envoy",
|
||||
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: "example.com",
|
||||
Port: 80,
|
||||
Host: "",
|
||||
}),
|
||||
PathPrefix: "/auth",
|
||||
Timeout: 1000,
|
||||
},
|
||||
MatchRules: expr.MatchRules{
|
||||
Mode: "whitelist",
|
||||
RuleList: []expr.Rule{
|
||||
{
|
||||
Domain: "*.foo.com",
|
||||
Path: func() expr.Matcher {
|
||||
pathMatcher, err := expr.BuildStringMatcher(expr.MatchPatternExact, "/api", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
FailureModeAllow: false,
|
||||
FailureModeAllowHeaderAdd: false,
|
||||
StatusOnError: 403,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Missing Match Type",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
},
|
||||
"match_list": [
|
||||
{
|
||||
"match_rule_domain": "*.bar.com",
|
||||
"match_rule_path": "/headers",
|
||||
"match_rule_type": "prefix"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectedErr: "missing match_type in config",
|
||||
},
|
||||
{
|
||||
name: "Invalid Match Type",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
},
|
||||
"match_type": "invalid_type",
|
||||
"match_list": [
|
||||
{
|
||||
"match_rule_domain": "*.bar.com",
|
||||
"match_rule_path": "/headers",
|
||||
"match_rule_type": "prefix"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectedErr: "invalid match_type in config, must be 'whitelist' or 'blacklist'",
|
||||
},
|
||||
{
|
||||
name: "Invalid Match Rule Type",
|
||||
json: `{
|
||||
"http_service": {
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": {
|
||||
"service_name": "example.com",
|
||||
"service_port": 80,
|
||||
"path_prefix": "/auth"
|
||||
}
|
||||
},
|
||||
"match_type": "blacklist",
|
||||
"match_list": [
|
||||
{
|
||||
"match_rule_domain": "*.bar.com",
|
||||
"match_rule_path": "/headers",
|
||||
"match_rule_type": "invalid_type"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectedErr: `failed to build string matcher for rule with domain "*.bar.com", path "/headers", type "invalid_type": unknown string matcher type`,
|
||||
},
|
||||
}
|
||||
return s == m.target
|
||||
}
|
||||
|
||||
type stringPrefixMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var config ExtAuthConfig
|
||||
result := gjson.Parse(tt.json)
|
||||
err := ParseConfig(result, &config, &wrapper.DefaultLog{})
|
||||
|
||||
func (m *stringPrefixMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.HasPrefix(strings.ToLower(s), m.target)
|
||||
if tt.expectedErr != "" {
|
||||
assert.EqualError(t, err, tt.expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
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 MatcherConstructor func(string, bool) (Matcher, error)
|
||||
|
||||
var matcherConstructors = map[string]MatcherConstructor{
|
||||
MatchPatternExact: newStringExactMatcher,
|
||||
MatchPatternPrefix: newStringPrefixMatcher,
|
||||
MatchPatternSuffix: newStringSuffixMatcher,
|
||||
MatchPatternContains: newStringContainsMatcher,
|
||||
MatchPatternRegex: newStringRegexMatcher,
|
||||
}
|
||||
|
||||
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 BuildStringMatcher(matchType, target string, ignoreCase bool) (Matcher, error) {
|
||||
for constructorType, constructor := range matcherConstructors {
|
||||
if constructorType == matchType {
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user