mirror of
https://github.com/alibaba/higress.git
synced 2026-04-21 12:07:26 +08:00
feat: Add ext-auth plugin support for authentication blacklists/whitelists (#1694)
This commit is contained in:
294
plugins/wasm-go/extensions/ext-auth/config/config.go
Normal file
294
plugins/wasm-go/extensions/ext-auth/config/config.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ext-auth/expr"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultStatusOnError = http.StatusForbidden
|
||||
|
||||
DefaultHttpServiceTimeout = 1000
|
||||
|
||||
DefaultMaxRequestBodyBytes = 10 * 1024 * 1024
|
||||
|
||||
EndpointModeEnvoy = "envoy"
|
||||
EndpointModeForwardAuth = "forward_auth"
|
||||
)
|
||||
|
||||
type ExtAuthConfig struct {
|
||||
HttpService HttpService
|
||||
MatchRules expr.MatchRules
|
||||
FailureModeAllow bool
|
||||
FailureModeAllowHeaderAdd bool
|
||||
StatusOnError uint32
|
||||
}
|
||||
|
||||
type HttpService struct {
|
||||
EndpointMode string
|
||||
Client wrapper.HttpClient
|
||||
// PathPrefix is only used when endpoint_mode is envoy
|
||||
PathPrefix string
|
||||
// RequestMethod is only used when endpoint_mode is forward_auth
|
||||
RequestMethod string
|
||||
// Path is only used when endpoint_mode is forward_auth
|
||||
Path string
|
||||
Timeout uint32
|
||||
AuthorizationRequest AuthorizationRequest
|
||||
AuthorizationResponse AuthorizationResponse
|
||||
}
|
||||
|
||||
type AuthorizationRequest struct {
|
||||
AllowedHeaders expr.Matcher
|
||||
HeadersToAdd map[string]string
|
||||
WithRequestBody bool
|
||||
MaxRequestBodyBytes uint32
|
||||
}
|
||||
|
||||
type AuthorizationResponse struct {
|
||||
AllowedUpstreamHeaders expr.Matcher
|
||||
AllowedClientHeaders expr.Matcher
|
||||
}
|
||||
|
||||
func ParseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
httpServiceConfig := json.Get("http_service")
|
||||
if !httpServiceConfig.Exists() {
|
||||
return errors.New("missing http_service in config")
|
||||
}
|
||||
if err := parseHttpServiceConfig(httpServiceConfig, config, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := parseMatchRules(json, config, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
failureModeAllow := json.Get("failure_mode_allow")
|
||||
if failureModeAllow.Exists() {
|
||||
config.FailureModeAllow = failureModeAllow.Bool()
|
||||
}
|
||||
|
||||
failureModeAllowHeaderAdd := json.Get("failure_mode_allow_header_add")
|
||||
if failureModeAllowHeaderAdd.Exists() {
|
||||
config.FailureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
|
||||
}
|
||||
|
||||
statusOnError := uint32(json.Get("status_on_error").Uint())
|
||||
if statusOnError == 0 {
|
||||
statusOnError = DefaultStatusOnError
|
||||
}
|
||||
config.StatusOnError = statusOnError
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
var httpService HttpService
|
||||
|
||||
if err := parseEndpointConfig(json, &httpService, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := uint32(json.Get("timeout").Uint())
|
||||
if timeout == 0 {
|
||||
timeout = DefaultHttpServiceTimeout
|
||||
}
|
||||
httpService.Timeout = timeout
|
||||
|
||||
if err := parseAuthorizationRequestConfig(json, &httpService); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := parseAuthorizationResponseConfig(json, &httpService); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.HttpService = httpService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrapper.Log) error {
|
||||
endpointMode := json.Get("endpoint_mode").String()
|
||||
if endpointMode == "" {
|
||||
endpointMode = EndpointModeEnvoy
|
||||
} else if endpointMode != EndpointModeEnvoy && endpointMode != EndpointModeForwardAuth {
|
||||
return errors.New(fmt.Sprintf("endpoint_mode %s is not supported", endpointMode))
|
||||
}
|
||||
httpService.EndpointMode = endpointMode
|
||||
|
||||
endpointConfig := json.Get("endpoint")
|
||||
if !endpointConfig.Exists() {
|
||||
return errors.New("missing endpoint in config")
|
||||
}
|
||||
|
||||
serviceName := endpointConfig.Get("service_name").String()
|
||||
if serviceName == "" {
|
||||
return errors.New("endpoint service name must not be empty")
|
||||
}
|
||||
servicePort := endpointConfig.Get("service_port").Int()
|
||||
if servicePort == 0 {
|
||||
servicePort = 80
|
||||
}
|
||||
serviceHost := endpointConfig.Get("service_host").String()
|
||||
|
||||
httpService.Client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
Host: serviceHost,
|
||||
})
|
||||
|
||||
switch endpointMode {
|
||||
case EndpointModeEnvoy:
|
||||
pathPrefixConfig := endpointConfig.Get("path_prefix")
|
||||
if !pathPrefixConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is envoy, endpoint path_prefix must not be empty")
|
||||
}
|
||||
httpService.PathPrefix = pathPrefixConfig.String()
|
||||
|
||||
if endpointConfig.Get("request_method").Exists() || endpointConfig.Get("path").Exists() {
|
||||
log.Warn("when endpoint_mode is envoy, endpoint request_method and path will be ignored")
|
||||
}
|
||||
case EndpointModeForwardAuth:
|
||||
requestMethodConfig := endpointConfig.Get("request_method")
|
||||
if !requestMethodConfig.Exists() {
|
||||
httpService.RequestMethod = http.MethodGet
|
||||
} else {
|
||||
httpService.RequestMethod = strings.ToUpper(requestMethodConfig.String())
|
||||
}
|
||||
|
||||
pathConfig := endpointConfig.Get("path")
|
||||
if !pathConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is forward_auth, endpoint path must not be empty")
|
||||
}
|
||||
httpService.Path = pathConfig.String()
|
||||
|
||||
if endpointConfig.Get("path_prefix").Exists() {
|
||||
log.Warn("when endpoint_mode is forward_auth, endpoint path_prefix will be ignored")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService) error {
|
||||
authorizationRequestConfig := json.Get("authorization_request")
|
||||
if authorizationRequestConfig.Exists() {
|
||||
var authorizationRequest AuthorizationRequest
|
||||
|
||||
allowedHeaders := authorizationRequestConfig.Get("allowed_headers")
|
||||
if allowedHeaders.Exists() {
|
||||
result, err := expr.BuildRepeatedStringMatcherIgnoreCase(allowedHeaders.Array())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationRequest.AllowedHeaders = result
|
||||
}
|
||||
|
||||
authorizationRequest.HeadersToAdd = convertToStringMap(authorizationRequestConfig.Get("headers_to_add"))
|
||||
|
||||
withRequestBody := authorizationRequestConfig.Get("with_request_body")
|
||||
if withRequestBody.Exists() {
|
||||
// withRequestBody is true and the request method is GET, OPTIONS or HEAD
|
||||
if withRequestBody.Bool() &&
|
||||
(httpService.RequestMethod == http.MethodGet || httpService.RequestMethod == http.MethodOptions || httpService.RequestMethod == http.MethodHead) {
|
||||
return errors.New(fmt.Sprintf("requestMethod %s does not support with_request_body set to true", httpService.RequestMethod))
|
||||
}
|
||||
authorizationRequest.WithRequestBody = withRequestBody.Bool()
|
||||
}
|
||||
|
||||
maxRequestBodyBytes := uint32(authorizationRequestConfig.Get("max_request_body_bytes").Uint())
|
||||
if maxRequestBodyBytes == 0 {
|
||||
maxRequestBodyBytes = DefaultMaxRequestBodyBytes
|
||||
}
|
||||
authorizationRequest.MaxRequestBodyBytes = maxRequestBodyBytes
|
||||
|
||||
httpService.AuthorizationRequest = authorizationRequest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpService) error {
|
||||
authorizationResponseConfig := json.Get("authorization_response")
|
||||
if authorizationResponseConfig.Exists() {
|
||||
var authorizationResponse AuthorizationResponse
|
||||
|
||||
allowedUpstreamHeaders := authorizationResponseConfig.Get("allowed_upstream_headers")
|
||||
if allowedUpstreamHeaders.Exists() {
|
||||
result, err := expr.BuildRepeatedStringMatcherIgnoreCase(allowedUpstreamHeaders.Array())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationResponse.AllowedUpstreamHeaders = result
|
||||
}
|
||||
|
||||
allowedClientHeaders := authorizationResponseConfig.Get("allowed_client_headers")
|
||||
if allowedClientHeaders.Exists() {
|
||||
result, err := expr.BuildRepeatedStringMatcherIgnoreCase(allowedClientHeaders.Array())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationResponse.AllowedClientHeaders = result
|
||||
}
|
||||
|
||||
httpService.AuthorizationResponse = authorizationResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMatchRules(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
matchListConfig := json.Get("match_list")
|
||||
if !matchListConfig.Exists() {
|
||||
config.MatchRules = expr.MatchRulesDefaults()
|
||||
return nil
|
||||
}
|
||||
|
||||
matchType := json.Get("match_type")
|
||||
if !matchType.Exists() {
|
||||
return errors.New("missing match_type in config")
|
||||
}
|
||||
if matchType.Str != expr.ModeWhitelist && matchType.Str != expr.ModeBlacklist {
|
||||
return errors.New("invalid match_type in config, must be 'whitelist' or 'blacklist'")
|
||||
}
|
||||
|
||||
ruleList := make([]expr.Rule, 0)
|
||||
var err error
|
||||
|
||||
matchListConfig.ForEach(func(key, value gjson.Result) bool {
|
||||
pathMatcher, err := expr.BuildStringMatcher(
|
||||
value.Get("match_rule_type").Str,
|
||||
value.Get("match_rule_path").Str, false)
|
||||
if err != nil {
|
||||
return false // stop iterating
|
||||
}
|
||||
ruleList = append(ruleList, expr.Rule{
|
||||
Domain: value.Get("match_rule_domain").Str,
|
||||
Path: pathMatcher,
|
||||
})
|
||||
return true // keep iterating
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build string matcher for rule %v: %w", matchListConfig, err)
|
||||
}
|
||||
|
||||
config.MatchRules = expr.MatchRules{
|
||||
Mode: matchType.Str,
|
||||
RuleList: ruleList,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToStringMap(result gjson.Result) map[string]string {
|
||||
m := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
m[key.String()] = value.String()
|
||||
return true // keep iterating
|
||||
})
|
||||
return m
|
||||
}
|
||||
136
plugins/wasm-go/extensions/ext-auth/config/config_test.go
Normal file
136
plugins/wasm-go/extensions/ext-auth/config/config_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
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 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