feat: add key-auth plugin (#586)

Signed-off-by: dongjiang1989 <dongjiang1989@126.com>
This commit is contained in:
dongjiang
2023-12-11 10:03:52 +08:00
committed by GitHub
parent 4426f18a84
commit d2ee6065a0
9 changed files with 891 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
# 功能说明
`key-auth`插件实现了基于 API Key 进行认证鉴权的功能,支持从 HTTP 请求的 URL 参数或者请求头解析 API Key同时验证该 API Key 是否有权限访问。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- |
| `global_auth` | bool | 选填 | - | 若配置为true则全局生效认证机制; 若配置为false则只对做了配置的域名和路由生效认证机制; 若不配置则仅当没有域名和路由配置时全局生效(兼容机制) |
| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 |
| `keys` | array of string | 必填 | - | API Key 的来源字段名称,可以是 URL 参数或者 HTTP 请求头名称 |
| `in_query` | bool | `in_query``in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 URL 参数中解析 API Key |
| `in_header` | bool | `in_query``in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 HTTP 请求头中解析 API Key |
`consumers`中每一项的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ------------ | -------- | -------- | ------ | ------------------------ |
| `credential` | string | 必填 | - | 配置该consumer的访问凭证 |
| `name` | string | 必填 | - | 配置该consumer的名称 |
**注意:**
- 对于通过认证鉴权的请求请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。
# 配置示例
## 对特定路由或域名开启
以下配置将对网关特定路由或域名开启 Key Auth 认证和鉴权,注意`credential`字段不能重复
```yaml
global_auth: true
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
- x-api-key
```
**路由级配置**
对 route-a 和 route-b 这两个路由做如下配置:
```yaml
allow:
- consumer1
```
对 *.example.com 和 test.com 在这两个域名做如下配置:
```yaml
allow:
- consumer2
```
### 根据该配置,下列请求可以允许访问:
假设以下请求会匹配到route-a这条路由
**将 API Key 设置在 url 参数中**
```bash
curl http://xxx.hello.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
```
**将 API Key 设置在 http 请求头中**
```bash
curl http://xxx.hello.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5'
```
认证鉴权通过后请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称
### 下列请求将拒绝访问:
**请求未提供 API Key返回401**
```bash
curl http://xxx.hello.com/test
```
**请求提供的 API Key 无权访问返回401**
```bash
curl http://xxx.hello.com/test?apikey=926d90ac-ba2e-11ec-ab68-00163e1250b5
```
**根据请求提供的 API Key匹配到的调用者无访问权限返回403**
```bash
# consumer2不在route-a的allow列表里
curl http://xxx.hello.com/test?apikey=c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
```
## 网关实例级别开启
以下配置未指定`matchRules`字段,因此将对网关实例级别开启全局 Key Auth 认证.
```yaml
defaultConfig
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
in_query: true
```
开启`matchRules`方式如下:
```yaml
matchRules:
- config:
allow:
- consumer1
```
# 相关错误码
| HTTP 状态码 | 出错信息 | 原因说明 |
| ----------- | --------------------------------------------------------- | ----------------------- |
| 401 | Request denied by Key Auth check. Muti API key found in request | 请求提供多个 API Key |
| 401 | Request denied by Key Auth check. No API key found in request | 请求未提供 API Key |
| 401 | Request denied by Key Auth check. Invalid API key | 不允许当前 API Key 访问 |
| 403 | Request denied by Key Auth check. Unauthorized consumer | 请求的调用方无访问权限 |

View File

@@ -0,0 +1,109 @@
# Features
The `key-auth` plug-in implements the authentication function based on the API Key, supports parsing the API Key from the URL parameter or request header of the HTTP request, and verifies whether the API Key has permission to access.
# Configuration field
| Name | Data Type | Parameter requirements | Default| Description |
| ----------- | --------------- | -------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `global_auth` | bool | Optional | - | If configured to true, the authentication mechanism will take effect globally; if configured to false, the authentication mechanism will only take effect for the configured domain names and routes; if not configured, the authentication mechanism will only take effect globally when no domain names and routes are configured (compatibility mechanism) |
| `consumers` | array of object | Required | - | Configure the caller of the service to authenticate the request. |
| `keys` | array of string | Required | - | The name of the source field of the API Key, which can be a URL parameter or an HTTP request header name. |
| `in_query` | bool | At least one of `in_query` and `in_header` must be true. | true | When configured true, the gateway will try to parse the API Key from the URL parameters. |
| `in_header` | bool | The same as above. | true | The same as above. |
The configuration fields of each item in `consumers` are described as follows:
| Name | Data Type | Parameter requirements | Default | Description |
| ------------ | --------- | -----------------------| ------ | ------------------------------------------- |
| `credential` | string | Required | - | Configure the consumer's access credentials. |
| `name` | string | Required | - | Configure the name of the consumer. |
**Warning**
- For a request that passes authentication, an `X-Mse-Consumer` field will be added to the request header to identify the name of the caller.
# Example configuration
## Enabled for specific routes or domains
The following configuration will enable Key Auth authentication and authentication for gateway-specific routes or domain names. Note that the `credential` field can not be repeated.
```yaml
global_auth: true
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
- x-api-key
in_query: true
```
The `route-a` and `route-b` specified in `_match_route_` in this example are the route names filled in when creating the gateway route. When these two routes are matched, calls whose `name` is `consumer1` will be allowed Access by callers, other callers are not allowed to access;
`*.example.com` and `test.com` specified in `_match_domain_` in this example are used to match the domain name of the request. When the domain name matches, the caller whose `name` is `consumer2` will be allowed to access, and other calls access is not allowed.
### Depending on this configuration, the following requests would allow access
Assume that the following request will match the route-a route:
**Set the API Key in the url parameter**
```bash
curl http://xxx.hello.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
```
**Set the API Key in the http request header**
```bash
curl http://xxx.hello.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5'
```
After the authentication is passed, an `X-Mse-Consumer` field will be added to the header of the request. In this example, its value is `consumer1`, which is used to identify the name of the caller.
### The following requests will deny access
**The request does not provide an API Key, return 401**
```bash
curl http://xxx.hello.com/test
```
**The API Key provided by the request is not authorized to access, return 401**
```bash
curl http://xxx.hello.com/test?apikey=926d90ac-ba2e-11ec-ab68-00163e1250b5
```
**The caller matched according to the API Key provided in the request has no access rights, return 403**
```bash
# consumer2 is not in the allow list of route-a
curl http://xxx.hello.com/test?apikey=c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
```
## Gateway instance level enabled
The following configuration does not specify the `matchRules` field, so Key Auth authentication will be enabled at the gateway instance level.
```yaml
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
in_query: true
```
configuration specify the `matchRules` field, like
```yaml
matchRules:
- config:
allow:
- consumer1
```
# Error code
| HTTP status code | Error information | Reason |
| ---------------- | --------------------------------------------------------- | -------------------------------------------- |
| 401 | Muti API key found in request. | Muti API provided by request Key. |
| 401 | No API key found in request. | API not provided by request Key. |
| 401 | Request denied by Key Auth check. Invalid API key. | Current API Key access is not allowed. |
| 403 | Request denied by Key Auth check. Unauthorized consumer. | The requested caller does not have access. |

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,19 @@
module key-auth
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20231017105619-a18879bf867c
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
github.com/tidwall/gjson v1.14.4
)
require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)

View File

@@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/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=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,62 @@
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: mcp-keyauth-httpbin
namespace: higress-system
spec:
registries:
- domain: httpbin.org
name: httpbin
port: 80
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: httpbin.dns
higress.io/upstream-vhost: "httpbin.org"
higress.io/backend-protocol: HTTP
name: ingress-keyauth-httpbin
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: httpbin.example.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: mcp-keyauth-httpbin
path: /
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: wasm-keyauth-httpbin
namespace: higress-system
spec:
defaultConfig:
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
global_auth: false
keys:
- x-api-key
- apikey
in_header: true
defaultConfigDisable: false
matchRules:
- config:
allow:
- consumer1
configDisable: false
ingress:
- ingress-keyauth-httpbin
url: oci://docker.io/dongjiang1989/keyauth:1.0.0
imagePullPolicy: Always

View File

@@ -0,0 +1,357 @@
// Copyright (c) 2023 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 (
"errors"
"fmt"
"net/url"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
var (
ruleSet bool // 插件是否至少在一个 domain 或 route 上生效
protectionSpace = "MSE Gateway" // 认证失败时,返回响应头 WWW-Authenticate: Key realm=MSE Gateway
)
func main() {
wrapper.SetCtx(
"key-auth", // middleware name
wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type Consumer struct {
// @Title 名称
// @Title en-US Name
// @Description 该调用方的名称。
// @Description en-US The name of the consumer.
Name string `yaml:"name"`
// @Title 访问凭证
// @Title en-US Credential
// @Description 该调用方的访问凭证。
// @Description en-US The credential of the consumer.
// @Scope GLOBAL
Credential string `yaml:"credential"`
}
// @Name key-auth
// @Category auth
// @Phase AUTHN
// @Priority 321
// @Title zh-CN Key Auth
// @Description zh-CN 本插件实现了实现了基于 API Key 进行认证鉴权的功能.
// @Description en-US This plugin implements an authentication function based on API Key Auth standard.
// @IconUrl https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png
// @Version 1.0.0
//
// @Contact.name Higress Team
// @Contact.url http://higress.io/
// @Contact.email admin@higress.io
//
// @Example
// global_auth: false
// consumers:
// - name: consumer1
// credential: token1
// - name: consumer2
// credential: token2
// keys:
// - x-api-key
// - token
// in_query: true
// @End
type KeyAuthConfig struct {
// @Title 是否开启全局认证
// @Title en-US Enable Global Auth
// @Description 若不开启全局认证,则全局配置只提供凭证信息。只有在域名或路由上进行了配置才会启用认证。
// @Description en-US If set to false, only consumer info will be accepted from the global config. Auth feature shall only be enabled if the corresponding domain or route is configured.
// @Scope GLOBAL
globalAuth *bool `yaml:"global_auth,omitempty"` //是否开启全局认证. 若不开启全局认证,则全局配置只提供凭证信息。只有在域名或路由上进行了配置才会启用认证。
// @Title API Key 的来源字段名称列表
// @Title en-US The name of the source field of the API Key
// @Description API Key 的来源字段名称,可以是 URL 参数或者 HTTP 请求头名称.
// @Description en-US The name of the source field of the API Key, which can be a URL parameter or an HTTP request header name.
// @Scope GLOBAL
Keys []string `yaml:"keys"` // key auth names
// @Title key是否来源于URL参数
// @Title en-US the API Key from the URL parameters.
// @Description 如果配置 true 时,网关会尝试从 URL 参数中解析 API Key
// @Description en-US When configured true, the gateway will try to parse the API Key from the URL parameters.
// @Scope GLOBAL
InQuery bool `yaml:"in_query,omitempty"`
// @Title key是否来源于Header
// @Title en-US the API Key from the HTTP request header name.
// @Description 配置 true 时,网关会尝试从 URL header头中解析 API Key
// @Description en-US When configured true, the gateway will try to parse the API Key from the HTTP request header name.
// @Scope GLOBAL
InHeader bool `yaml:"in_header,omitempty"`
// @Title 调用方列表
// @Title en-US Consumer List
// @Description 服务调用方列表,用于对请求进行认证。
// @Description en-US List of service consumers which will be used in request authentication.
// @Scope GLOBAL
consumers []Consumer `yaml:"consumers"`
// @Title 授权访问的调用方列表
// @Title en-US Allowed Consumers
// @Description 对于匹配上述条件的请求,允许访问的调用方列表。
// @Description en-US Consumers to be allowed for matched requests.
allow []string `yaml:"allow"`
credential2Name map[string]string `yaml:"-"`
}
func parseGlobalConfig(json gjson.Result, global *KeyAuthConfig, log wrapper.Log) error {
log.Debug("global config")
// init
ruleSet = false
global.credential2Name = make(map[string]string)
// global_auth
globalAuth := json.Get("global_auth")
if globalAuth.Exists() {
ga := globalAuth.Bool()
global.globalAuth = &ga
}
// keys
names := json.Get("keys")
if !names.Exists() {
return errors.New("keys is required")
}
if len(names.Array()) == 0 {
return errors.New("keys cannot be empty")
}
for _, name := range names.Array() {
global.Keys = append(global.Keys, name.String())
}
// in_query and in_header
in_query := json.Get("in_query")
in_header := json.Get("in_header")
if !in_query.Exists() && !in_header.Exists() {
return errors.New("must one of in_query/in_header required")
}
if in_query.Exists() {
global.InQuery = in_query.Bool()
}
if in_header.Exists() {
global.InHeader = in_header.Bool()
}
// consumers
consumers := json.Get("consumers")
if !consumers.Exists() {
return errors.New("consumers is required")
}
if len(consumers.Array()) == 0 {
return errors.New("consumers cannot be empty")
}
for _, item := range consumers.Array() {
name := item.Get("name")
if !name.Exists() || name.String() == "" {
return errors.New("consumer name is required")
}
credential := item.Get("credential")
if !credential.Exists() || credential.String() == "" {
return errors.New("consumer credential is required")
}
if _, ok := global.credential2Name[credential.String()]; ok {
return errors.New("duplicate consumer credential: " + credential.String())
}
consumer := Consumer{
Name: name.String(),
Credential: credential.String(),
}
global.consumers = append(global.consumers, consumer)
global.credential2Name[credential.String()] = name.String()
}
return nil
}
func parseOverrideRuleConfig(json gjson.Result, global KeyAuthConfig, config *KeyAuthConfig, log wrapper.Log) error {
log.Debug("domain/route config")
*config = global
allow := json.Get("allow")
if !allow.Exists() {
return errors.New("allow is required")
}
if len(allow.Array()) == 0 {
return errors.New("allow cannot be empty")
}
for _, item := range allow.Array() {
config.allow = append(config.allow, item.String())
}
ruleSet = true
return nil
}
// key-auth 插件认证逻辑:
// - global_auth == true 开启全局生效:
// - 若当前 domain/route 未配置 allow 列表,即未配置该插件:则在所有 consumers 中查找,如果找到则认证通过,否则认证失败 (1*)
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
//
// - global_auth == false 非全局生效:(2*)
// - 若当前 domain/route 未配置该插件:则直接放行
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
//
// - global_auth 未设置:
// - 若没有一个 domain/route 配置该插件:则遵循 (1*)
// - 若有至少一个 domain/route 配置该插件:则遵循 (2*)
func onHttpRequestHeaders(ctx wrapper.HttpContext, config KeyAuthConfig, log wrapper.Log) types.Action {
var (
noAllow = len(config.allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效
globalAuthNoSet = config.globalAuth == nil
globalAuthSetTrue = !globalAuthNoSet && *config.globalAuth
globalAuthSetFalse = !globalAuthNoSet && !*config.globalAuth
)
// 不需要认证而直接放行的情况:
// - global_auth == false 且 当前 domain/route 未配置该插件
// - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
if noAllow {
log.Info("authorization is not required")
return types.ActionContinue
}
}
// 以下需要认证:
// - 从 header 中获取 tokens 信息
// - 从 query 中获取 tokens 信息
var tokens []string
if config.InHeader {
// 匹配keys中的 keyname
for _, key := range config.Keys {
value, err := proxywasm.GetHttpRequestHeader(key)
if err == nil && value != "" {
tokens = append(tokens, value)
}
}
} else if config.InQuery {
requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
url, _ := url.Parse(requestUrl)
queryValues := url.Query()
for _, key := range config.Keys {
values, ok := queryValues[key]
if ok && len(values) > 0 {
tokens = append(tokens, values...)
}
}
}
// header/query
if len(tokens) > 1 {
return deniedMutiKeyAuthData()
} else if len(tokens) <= 0 {
return deniedNoKeyAuthData()
}
// 验证token
name, ok := config.credential2Name[tokens[0]]
if !ok {
log.Warnf("credential %q is not configured", tokens[0])
return deniedUnauthorizedConsumer()
}
// 全局生效:
// - global_auth == true 且 当前 domain/route 未配置该插件
// - global_auth 未设置 且 没有任何一个 domain/route 配置该插件
if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !ruleSet) {
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
// 全局生效,但当前 domain/route 配置了 allow 列表
if globalAuthSetTrue && !noAllow {
if !contains(config.allow, name) {
log.Warnf("consumer %q is not allowed", name)
return deniedUnauthorizedConsumer()
}
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
// 非全局生效
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
if !noAllow { // 配置了 allow 列表
if !contains(config.allow, name) {
log.Warnf("consumer %q is not allowed", name)
return deniedUnauthorizedConsumer()
}
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
}
return types.ActionContinue
}
func deniedMutiKeyAuthData() types.Action {
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. Muti Key Authentication information found."), -1)
return types.ActionContinue
}
func deniedNoKeyAuthData() types.Action {
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. No Key Authentication information found."), -1)
return types.ActionContinue
}
func deniedUnauthorizedConsumer() types.Action {
_ = proxywasm.SendHttpResponse(403, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. Unauthorized consumer."), -1)
return types.ActionContinue
}
func authenticated(name string) types.Action {
_ = proxywasm.AddHttpRequestHeader("X-Mse-Consumer", name)
return types.ActionContinue
}
func contains(arr []string, item string) bool {
for _, i := range arr {
if i == item {
return true
}
}
return false
}
func WWWAuthenticateHeader(realm string) [][2]string {
return [][2]string{
{"WWW-Authenticate", fmt.Sprintf("Key realm=%s", realm)},
}
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) 2023 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 tests
import (
"testing"
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
)
func init() {
Register(WasmPluginsKeyAuth)
}
var WasmPluginsKeyAuth = suite.ConformanceTest{
ShortName: "WasmPluginsKeyAuth",
Description: "The Ingress in the higress-conformance-infra namespace test the key-auth WASM plugin.",
Manifests: []string{"tests/go-wasm-key-auth.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: Successful authentication",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "token11111111111111111111"},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"X-Mse-Consumer": "consumer1"},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: No Key Authentication information found",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 401,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: Invalid token",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "xxxxxxxxxnotfoundtoken"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: Unauthorized consumer",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "token22222222222222222222"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 5: Muti Key Authentication information found",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"apikey": "token11111111111111111111", "x-api-key": "token11111111111111111111"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 401,
},
},
},
}
t.Run("WasmPlugins key-auth", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}

View File

@@ -0,0 +1,60 @@
# Copyright (c) 2022 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: wasmplugin-key-auth
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/foo"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: key-auth
namespace: higress-system
spec:
defaultConfig:
consumers:
- credential: token11111111111111111111
name: consumer1
- credential: token22222222222222222222
name: consumer2
global_auth: false
keys:
- x-api-key
- apikey
in_header: true
defaultConfigDisable: false
matchRules:
- config:
allow:
- consumer1
configDisable: false
ingress:
- higress-conformance-infra/wasmplugin-key-auth
url: file:///opt/plugins/wasm-go/extensions/key-auth/plugin.wasm