feat: Add ext-auth plugin support for authentication blacklists/whitelists (#1694)

This commit is contained in:
韩贤涛
2025-01-21 14:28:49 +08:00
committed by GitHub
parent cfa3baddf8
commit 0259eaddbb
14 changed files with 1126 additions and 425 deletions

View File

@@ -16,70 +16,114 @@ description: Ext 认证插件实现了调用外部授权服务进行认证鉴权
## 配置字段
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ------------------------------- | -------- | ---- | ------ |------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `http_service` | object | 是 | - | 外部授权服务配置 |
| `failure_mode_allow` | bool | 否 | false | 当设置为 true 时,即使与授权服务的通信失败,或者授权服务返回了 HTTP 5xx 错误,仍会接受客户端请求 |
| `failure_mode_allow_header_add` | bool | 否 | false | 当 `failure_mode_allow``failure_mode_allow_header_add` 都设置为 true 时,若与授权服务的通信失败,或授权服务返回了 HTTP 5xx 错误,那么请求头中将会添加 `x-envoy-auth-failure-mode-allowed: true` |
| `status_on_error` | int | 否 | 403 | 当授权服务无法访问或状态码为 5xx 时,设置返回给客户端的 HTTP 状态码。默认状态码是 `403` |
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ------------------------------- | ------------------ | ---- | ------ | ------------------------------------------------------------ |
| `http_service` | object | 是 | - | 外部授权服务配置 |
| `match_type` | string | 否 | | 可选 `whitelist``blacklist` |
| `match_list` | array of MatchRule | 否 | | 一个包含 (`match_rule_domain`, `match_rule_path`, `match_rule_type`) 的列表 |
| `failure_mode_allow` | bool | 否 | false | 当设置为 true 时,即使与授权服务的通信失败,或者授权服务返回了 HTTP 5xx 错误,仍会接受客户端请求 |
| `failure_mode_allow_header_add` | bool | 否 | false | 当 `failure_mode_allow``failure_mode_allow_header_add` 都设置为 true 时,若与授权服务的通信失败,或授权服务返回了 HTTP 5xx 错误,那么请求头中将会添加 `x-envoy-auth-failure-mode-allowed: true` |
| `status_on_error` | int | 否 | 403 | 当授权服务无法访问或状态码为 5xx 时,设置返回给客户端的 HTTP 状态码。默认状态码是 `403` |
`http_service`中每一项的配置字段说明
`http_service` 中每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ------------------------ | -------- | ---- | ------ | ------------------------------------- |
| `endpoint_mode` | string | 否 | envoy | `envoy` , `forward_auth` 中选填一项 |
|--------------------------|----------|------|--------|---------------------------------------|
| `endpoint_mode` | string | 否 | envoy | 可选 `envoy` `forward_auth` |
| `endpoint` | object | 是 | - | 发送鉴权请求的 HTTP 服务信息 |
| `timeout` | int | 否 | 1000 | `ext-auth` 服务连接超时时间,单位毫秒 |
| `authorization_request` | object | 否 | - | 发送鉴权请求配置 |
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
`endpoint`中每一项的配置字段说明
`endpoint` 中每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| -------- | -------- | -- | ------ | ----------------------------------------------------------------------------------------- |
| `service_name` | string | 必填 | - | 输入授权服务名称,带服务类型的完整 FQDN 名称,例如 `ext-auth.dns``ext-auth.my-ns.svc.cluster.local` |
| `service_port` | int | 否 | 80 | 输入授权服务的服务端口 |
| `service_host` | string | 否 | - | 请求授权服务时设置的Host头不填时和FQDN保持一致 |
| `path_prefix` | string | `endpoint_mode``envoy`时必填 | | `endpoint_mode``envoy` 时,客户端向授权服务发送请求的请求路径前缀 |
| `request_method` | string | 否 | GET | `endpoint_mode``forward_auth`客户端向授权服务发送请求的HTTP Method |
| `path` | string | `endpoint_mode``forward_auth`时必填 | - | `endpoint_mode``forward_auth` 时,客户端向授权服务发送请求的请求路径 |
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|------------------|----------|----------------------------------------|--------|--------------------------------------------------------------|
| `service_name` | string | | - | 输入授权服务名称,带服务类型的完整 FQDN 名称,例如 `ext-auth.dns``ext-auth.my-ns.svc.cluster.local` |
| `service_port` | int | 否 | 80 | 输入授权服务的服务端口 |
| `service_host` | string | 否 | - | 请求授权服务时设置的 Host 头,不填时和 FQDN 保持一致 |
| `path_prefix` | string | `endpoint_mode` `envoy` 时必填 | - | `endpoint_mode` `envoy` 时,客户端向授权服务发送请求的请求路径前缀 |
| `request_method` | string | 否 | GET | `endpoint_mode` `forward_auth` 时,客户端向授权服务发送请求的 HTTP Method |
| `path` | string | `endpoint_mode` `forward_auth` 时必填 | - | `endpoint_mode` `forward_auth` 时,客户端向授权服务发送请求的请求路径 |
`authorization_request`中每一项的配置字段说明
`authorization_request` 中每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ------------------------ | ---------------------- | ---- | ------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `allowed_headers` | array of StringMatcher | 否 | - | 设置后,具有相应匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头 `endpoint_mode``forward_auth` 时,会把原始请求的请求路径设置到 `X-Original-Uri` 原始请求的HTTP Method设置到 `X-Original-Method` |
| `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` 的配置 |
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|--------------------------|------------------------|------|--------|--------------------------------------------------------------|
| `allowed_headers` | array of StringMatcher | 否 | - | 设置后,匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头`endpoint_mode``forward_auth` 时,会添加 `X-Forwarded-*` 的请求头 |
| `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` 的配置 |
`authorization_response`中每一项的配置字段说明
`authorization_response` 中每一项的配置字段说明
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| -------------------------- | ---------------------- | ---- | ------ |---------------------------------------------------------------------------------|
| `allowed_upstream_headers` | array of StringMatcher | 否 | - | 当设置后,具有相应匹配项的鉴权请求的响应头将添加到原始的客户端请求头中。请注意,同名的请求头将被覆盖 |
| `allowed_client_headers` | array of StringMatcher | 否 | - | 如果不设置,在请求被拒绝时,所有的鉴权请求的响应头将添加到客户端的响应头中。当设置后,在请求被拒绝时,具有相应匹配项的鉴权请求的响应头将添加到客户端的响应头中 |
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|----------------------------|------------------------|------|--------|--------------------------------------------------------------|
| `allowed_upstream_headers` | array of StringMatcher | 否 | - | 匹配项的鉴权请求的响应头将添加到原始的客户端请求头中。请注意,同名的请求头将被覆盖 |
| `allowed_client_headers` | array of StringMatcher | 否 | - | 如果不设置,在请求被拒绝时,所有的鉴权请求的响应头将添加到客户端的响应头中。当设置后,在请求被拒绝时,匹配项的鉴权请求的响应头将添加到客户端的响应头中 |
`StringMatcher`类型每一项的配置字段说明,在使用`array of StringMatcher`时会按照数组中定义的StringMatcher顺序依次进行配置
`StringMatcher` 类型每一项的配置字段说明,在使用 `array of StringMatcher` 时会按照数组中定义的 StringMatcher 顺序依次进行配置
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ---------- | -------- | ------------------------------------------------------------ | ------ | -------- |
|------------|----------|-------------------------------------------------------------|--------|----------|
| `exact` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 精确匹配 |
| `prefix` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 前缀匹配 |
| `suffix` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 后缀匹配 |
| `contains` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 是否包含 |
| `regex` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 正则匹配 |
MatchRule 类型每一项的配置字段说明,在使用 `array of MatchRule` 时会按照数组中定义的 MatchRule 顺序依次进行配置
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
| ------------------- | -------- | ---- | ------ | ------------------------------------------------------------ |
| `match_rule_domain` | string | 否 | - | 匹配规则域名,支持通配符模式,例如 `*.bar.com` |
| `match_rule_path` | string | 否 | - | 匹配请求路径的规则 |
| `match_rule_type` | string | 否 | - | 匹配请求路径的规则类型,可选 `exact` , `prefix` , `suffix`, `contains`, `regex` |
### 两种 `endpoint_mode` 的区别
`endpoint_mode``envoy` 时,鉴权请求会使用原始请求的 HTTP Method和配置的 `path_prefix` 作为请求路径前缀拼接上原始的请求路径
`endpoint_mode``forward_auth` 时,鉴权请求会使用配置的 `request_method` 作为 HTTP Method和配置的 `path` 作为请求路径,并且 Higress 会自动生成并发送以下 header 至鉴权服务:
| Header | 说明 |
| -------------------- | ------------------------------------------------------ |
| `x-forwarded-proto` | 原始请求的scheme比如 http/https |
| `x-forwarded-method` | 原始请求的方法,比如 get/post/delete/patch |
| `x-forwarded-host` | 原始请求的host |
| `x-forwarded-uri` | 原始请求的path包含路径参数比如 `/v1/app?test=true` |
### 黑白名单模式
支持黑白名单模式配置,默认为白名单模式,白名单为空,即所有请求都需要经过验证,匹配域名支持泛域名例如 `*.bar.com` ,匹配规则支持 `exact` , `prefix` , `suffix`, `contains`, `regex`
**白名单模式**
```yaml
match_type: 'whitelist'
match_list:
- match_rule_domain: '*.bar.com'
match_rule_path: '/foo'
match_rule_type: 'prefix'
```
泛域名 `*.bar.com` 下前缀匹配 `/foo` 的请求无需验证
**黑名单模式**
```yaml
match_type: 'blacklist'
match_list:
- match_rule_domain: '*.bar.com'
match_rule_path: '/headers'
match_rule_type: 'prefix'
```
只有泛域名 `*.bar.com` 下前缀匹配 `/header` 的请求需要验证
## 配置示例
下面假设 `ext-auth` 服务在KubernetesserviceName为 `ext-auth`,端口 `8090`,路径为 `/auth`,命名空间为 `backend`
支持两种 `endpoint_mode`
- `endpoint_mode``envoy`鉴权请求会使用原始请求的HTTP Method和配置的 `path_prefix` 作为请求路径前缀拼接上原始的请求路径
- `endpoint_mode``forward_auth` 时,鉴权请求会使用配置的 `request_method` 作为HTTP Method和配置的 `path` 作为请求路径
下面假设 `ext-auth` 服务在 KubernetesserviceName `ext-auth`,端口 `8090`,路径为 `/auth`,命名空间为 `backend`
### endpoint_mode为envoy时
@@ -198,7 +242,7 @@ http_service:
使用如下请求网关,当开启 `ext-auth` 插件后:
```shell
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "Host: foo.bar.com"
```
**请求 `ext-auth` 服务成功:**
@@ -209,8 +253,10 @@ curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
POST /auth HTTP/1.1
Host: ext-auth.backend.svc.cluster.local
Authorization: xxx
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Original-Method: GET
X-Forwarded-Proto: HTTP
X-Forwarded-Host: foo.bar.com
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Forwarded-Method: GET
Content-Length: 0
```
@@ -261,7 +307,7 @@ http_service:
使用如下请求网关,当开启 `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"
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` 服务将接收到如下的鉴权请求:
@@ -270,22 +316,13 @@ curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
POST /auth HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Original-Method: GET
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
Content-Length: 0
```
`ext-auth` 服务返回响应头中如果包含 `x-user-id``x-auth-version`网关调用upstream时的请求中会带上这两个请求头
#### x-forwarded-* header
在endpoint_mode为forward_auth时higress会自动生成并发送以下header至鉴权服务。
| Header | 说明 |
|--------------------|-------------------------------------|
| x-forwarded-proto | 原始请求的scheme比如http/https |
| x-forwarded-method | 原始请求的方法比如get/post/delete/patch |
| x-forwarded-host | 原始请求的host |
| x-forwarded-uri | 原始请求的path包含路径参数比如/v1/app?test=true |
| x-forwarded-for | 原始请求的客户端IP地址 |
`ext-auth` 服务返回响应头中如果包含 `x-user-id``x-auth-version`网关调用upstream时的请求中会带上这两个请求头

View File

@@ -3,73 +3,135 @@ title: External Authentication
keywords: [higress, auth]
description: The Ext Authentication plugin implements the capability to call external authorization services for authentication and authorization.
---
## Function Description
The `ext-auth` plugin implements sending authentication requests to an external authorization service to check whether the client request is authorized. This plugin is implemented with reference to Envoy's native [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter), which covers some capabilities for connecting to HTTP services.
## Execution Properties
Plugin Execution Phase: `Authentication Phase`
## Feature Description
The `ext-auth` plugin sends an authorization request to an external authorization service to check if the client request is authorized. When implementing this plugin, it refers to the native [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter) of Envoy, and realizes part of the capabilities of the native filter to connect to an HTTP service.
## Operating Attributes
Plugin Execution Phase: `Authentication Phase`
Plugin Execution Priority: `360`
## Configuration Fields
| Name | Data Type | Required | Default Value | Description |
| ------------------------------- | --------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `http_service` | object | Yes | - | Configuration for the external authorization service |
| `failure_mode_allow` | bool | No | false | When set to true, client requests will still be accepted even if communication with the authorization service fails or the authorization service returns an HTTP 5xx error |
| `failure_mode_allow_header_add` | bool | No | false | When both `failure_mode_allow` and `failure_mode_allow_header_add` are set to true, if communication with the authorization service fails or returns an HTTP 5xx error, the request header will include `x-envoy-auth-failure-mode-allowed: true` |
| `status_on_error` | int | No | 403 | Sets the HTTP status code returned to the client when the authorization service is unreachable or returns a 5xx status code. The default status code is `403` |
### Configuration Fields for Each Item in `http_service`
| Name | Data Type | Required | Default Value | Description |
| ------------------------ | --------- | -------- | ------------- | -------------------------------------------- |
| `endpoint_mode` | string | No | envoy | Select either `envoy` or `forward_auth` as an optional choice |
| `endpoint` | object | Yes | - | Information about the HTTP service for sending authentication requests |
| `timeout` | int | No | 1000 | Connection timeout for `ext-auth` service, in milliseconds |
| `authorization_request` | object | No | - | Configuration for sending authentication requests |
| `authorization_response` | object | No | - | Configuration for processing authentication responses |
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `http_service` | object | Yes | - | Configuration for the external authorization service |
| `match_type` | string | No | | Can be `whitelist` or `blacklist` |
| `match_list` | array of MatchRule | No | | A list containing (`match_rule_domain`, `match_rule_path`, `match_rule_type`) |
| `failure_mode_allow` | bool | No | false | When set to true, client requests will be accepted even if the communication with the authorization service fails or the authorization service returns an HTTP 5xx error |
| `failure_mode_allow_header_add` | bool | No | false | When both `failure_mode_allow` and `failure_mode_allow_header_add` are set to true, if the communication with the authorization service fails or the authorization service returns an HTTP 5xx error, the `x-envoy-auth-failure-mode-allowed: true` header will be added to the request header |
| `status_on_error` | int | No | 403 | Sets the HTTP status code returned to the client when the authorization service is inaccessible or has a 5xx status code. The default status code is `403` |
### Configuration Fields for Each Item in `endpoint`
| Name | Data Type | Required | Default Value | Description |
| ---------------- | --------- | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------------- |
| `service_name` | string | Required | - | Input the name of the authorization service, in complete FQDN format, e.g., `ext-auth.dns` or `ext-auth.my-ns.svc.cluster.local` |
| `service_port` | int | No | 80 | Input the port of the authorization service |
| `service_host` | string | No | - | The Host header set when requesting the authorization service; remains the same as FQDN if not filled |
| `path_prefix` | string | Required when `endpoint_mode` is `envoy` | | Request path prefix for the client when sending requests to the authorization service |
| `request_method` | string | No | GET | HTTP Method for client requests to the authorization service when `endpoint_mode` is `forward_auth` |
| `path` | string | Required when `endpoint_mode` is `forward_auth` | - | Request path for the client when sending requests to the authorization service |
Configuration fields for each item in `http_service`
### Configuration Fields for Each Item in `authorization_request`
| Name | Data Type | Required | Default Value | Description |
| ------------------------ | ---------------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowed_headers` | array of StringMatcher | No | - | When set, client request headers with matching criteria will be added to the headers of the request to the authorization service. The `Authorization` HTTP header will be automatically included in the authorization service request, and if `endpoint_mode` is `forward_auth`, the original request path will be set to `X-Original-Uri` and the original request HTTP method will be set to `X-Original-Method`. |
| `headers_to_add` | `map[string]string` | No | - | Sets the list of request headers to include in the authorization service request. Note that headers with the same name from the client will be overwritten. |
| `with_request_body` | bool | No | false | Buffer the client request body and send it in the authentication request (does not take effect for HTTP Methods GET, OPTIONS, and HEAD) |
| `max_request_body_bytes` | int | No | 10MB | Sets the maximum size of the client request body to keep 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 start. Note that this setting takes precedence over the `failure_mode_allow` configuration. |
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `endpoint_mode` | string | No | envoy | Can be `envoy` or `forward_auth` |
| `endpoint` | object | Yes | - | Information about the HTTP service to which the authentication request is sent |
| `timeout` | int | No | 1000 | The connection timeout for the `ext-auth` service in milliseconds |
| `authorization_request` | object | No | - | Configuration for sending the authentication request |
| `authorization_response` | object | No | - | Configuration for handling the authentication response |
### Configuration Fields for Each Item in `authorization_response`
| Name | Data Type | Required | Default Value | Description |
| -------------------------- | ---------------------- | -------- | ------------- | ----------------------------------------------------------------------------------------------- |
| `allowed_upstream_headers` | array of StringMatcher | No | - | When set, the response headers of the authorization request with matching criteria will be added to the original client request headers. Note that headers with the same name will be overwritten. |
| `allowed_client_headers` | array of StringMatcher | No | - | If not set, all response headers from authorization requests will be added to the clients response when a request is denied. When set, response headers from authorization requests with matching criteria will be added to the client's response when a request is denied. |
Configuration fields for each item in `endpoint`
### Field Descriptions for `StringMatcher` Type
When using `array of StringMatcher`, the fields are configured according to the order defined in the array.
| Name | Data Type | Required | Default Value | Description |
| ---------- | --------- | --------------------------------------------------- | ------------- | ----------- |
| `exact` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Exact match |
| `prefix` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Prefix match |
| `suffix` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Suffix match |
| `contains` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Contains match |
| `regex` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Regex match |
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `service_name` | string | Yes | - | Enter the name of the authorization service, the full FQDN name with service type, e.g., `ext-auth.dns`, `ext-auth.my-ns.svc.cluster.local` |
| `service_port` | int | No | 80 | Enter the service port of the authorization service |
| `service_host` | string | No | - | The Host header set when requesting the authorization service. If not filled, it will be the same as the FQDN |
| `path_prefix` | string | Required when `endpoint_mode` is `envoy` | - | When `endpoint_mode` is `envoy`, the request path prefix for the client to send a request to the authorization service |
| `request_method` | string | No | GET | When `endpoint_mode` is `forward_auth`, the HTTP Method for the client to send a request to the authorization service |
| `path` | string | Required when `endpoint_mode` is `forward_auth` | - | When `endpoint_mode` is `forward_auth`, the request path for the client to send a request to the authorization service |
## Configuration Example
Assuming the `ext-auth` service has a serviceName of `ext-auth`, port `8090`, path `/auth`, and namespace `backend` in Kubernetes.
Configuration fields for each item in `authorization_request`
Two types of `endpoint_mode` are supported:
- When `endpoint_mode` is `envoy`, the authentication request will use the original request HTTP Method, and the configured `path_prefix` will be concatenated with the original request path.
- When `endpoint_mode` is `forward_auth`, the authentication request will use the configured `request_method` as the HTTP Method and the configured `path` as the request path.
| 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) |
| `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 in `authorization_response`
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `allowed_upstream_headers` | array of StringMatcher | No | - | The response headers of the authentication request that match the items will be added to the original client request headers. Please note that the request headers with the same name will be overwritten |
| `allowed_client_headers` | array of StringMatcher | No | - | If not set, when the request is rejected, all the response headers of the authentication request will be added to the client's response headers. When set, when the request is rejected, the response headers of the authentication request that match the items will be added to the client's response headers |
Configuration fields for each item of `StringMatcher` type. When using `array of StringMatcher`, the StringMatchers defined in the array will be configured in order.
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `exact` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Exact match |
| `prefix` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Prefix match |
| `suffix` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Suffix match |
| `contains` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Contains |
| `regex` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Regular expression match |
Configuration fields for each item of `MatchRule` type. When using `array of MatchRule`, the MatchRules defined in the array will be configured in order.
| Name | Data Type | Required | Default Value | Description |
| --- | --- | --- | --- | --- |
| `match_rule_domain` | string | No | - | The domain of the matching rule, supports wildcard patterns, e.g., `*.bar.com` |
| `match_rule_path` | string | No | - | The rule for matching the request path |
| `match_rule_type` | string | No | - | The type of the rule for matching the request path, can be `exact`, `prefix`, `suffix`, `contains`, `regex` |
### Differences between the two `endpoint_mode`
When `endpoint_mode` is `envoy`, the authentication request will use the original request's HTTP Method and the configured `path_prefix` as the request path prefix, concatenated with the original request path.
When `endpoint_mode` is `forward_auth`, the authentication request will use the configured `request_method` as the HTTP Method and the configured `path` as the request path. Higress will automatically generate and send the following headers to the authorization service:
| Header | Description |
| --- | --- |
| `x-forwarded-proto` | The scheme of the original request, such as http/https |
| `x-forwarded-method` | The method of the original request, such as get/post/delete/patch |
| `x-forwarded-host` | The host of the original request |
| `x-forwarded-uri` | The path of the original request, including path parameters, e.g., `/v1/app?test=true` |
### Blacklist and Whitelist Modes
Supports blacklist and whitelist mode configuration. The default is the whitelist mode. If the whitelist is empty, all requests need to be verified. The matching domain supports wildcard domains such as `*.bar.com`, and the matching rule supports `exact`, `prefix`, `suffix`, `contains`, `regex`.
**Whitelist Mode**
```yaml
match_type: 'whitelist'
match_list:
- match_rule_domain: '*.bar.com'
match_rule_path: '/foo'
match_rule_type: 'prefix'
```
Requests with a prefix match of `/foo` under the wildcard domain `*.bar.com` do not need to be verified.
**Blacklist Mode**
```yaml
match_type: 'blacklist'
match_list:
- match_rule_domain: '*.bar.com'
match_rule_path: '/headers'
match_rule_type: 'prefix'
```
Only requests with a prefix match of `/header` under the wildcard domain `*.bar.com` need to be verified.
## Configuration Examples
Assume that in Kubernetes, the `ext-auth` service has a `serviceName` of `ext-auth`, a port of `8090`, a path of `/auth`, and is in the `backend` namespace.
### When endpoint_mode is envoy
#### Example 1
Configuration of the `ext-auth` plugin:
### Example 1: `endpoint_mode` is `envoy`
#### Configuration of `ext-auth` Plugin:
```yaml
http_service:
endpoint_mode: envoy
@@ -80,13 +142,16 @@ http_service:
timeout: 1000
```
Using the following request to the gateway, after enabling the `ext-auth` plugin:
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"
```
**Successful request to the `ext-auth` service:**
The `ext-auth` service will receive the following authentication request:
**When the request to the `ext-auth` service is successful**:
The `ext-auth` service will receive the following authorization request:
```
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
Host: ext-auth.backend.svc.cluster.local
@@ -94,10 +159,12 @@ Authorization: xxx
Content-Length: 0
```
**Failed request to the `ext-auth` service:**
When the `ext-auth` service responds with a 5xx error, the client will receive an HTTP response code of 403 along with all response headers returned by the `ext-auth` service.
**When the request to the `ext-auth` service fails**:
When the response from the `ext-auth` service is 5xx, the client will receive an HTTP response code of 403 and all the response headers returned by the `ext-auth` service.
If the `ext-auth` service returns response headers of `x-auth-version: 1.0` and `x-auth-failed: true`, they will be passed to the client.
If the `ext-auth` service returns `x-auth-version: 1.0` and `x-auth-failed: true` headers, these will be conveyed to the client:
```
HTTP/1.1 403 Forbidden
x-auth-version: 1.0
@@ -107,9 +174,14 @@ server: istio-envoy
content-length: 0
```
When the `ext-auth` service is inaccessible or returns a status code of 5xx, the client request will be denied with the status code configured in `status_on_error`. When the `ext-auth` service returns other HTTP status codes, the client request will be denied with the returned status code. If `allowed_client_headers` is configured, the matching response headers will be added to the client's response.
When the `ext-auth` service is inaccessible or the status code is 5xx, the client request will be rejected with the status code configured in `status_on_error`.
When the `ext-auth` service returns other HTTP status codes, the client request will be rejected with the returned status code. If `allowed_client_headers` is configured, the response headers with corresponding matching items will be added to the client's response.
#### Example 2
Configuration of the `ext-auth` plugin:
#### Example 2: `ext-auth` Plugin Configuration:
```yaml
http_service:
authorization_request:
@@ -130,12 +202,14 @@ http_service:
timeout: 1000
```
Using the following request to the gateway after enabling the `ext-auth` plugin:
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 authentication request:
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
@@ -145,10 +219,14 @@ x-envoy-header: true
Content-Length: 0
```
If the `ext-auth` service returns headers containing `x-user-id` and `x-auth-version`, these two request headers will be included in requests to the upstream when the gateway calls it.
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.
### When endpoint_mode is forward_auth
#### Example 1
Configuration of the `ext-auth` plugin:
### Example 1: `endpoint_mode` is `forward_auth`
`ext-auth` Plugin Configuration:
```yaml
http_service:
endpoint_mode: forward_auth
@@ -160,26 +238,33 @@ http_service:
timeout: 1000
```
Using the following request to the gateway after enabling the `ext-auth` plugin:
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"
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "Host: foo.bar.com"
```
**Successful request to the `ext-auth` service:**
The `ext-auth` service will receive the following authentication request:
**When the request to the `ext-auth` service is successful**:
The `ext-auth` service will receive the following authorization request:
```
POST /auth HTTP/1.1
Host: ext-auth.backend.svc.cluster.local
Authorization: xxx
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Original-Method: GET
X-Forwarded-Proto: HTTP
X-Forwarded-Host: foo.bar.com
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Forwarded-Method: GET
Content-Length: 0
```
**Failed request to the `ext-auth` service:**
When the `ext-auth` service responds with a 5xx error, the client will receive an HTTP response code of 403 along with all response headers returned by the `ext-auth` service.
**When the request to the `ext-auth` service fails**:
When the response from the `ext-auth` service is 5xx, the client will receive an HTTP response code of 403 and all the response headers returned by the `ext-auth` service.
If the `ext-auth` service returns response headers of `x-auth-version: 1.0` and `x-auth-failed: true`, they will be passed to the client.
If the `ext-auth` service returns `x-auth-version: 1.0` and `x-auth-failed: true` headers, these will be conveyed to the client:
```
HTTP/1.1 403 Forbidden
x-auth-version: 1.0
@@ -189,9 +274,14 @@ server: istio-envoy
content-length: 0
```
When the `ext-auth` service is inaccessible or returns a status code of 5xx, the client request will be denied with the status code configured in `status_on_error`. When the `ext-auth` service returns other HTTP status codes, the client request will be denied with the returned status code. If `allowed_client_headers` is configured, the matching response headers will be added to the client's response.
When the `ext-auth` service is inaccessible or the status code is 5xx, the client request will be rejected with the status code configured in `status_on_error`.
When the `ext-auth` service returns other HTTP status codes, the client request will be rejected with the returned status code. If `allowed_client_headers` is configured, the response headers with corresponding matching items will be added to the client's response.
#### Example 2
Configuration of the `ext-auth` plugin:
#### Example 2: `ext-auth` Plugin Configuration:
```yaml
http_service:
authorization_request:
@@ -213,31 +303,25 @@ http_service:
timeout: 1000
```
Using the following request to the gateway after enabling the `ext-auth` plugin:
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"
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 authentication request:
The `ext-auth` service will receive the following authorization request:
```
POST /auth HTTP/1.1
Host: my-domain.local
Authorization: xxx
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
X-Original-Method: GET
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
Content-Length: 0
```
If the `ext-auth` service returns headers containing `x-user-id` and `x-auth-version`, these two request headers will be included in requests to the upstream when the gateway calls it.
#### x-forwarded-* Header
When `endpoint_mode` is `forward_auth`, Higress will automatically generate and send the following headers to the authorization service.
| Header | Description |
|--------------------|-----------------------------------------------|
| x-forwarded-proto | The scheme of the original request, e.g., http/https |
| x-forwarded-method | The method of the original request, e.g., get/post/delete/patch |
| x-forwarded-host | The host of the original request |
| x-forwarded-uri | The path of the original request, including path parameters, e.g., /v1/app?test=true |
| x-forwarded-for | The client IP address of the original request |
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.

View File

@@ -1,4 +1,4 @@
package main
package config
import (
"errors"
@@ -12,80 +12,78 @@ import (
)
const (
DefaultStatusOnError uint32 = http.StatusForbidden
DefaultStatusOnError = http.StatusForbidden
DefaultHttpServiceTimeout uint32 = 1000
DefaultHttpServiceTimeout = 1000
DefaultMaxRequestBodyBytes uint32 = 10 * 1024 * 1024
EndpointModeEnvoy = "envoy"
DefaultMaxRequestBodyBytes = 10 * 1024 * 1024
EndpointModeEnvoy = "envoy"
EndpointModeForwardAuth = "forward_auth"
)
type ExtAuthConfig struct {
httpService HttpService
failureModeAllow bool
failureModeAllowHeaderAdd bool
statusOnError uint32
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
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 In addition to the users supplied matchers,
// Authorization are automatically included to the list.
// When the endpoint_mode is set to forward_auth,
// the original request's path is set in the X-Original-Uri header,
// and the original request's HTTP method is set in the X-Original-Method header.
allowedHeaders expr.Matcher
headersToAdd map[string]string
withRequestBody bool
maxRequestBodyBytes uint32
AllowedHeaders expr.Matcher
HeadersToAdd map[string]string
WithRequestBody bool
MaxRequestBodyBytes uint32
}
type AuthorizationResponse struct {
allowedUpstreamHeaders expr.Matcher
allowedClientHeaders expr.Matcher
AllowedUpstreamHeaders expr.Matcher
AllowedClientHeaders expr.Matcher
}
func parseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
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")
}
err := parseHttpServiceConfig(httpServiceConfig, config, log)
if err != nil {
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()
config.FailureModeAllow = failureModeAllow.Bool()
}
failureModeAllowHeaderAdd := json.Get("failure_mode_allow_header_add")
if failureModeAllowHeaderAdd.Exists() {
config.failureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
config.FailureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
}
statusOnError := uint32(json.Get("status_on_error").Uint())
if statusOnError == 0 {
statusOnError = DefaultStatusOnError
}
config.statusOnError = statusOnError
config.StatusOnError = statusOnError
return nil
}
@@ -101,7 +99,7 @@ func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrappe
if timeout == 0 {
timeout = DefaultHttpServiceTimeout
}
httpService.timeout = timeout
httpService.Timeout = timeout
if err := parseAuthorizationRequestConfig(json, &httpService); err != nil {
return err
@@ -111,7 +109,7 @@ func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrappe
return err
}
config.httpService = httpService
config.HttpService = httpService
return nil
}
@@ -123,7 +121,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
} else if endpointMode != EndpointModeEnvoy && endpointMode != EndpointModeForwardAuth {
return errors.New(fmt.Sprintf("endpoint_mode %s is not supported", endpointMode))
}
httpService.endpointMode = endpointMode
httpService.EndpointMode = endpointMode
endpointConfig := json.Get("endpoint")
if !endpointConfig.Exists() {
@@ -140,7 +138,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
}
serviceHost := endpointConfig.Get("service_host").String()
httpService.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
httpService.Client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
Host: serviceHost,
@@ -152,7 +150,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
if !pathPrefixConfig.Exists() {
return errors.New("when endpoint_mode is envoy, endpoint path_prefix must not be empty")
}
httpService.pathPrefix = pathPrefixConfig.String()
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")
@@ -160,16 +158,16 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
case EndpointModeForwardAuth:
requestMethodConfig := endpointConfig.Get("request_method")
if !requestMethodConfig.Exists() {
httpService.requestMethod = http.MethodGet
httpService.RequestMethod = http.MethodGet
} else {
httpService.requestMethod = strings.ToUpper(requestMethodConfig.String())
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()
httpService.Path = pathConfig.String()
if endpointConfig.Get("path_prefix").Exists() {
log.Warn("when endpoint_mode is forward_auth, endpoint path_prefix will be ignored")
@@ -189,35 +187,28 @@ func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService
if err != nil {
return err
}
authorizationRequest.allowedHeaders = result
authorizationRequest.AllowedHeaders = result
}
headersToAdd := map[string]string{}
headersToAddConfig := authorizationRequestConfig.Get("headers_to_add")
if headersToAddConfig.Exists() {
for key, value := range headersToAddConfig.Map() {
headersToAdd[key] = value.Str
}
}
authorizationRequest.headersToAdd = headersToAdd
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))
(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()
authorizationRequest.WithRequestBody = withRequestBody.Bool()
}
maxRequestBodyBytes := uint32(authorizationRequestConfig.Get("max_request_body_bytes").Uint())
if maxRequestBodyBytes == 0 {
maxRequestBodyBytes = DefaultMaxRequestBodyBytes
}
authorizationRequest.maxRequestBodyBytes = maxRequestBodyBytes
authorizationRequest.MaxRequestBodyBytes = maxRequestBodyBytes
httpService.authorizationRequest = authorizationRequest
httpService.AuthorizationRequest = authorizationRequest
}
return nil
}
@@ -233,7 +224,7 @@ func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpServic
if err != nil {
return err
}
authorizationResponse.allowedUpstreamHeaders = result
authorizationResponse.AllowedUpstreamHeaders = result
}
allowedClientHeaders := authorizationResponseConfig.Get("allowed_client_headers")
@@ -242,10 +233,62 @@ func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpServic
if err != nil {
return err
}
authorizationResponse.allowedClientHeaders = result
authorizationResponse.AllowedClientHeaders = result
}
httpService.authorizationResponse = authorizationResponse
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
}

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

View File

@@ -0,0 +1,79 @@
package expr
import (
"strings"
regexp "github.com/wasilibs/go-re2"
)
const (
ModeWhitelist = "whitelist"
ModeBlacklist = "blacklist"
)
type MatchRules struct {
Mode string
RuleList []Rule
}
type Rule struct {
Domain string
Path Matcher
}
func MatchRulesDefaults() MatchRules {
return MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{},
}
}
// IsAllowedByMode checks if the given domain and path are allowed based on the configuration mode.
func (config *MatchRules) IsAllowedByMode(domain, path string) bool {
switch config.Mode {
case ModeWhitelist:
for _, rule := range config.RuleList {
if rule.matchDomainAndPath(domain, path) {
return true
}
}
return false
case ModeBlacklist:
for _, rule := range config.RuleList {
if rule.matchDomainAndPath(domain, path) {
return false
}
}
return true
default:
return false
}
}
// matchDomainAndPath checks if the given domain and path match the rule.
// If rule.Domain is empty, it only checks rule.Path.
// If rule.Path is empty, it only checks rule.Domain.
// If both are empty, it returns false.
func (rule *Rule) matchDomainAndPath(domain, path string) bool {
if rule.Domain == "" && rule.Path == nil {
return false
}
domainMatch := rule.Domain == "" || matchDomain(domain, rule.Domain)
pathMatch := rule.Path == nil || rule.Path.Match(path)
return domainMatch && pathMatch
}
// matchDomain checks if the given domain matches the pattern.
func matchDomain(domain string, pattern string) bool {
// Convert wildcard pattern to regex pattern
regexPattern := convertWildcardToRegex(pattern)
matched, _ := regexp.MatchString(regexPattern, domain)
return matched
}
// convertWildcardToRegex converts a wildcard pattern to a regex pattern.
func convertWildcardToRegex(pattern string) string {
pattern = regexp.QuoteMeta(pattern)
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
return pattern
}

View File

@@ -0,0 +1,259 @@
package expr
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsAllowedByMode(t *testing.T) {
tests := []struct {
name string
config MatchRules
domain string
path string
expected bool
}{
{
name: "Whitelist mode, rule matches",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{
Domain: "example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: true,
},
{
name: "Whitelist mode, rule does not match",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{
Domain: "example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/bar",
expected: false,
},
{
name: "Blacklist mode, rule matches",
config: MatchRules{
Mode: ModeBlacklist,
RuleList: []Rule{
{
Domain: "example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: false,
},
{
name: "Blacklist mode, rule does not match",
config: MatchRules{
Mode: ModeBlacklist,
RuleList: []Rule{
{
Domain: "example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/bar",
expected: true,
},
{
name: "Domain matches, Path is empty",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{Domain: "example.com", Path: nil},
},
},
domain: "example.com",
path: "/foo",
expected: true,
},
{
name: "Domain is empty, Path matches",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{
Domain: "",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: true,
},
{
name: "Both Domain and Path are empty",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{Domain: "", Path: nil},
},
},
domain: "example.com",
path: "/foo",
expected: false,
},
{
name: "Invalid mode",
config: MatchRules{
Mode: "invalid",
RuleList: []Rule{
{
Domain: "example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: false,
},
{
name: "Whitelist mode, generic domain matches",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{
Domain: "*.example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "sub.example.com",
path: "/foo",
expected: true,
},
{
name: "Whitelist mode, generic domain does not match",
config: MatchRules{
Mode: ModeWhitelist,
RuleList: []Rule{
{
Domain: "*.example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: false,
},
{
name: "Blacklist mode, generic domain matches",
config: MatchRules{
Mode: ModeBlacklist,
RuleList: []Rule{
{
Domain: "*.example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "sub.example.com",
path: "/foo",
expected: false,
},
{
name: "Blacklist mode, generic domain does not match",
config: MatchRules{
Mode: ModeBlacklist,
RuleList: []Rule{
{
Domain: "*.example.com",
Path: func() Matcher {
pathMatcher, err := newStringExactMatcher("/foo", true)
if err != nil {
t.Fatalf("Failed to create Matcher: %v", err)
}
return pathMatcher
}(),
},
},
},
domain: "example.com",
path: "/foo",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.IsAllowedByMode(tt.domain, tt.path)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -4,18 +4,17 @@ import (
"errors"
"strings"
"github.com/tidwall/gjson"
regexp "github.com/wasilibs/go-re2"
)
const (
matchPatternExact string = "exact"
matchPatternPrefix string = "prefix"
matchPatternSuffix string = "suffix"
matchPatternContains string = "contains"
matchPatternRegex string = "regex"
MatchPatternExact string = "exact"
MatchPatternPrefix string = "prefix"
MatchPatternSuffix string = "suffix"
MatchPatternContains string = "contains"
MatchPatternRegex string = "regex"
matchIgnoreCase string = "ignore_case"
MatchIgnoreCase string = "ignore_case"
)
type Matcher interface {
@@ -78,78 +77,16 @@ func (m *stringRegexMatcher) Match(s string) bool {
return m.regex.MatchString(s)
}
type repeatedStringMatcher struct {
matchers []Matcher
}
func (rsm *repeatedStringMatcher) Match(s string) bool {
for _, m := range rsm.matchers {
if m.Match(s) {
return true
}
}
return false
}
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
builtMatchers := make([]Matcher, len(matchers))
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, matcherType MatcherConstructor) (Matcher, error) {
result := json.Get(targetKey)
if result.Exists() && result.String() != "" {
target := result.String()
return matcherType(target, ignoreCase)
}
return nil, nil
}
for i, item := range matchers {
var matcher Matcher
var err error
// If allIgnoreCase is true, it takes precedence over any user configuration,
// forcing case-insensitive matching regardless of individual item settings.
ignoreCase := allIgnoreCase
if !allIgnoreCase {
ignoreCaseResult := item.Get(matchIgnoreCase)
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
ignoreCase = true
}
}
for _, matcherType := range []struct {
key string
creator MatcherConstructor
}{
{matchPatternExact, newStringExactMatcher},
{matchPatternPrefix, newStringPrefixMatcher},
{matchPatternSuffix, newStringSuffixMatcher},
{matchPatternContains, newStringContainsMatcher},
{matchPatternRegex, newStringRegexMatcher},
} {
if matcher, err = createMatcher(item, matcherType.key, ignoreCase, matcherType.creator); err != nil {
return nil, err
}
if matcher != nil {
break
}
}
if matcher == nil {
return nil, errors.New("unknown string matcher type")
}
builtMatchers[i] = matcher
}
return &repeatedStringMatcher{
matchers: builtMatchers,
}, nil
}
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)
@@ -189,14 +126,11 @@ func newStringRegexMatcher(target string, ignoreCase bool) (Matcher, error) {
return &stringRegexMatcher{regex: re}, nil
}
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
return buildRepeatedStringMatcher(matchers, true)
}
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
return buildRepeatedStringMatcher(matchers, false)
}
func BuildStringMatcher(matcher gjson.Result) (Matcher, error) {
return BuildRepeatedStringMatcher([]gjson.Result{matcher})
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")
}

View File

@@ -4,78 +4,96 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
func TestStringMatcher(t *testing.T) {
tests := []struct {
name string
cfg string
matchType string
target string
ignoreCase bool
matched []string
mismatched []string
}{
{
name: "exact",
cfg: `{"exact": "foo"}`,
matchType: MatchPatternExact,
target: "foo",
matched: []string{"foo"},
mismatched: []string{"fo", "fooo"},
},
{
name: "exact, ignore_case",
cfg: `{"exact": "foo", "ignore_case": true}`,
matched: []string{"Foo", "foo"},
name: "exact, ignore_case",
matchType: MatchPatternExact,
target: "foo",
ignoreCase: true,
matched: []string{"Foo", "foo"},
},
{
name: "prefix",
cfg: `{"prefix": "/p"}`,
matchType: MatchPatternPrefix,
target: "/p",
matched: []string{"/p", "/pa"},
mismatched: []string{"/P"},
},
{
name: "prefix, ignore_case",
cfg: `{"prefix": "/p", "ignore_case": true}`,
matchType: MatchPatternPrefix,
target: "/p",
ignoreCase: true,
matched: []string{"/P", "/p", "/pa", "/Pa"},
mismatched: []string{"/"},
},
{
name: "suffix",
cfg: `{"suffix": "foo"}`,
matchType: MatchPatternSuffix,
target: "foo",
matched: []string{"foo", "0foo"},
mismatched: []string{"fo", "fooo", "aFoo"},
},
{
name: "suffix, ignore_case",
cfg: `{"suffix": "foo", "ignore_case": true}`,
matchType: MatchPatternSuffix,
target: "foo",
ignoreCase: true,
matched: []string{"aFoo", "foo"},
mismatched: []string{"fo", "fooo"},
},
{
name: "contains",
cfg: `{"contains": "foo"}`,
matchType: MatchPatternContains,
target: "foo",
matched: []string{"foo", "0foo", "fooo"},
mismatched: []string{"fo", "aFoo"},
},
{
name: "contains, ignore_case",
cfg: `{"contains": "foo", "ignore_case": true}`,
matchType: MatchPatternContains,
target: "foo",
ignoreCase: true,
matched: []string{"aFoo", "foo", "FoO"},
mismatched: []string{"fo"},
},
{
name: "regex",
cfg: `{"regex": "fo{2}"}`,
matchType: MatchPatternRegex,
target: "fo{2}",
matched: []string{"foo", "0foo", "fooo"},
mismatched: []string{"aFoo", "fo"},
},
{
name: "regex, ignore_case",
cfg: `{"regex": "fo{2}", "ignore_case": true}`,
matchType: MatchPatternRegex,
target: "fo{2}",
ignoreCase: true,
matched: []string{"foo", "0foo", "fooo", "aFoo"},
mismatched: []string{"fo"},
},
{
name: "regex, ignore_case & case insensitive specified in regex",
cfg: `{"regex": "(?i)fo{2}", "ignore_case": true}`,
matchType: MatchPatternRegex,
target: "(?i)fo{2}",
ignoreCase: true,
matched: []string{"foo", "0foo", "fooo", "aFoo"},
mismatched: []string{"fo"},
},
@@ -83,7 +101,7 @@ func TestStringMatcher(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
built, _ := BuildStringMatcher(gjson.Parse(tt.cfg))
built, _ := BuildStringMatcher(tt.matchType, tt.target, tt.ignoreCase)
for _, s := range tt.matched {
assert.True(t, built.Match(s))
}
@@ -93,30 +111,3 @@ func TestStringMatcher(t *testing.T) {
})
}
}
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
cfgs := []string{
`{"exact":"foo"}`,
`{"prefix":"pre"}`,
`{"regex":"^Cache"}`,
}
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
mismatched := []string{"afoo", "fo"}
ms := []gjson.Result{}
for _, cfg := range cfgs {
ms = append(ms, gjson.Parse(cfg))
}
built, _ := BuildRepeatedStringMatcherIgnoreCase(ms)
for _, s := range matched {
assert.True(t, built.Match(s))
}
for _, s := range mismatched {
assert.False(t, built.Match(s))
}
}
func TestPassOutRegexCompileErr(t *testing.T) {
cfg := `{"regex":"(?!)aa"}`
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
assert.NotNil(t, err)
}

View File

@@ -0,0 +1,76 @@
package expr
import (
"errors"
"github.com/tidwall/gjson"
)
type repeatedStringMatcher struct {
matchers []Matcher
}
func (rsm *repeatedStringMatcher) Match(s string) bool {
for _, m := range rsm.matchers {
if m.Match(s) {
return true
}
}
return false
}
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
builtMatchers := make([]Matcher, len(matchers))
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, constructor MatcherConstructor) (Matcher, error) {
result := json.Get(targetKey)
if result.Exists() && result.String() != "" {
target := result.String()
return constructor(target, ignoreCase)
}
return nil, nil
}
for i, item := range matchers {
var matcher Matcher
var err error
// If allIgnoreCase is true, it takes precedence over any user configuration,
// forcing case-insensitive matching regardless of individual item settings.
ignoreCase := allIgnoreCase
if !allIgnoreCase {
ignoreCaseResult := item.Get(MatchIgnoreCase)
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
ignoreCase = true
}
}
for key, creator := range matcherConstructors {
if matcher, err = createMatcher(item, key, ignoreCase, creator); err != nil {
return nil, err
}
if matcher != nil {
break
}
}
if matcher == nil {
return nil, errors.New("unknown string matcher type")
}
builtMatchers[i] = matcher
}
return &repeatedStringMatcher{
matchers: builtMatchers,
}, nil
}
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
return buildRepeatedStringMatcher(matchers, true)
}
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
return buildRepeatedStringMatcher(matchers, false)
}

View File

@@ -0,0 +1,36 @@
package expr
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
cfg := `[
{"exact":"foo"},
{"prefix":"pre"},
{"regex":"^Cache"}
]`
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
mismatched := []string{"afoo", "fo"}
jsonArray := gjson.Parse(cfg).Array()
built, err := BuildRepeatedStringMatcherIgnoreCase(jsonArray)
if err != nil {
t.Fatalf("Failed to build RepeatedStringMatcher: %v", err)
}
for _, s := range matched {
assert.True(t, built.Match(s))
}
for _, s := range mismatched {
assert.False(t, built.Match(s))
}
}
func TestPassOutRegexCompileErr(t *testing.T) {
cfg := `{"regex":"(?!)aa"}`
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
assert.NotNil(t, err)
}

View File

@@ -4,8 +4,7 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho=
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
@@ -15,8 +14,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=

View File

@@ -18,6 +18,8 @@ import (
"net/http"
"net/url"
"ext-auth/config"
"ext-auth/util"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
@@ -26,32 +28,41 @@ import (
func main() {
wrapper.SetCtx(
"ext-auth",
wrapper.ParseConfigBy(parseConfig),
wrapper.ParseConfigBy(config.ParseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
)
}
const (
HeaderAuthorization string = "authorization"
HeaderFailureModeAllow string = "x-envoy-auth-failure-mode-allowed"
HeaderOriginalMethod string = "x-original-method"
HeaderOriginalUri string = "x-original-uri"
// Currently, x-forwarded-xxx headers only apply for forward_auth.
HeaderXForwardedProto = "x-forwarded-proto"
HeaderXForwardedMethod = "x-forwarded-method"
HeaderXForwardedUri = "x-Forwarded-uri"
HeaderXForwardedHost = "x-Forwarded-host"
HeaderAuthorization = "authorization"
HeaderFailureModeAllow = "x-envoy-auth-failure-mode-allowed"
)
func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wrapper.Log) types.Action {
// Currently, x-forwarded-xxx headers only apply for forward_auth.
const (
HeaderOriginalMethod = "x-original-method"
HeaderOriginalUri = "x-original-uri"
HeaderXForwardedProto = "x-forwarded-proto"
HeaderXForwardedMethod = "x-forwarded-method"
HeaderXForwardedUri = "x-forwarded-uri"
HeaderXForwardedHost = "x-forwarded-host"
)
func onHttpRequestHeaders(ctx wrapper.HttpContext, config config.ExtAuthConfig, log wrapper.Log) types.Action {
path, _ := wrapper.GetRequestPathWithoutQuery()
// If the request's domain and path match the MatchRules, skip authentication
if config.MatchRules.IsAllowedByMode(ctx.Host(), path) {
ctx.DontReadRequestBody()
return types.ActionContinue
}
if wrapper.HasRequestBody() {
ctx.SetRequestBodyBufferLimit(config.httpService.authorizationRequest.maxRequestBodyBytes)
ctx.SetRequestBodyBufferLimit(config.HttpService.AuthorizationRequest.MaxRequestBodyBytes)
// If withRequestBody is true AND the HTTP request contains a request body,
// it will be handled in the onHttpRequestBody phase.
if config.httpService.authorizationRequest.withRequestBody {
if config.HttpService.AuthorizationRequest.WithRequestBody {
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
ctx.DisableReroute()
// The request has a body and requires delaying the header transmission until a cache miss occurs,
@@ -64,108 +75,115 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wra
return checkExtAuth(ctx, config, nil, log, types.HeaderStopAllIterationAndWatermark)
}
func onHttpRequestBody(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
if config.httpService.authorizationRequest.withRequestBody {
func onHttpRequestBody(ctx wrapper.HttpContext, config config.ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
if config.HttpService.AuthorizationRequest.WithRequestBody {
return checkExtAuth(ctx, config, body, log, types.ActionPause)
}
return types.ActionContinue
}
func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log, pauseAction types.Action) types.Action {
// build extAuth request headers
extAuthReqHeaders := http.Header{}
func checkExtAuth(ctx wrapper.HttpContext, cfg config.ExtAuthConfig, body []byte, log wrapper.Log, pauseAction types.Action) types.Action {
httpServiceConfig := cfg.HttpService
httpServiceConfig := config.httpService
requestConfig := httpServiceConfig.authorizationRequest
reqHeaders, _ := proxywasm.GetHttpRequestHeaders()
if requestConfig.allowedHeaders != nil {
for _, header := range reqHeaders {
headK := header[0]
if requestConfig.allowedHeaders.Match(headK) {
extAuthReqHeaders.Set(headK, header[1])
}
}
}
for key, value := range requestConfig.headersToAdd {
extAuthReqHeaders.Set(key, value)
}
// add Authorization header
authorization := extractFromHeader(reqHeaders, HeaderAuthorization)
if authorization != "" {
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
}
// when endpoint_mode is forward_auth, add x-original-method and x-original-uri headers
if httpServiceConfig.endpointMode == EndpointModeForwardAuth {
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
extAuthReqHeaders.Set(HeaderXForwardedProto, ctx.Scheme())
extAuthReqHeaders.Set(HeaderXForwardedMethod, ctx.Method())
extAuthReqHeaders.Set(HeaderXForwardedUri, ctx.Path())
extAuthReqHeaders.Set(HeaderXForwardedHost, ctx.Host())
}
requestMethod := httpServiceConfig.requestMethod
requestPath := httpServiceConfig.path
if httpServiceConfig.endpointMode == EndpointModeEnvoy {
extAuthReqHeaders := buildExtAuthRequestHeaders(ctx, cfg)
requestMethod := httpServiceConfig.RequestMethod
requestPath := httpServiceConfig.Path
if httpServiceConfig.EndpointMode == config.EndpointModeEnvoy {
requestMethod = ctx.Method()
requestPath, _ = url.JoinPath(httpServiceConfig.pathPrefix, ctx.Path())
requestPath, _ = url.JoinPath(httpServiceConfig.PathPrefix, ctx.Path())
}
// call ext auth server
err := httpServiceConfig.client.Call(requestMethod, requestPath, reconvertHeaders(extAuthReqHeaders), body,
// Call ext auth server
err := httpServiceConfig.Client.Call(requestMethod, requestPath, util.ReconvertHeaders(extAuthReqHeaders), body,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
defer proxywasm.ResumeHttpRequest()
if statusCode != http.StatusOK {
log.Errorf("failed to call ext auth server, status: %d", statusCode)
callExtAuthServerErrorHandler(config, statusCode, responseHeaders, responseBody)
callExtAuthServerErrorHandler(cfg, statusCode, responseHeaders, responseBody)
return
}
if httpServiceConfig.authorizationResponse.allowedUpstreamHeaders != nil {
if httpServiceConfig.AuthorizationResponse.AllowedUpstreamHeaders != nil {
for headK, headV := range responseHeaders {
if httpServiceConfig.authorizationResponse.allowedUpstreamHeaders.Match(headK) {
if httpServiceConfig.AuthorizationResponse.AllowedUpstreamHeaders.Match(headK) {
_ = proxywasm.ReplaceHttpRequestHeader(headK, headV[0])
}
}
}
}, httpServiceConfig.timeout)
}, httpServiceConfig.Timeout)
if err != nil {
log.Errorf("failed to call ext auth server: %v", err)
// Since the handling logic for call errors and HTTP status code 500 is the same, we directly use 500 here.
callExtAuthServerErrorHandler(config, http.StatusInternalServerError, nil, nil)
callExtAuthServerErrorHandler(cfg, http.StatusInternalServerError, nil, nil)
return types.ActionContinue
}
return pauseAction
}
func callExtAuthServerErrorHandler(config ExtAuthConfig, statusCode int, extAuthRespHeaders http.Header, responseBody []byte) {
if statusCode >= http.StatusInternalServerError && config.failureModeAllow {
if config.failureModeAllowHeaderAdd {
func buildExtAuthRequestHeaders(ctx wrapper.HttpContext, cfg config.ExtAuthConfig) http.Header {
extAuthReqHeaders := http.Header{}
httpServiceConfig := cfg.HttpService
requestConfig := httpServiceConfig.AuthorizationRequest
reqHeaders, _ := proxywasm.GetHttpRequestHeaders()
if requestConfig.AllowedHeaders != nil {
for _, header := range reqHeaders {
headK := header[0]
if requestConfig.AllowedHeaders.Match(headK) {
extAuthReqHeaders.Set(headK, header[1])
}
}
}
for key, value := range requestConfig.HeadersToAdd {
extAuthReqHeaders.Set(key, value)
}
// Add the Authorization header if present
authorization := util.ExtractFromHeader(reqHeaders, HeaderAuthorization)
if authorization != "" {
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
}
// Add additional headers when endpoint_mode is forward_auth
if httpServiceConfig.EndpointMode == config.EndpointModeForwardAuth {
// Compatible with older versions
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
// Add x-forwarded-xxx headers
extAuthReqHeaders.Set(HeaderXForwardedProto, ctx.Scheme())
extAuthReqHeaders.Set(HeaderXForwardedMethod, ctx.Method())
extAuthReqHeaders.Set(HeaderXForwardedUri, ctx.Path())
extAuthReqHeaders.Set(HeaderXForwardedHost, ctx.Host())
}
return extAuthReqHeaders
}
func callExtAuthServerErrorHandler(config config.ExtAuthConfig, statusCode int, extAuthRespHeaders http.Header, responseBody []byte) {
if statusCode >= http.StatusInternalServerError && config.FailureModeAllow {
if config.FailureModeAllowHeaderAdd {
_ = proxywasm.ReplaceHttpRequestHeader(HeaderFailureModeAllow, "true")
}
return
}
var respHeaders = extAuthRespHeaders
if config.httpService.authorizationResponse.allowedClientHeaders != nil {
if config.HttpService.AuthorizationResponse.AllowedClientHeaders != nil {
respHeaders = http.Header{}
for headK, headV := range extAuthRespHeaders {
if config.httpService.authorizationResponse.allowedClientHeaders.Match(headK) {
if config.HttpService.AuthorizationResponse.AllowedClientHeaders.Match(headK) {
respHeaders.Set(headK, headV[0])
}
}
}
// rejects client requests with statusOnError on extAuth unavailability or 5xx.
// otherwise, uses the extAuth's returned status code to reject requests
// Rejects client requests with StatusOnError if extAuth is unavailable or returns a 5xx status.
// Otherwise, uses the status code returned by extAuth to reject requests.
statusToUse := statusCode
if statusCode >= http.StatusInternalServerError {
statusToUse = int(config.statusOnError)
statusToUse = int(config.StatusOnError)
}
_ = sendResponse(uint32(statusToUse), "ext-auth.unauthorized", respHeaders, responseBody)
_ = util.SendResponse(uint32(statusToUse), "ext-auth.unauthorized", respHeaders, responseBody)
}

View File

@@ -1,4 +1,4 @@
package main
package util
import (
"net/http"
@@ -8,11 +8,11 @@ import (
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
)
func sendResponse(statusCode uint32, statusCodeDetailData string, headers http.Header, body []byte) error {
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetailData, reconvertHeaders(headers), body, -1)
func SendResponse(statusCode uint32, statusCodeDetailData string, headers http.Header, body []byte) error {
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetailData, ReconvertHeaders(headers), body, -1)
}
func reconvertHeaders(headers http.Header) [][2]string {
func ReconvertHeaders(headers http.Header) [][2]string {
var ret [][2]string
if headers == nil {
return ret
@@ -28,7 +28,7 @@ func reconvertHeaders(headers http.Header) [][2]string {
return ret
}
func extractFromHeader(headers [][2]string, headerKey string) string {
func ExtractFromHeader(headers [][2]string, headerKey string) string {
for _, header := range headers {
key := header[0]
if strings.ToLower(key) == headerKey {

View File

@@ -15,6 +15,7 @@
package wrapper
import (
"net/url"
"strconv"
"strings"
@@ -48,6 +49,15 @@ func GetRequestPath() string {
return path
}
func GetRequestPathWithoutQuery() (string, error) {
rawPath := GetRequestPath()
path, err := url.Parse(rawPath)
if err != nil {
return "", err
}
return path.Path, nil
}
func GetRequestMethod() string {
method, err := proxywasm.GetHttpRequestHeader(":method")
if err != nil {