diff --git a/plugins/wasm-go/extensions/ext-auth/config/config.go b/plugins/wasm-go/extensions/ext-auth/config/config.go index 886d4761cd..5709bbf9b0 100644 --- a/plugins/wasm-go/extensions/ext-auth/config/config.go +++ b/plugins/wasm-go/extensions/ext-auth/config/config.go @@ -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{ diff --git a/plugins/wasm-go/extensions/ext-auth/config/config_test.go b/plugins/wasm-go/extensions/ext-auth/config/config_test.go index 5472491cfc..02750356e7 100644 --- a/plugins/wasm-go/extensions/ext-auth/config/config_test.go +++ b/plugins/wasm-go/extensions/ext-auth/config/config_test.go @@ -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 - } - 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) +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 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 -} + 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 BuildStringMatcher(matchType, target string, ignoreCase bool) (Matcher, error) { - for constructorType, constructor := range matcherConstructors { - if constructorType == matchType { - return constructor(target, ignoreCase) - } + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) } - return nil, errors.New("unknown string matcher type") }