ut: add ext-auth unit tests (#1710)

This commit is contained in:
韩贤涛
2025-02-05 13:39:10 +08:00
committed by GitHub
parent 1431ff9cfe
commit fab3ebb35a
2 changed files with 367 additions and 130 deletions

View File

@@ -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{

View File

@@ -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")
}