feat(ext-auth): add support for allowed properties forwarding in external authorization requests (#3694)

Signed-off-by: CH3CHO <ch3cho@qq.com>
This commit is contained in:
Kent Dong
2026-05-15 16:03:50 +08:00
committed by GitHub
parent e497d8017a
commit ba774da55e
8 changed files with 553 additions and 8 deletions

View File

@@ -51,10 +51,18 @@ description: Ext 认证插件实现了调用外部授权服务进行认证鉴权
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|--------------------------|------------------------|------|--------|--------------------------------------------------------------|
| `allowed_headers` | array of StringMatcher | 否 | - | 设置后,匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头`endpoint_mode``forward_auth` 时,会添加 `X-Forwarded-*` 的请求头) |
| `allowed_properties` | array of AllowedProperty | 否 | - | 设置后将把 Envoy filter state 中的 property 映射为 HTTP header 发送给授权服务。<br>Envoy 支持的 property 列表参见下方文档:<br><ul><li>Envoy 1.27Higress < 2.2.0https://www.envoyproxy.io/docs/envoy/v1.27.0/intro/arch_overview/advanced/attributes</li><li>Envoy 1.36Higress >= 2.2.0https://www.envoyproxy.io/docs/envoy/v1.36.0/intro/arch_overview/advanced/attributes</li></ul> |
| `headers_to_add` | map[string]string | 否 | - | 设置将包含在授权服务请求中的请求头列表。请注意,同名的客户端请求头将被覆盖 |
| `with_request_body` | bool | 否 | false | 缓冲客户端请求体并将其发送至鉴权请求中HTTP Method为GET、OPTIONS、HEAD请求时不生效 |
| `max_request_body_bytes` | int | 否 | 10MB | 设置在内存中保存客户端请求体的最大尺寸。当客户端请求体达到在此字段中设置的数值时将会返回HTTP 413状态码并且不会启动授权过程。注意这个设置会优先于 `failure_mode_allow` 的配置 |
`AllowedProperty` 类型每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|------------|----------|------|--------|--------------------------------------------------------------|
| `path` | array of string | 是 | - | 属性路径,如 `["route_name"]``["metadata", "user_id"]` |
| `header` | string | 是 | - | 映射到的请求头名称 |
`authorization_response` 中每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
@@ -235,7 +243,52 @@ Content-Length: 0
`ext-auth` 服务返回响应头中如果包含 `x-user-id``x-auth-version`网关调用upstream时的请求中会带上这两个请求头
#### 示例3传递路由名称到授权服务
`ext-auth` 插件的配置:
```yaml
http_service:
authorization_request:
allowed_headers:
- exact: x-auth-version
allowed_properties:
- path: [route_name]
header: x-route-name
headers_to_add:
x-envoy-header: true
authorization_response:
allowed_upstream_headers:
- exact: x-user-id
- exact: x-auth-version
endpoint_mode: envoy
endpoint:
service_name: ext-auth.backend.svc.cluster.local
service_host: my-domain.local
service_port: 8090
path_prefix: /auth
timeout: 1000
```
使用如下请求网关,当开启 `ext-auth` 插件后:
```shell
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
```
`ext-auth` 服务将接收到如下的鉴权请求:
```
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Auth-Version: 1.0
x-envoy-header: true
Content-Length: 0
X-Route-Name: your-route-name
```
通过 `allowed_properties` 配置,可以将 Envoy filter state 中的 `route_name` 等属性映射为 HTTP header 发送给授权服务,便于授权服务根据路由信息进行鉴权决策。
### endpoint_mode为forward_auth时
@@ -340,4 +393,53 @@ x-envoy-header: true
Content-Length: 0
```
`ext-auth` 服务返回响应头中如果包含 `x-user-id``x-auth-version`网关调用upstream时的请求中会带上这两个请求头
`ext-auth` 服务返回响应头中如果包含 `x-user-id``x-auth-version`网关调用upstream时的请求中会带上这两个请求头
#### 示例3传递路由名称到授权服务
`ext-auth` 插件的配置:
```yaml
http_service:
authorization_request:
allowed_headers:
- exact: x-auth-version
allowed_properties:
- path: [route_name]
header: x-route-name
authorization_response:
allowed_upstream_headers:
- exact: x-mse-consumer
- exact: x-ext-auth-user
endpoint_mode: forward_auth
endpoint:
service_name: ext-auth.backend.svc.cluster.local
service_port: 8090
path: /auth
request_method: POST
timeout: 1000
```
使用如下请求网关,当开启 `ext-auth` 插件后:
```shell
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0" -H "Host: foo.bar.com"
```
`ext-auth` 服务将接收到如下的鉴权请求:
```
POST /auth HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Forwarded-Proto: HTTP
X-Forwarded-Host: foo.bar.com
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Forwarded-Method: GET
X-Auth-Version: 1.0
x-envoy-header: true
X-Route-Name: your-route-name
Content-Length: 0
```
通过 `allowed_properties` 配置,可以将 Envoy filter state 中的 `route_name` 等属性映射为 HTTP header 发送给授权服务,便于授权服务根据路由信息进行鉴权决策。

View File

@@ -51,10 +51,19 @@ Configuration fields for each item in `authorization_request`
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `allowed_headers` | array of StringMatcher | No | - | After setting, the client request headers that match the items will be added to the request headers in the authorization service request. In addition to the user-defined header matching rules, the `Authorization` HTTP header will be automatically included in the authorization service request (when `endpoint_mode` is `forward_auth`, the `X-Forwarded-*` request headers will be added) |
| `allowed_properties` | array of AllowedProperty | No | - | When set, Envoy filter state properties will be mapped to HTTP headers and sent to the authorization service.<br>Check out following documents for the property list supported by Envoy:<br><ul><li>Envoy 1.27 (Higress < 2.2.0): https://www.envoyproxy.io/docs/envoy/v1.27.0/intro/arch_overview/advanced/attributes</li><li>Envoy 1.36 (Higress >= 2.2.0): https://www.envoyproxy.io/docs/envoy/v1.36.0/intro/arch_overview/advanced/attributes</li></ul> |
| `headers_to_add` | map[string]string | No | - | Sets the list of request headers to be included in the authorization service request. Please note that the client request headers with the same name will be overwritten |
| `with_request_body` | bool | No | false | Buffer the client request body and send it to the authentication request (not effective for HTTP Method GET, OPTIONS, HEAD requests) |
| `max_request_body_bytes` | int | No | 10MB | Sets the maximum size of the client request body to be saved in memory. When the client request body reaches the value set in this field, an HTTP 413 status code will be returned and the authorization process will not be started. Note that this setting takes precedence over the `failure_mode_allow` configuration |
Configuration fields for each item of `AllowedProperty` type
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `path` | array of string | Yes | - | Property path, e.g., `["route_name"]` or `["metadata", "user_id"]` |
| `header` | string | Yes | - | The request header name to map the property to |
Configuration fields for each item in `authorization_response`
| Name | Data Type | Required | Default Value | Description |
@@ -236,6 +245,53 @@ Content-Length: 0
If the response headers returned by the `ext-auth` service contain `x-user-id` and `x-auth-version`, these two headers will be included in the request when the gateway calls the upstream.
#### Example 3: Passing Route Name to Authorization Service
Configuration of the `ext-auth` plugin:
```yaml
http_service:
authorization_request:
allowed_headers:
- exact: x-auth-version
allowed_properties:
- path: [route_name]
header: x-route-name
headers_to_add:
x-envoy-header: true
authorization_response:
allowed_upstream_headers:
- exact: x-user-id
- exact: x-auth-version
endpoint_mode: envoy
endpoint:
service_name: ext-auth.backend.svc.cluster.local
service_host: my-domain.local
service_port: 8090
path_prefix: /auth
timeout: 1000
```
When using the following request to the gateway after enabling the `ext-auth` plugin:
```shell
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
```
The `ext-auth` service will receive the following authorization request:
```
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Auth-Version: 1.0
x-envoy-header: true
Content-Length: 0
X-Route-Name: your-route-name
```
By configuring `allowed_properties`, you can map Envoy filter state properties like `route_name` to HTTP headers and send them to the authorization service, enabling the authorization service to make decisions based on routing information.
### When endpoint_mode is forward_auth
#### Example 1
@@ -339,4 +395,53 @@ x-envoy-header: true
Content-Length: 0
```
If the response headers returned by the `ext-auth` service contain `x-user-id` and `x-auth-version`, these two headers will be included in the request when the gateway calls the upstream.
If the response headers returned by the `ext-auth` service contain `x-user-id` and `x-auth-version`, these two headers will be included in the request when the gateway calls the upstream.
#### Example 3: Passing Route Name to Authorization Service
Configuration of the `ext-auth` plugin:
```yaml
http_service:
authorization_request:
allowed_headers:
- exact: x-auth-version
allowed_properties:
- path: [route_name]
header: x-route-name
authorization_response:
allowed_upstream_headers:
- exact: x-mse-consumer
- exact: x-ext-auth-user
endpoint_mode: forward_auth
endpoint:
service_name: ext-auth.backend.svc.cluster.local
service_port: 8090
path: /auth
request_method: POST
timeout: 1000
```
When using the following request to the gateway after enabling the `ext-auth` plugin:
```shell
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0" -H "Host: foo.bar.com"
```
The `ext-auth` service will receive the following authorization request:
```
POST /auth HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Forwarded-Proto: HTTP
X-Forwarded-Host: foo.bar.com
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Forwarded-Method: GET
X-Auth-Version: 1.0
x-envoy-header: true
X-Route-Name: your-route-name
Content-Length: 0
```
By configuring `allowed_properties`, you can map Envoy filter state properties like `route_name` to HTTP headers and send them to the authorization service, enabling the authorization service to make decisions based on routing information.

View File

@@ -51,6 +51,12 @@ type AuthorizationRequest struct {
HeadersToAdd map[string]string
WithRequestBody bool
MaxRequestBodyBytes uint32
AllowedProperties []AllowedProperty
}
type AllowedProperty struct {
Path []string
Header string
}
type AuthorizationResponse struct {
@@ -210,6 +216,13 @@ func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService
}
authorizationRequest.MaxRequestBodyBytes = maxRequestBodyBytes
allowedProperties := authorizationRequestConfig.Get("allowed_properties").Array()
var err error
authorizationRequest.AllowedProperties, err = parseAllowedProperties(allowedProperties)
if err != nil {
return err
}
httpService.AuthorizationRequest = authorizationRequest
}
return nil
@@ -316,3 +329,33 @@ func convertToStringList(results []gjson.Result) []string {
}
return interfaces
}
func parseAllowedProperties(results []gjson.Result) ([]AllowedProperty, error) {
props := make([]AllowedProperty, 0, len(results))
for i, result := range results {
pathVal := result.Get("path")
headerVal := result.Get("header")
if !pathVal.Exists() {
return nil, fmt.Errorf("allowed_properties[%d]: missing required field 'path'", i)
}
if !headerVal.Exists() {
return nil, fmt.Errorf("allowed_properties[%d]: missing required field 'header'", i)
}
// path can be array format: [route_name] or [metadata, test]
// or single value format: route_name
var path []string
if pathVal.IsArray() {
pathVal.ForEach(func(key, value gjson.Result) bool {
path = append(path, value.String())
return true
})
} else {
path = []string{pathVal.String()}
}
props = append(props, AllowedProperty{
Path: path,
Header: headerVal.String(),
})
}
return props, nil
}

View File

@@ -89,6 +89,7 @@ func TestParseConfig(t *testing.T) {
},
WithRequestBody: true,
MaxRequestBodyBytes: 1048576,
AllowedProperties: []AllowedProperty{},
},
},
MatchRules: expr.MatchRulesDefaults(),
@@ -398,6 +399,165 @@ func TestParseConfig(t *testing.T) {
}`,
expectedErr: `failed to build string matcher for rule with domain "*.bar.com", method [POST PUT DELETE], path "/headers", type "invalid_type": unknown string matcher type`,
},
{
name: "Valid AllowedProperties with Array Path",
json: `{
"http_service": {
"endpoint_mode": "envoy",
"endpoint": {
"service_name": "example.com",
"service_port": 80,
"path_prefix": "/auth"
},
"authorization_request": {
"allowed_properties": [
{"path": ["route_name"], "header": "x-route-name"},
{"path": ["metadata", "test"], "header": "x-metadata"}
]
}
}
}`,
expected: ExtAuthConfig{
HttpService: HttpService{
EndpointMode: "envoy",
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "example.com",
Port: 80,
Host: "",
}),
PathPrefix: "/auth",
Timeout: 1000,
AuthorizationRequest: AuthorizationRequest{
HeadersToAdd: map[string]string{},
MaxRequestBodyBytes: 10 * 1024 * 1024,
AllowedProperties: []AllowedProperty{
{Path: []string{"route_name"}, Header: "x-route-name"},
{Path: []string{"metadata", "test"}, Header: "x-metadata"},
},
},
},
MatchRules: expr.MatchRulesDefaults(),
FailureModeAllow: false,
FailureModeAllowHeaderAdd: false,
StatusOnError: 403,
},
},
{
name: "Valid AllowedProperties with Single Value Path",
json: `{
"http_service": {
"endpoint_mode": "envoy",
"endpoint": {
"service_name": "example.com",
"service_port": 80,
"path_prefix": "/auth"
},
"authorization_request": {
"allowed_properties": [
{"path": "route_name", "header": "x-route-name"}
]
}
}
}`,
expected: ExtAuthConfig{
HttpService: HttpService{
EndpointMode: "envoy",
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "example.com",
Port: 80,
Host: "",
}),
PathPrefix: "/auth",
Timeout: 1000,
AuthorizationRequest: AuthorizationRequest{
HeadersToAdd: map[string]string{},
MaxRequestBodyBytes: 10 * 1024 * 1024,
AllowedProperties: []AllowedProperty{
{Path: []string{"route_name"}, Header: "x-route-name"},
},
},
},
MatchRules: expr.MatchRulesDefaults(),
FailureModeAllow: false,
FailureModeAllowHeaderAdd: false,
StatusOnError: 403,
},
},
{
name: "Valid AllowedProperties Empty Array",
json: `{
"http_service": {
"endpoint_mode": "envoy",
"endpoint": {
"service_name": "example.com",
"service_port": 80,
"path_prefix": "/auth"
},
"authorization_request": {
"allowed_properties": []
}
}
}`,
expected: ExtAuthConfig{
HttpService: HttpService{
EndpointMode: "envoy",
Client: wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "example.com",
Port: 80,
Host: "",
}),
PathPrefix: "/auth",
Timeout: 1000,
AuthorizationRequest: AuthorizationRequest{
HeadersToAdd: map[string]string{},
MaxRequestBodyBytes: 10 * 1024 * 1024,
AllowedProperties: []AllowedProperty{},
},
},
MatchRules: expr.MatchRulesDefaults(),
FailureModeAllow: false,
FailureModeAllowHeaderAdd: false,
StatusOnError: 403,
},
},
{
name: "AllowedProperties Missing Path Field",
json: `{
"http_service": {
"endpoint_mode": "envoy",
"endpoint": {
"service_name": "example.com",
"service_port": 80,
"path_prefix": "/auth"
},
"authorization_request": {
"allowed_properties": [
{"header": "x-route-name"}
]
}
}
}`,
expectedErr: "allowed_properties[0]: missing required field 'path'",
},
{
name: "AllowedProperties Missing Header Field",
json: `{
"http_service": {
"endpoint_mode": "envoy",
"endpoint": {
"service_name": "example.com",
"service_port": 80,
"path_prefix": "/auth"
},
"authorization_request": {
"allowed_properties": [
{"path": ["route_name"]}
]
}
}
}`,
expectedErr: "allowed_properties[0]: missing required field 'header'",
},
}
for _, tt := range tests {

View File

@@ -5,8 +5,8 @@ go 1.24.1
toolchain go1.24.4
require (
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.18.0
)

View File

@@ -2,10 +2,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2 h1:NY33OrWCJJ+DFiLc+lsBY4Ywor2Ik61ssk6qkGF8Ypo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d h1:LgYbzEBtg0+LEqoebQeMVgAB6H5SgqG+KN+gBhNfKbM=
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

View File

@@ -152,6 +152,15 @@ func buildExtAuthRequestHeaders(ctx wrapper.HttpContext, cfg config.ExtAuthConfi
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
}
// Add filter properties forwarding
if requestConfig.AllowedProperties != nil {
for _, prop := range requestConfig.AllowedProperties {
if raw, err := proxywasm.GetProperty(prop.Path); err == nil {
extAuthReqHeaders.Set(prop.Header, string(raw))
}
}
}
// Add additional headers when endpoint_mode is forward_auth
if httpServiceConfig.EndpointMode == config.EndpointModeForwardAuth {
// Compatible with older versions

View File

@@ -16,6 +16,7 @@ package main
import (
"encoding/json"
"strings"
"testing"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
@@ -158,6 +159,28 @@ var failureModeConfig = func() json.RawMessage {
return data
}()
// 测试配置:带 allowed_properties 的配置
var allowedPropertiesConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"http_service": map[string]interface{}{
"endpoint_mode": "envoy",
"endpoint": map[string]interface{}{
"service_name": "ext-auth.backend.svc.cluster.local",
"service_port": 8090,
"path_prefix": "/auth",
},
"timeout": 1000,
"authorization_request": map[string]interface{}{
"allowed_properties": []map[string]interface{}{
{"path": []string{"route_name"}, "header": "x-route-name"},
{"path": []string{"metadata", "user_id"}, "header": "x-user-id"},
},
},
},
})
return data
}()
func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// 测试基本 envoy 模式配置解析
@@ -225,6 +248,17 @@ func TestParseConfig(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, config)
})
// 测试带 allowed_properties 的配置解析
t.Run("allowed properties config", func(t *testing.T) {
host, status := test.NewTestHost(allowedPropertiesConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
})
}
@@ -472,6 +506,98 @@ func TestOnHttpRequestHeaders(t *testing.T) {
host.CompleteHttp()
})
// 测试 allowed_properties 正确转发属性到 ext auth server
t.Run("allowed properties forward properties to ext auth server", func(t *testing.T) {
host, status := test.NewTestHost(allowedPropertiesConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置属性,供插件获取并转发到 ext auth server
_ = host.SetProperty([]string{"route_name"}, []byte("user-service"))
_ = host.SetProperty([]string{"metadata", "user_id"}, []byte("user-12345"))
// 设置请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/users"},
{":method", "POST"},
{"authorization", "Bearer token123"},
})
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 获取发送到的 ext auth server 的请求 headers验证属性已被正确转发
calloutAttrs := host.GetHttpCalloutAttributes()
require.Len(t, calloutAttrs, 1, "should have exactly one HTTP callout")
calloutHeaders := calloutAttrs[0].Headers
// 验证 x-route-name 和 x-user-id header 已被正确设置
foundHeaders := make(map[string]string)
for _, h := range calloutHeaders {
foundHeaders[strings.ToLower(h[0])] = h[1]
}
require.Equal(t, "user-service", foundHeaders["x-route-name"], "x-route-name should be set to property value")
require.Equal(t, "user-12345", foundHeaders["x-user-id"], "x-user-id should be set to property value")
// 模拟外部认证服务的HTTP调用响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{"authorized": true}`))
// 验证请求是否继续属性转发成功ext auth 返回 200
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction())
host.CompleteHttp()
})
// 测试 allowed_properties 当 GetProperty 失败时不阻塞请求
t.Run("allowed properties continues when property not found", func(t *testing.T) {
host, status := test.NewTestHost(allowedPropertiesConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 只设置部分属性metadata.user_id 不设置,模拟 GetProperty 失败的情况
_ = host.SetProperty([]string{"route_name"}, []byte("user-service"))
// 不设置 metadata.user_id
// 设置请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/users"},
{":method", "POST"},
{"authorization", "Bearer token123"},
})
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 获取发送到的 ext auth server 的请求 headers
calloutAttrs := host.GetHttpCalloutAttributes()
require.Len(t, calloutAttrs, 1, "should have exactly one HTTP callout")
calloutHeaders := calloutAttrs[0].Headers
// 验证只有 x-route-name 被发送x-user-id 不应该存在(因为 property 获取失败)
foundHeaders := make(map[string]string)
for _, h := range calloutHeaders {
foundHeaders[strings.ToLower(h[0])] = h[1]
}
require.Equal(t, "user-service", foundHeaders["x-route-name"], "x-route-name should be set")
require.NotContains(t, foundHeaders, "x-user-id", "x-user-id should NOT be set when property not found")
// 模拟外部认证服务的HTTP调用响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{"authorized": true}`))
// 验证请求是否继续(即使部分属性获取失败也不阻塞)
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction())
host.CompleteHttp()
})
})
}