diff --git a/charts/hub-gateway/Chart.lock b/charts/hub-gateway/Chart.lock index 98f1224..bd03825 100644 --- a/charts/hub-gateway/Chart.lock +++ b/charts/hub-gateway/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: apisix repository: https://charts.apiseven.com - version: 1.1.0 -digest: sha256:0fe161042c77815e8e36847494825ed1232afdccc032635dbaaf347625cb3bfc -generated: "2023-01-30T10:53:37.535884-03:00" + version: 1.3.1 +digest: sha256:f5e4c06ee49ce8bdf2ee3cf997ece55fb80071c8e122fad43197d9be8ddd32f3 +generated: "2023-04-22T00:12:14.986923-03:00" diff --git a/charts/hub-gateway/Chart.yaml b/charts/hub-gateway/Chart.yaml index bf17a80..b49e8a4 100644 --- a/charts/hub-gateway/Chart.yaml +++ b/charts/hub-gateway/Chart.yaml @@ -19,7 +19,7 @@ type: application # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: '0.10.0' +version: "0.11.0" # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -31,6 +31,6 @@ sources: dependencies: - name: apisix - version: 1.1.0 + version: 1.3.1 repository: https://charts.apiseven.com condition: apisix.enabled diff --git a/charts/hub-gateway/charts/apisix-1.1.0.tgz b/charts/hub-gateway/charts/apisix-1.1.0.tgz deleted file mode 100644 index f814214..0000000 Binary files a/charts/hub-gateway/charts/apisix-1.1.0.tgz and /dev/null differ diff --git a/charts/hub-gateway/charts/apisix-1.3.1.tgz b/charts/hub-gateway/charts/apisix-1.3.1.tgz new file mode 100644 index 0000000..c1a9595 Binary files /dev/null and b/charts/hub-gateway/charts/apisix-1.3.1.tgz differ diff --git a/charts/hub-gateway/plugins/authz-helper.lua b/charts/hub-gateway/plugins/authz-helper.lua index 5b81d42..80c0174 100644 --- a/charts/hub-gateway/plugins/authz-helper.lua +++ b/charts/hub-gateway/plugins/authz-helper.lua @@ -16,18 +16,15 @@ local core = require("apisix.core") local get_service = require("apisix.http.service").get -local http = require("resty.http") local json = require("apisix.core.json") local ngx = ngx local ngx_time = ngx.time -local re_match = ngx.re.match -local type = type local _M = {} -- build a table of Nginx variables with some generality -- between http subsystem and stream subsystem -local function build_var(conf, ctx) +local function build_var(ctx) return { server_addr = ctx.var.server_addr, server_port = ctx.var.server_port, @@ -37,7 +34,7 @@ local function build_var(conf, ctx) } end -local function build_http_request(conf, ctx) +local function build_http_request(ctx) return { scheme = core.request.get_scheme(ctx), method = core.request.get_method(), @@ -50,7 +47,7 @@ local function build_http_request(conf, ctx) end -local function build_http_route(conf, ctx, remove_upstream) +local function build_http_route(ctx, remove_upstream) local route = core.table.clone(ctx.matched_route).value if remove_upstream and route and route.upstream then @@ -61,7 +58,7 @@ local function build_http_route(conf, ctx, remove_upstream) end -local function build_http_service(conf, ctx) +local function build_http_service(ctx) local service_id = ctx.service_id -- possible that there is no service bound to the route @@ -81,7 +78,7 @@ local function build_http_service(conf, ctx) return nil end -local function build_graphql_data(conf, ctx) +local function build_graphql_data(ctx) local input = json.decode(core.request.get_body()) return { query = input['query'], @@ -91,7 +88,7 @@ local function build_graphql_data(conf, ctx) } end -local function build_http_consumer(conf, ctx) +local function build_http_consumer(ctx) -- possible that there is no consumer bound to the route if ctx.consumer then return core.table.clone(ctx.consumer) @@ -103,24 +100,24 @@ end function _M.build_opa_input(conf, ctx, subsystem) local data = { type = subsystem, - request = build_http_request(conf, ctx), - var = build_var(conf, ctx), - graphql = build_graphql_data(conf, ctx), + request = build_http_request(ctx), + var = build_var(ctx), + graphql = build_graphql_data(ctx), keto_endpoint = conf.keto_endpoint } if conf.with_route then - data.route = build_http_route(conf, ctx, true) + data.route = build_http_route(ctx, true) end - + if conf.with_consumer then - data.consumer = build_http_consumer(conf, ctx) + data.consumer = build_http_consumer(ctx) end if conf.with_service then - data.service = build_http_service(conf, ctx) + data.service = build_http_service(ctx) end - + return { input = data, } diff --git a/charts/hub-gateway/plugins/authz.lua b/charts/hub-gateway/plugins/authz.lua index c380a19..e7ad6b4 100644 --- a/charts/hub-gateway/plugins/authz.lua +++ b/charts/hub-gateway/plugins/authz.lua @@ -90,16 +90,16 @@ function _M.access(conf, ctx) local res, err = httpc:request_uri(endpoint, params) -- block by default when decision is unavailable - if not res then + if err then return 403, json.encode({ message = "OPA Decision is unavailable. Errors found in policy" }) end -- parse the results of the decision - local data, err = json.decode(res.body) + local data, err_json = json.decode(res.body) - if not data then + if err_json then return 503, json.encode({ message = "Invalid response body" }) diff --git a/charts/hub-gateway/plugins/credits.lua b/charts/hub-gateway/plugins/credits.lua new file mode 100644 index 0000000..7d889be --- /dev/null +++ b/charts/hub-gateway/plugins/credits.lua @@ -0,0 +1,125 @@ +-- +-- 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 http = require("resty.http") +local json = require("apisix.core.json") + +local schema = { + type = "object", + properties = { + host = { + type = "string" + }, + ssl_verify = { + type = "boolean", + default = true + }, + timeout = { + type = "integer", + minimum = 1, + maximum = 60000, + default = 3000, + description = "timeout in milliseconds" + }, + keepalive = { + type = "boolean", + default = true + }, + keepalive_timeout = { + type = "integer", + minimum = 1000, + default = 60000 + }, + keepalive_pool = { + type = "integer", + minimum = 1, + default = 5 + }, + }, + required = {"host"} +} + +local _M = { + version = 0.1, + priority = 3, + name = "credits", + schema = schema +} + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + +function _M.access(conf, ctx) + + -- return early if operation is query + local operation = ctx.var.graphql_operation + if not operation or operation == "query" then + return + end + + local input = json.decode(core.request.get_body()) + local graphql_query = input['query'] + + -- Check if query is createOrganization + if graphql_query:match("createOrganization") then + core.log.info("Skipping credits plugin - Creating Organization") + return + end + + local org_id = core.request.header(ctx, "X-Organization-Id") + + local params = { + method = "GET", + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + end + + local endpoint = conf.host .. "/internal/organizations/" .. org_id + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + local res, err = httpc:request_uri(endpoint, params) + + -- return internal server error if unable to contact credits service + if err then + return 500, json.encode({ message = err }) + end + + local data, err_json = json.decode(res.body) + + if err_json then + return 500, json.encode({ message = err }) + end + + if not data.balance then + return 500, json.encode({ message = err }) + end + + -- respond the credit balance to the user too + if conf.expose_credit_balance then + core.request.set_header(ctx, "X-Credit-Balance", data.balance) + core.response.set_header(ctx, "X-Credit-Balance", data.balance) + end +end + +return _M diff --git a/charts/hub-gateway/plugins/oauth2.lua b/charts/hub-gateway/plugins/oauth2.lua index 1091b15..91b93a2 100644 --- a/charts/hub-gateway/plugins/oauth2.lua +++ b/charts/hub-gateway/plugins/oauth2.lua @@ -63,7 +63,7 @@ local schema = { local _M = { version = 0.1, - priority = 3, + priority = 6, name = "oauth2", schema = schema } @@ -103,15 +103,16 @@ function _M.access(conf, ctx) local res, err = httpc:request_uri(endpoint, params) -- block by default when introspection failed - if not res then + if err then return 401, json.encode({ message = err }) end -- parse the introspection data - local data, err = json.decode(res.body) - if not data then + local data, err_json = json.decode(res.body) + + if err_json then return 401, err end @@ -123,10 +124,10 @@ function _M.access(conf, ctx) end if conf.expose_client_id then - core.request.set_header(ctx, "X-CLIENT-ID", data.client_id) - core.response.set_header("X-CLIENT-ID", data.client_id) + core.request.set_header(ctx, "X-Client-Id", data.client_id) + core.response.set_header("X-Client-Id", data.client_id) end - + -- Lookup full hydra client data local params = { method = "GET", @@ -146,21 +147,26 @@ function _M.access(conf, ctx) local res, err = httpc:request_uri(endpoint, params) + -- block by default when unable to get client data + if err then + return 401, json.encode({message = err}) + end + local data, err = json.decode(res.body) - if not data then + if err then return 401, err - end + end -- Expose hydra client owner id on request header if conf.expose_owner then if not data.owner then core.log.error("unable to get owner from response:", json.encode(data)) end - core.request.set_header(ctx, "X-CLIENT-OWNER-ID", data.owner) + core.request.set_header(ctx, "X-Client-Owner-Id", data.owner) -- Get kratos user id from hydra client contacts and expose on x-user-id header - core.request.set_header(ctx, "X-USER-ID", data.contacts[1]) - core.response.set_header("X-USER-ID", data.contacts[1]) + core.request.set_header(ctx, "X-User-Id", data.contacts[1]) + core.response.set_header("X-User-Id", data.contacts[1]) end end diff --git a/charts/hub-gateway/plugins/org-lookup.lua b/charts/hub-gateway/plugins/org-lookup.lua new file mode 100644 index 0000000..31bec1e --- /dev/null +++ b/charts/hub-gateway/plugins/org-lookup.lua @@ -0,0 +1,67 @@ +-- +-- 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 schema = { + type = "object", + properties = { + query_cookie_name = { + type = "string", + default = "_hub_org" + }, + query_header_name = { + type = "string", + default = "X-Client-Owner-Id" + }, + output_header_name = { + type = "string", + default = "X-Organization-Id" + }, + }, +} + +local _M = { + version = 0.1, + priority = 4, + name = "org-lookup", + schema = schema +} + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + +function _M.access(conf, ctx) + local cookie_name = conf.query_cookie_name + local cookie_header = "cookie_" .. cookie_name + + -- Get org id from cookie or header + local cookie_value = ngx.var[cookie_header] + local client_owner = core.request.header(ctx, conf.query_header_name) + + local org_id = cookie_value or client_owner + + -- Return if no org id is found + if not org_id then + return + end + + -- Move value to header in conf.output_header_name + core.request.set_header(ctx, conf.output_header_name, org_id) +end + +return _M diff --git a/charts/hub-gateway/plugins/session-json.lua b/charts/hub-gateway/plugins/session-json.lua index 85ef177..f2f3d5b 100644 --- a/charts/hub-gateway/plugins/session-json.lua +++ b/charts/hub-gateway/plugins/session-json.lua @@ -15,8 +15,6 @@ -- limitations under the License. -- local core = require("apisix.core") -local http = require("resty.http") -local json = require("apisix.core.json") local schema = { type = "object", @@ -34,9 +32,8 @@ function _M.check_schema(conf) return core.schema.check(schema, conf) end -function _M.access(conf, ctx) - local user_id = core.request.header(ctx, "X-USER-ID") - local uri = ctx.var.uri +function _M.access(ctx) + local user_id = core.request.header(ctx, "X-User-Id") if not user_id then core.response.set_header("Content-Type", "application/json") diff --git a/charts/hub-gateway/plugins/session-redirect.lua b/charts/hub-gateway/plugins/session-redirect.lua index 99c1dde..0a010ba 100644 --- a/charts/hub-gateway/plugins/session-redirect.lua +++ b/charts/hub-gateway/plugins/session-redirect.lua @@ -15,8 +15,6 @@ -- limitations under the License. -- local core = require("apisix.core") -local http = require("resty.http") -local json = require("apisix.core.json") local schema = { type = "object", @@ -45,7 +43,7 @@ end function _M.access(conf, ctx) local redirect_to = conf.redirect_to - local user_id = core.request.header(ctx, "X-USER-ID") + local user_id = core.request.header(ctx, "X-User-Id") local uri = ctx.var.uri local redirect_uri = conf.login_uri diff --git a/charts/hub-gateway/plugins/session.lua b/charts/hub-gateway/plugins/session.lua index eb1d5e8..3f464ef 100644 --- a/charts/hub-gateway/plugins/session.lua +++ b/charts/hub-gateway/plugins/session.lua @@ -62,7 +62,7 @@ local schema = { local _M = { version = 0.1, - priority = 2, + priority = 5, name = "session", schema = schema } @@ -81,7 +81,7 @@ function _M.access(conf, ctx) local session_token = cookie_value if not session_token then - return + return end local kratos_cookie = session_cookie_name .. "=" .. session_token @@ -114,8 +114,8 @@ function _M.access(conf, ctx) end -- parse the user data - local data, err = json.decode(res.body) - if not data then + local data, err_json = json.decode(res.body) + if err_json then return 401, json.encode({ message = err }) @@ -131,10 +131,9 @@ function _M.access(conf, ctx) -- Expose user email and id on headers if conf.expose_user_id then - core.request.set_header(ctx, "X-USER-ID", data.identity.id) - core.response.set_header("X-USER-ID", data.identity.id) - core.request.set_header(ctx, "X-USER-EMAIL", data.identity.traits.email) - core.response.set_header("X-USER-EMAIL", data.identity.traits.email) + core.request.set_header(ctx, "X-User-Id", data.identity.id) + core.response.set_header("X-User-Id", data.identity.id) + core.request.set_header(ctx, "X-User-Email", data.identity.traits.email) end end diff --git a/charts/hub-gateway/templates/_helpers.tpl b/charts/hub-gateway/templates/_helpers.tpl index 2c2266f..49ee91b 100644 --- a/charts/hub-gateway/templates/_helpers.tpl +++ b/charts/hub-gateway/templates/_helpers.tpl @@ -46,7 +46,7 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} Selector labels */}} {{- define "hub-gateway.selectorLabels" -}} -app.kubernetes.io/name: {{ include "hub-gateway.name" . }} +app.kubernetes.io/name: apisix app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} diff --git a/charts/hub-gateway/templates/apisixroute.yaml b/charts/hub-gateway/templates/apisixroute.yaml index ae0ec3b..572faee 100644 --- a/charts/hub-gateway/templates/apisixroute.yaml +++ b/charts/hub-gateway/templates/apisixroute.yaml @@ -2,6 +2,7 @@ {{- $namespace := .Values.hubNamespace -}} {{- $domain := .Values.domain -}} {{- $sessionCookie := .Values.sessionCookieName -}} +{{- $orgCookie := .Values.orgCookieName -}} {{- $loginUri := .Values.loginUri -}} {{- with .Values.routes }} {{- range . }} @@ -27,6 +28,8 @@ spec: methods: {{- .methods | toYaml | nindent 8 }} plugins: + - name: request-id + enable: true {{- with .session }} {{- if .enabled | default false }} - name: session @@ -69,6 +72,24 @@ spec: with_consumer: false {{- end }} {{- end }} + {{- with .orgLookup }} + {{- if .enabled }} + - name: org-lookup + enable: true + config: + from_cookie_name: {{ $apisixPlugins.orgLookup.query.cookieName }} + from_header_name: {{ $apisixPlugins.orgLookup.query.headerName }} + to_header_name: {{ $apisixPlugins.orgLookup.output.headerName }} + {{- end }} + {{- end }} + {{- with .credits }} + {{- if .enabled }} + - name: credits + enable: true + config: + host: {{ print "http://" $apisixPlugins.credits.serviceName "." $namespace ".svc:" $apisixPlugins.credits.servicePort | quote }} + {{- end }} + {{- end }} {{- if .regexUri }} - name: proxy-rewrite enable: true diff --git a/charts/hub-gateway/templates/svc.yaml b/charts/hub-gateway/templates/svc.yaml index 921ca54..bc2ccf4 100644 --- a/charts/hub-gateway/templates/svc.yaml +++ b/charts/hub-gateway/templates/svc.yaml @@ -12,5 +12,4 @@ spec: protocol: TCP name: http selector: - app.kubernetes.io/instance: hub-gateway - app.kubernetes.io/name: apisix + {{- include "hub-gateway.selectorLabels" $ | nindent 4 }} diff --git a/charts/hub-gateway/values.yaml b/charts/hub-gateway/values.yaml index 57d5c89..b01687e 100644 --- a/charts/hub-gateway/values.yaml +++ b/charts/hub-gateway/values.yaml @@ -19,9 +19,13 @@ routes: authz: enabled: true policy: 'hub/graphql/main' + orgLookup: + enabled: true + credits: + enabled: false regexUri: - - '/graphql' - - '/' + - "/graphql" + - "/" - name: api-internal serviceName: federated-router @@ -37,9 +41,13 @@ routes: authz: enabled: true policy: 'hub/graphql/main' + orgLookup: + enabled: true + credits: + enabled: false regexUri: - - '/graphql' - - '/' + - "/graphql" + - "/" - name: hub-browser-graphql serviceName: federated-router @@ -57,9 +65,13 @@ routes: authz: enabled: true policy: 'hub/graphql/main' + orgLookup: + enabled: true + credits: + enabled: false regexUri: - - '/graphql' - - '/' + - "/graphql" + - "/" - name: hub-browser-api serviceName: hub-orgs @@ -173,6 +185,20 @@ apisixPlugins: files: - plugins/authz.lua - plugins/authz-helper.lua + credits: + serviceName: hub-credits + servicePort: 80 + files: + - plugins/credits.lua + orgLookup: + query: + cookieName: "_hub_org" + headerName: "X-Client-Owner-Id" + output: + headerName: "X-Organization-Id" + files: + - plugins/org-lookup.lua + apisix: enabled: true fullnameOverride: apisix @@ -232,6 +258,7 @@ apisix: plugins: - mocking - cors + - request-id - redirect - serverless-pre-function - serverless-post-function @@ -269,10 +296,22 @@ apisix: configMap: name: 'authz-plugin' mounts: - - key: 'authz.lua' - path: '/opts/custom_plugins/apisix/plugins/authz.lua' - - key: 'authz-helper.lua' - path: '/opts/custom_plugins/apisix/plugins/authz/helper.lua' + - key: "authz.lua" + path: "/opts/custom_plugins/apisix/plugins/authz.lua" + - key: "authz-helper.lua" + path: "/opts/custom_plugins/apisix/plugins/authz/helper.lua" + - name: "credits" + configMap: + name: "credits-plugin" + mounts: + - key: "credits.lua" + path: "/opts/custom_plugins/apisix/plugins/credits.lua" + - name: "org-lookup" + configMap: + name: "org-lookup-plugin" + mounts: + - key: "org-lookup.lua" + path: "/opts/custom_plugins/apisix/plugins/org-lookup.lua" logs: enableAccessLog: true