From ba774da55e2660299a592efee6835cc141d18122 Mon Sep 17 00:00:00 2001 From: Kent Dong Date: Fri, 15 May 2026 16:03:50 +0800 Subject: [PATCH] feat(ext-auth): add support for allowed properties forwarding in external authorization requests (#3694) Signed-off-by: CH3CHO --- plugins/wasm-go/extensions/ext-auth/README.md | 104 +++++++++++- .../wasm-go/extensions/ext-auth/README_EN.md | 107 +++++++++++- .../extensions/ext-auth/config/config.go | 43 +++++ .../extensions/ext-auth/config/config_test.go | 160 ++++++++++++++++++ plugins/wasm-go/extensions/ext-auth/go.mod | 4 +- plugins/wasm-go/extensions/ext-auth/go.sum | 8 +- plugins/wasm-go/extensions/ext-auth/main.go | 9 + .../wasm-go/extensions/ext-auth/main_test.go | 126 ++++++++++++++ 8 files changed, 553 insertions(+), 8 deletions(-) diff --git a/plugins/wasm-go/extensions/ext-auth/README.md b/plugins/wasm-go/extensions/ext-auth/README.md index de7e2feb..b751bd89 100644 --- a/plugins/wasm-go/extensions/ext-auth/README.md +++ b/plugins/wasm-go/extensions/ext-auth/README.md @@ -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 发送给授权服务。
Envoy 支持的 property 列表参见下方文档:
| | `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时的请求中会带上这两个请求头 \ No newline at end of file +`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 发送给授权服务,便于授权服务根据路由信息进行鉴权决策。 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/ext-auth/README_EN.md b/plugins/wasm-go/extensions/ext-auth/README_EN.md index 8a012160..8c887b91 100644 --- a/plugins/wasm-go/extensions/ext-auth/README_EN.md +++ b/plugins/wasm-go/extensions/ext-auth/README_EN.md @@ -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.
Check out following documents for the property list supported by Envoy:
| + | `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. \ No newline at end of file +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. \ No newline at end of file diff --git a/plugins/wasm-go/extensions/ext-auth/config/config.go b/plugins/wasm-go/extensions/ext-auth/config/config.go index 3c2ec8c7..71d6d37f 100644 --- a/plugins/wasm-go/extensions/ext-auth/config/config.go +++ b/plugins/wasm-go/extensions/ext-auth/config/config.go @@ -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 +} 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 ec551c04..d765c8bb 100644 --- a/plugins/wasm-go/extensions/ext-auth/config/config_test.go +++ b/plugins/wasm-go/extensions/ext-auth/config/config_test.go @@ -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 { diff --git a/plugins/wasm-go/extensions/ext-auth/go.mod b/plugins/wasm-go/extensions/ext-auth/go.mod index cf45844e..24ab04ba 100644 --- a/plugins/wasm-go/extensions/ext-auth/go.mod +++ b/plugins/wasm-go/extensions/ext-auth/go.mod @@ -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 ) diff --git a/plugins/wasm-go/extensions/ext-auth/go.sum b/plugins/wasm-go/extensions/ext-auth/go.sum index b055378c..aac0e804 100644 --- a/plugins/wasm-go/extensions/ext-auth/go.sum +++ b/plugins/wasm-go/extensions/ext-auth/go.sum @@ -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= diff --git a/plugins/wasm-go/extensions/ext-auth/main.go b/plugins/wasm-go/extensions/ext-auth/main.go index 3cdb60ce..51b7b01f 100644 --- a/plugins/wasm-go/extensions/ext-auth/main.go +++ b/plugins/wasm-go/extensions/ext-auth/main.go @@ -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 diff --git a/plugins/wasm-go/extensions/ext-auth/main_test.go b/plugins/wasm-go/extensions/ext-auth/main_test.go index 00f55875..c39dace2 100644 --- a/plugins/wasm-go/extensions/ext-auth/main_test.go +++ b/plugins/wasm-go/extensions/ext-auth/main_test.go @@ -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() + }) }) }