diff --git a/apisix/secret/gcp.lua b/apisix/secret/gcp.lua new file mode 100644 index 000000000000..6b6e661c4ff2 --- /dev/null +++ b/apisix/secret/gcp.lua @@ -0,0 +1,202 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. +-- + +--- GCP Tools. +local core = require("apisix.core") +local http = require("resty.http") +local google_oauth = require("apisix.utils.google-cloud-oauth") + +local str_sub = core.string.sub +local str_find = core.string.find +local decode_base64 = ngx.decode_base64 + +local lrucache = core.lrucache.new({ ttl = 300, count = 8 }) + +local schema = { + type = "object", + properties = { + auth_config = { + type = "object", + properties = { + client_email = { type = "string" }, + private_key = { type = "string" }, + project_id = { type = "string" }, + token_uri = { + type = "string", + default = "https://oauth2.googleapis.com/token" + }, + scope = { + type = "array", + items = { + type = "string" + }, + default = { + "https://www.googleapis.com/auth/cloud-platform" + } + }, + entries_uri = { + type = "string", + default = "https://secretmanager.googleapis.com/v1" + }, + }, + required = { "client_email", "private_key", "project_id" } + }, + ssl_verify = { + type = "boolean", + default = true + }, + auth_file = { type = "string" }, + }, + oneOf = { + { required = { "auth_config" } }, + { required = { "auth_file" } }, + }, +} + +local _M = { + schema = schema +} + +local function fetch_oauth_conf(conf) + if conf.auth_config then + return conf.auth_config + end + + local file_content, err = core.io.get_file(conf.auth_file) + if not file_content then + return nil, "failed to read configuration, file: " .. conf.auth_file .. ", err: " .. err + end + + local config_tab, err = core.json.decode(file_content) + if not config_tab then + return nil, "config parse failure, data: " .. file_content .. ", err: " .. err + end + + local config = { + auth_config = { + client_email = config_tab.client_email, + private_key = config_tab.private_key, + project_id = config_tab.project_id + } + } + + local ok, err = core.schema.check(schema, config) + if not ok then + return nil, "config parse failure, file: " .. conf.auth_file .. ", err: " .. err + end + + return config_tab +end + + +local function get_secret(oauth, secrets_id) + local httpc = http.new() + + local access_token = oauth:generate_access_token() + if not access_token then + return nil, "failed to get google oauth token" + end + + local entries_uri = oauth.entries_uri .. "/projects/" .. oauth.project_id + .. "/secrets/" .. secrets_id .. "/versions/latest:access" + + local res, err = httpc:request_uri(entries_uri, { + ssl_verify = oauth.ssl_verify, + method = "GET", + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = (oauth.access_token_type or "Bearer") .. " " .. access_token, + }, + }) + + if not res then + return nil, err + end + + if res.status ~= 200 then + return nil, res.body + end + + local body, err = core.json.decode(res.body) + if not body then + return nil, "failed to parse response data, " .. err + end + + local payload = body.payload + if not payload then + return nil, "invalid payload" + end + + return decode_base64(payload.data) +end + + +local function make_request_to_gcp(conf, secrets_id) + local auth_config, err = fetch_oauth_conf(conf) + if not auth_config then + return nil, err + end + + local lru_key = auth_config.client_email .. "#" .. auth_config.project_id + + local oauth, err = lrucache(lru_key, "gcp", google_oauth.new, auth_config, conf.ssl_verify) + if not oauth then + return nil, "failed to create oauth object, " .. err + end + + local secret, err = get_secret(oauth, secrets_id) + if not secret then + return nil, err + end + + return secret +end + + +function _M.get(conf, key) + core.log.info("fetching data from gcp for key: ", key) + + local idx = str_find(key, '/') + + local main_key = idx and str_sub(key, 1, idx - 1) or key + if main_key == "" then + return nil, "can't find main key, key: " .. key + end + + local sub_key = idx and str_sub(key, idx + 1) + + core.log.info("main: ", main_key, sub_key and ", sub: " .. sub_key or "") + + local res, err = make_request_to_gcp(conf, main_key) + if not res then + return nil, "failed to retrtive data from gcp secret manager: " .. err + end + + if not sub_key then + return res + end + + local data, err = core.json.decode(res) + if not data then + return nil, "failed to decode result, err: " .. err + end + + return data[sub_key] +end + + +return _M diff --git a/apisix/utils/google-cloud-oauth.lua b/apisix/utils/google-cloud-oauth.lua new file mode 100644 index 000000000000..6cb352848bad --- /dev/null +++ b/apisix/utils/google-cloud-oauth.lua @@ -0,0 +1,130 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. +-- + +local core = require("apisix.core") +local type = type +local setmetatable = setmetatable + +local ngx_update_time = ngx.update_time +local ngx_time = ngx.time +local ngx_encode_args = ngx.encode_args + +local http = require("resty.http") +local jwt = require("resty.jwt") + + +local function get_timestamp() + ngx_update_time() + return ngx_time() +end + + +local _M = {} + + +function _M.generate_access_token(self) + if not self.access_token or get_timestamp() > self.access_token_expire_time - 60 then + self:refresh_access_token() + end + return self.access_token +end + + +function _M.refresh_access_token(self) + local http_new = http.new() + local res, err = http_new:request_uri(self.token_uri, { + ssl_verify = self.ssl_verify, + method = "POST", + body = ngx_encode_args({ + grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion = self:generate_jwt_token() + }), + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + }, + }) + + if not res then + core.log.error("failed to refresh google oauth access token, ", err) + return + end + + if res.status ~= 200 then + core.log.error("failed to refresh google oauth access token: ", res.body) + return + end + + res, err = core.json.decode(res.body) + if not res then + core.log.error("failed to parse google oauth response data: ", err) + return + end + + self.access_token = res.access_token + self.access_token_type = res.token_type + self.access_token_expire_time = get_timestamp() + res.expires_in +end + + +function _M.generate_jwt_token(self) + local payload = core.json.encode({ + iss = self.client_email, + aud = self.token_uri, + scope = self.scope, + iat = get_timestamp(), + exp = get_timestamp() + (60 * 60) + }) + + local jwt_token = jwt:sign(self.private_key, { + header = { alg = "RS256", typ = "JWT" }, + payload = payload, + }) + + return jwt_token +end + + +function _M.new(config, ssl_verify) + local oauth = { + client_email = config.client_email, + private_key = config.private_key, + project_id = config.project_id, + token_uri = config.token_uri or "https://oauth2.googleapis.com/token", + auth_uri = config.auth_uri or "https://accounts.google.com/o/oauth2/auth", + entries_uri = config.entries_uri, + access_token = nil, + access_token_type = nil, + access_token_expire_time = 0, + } + + oauth.ssl_verify = ssl_verify + + if config.scope then + if type(config.scope) == "string" then + oauth.scope = config.scope + end + + if type(config.scope) == "table" then + oauth.scope = core.table.concat(config.scope, " ") + end + end + + return setmetatable(oauth, { __index = _M }) +end + + +return _M diff --git a/docs/en/latest/terminology/secret.md b/docs/en/latest/terminology/secret.md index e27ee79fc1ac..94c1b8843b0a 100644 --- a/docs/en/latest/terminology/secret.md +++ b/docs/en/latest/terminology/secret.md @@ -40,6 +40,7 @@ APISIX currently supports storing secrets in the following ways: - [Environment Variables](#use-environment-variables-to-manage-secrets) - [HashiCorp Vault](#use-hashicorp-vault-to-manage-secrets) - [AWS Secrets Manager](#use-aws-secrets-manager-to-manage-secrets) +- [GCP Secrets Manager](#use-gcp-secrets-manager-to-manage-secrets) You can use APISIX Secret functions by specifying format variables in the consumer configuration of the following plugins, such as `key-auth`. @@ -293,3 +294,56 @@ curl -i http://127.0.0.1:9080/your_route -H 'apikey: value' ``` This will verify whether the `key-auth` plugin is correctly using the key from AWS Secrets Manager. + +## Use GCP Secrets Manager to manage secrets + +Using the GCP Secrets Manager to manage secrets means you can store the secret information in the GCP service, and reference it using a specific format of variables when configuring plugins. APISIX currently supports integration with the GCP Secrets Manager, and the supported authentication method is [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2). + +### Reference Format + +``` +$secret://$manager/$id/$secret_name/$key +``` + +The reference format is the same as before: + +- manager: secrets management service, could be the HashiCorp Vault, AWS, GCP etc. +- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource +- secret_name: the secret name in the secrets management service +- key: get the value of a property when the value of the secret is a JSON string + +### Required Parameters + +| Name | Required | Default | Description | +|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| auth_config | True | | Either `auth_config` or `auth_file` must be provided. | +| auth_config.client_email | True | | Email address of the Google Cloud service account. | +| auth_config.private_key | True | | Private key of the Google Cloud service account. | +| auth_config.project_id | True | | Project ID in the Google Cloud service account. | +| auth_config.token_uri | False | https://oauth2.googleapis.com/token | Token URI of the Google Cloud service account. | +| auth_config.entries_uri | False | https://secretmanager.googleapis.com/v1 | The API access endpoint for the Google Secrets Manager. | +| auth_config.scope | False | https://www.googleapis.com/auth/cloud-platform | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) | +| auth_file | True | | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided. | +| ssl_verify | False | true | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake). | + +You need to configure the corresponding authentication parameters, or specify the authentication file through auth_file, where the content of auth_file is in JSON format. + +### Example + +Here is a correct configuration example: + +``` +curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' + +``` diff --git a/docs/zh/latest/terminology/secret.md b/docs/zh/latest/terminology/secret.md index 03dbf0c1be2b..810abb7ddf6f 100644 --- a/docs/zh/latest/terminology/secret.md +++ b/docs/zh/latest/terminology/secret.md @@ -40,6 +40,7 @@ APISIX 目前支持通过以下方式存储密钥: - [环境变量](#使用环境变量管理密钥) - [HashiCorp Vault](#使用-vault-管理密钥) - [AWS Secrets Manager](#使用-aws-secrets-manager-管理密钥) +- [GCP Secrets Manager](#使用-gcp-secrets-manager-管理密钥) 你可以在以下插件的 consumer 配置中通过指定格式的变量来使用 APISIX Secret 功能,比如 `key-auth` 插件。 @@ -135,7 +136,7 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \ $secret://$manager/$id/$secret_name/$key ``` -- manager: 密钥管理服务,可以是 Vault、AWS 等 +- manager: 密钥管理服务,可以是 Vault、AWS、GCP 等 - APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致 - secret_name: 密钥管理服务中的密钥名称 - key:密钥管理服务中密钥对应的 key @@ -295,3 +296,56 @@ curl -i http://127.0.0.1:9080/your_route -H 'apikey: value' ``` 这将验证 key-auth 插件是否正确地使用 AWS Secret Manager 中的密钥。 + +## 使用 GCP Secrets Manager 管理密钥 + +使用 GCP Secret Manager 来管理密钥意味着你可以将密钥信息保存在 GCP 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 GCP Secret Manager, 所支持的验证方式是[OAuth 2.0](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn)。 + +### 引用方式 + +``` +$secret://$manager/$id/$secret_name/$key +``` + +引用方式和之前保持一致: + +- manager: 密钥管理服务,可以是 Vault、AWS\GCP 等 +- APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致 +- secret_name: 密钥管理服务中的密钥名称 +- key:当密钥的值是 JSON 字符串时,获取某个属性的值 + +### 必要参数 + +| 名称 | 必选项 | 默认值 | 描述 | +| ----------------------- | -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| auth_config | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| auth_config.client_email | 是 | | 谷歌服务帐号的 email 参数。 | +| auth_config.private_key | 是 | | 谷歌服务帐号的私钥参数。 | +| auth_config.project_id | 是 | | 谷歌服务帐号的项目 ID。 | +| auth_config.token_uri | 否 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的 URI。 | +| auth_config.entries_uri | 否 | https://secretmanager.googleapis.com/v1 | 谷歌密钥服务访问端点 API。 | +| auth_config.scope | 否 | https://www.googleapis.com/auth/cloud-platform | 谷歌服务账号的访问范围,可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes)| +| auth_file | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| ssl_verify | 否 | true | 当设置为 `true` 时,启用 `SSL` 验证。 | + +你需要配置相应的认证参数,或者通过 auth_file 来指定认证文件,其中 auth_file 的内容为认证参数的 json 格式。 + +### 示例 + +以下一种正确的配置实例: + +``` +curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' + +``` diff --git a/t/lib/server.lua b/t/lib/server.lua index 7cc8101a3af7..309873636302 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -558,6 +558,192 @@ function _M.google_logging_entries() ngx.say(data) end +function _M.google_secret_token() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + ngx.req.read_body() + local data = ngx.decode_args(ngx.req.get_body_data()) + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + local verify = jwt:verify(rsa_public_key, data["assertion"]) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + expire_time = 0 + end + + local jwt_token = jwt:sign(rsa_private_key, { + header = { typ = "JWT", alg = "RS256" }, + payload = { exp = verify.payload.exp, scope = access_scopes } + }) + + ngx.say(json_encode({ + access_token = jwt_token, + expires_in = expire_time, + token_type = args_token_type + })) +end + +function _M.google_secret_apisix_jack() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix/versions/1", + payload = { + data = "eyJrZXkiOiJ2YWx1ZSJ9", + dataCrc32c = "2296192492" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + +function _M.google_secret_apisix_error_jack() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/root/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix_error/versions/1", + payload = { + data = "eyJrZXkiOiJ2YWx1ZSJ9", + dataCrc32c = "2296192492" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + +function _M.google_secret_apisix_mysql() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix/versions/1", + payload = { + data = "c2VjcmV0", + dataCrc32c = "0xB03C4D4D" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + function _M.plugin_proxy_rewrite_resp_header() ngx.req.read_body() local s = "plugin_proxy_rewrite_resp_header" diff --git a/t/secret/conf/error.json b/t/secret/conf/error.json new file mode 100644 index 000000000000..3d0bb6295393 --- /dev/null +++ b/t/secret/conf/error.json @@ -0,0 +1,9 @@ +{ + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "http://127.0.0.1:1980/google/logging/token", + "scope": [ + "https://apisix.apache.org/logs:admin" + ], + "entries_uri": "http://127.0.0.1:1980/google/logging/entries" +} diff --git a/t/secret/conf/success.json b/t/secret/conf/success.json new file mode 100644 index 000000000000..d9cfbc3c816e --- /dev/null +++ b/t/secret/conf/success.json @@ -0,0 +1,10 @@ +{ + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "http://127.0.0.1:1980/google/secret/token", + "scope": [ + "https://www.googleapis.com/auth/cloud" + ], + "entries_uri": "http://127.0.0.1:1984", + "client_email": "email@apisix.iam.gserviceaccount.com" +} diff --git a/t/secret/gcp.t b/t/secret/gcp.t new file mode 100644 index 000000000000..b7fc5331cf37 --- /dev/null +++ b/t/secret/gcp.t @@ -0,0 +1,737 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +log_level("info"); + +run_tests; + +__DATA__ + +=== TEST 1: validate different schema situation +--- config + location /t { + content_by_lua_block { + local test_case = { + {}, + {auth_file = "123"}, + {auth_file = 123}, + {auth_config = {client_email = "client", private_key = "private_key"}}, + {auth_config = {private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = "client", project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = 1234, private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = 1234, project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id"}, ssl_verify = 1234}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", token_uri = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", scope = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", entries_uri = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", token_uri = "token_uri", + scope = {"scope"}, entries_uri = "entries_uri"}, ssl_verify = true}, + } + local gcp = require("apisix.secret.gcp") + local core = require("apisix.core") + local metadata_schema = gcp.schema + + for _, conf in ipairs(test_case) do + local ok, err = core.schema.check(metadata_schema, conf) + ngx.say(ok and "done" or err) + end + } + } +--- request +GET /t +--- response_body +value should match only one schema, but matches none +done +property "auth_file" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "project_id" is required +property "auth_config" validation failed: property "client_email" is required +property "auth_config" validation failed: property "private_key" is required +done +property "auth_config" validation failed: property "client_email" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "private_key" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "project_id" validation failed: wrong type: expected string, got number +property "ssl_verify" validation failed: wrong type: expected boolean, got number +property "auth_config" validation failed: property "token_uri" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "scope" validation failed: wrong type: expected array, got number +property "auth_config" validation failed: property "entries_uri" validation failed: wrong type: expected string, got number +done + + + +=== TEST 2: check key: no main key +--- config + location /t { + content_by_lua_block { + local gcp = require("apisix.secret.gcp") + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + }, + } + local data, err = gcp.get(conf, "/apisix") + if err then + return ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +can't find main key, key: /apisix + + + +=== TEST 3: add secret && consumer && check +--- request +GET /t +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_PUT, conf) + + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + -- change consumer with secrets ref: gcp + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "$secret://gcp/mysecret/jack/key" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + + local secret = require("apisix.secret") + local value = secret.fetch_by_uri("$secret://gcp/mysecret/jack/key") + + + local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_DELETE) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "$secret://gcp/mysecret/jack/key" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + local secret = require("apisix.secret") + local value = secret.fetch_by_uri("$secret://gcp/mysecret/jack/key") + if value then + ngx.say("secret value: ", value) + end + ngx.say("all done") + } + } +--- response_body +all done + + + +=== TEST 4: setup route (/projects/apisix/secrets/jack/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_jack() + end" + ] + } + }, + "uri": "/projects/apisix/secrets/jack/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 5: setup route (/projects/apisix_error/secrets/jack/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_error_jack() + end" + ] + } + }, + "uri": "/projects/apisix_error/secrets/jack/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: setup route (/projects/apisix/secrets/mysql/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/3', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_mysql() + end" + ] + } + }, + "uri": "/projects/apisix/secrets/mysql/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 7: get value from gcp by auth_file(fetch_oatuh_conf failed, read failed) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/nofind.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: failed to read configuration, file: t/secret/conf/nofind.json, err: t/secret/conf/nofind.json: No such file or directory + + + +=== TEST 8: get value from gcp by auth_file(fetch_oatuh_conf success) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/success.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 9: get value from gcp by auth_file(fetch_oatuh_conf failed, undefined) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/error.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: config parse failure, file: t/secret/conf/error.json, err: property "auth_config" validation failed: property "client_email" is required + + + +=== TEST 10: get json value from gcp +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 11: get string value from gcp +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "mysql") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +secret + + + +=== TEST 12: get value from gcp(failed to get google oauth token) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/root/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: failed to get google oauth token +--- grep_error_log eval +qr/\{\"error\"\:\"[\w+\s+]*\"\}/ +--- grep_error_log_out +{"error":"no access to this scope"} + + + +=== TEST 13: get value from gcp (not res) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix_error", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say("err") + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +err + + + +=== TEST 14: get value from gcp (res status ~= 200) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix_error", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say("err") + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +err