// Copyright (c) 2024 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "encoding/json" "strings" "testing" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" "github.com/higress-group/wasm-go/pkg/test" "github.com/stretchr/testify/require" ) // 测试配置:基本 envoy 模式配置 var basicEnvoyConfig = 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, }, }) return data }() // 测试配置:forward_auth 模式配置 var forwardAuthConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "http_service": map[string]interface{}{ "endpoint_mode": "forward_auth", "endpoint": map[string]interface{}{ "service_name": "ext-auth.backend.svc.cluster.local", "service_port": 8090, "path": "/auth", "request_method": "POST", }, "timeout": 1000, }, }) return data }() // 测试配置:带请求头过滤的配置 var headersConfig = 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_headers": []map[string]interface{}{ {"exact": "x-auth-version"}, {"prefix": "x-custom"}, }, "headers_to_add": map[string]interface{}{ "x-envoy-header": "true", }, }, "authorization_response": map[string]interface{}{ "allowed_upstream_headers": []map[string]interface{}{ {"exact": "x-user-id"}, {"exact": "x-auth-version"}, }, "allowed_client_headers": []map[string]interface{}{ {"exact": "x-auth-failed"}, }, }, }, }) return data }() // 测试配置:带请求体的配置 var withRequestBodyConfig = 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{}{ "with_request_body": true, "max_request_body_bytes": 1024, }, }, }) return data }() // 测试配置:带黑白名单的配置 var matchRulesConfig = 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, }, "match_type": "whitelist", "match_list": []map[string]interface{}{ { "match_rule_domain": "api.example.com", "match_rule_path": "/public", "match_rule_type": "prefix", }, { "match_rule_method": []string{"GET"}, "match_rule_path": "/health", "match_rule_type": "exact", }, }, }) return data }() // 测试配置:失败模式配置 var failureModeConfig = 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, }, "failure_mode_allow": true, "failure_mode_allow_header_add": true, "status_on_error": 500, }) 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 模式配置解析 t.Run("basic envoy config", func(t *testing.T) { host, status := test.NewTestHost(basicEnvoyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) }) // 测试 forward_auth 模式配置解析 t.Run("forward auth config", func(t *testing.T) { host, status := test.NewTestHost(forwardAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) }) // 测试带请求头过滤的配置解析 t.Run("headers config", func(t *testing.T) { host, status := test.NewTestHost(headersConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) }) // 测试带请求体的配置解析 t.Run("with request body config", func(t *testing.T) { host, status := test.NewTestHost(withRequestBodyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) }) // 测试带黑白名单的配置解析 t.Run("match rules config", func(t *testing.T) { host, status := test.NewTestHost(matchRulesConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) }) // 测试失败模式配置解析 t.Run("failure mode config", func(t *testing.T) { host, status := test.NewTestHost(failureModeConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() 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) }) }) } func TestOnHttpRequestHeaders(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试基本 envoy 模式请求头处理 t.Run("basic envoy request headers", func(t *testing.T) { host, status := test.NewTestHost(basicEnvoyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, {"x-custom-header", "value"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟成功响应(200状态码) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"x-user-id", "user123"}, {"x-auth-version", "1.0"}, {"content-type", "application/json"}, }, []byte(`{"authorized": true, "user": "user123"}`)) // 验证请求是否被恢复 require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) host.CompleteHttp() }) // 测试 forward_auth 模式请求头处理 t.Run("forward auth request headers", func(t *testing.T) { host, status := test.NewTestHost(forwardAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "GET"}, {"authorization", "Bearer token123"}, {"x-custom-header", "value"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟成功响应(200状态码) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"x-user-id", "user456"}, {"x-auth-version", "1.0"}, {"content-type", "application/json"}, }, []byte(`{"authorized": true, "user": "user456"}`)) // 验证请求是否被恢复 require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) host.CompleteHttp() }) // 测试带请求头过滤的请求头处理 t.Run("headers filtered request headers", func(t *testing.T) { host, status := test.NewTestHost(headersConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, {"x-auth-version", "1.0"}, {"x-custom-header", "value"}, {"x-ignored-header", "ignored"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) host.CompleteHttp() }) // 测试带请求体的请求头处理 t.Run("with request body request headers", func(t *testing.T) { host, status := test.NewTestHost(withRequestBodyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, {"content-type", "application/json"}, }) // 由于需要读取请求体,应该返回 HeaderStopIteration require.Equal(t, types.HeaderStopIteration, action) host.CompleteHttp() }) // 测试黑白名单匹配的请求头处理 t.Run("match rules request headers", func(t *testing.T) { host, status := test.NewTestHost(matchRulesConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 测试白名单匹配的请求(应该跳过认证) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "api.example.com"}, {":path", "/public/users"}, {":method", "GET"}, }) // 白名单匹配的请求应该直接通过 require.Equal(t, types.ActionContinue, action) host.CompleteHttp() }) // 测试黑白名单不匹配的请求头处理 t.Run("match rules no match request headers", func(t *testing.T) { host, status := test.NewTestHost(matchRulesConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 测试不在白名单中的请求(应该进行认证) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "api.example.com"}, {":path", "/private/users"}, {":method", "POST"}, }) // 不在白名单中的请求应该进行认证 require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟认证失败响应(401状态码) host.CallOnHttpCall([][2]string{ {":status", "401"}, {"x-auth-failed", "true"}, {"content-type", "application/json"}, }, []byte(`{"authorized": false, "message": "Invalid token"}`)) host.CompleteHttp() }) // 测试认证失败的情况 t.Run("authentication failed", func(t *testing.T) { host, status := test.NewTestHost(basicEnvoyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer invalid-token"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟认证失败响应(403状态码) host.CallOnHttpCall([][2]string{ {":status", "403"}, {"x-auth-failed", "true"}, {"content-type", "application/json"}, }, []byte(`{"authorized": false, "message": "Access denied"}`)) host.CompleteHttp() }) // 测试认证服务返回5xx错误的情况 t.Run("authentication service error", func(t *testing.T) { host, status := test.NewTestHost(basicEnvoyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟服务错误响应(500状态码) host.CallOnHttpCall([][2]string{ {":status", "500"}, {"x-auth-error", "true"}, {"content-type", "application/json"}, }, []byte(`{"error": "Internal server error"}`)) host.CompleteHttp() }) // 测试失败模式允许的情况 t.Run("failure mode allow", func(t *testing.T) { host, status := test.NewTestHost(failureModeConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 设置请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, }) // 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark require.Equal(t, types.HeaderStopAllIterationAndWatermark, action) // 模拟外部认证服务的HTTP调用响应 // 模拟服务错误响应(500状态码),但由于配置了失败模式允许,请求应该通过 host.CallOnHttpCall([][2]string{ {":status", "500"}, {"x-auth-error", "true"}, {"content-type", "application/json"}, }, []byte(`{"error": "Internal server error"}`)) // 验证请求是否被恢复(失败模式允许的情况下) require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) 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() }) }) } func TestOnHttpRequestBody(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试带请求体的请求体处理 t.Run("with request body", func(t *testing.T) { host, status := test.NewTestHost(withRequestBodyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先处理请求头 host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, {"content-type", "application/json"}, }) // 处理请求体 requestBody := `{"username": "test", "password": "password123"}` action := host.CallOnHttpRequestBody([]byte(requestBody)) // 由于需要调用外部认证服务,应该返回 DataStopIterationAndBuffer require.Equal(t, types.DataStopIterationAndBuffer, action) host.CompleteHttp() }) // 测试不带请求体的请求体处理 t.Run("without request body", func(t *testing.T) { host, status := test.NewTestHost(basicEnvoyConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先处理请求头 host.CallOnHttpRequestHeaders([][2]string{ {":authority", "example.com"}, {":path", "/users"}, {":method", "POST"}, {"authorization", "Bearer token123"}, }) // 处理请求体 requestBody := `{"username": "test", "password": "password123"}` action := host.CallOnHttpRequestBody([]byte(requestBody)) // 不带请求体配置的请求应该直接通过 require.Equal(t, types.ActionContinue, action) host.CompleteHttp() }) }) }