Create v2 management API
diff --git a/conf.d/management_apis.conf b/conf.d/management_apis.conf
index 14b1a4e..6f1e953 100644
--- a/conf.d/management_apis.conf
+++ b/conf.d/management_apis.conf
@@ -33,23 +33,30 @@
access_log /var/log/api-gateway/access.log main;
error_log /var/log/api-gateway/management.log debug;
- location ~ /v1/apis/?(?<api_id>[^/]*)?/?(?<query>[^/]*)? {
+ location ~ /(?<version>v2)/(?<tenantId>[^/]*)/apis/?(?<api_id>[^/]*)?/?(?<query>[^/]*)? {
access_by_lua_block {
- local apis = require("management/apis")
+ local apis = require("management/routes/apis")
+ apis.requestHandler()
+ }
+ }
+
+ location ~ /(?<version>v1)/apis/?(?<api_id>[^/]*)?/?(?<query>[^/]*)? {
+ access_by_lua_block {
+ local apis = require("management/routes/apis")
apis.requestHandler()
}
}
location ~ /v1/tenants/?(?<tenant_id>[^/]*)?/?(?<query>[^/]*)? {
access_by_lua_block {
- local tenants = require("management/tenants")
+ local tenants = require("management/routes/tenants")
tenants.requestHandler()
}
}
location /v1/subscriptions {
access_by_lua_block {
- local subscriptions = require("management/subscriptions")
+ local subscriptions = require("management/routes/subscriptions")
subscriptions.requestHandler()
}
}
diff --git a/scripts/lua/lib/redis.lua b/scripts/lua/lib/redis.lua
index 826f19e..36fa4f7 100644
--- a/scripts/lua/lib/redis.lua
+++ b/scripts/lua/lib/redis.lua
@@ -116,7 +116,7 @@
if not ok then
request.err(500, utils.concatStrings({"Failed to save the API: ", err}))
end
- return apiObj
+ return cjson.decode(apiObj)
end
--- Get all APIs from redis
@@ -371,4 +371,39 @@
request.success(200, "Status: Gateway ready.")
end
+-----------------------------
+-------- v2 Swagger ---------
+-----------------------------
+
+function _M.addSwagger(red, id, swagger)
+ swagger = cjson.encode(swagger)
+ local ok, err = red:hset("swagger", id, swagger)
+ if not ok then
+ request.err(500, utils.concatStrings({"Failed to add swagger: ", err}))
+ end
+ return cjson.decode(swagger)
+end
+
+function _M.getSwagger(red, id)
+ local swagger, err = red:hget("swagger", id)
+ if not swagger then
+ request.err(500, utils.concatStrings({"Failed to add swagger: ", err}))
+ end
+ if swagger == ngx.null then
+ return nil
+ end
+ return cjson.decode(swagger)
+end
+
+function _M.deleteSwagger(red, id)
+ local existing = _M.getSwagger(red, id)
+ if existing == nil then
+ request.err(404, 'Swagger doesn\'t exist')
+ end
+ local ok, err = red:hdel("swagger", id)
+ if not ok then
+ request.err(500, utils.concatStrings({"Failed to delete swagger: ", err}))
+ end
+end
+
return _M
diff --git a/scripts/lua/lib/request.lua b/scripts/lua/lib/request.lua
index 9a61a3e..f28e708 100644
--- a/scripts/lua/lib/request.lua
+++ b/scripts/lua/lib/request.lua
@@ -23,6 +23,7 @@
-- @author Alex Song (songs)
local utils = require "lib/utils"
+local cjson = require "cjson"
local _Request = {}
@@ -30,17 +31,24 @@
-- @param code error code
-- @param msg error message
function err(code, msg)
+ ngx.header.content_type = "application/json; charset=utf-8"
ngx.status = code
- ngx.say(utils.concatStrings({"Error: ", msg}))
+ local errObj = cjson.encode({
+ status = code,
+ message = utils.concatStrings({"Error: ", msg})
+ })
+ ngx.say(errObj)
ngx.exit(ngx.status)
end
--- Function to call when request is successful
-- @param code status code
--- @param obj object to return
+-- @param obj JSON encoded object to return
function success(code, obj)
ngx.status = code
- ngx.say(obj)
+ if obj ~= nil then
+ ngx.say(obj)
+ end
ngx.exit(ngx.status)
end
diff --git a/scripts/lua/management/apis.lua b/scripts/lua/management/apis.lua
deleted file mode 100644
index 53a0806..0000000
--- a/scripts/lua/management/apis.lua
+++ /dev/null
@@ -1,383 +0,0 @@
--- Copyright (c) 2016 IBM. All rights reserved.
---
--- Permission is hereby granted, free of charge, to any person obtaining a
--- copy of this software and associated documentation files (the "Software"),
--- to deal in the Software without restriction, including without limitation
--- the rights to use, copy, modify, merge, publish, distribute, sublicense,
--- and/or sell copies of the Software, and to permit persons to whom the
--- Software is furnished to do so, subject to the following conditions:
---
--- The above copyright notice and this permission notice shall be included in
--- all copies or substantial portions of the Software.
---
--- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
--- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
--- DEALINGS IN THE SOFTWARE.
-
---- @module apis
--- Management interface for apis for the gateway
-
-local cjson = require "cjson"
-local redis = require "lib/redis"
-local utils = require "lib/utils"
-local request = require "lib/request"
-local resources = require "management/resources"
-
-local MANAGEDURL_HOST = os.getenv("PUBLIC_MANAGEDURL_HOST")
-MANAGEDURL_HOST = (MANAGEDURL_HOST ~= nil and MANAGEDURL_HOST ~= '') and MANAGEDURL_HOST or "0.0.0.0"
-local MANAGEDURL_PORT = os.getenv("PUBLIC_MANAGEDURL_PORT")
-MANAGEDURL_PORT = (MANAGEDURL_PORT ~= nil and MANAGEDURL_PORT ~= '') and MANAGEDURL_PORT or "8080"
-local REDIS_HOST = os.getenv("REDIS_HOST")
-local REDIS_PORT = os.getenv("REDIS_PORT")
-local REDIS_PASS = os.getenv("REDIS_PASS")
-
-local _M = {}
-
---- Request handler for routing API calls appropriately
-function _M.requestHandler()
- local requestMethod = ngx.req.get_method()
- if requestMethod == "GET" then
- getAPIs()
- elseif requestMethod == "PUT" then
- addAPI()
- elseif requestMethod == "POST" then
- addAPI()
- elseif requestMethod == "DELETE" then
- deleteAPI()
- else
- ngx.status = 400
- ngx.say("Invalid verb")
- end
-end
-
---- Add an api to the Gateway
--- PUT /v1/apis
--- body:
--- {
--- "name": *(String) name of API
--- "basePath": *(String) base path for api
--- "tenantId": *(String) tenant id
--- "resources": *(String) resources to add
--- }
-function addAPI()
- -- Open connection to redis or use one from connection pool
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- -- Check for api id from uri and use existingAPI if it already exists in redis
- local existingAPI = checkForExistingAPI(red)
- -- Read in the PUT JSON Body
- ngx.req.read_body()
- local args = ngx.req.get_body_data()
- if not args then
- request.err(400, "Missing request body")
- end
- -- Convert json into Lua table
- local decoded = cjson.decode(args)
- -- Check for api id in JSON body
- if existingAPI == nil and decoded.id ~= nil then
- existingAPI = redis.getAPI(red, decoded.id)
- if existingAPI == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", decoded.id}))
- end
- end
- -- Error checking
- local fields = {"name", "basePath", "tenantId", "resources"}
- for k, v in pairs(fields) do
- local res, err = isValid(red, v, decoded[v])
- if res == false then
- request.err(err.statusCode, err.message)
- end
- end
- -- Format basePath
- local basePath = decoded.basePath:sub(1,1) == '/' and decoded.basePath:sub(2) or decoded.basePath
- basePath = basePath:sub(-1) == '/' and basePath:sub(1, -2) or basePath
- -- Create managedUrl object
- local uuid = existingAPI ~= nil and existingAPI.id or utils.uuid()
- local managedUrl = utils.concatStrings({"http://", MANAGEDURL_HOST, ":", MANAGEDURL_PORT, "/api/", decoded.tenantId})
- if basePath:sub(1,1) ~= '' then
- managedUrl = utils.concatStrings({managedUrl, "/", basePath})
- end
- local tenantObj = redis.getTenant(red, decoded.tenantId)
- local managedUrlObj = {
- id = uuid,
- name = decoded.name,
- basePath = utils.concatStrings({"/", basePath}),
- tenantId = decoded.tenantId,
- tenantNamespace = tenantObj.namespace,
- tenantInstance = tenantObj.instance,
- cors = decoded.cors,
- resources = decoded.resources,
- managedUrl = managedUrl
- }
- -- Add API object to redis
- managedUrlObj = redis.addAPI(red, uuid, managedUrlObj, existingAPI)
- -- Add resources to redis
- for path, resource in pairs(decoded.resources) do
- local gatewayPath = utils.concatStrings({basePath, path})
- gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
- resources.addResource(red, resource, gatewayPath, tenantObj)
- end
- redis.close(red)
- -- Return managed url object
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, managedUrlObj)
-end
-
---- Check for api id from uri and use existing API if it already exists in redis
--- @param red Redis client instance
-function checkForExistingAPI(red)
- local id = ngx.var.api_id
- local existing
- -- Get object from redis
- if id ~= nil and id ~= '' then
- existing = redis.getAPI(red, id)
- if existing == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", id}))
- end
- end
- return existing
-end
-
---- Check JSON body fields for errors
--- @param red Redis client instance
--- @param field name of field
--- @param object field object
-function isValid(red, field, object)
- -- Check that field exists in body
- if not object then
- return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", field, "' in request body."}) }
- end
- -- Additional check for basePath
- if field == "basePath" then
- local basePath = object
- if string.match(basePath, "'") then
- return false, { statusCode = 400, message = "basePath contains illegal character \"'\"." }
- end
- end
- -- Additional check for tenantId
- if field == "tenantId" then
- local tenant = redis.getTenant(red, object)
- if tenant == nil then
- return false, { statusCode = 404, message = utils.concatStrings({"Unknown tenant id ", object }) }
- end
- end
- if field == "resources" then
- local res, err = checkResources(object)
- if res ~= nil and res == false then
- return res, err
- end
- end
- -- All error checks passed
- return true
-end
-
---- Error checking for resources
--- @param resources resources object
-function checkResources(resources)
- if next(resources) == nil then
- return false, { statusCode = 400, message = "Empty resources object." }
- end
- for path, resource in pairs(resources) do
- -- Check resource path for illegal characters
- if string.match(path, "'") then
- return false, { statusCode = 400, message = "resource path contains illegal character \"'\"." }
- end
- -- Check that resource path begins with slash
- if path:sub(1,1) ~= '/' then
- return false, { statusCode = 400, message = "Resource path must begin with '/'." }
- end
- -- Check operations object
- local res, err = checkOperations(resource.operations)
- if res ~= nil and res == false then
- return res, err
- end
- end
-end
-
---- Error checking for operations
--- @param operations operations object
-function checkOperations(operations)
- if not operations or next(operations) == nil then
- return false, { statusCode = 400, message = "Missing or empty field 'operations' or in resource path object." }
- end
- local allowedVerbs = {GET=true, POST=true, PUT=true, DELETE=true, PATCH=true, HEAD=true, OPTIONS=true}
- for verb, verbObj in pairs(operations) do
- if allowedVerbs[verb:upper()] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"Resource verb '", verb, "' not supported."}) }
- end
- -- Check required fields
- local requiredFields = {"backendMethod", "backendUrl"}
- for k, v in pairs(requiredFields) do
- if verbObj[v] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", v, "' for '", verb, "' operation."}) }
- end
- if v == "backendMethod" then
- local backendMethod = verbObj[v]
- if allowedVerbs[backendMethod:upper()] == nil then
- return false, { statusCode = 400, message = utils.concatStrings({"backendMethod '", backendMethod, "' not supported."}) }
- end
- end
- end
- -- Check optional fields
- local res, err = checkOptionalPolicies(verbObj.policies, verbObj.security)
- if res ~= nil and res == false then
- return res, err
- end
- end
-end
-
---- Error checking for policies and security
--- @param policies policies object
--- @param security security object
-function checkOptionalPolicies(policies, security)
- if policies then
- for _, v in pairs(policies) do
- local validTypes = {"reqMapping", "rateLimit", "backendRouting"}
- if (v.type == nil or v.value == nil) then
- return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"value\"." }
- elseif utils.tableContains(validTypes, v.type) == false then
- return false, { statusCode = 400, message = "Invalid type in policy object. Valid types: " .. cjson.encode(validTypes) }
- end
- end
- end
- if security then
- for _, sec in ipairs(security) do
- local validScopes = {"tenant", "api", "resource"}
- if (sec.type == nil or sec.scope == nil) then
- return false, { statusCode = 400, message = "Missing field in security object. Need \"type\" and \"scope\"." }
- elseif utils.tableContains(validScopes, sec.scope) == false == nil then
- return false, { statusCode = 400, message = "Invalid scope in security object. Valid scopes:" .. cjson.encode(validScopes) }
- end
- end
- end
-end
-
---- Get one or all APIs from the gateway
--- GET /v1/apis
-function getAPIs()
- local queryParams = ngx.req.get_uri_args()
- local id = ngx.var.api_id
- if id == nil or id == '' then
- getAllAPIs(queryParams)
- else
- local query = ngx.var.query
- if (query ~= nil and query ~= '') then
- if query ~= "tenant" then
- request.err(400, "Invalid request")
- else
- getAPITenant(id)
- end
- else
- getAPI(id)
- end
- end
-end
-
---- Get all APIs in redis
--- @param queryParams object containing optional query parameters
-function getAllAPIs(queryParams)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local apis = redis.getAllAPIs(red)
- redis.close(red)
- local apiList
- if next(queryParams) ~= nil then
- apiList = filterAPIs(apis, queryParams);
- end
- if apiList == nil then
- apiList = {}
- for k, v in pairs(apis) do
- if k%2 == 0 then
- apiList[#apiList+1] = cjson.decode(v)
- end
- end
- end
- ngx.header.content_type = "application/json; charset=utf-8"
- apiList = (next(apiList) == nil) and "[]" or cjson.encode(apiList)
- request.success(200, apiList)
-end
-
---- Filter APIs based on query parameters
--- @param apis list of apis
--- @param queryParams query parameters to filter tenants
-function filterAPIs(apis, queryParams)
- local basePath = queryParams['filter[where][basePath]']
- local name = queryParams['filter[where][name]']
- -- missing or invalid query parameters
- if (basePath == nil and name == nil) then
- return nil
- end
- -- filter tenants
- local apiList = {}
- for k, v in pairs(apis) do
- if k%2 == 0 then
- local api = cjson.decode(v)
- if (basePath ~= nil and name == nil and api.basePath == basePath) or
- (name ~= nil and basePath == nil and api.name == name) or
- (basePath ~= nil and name ~= nil and api.basePath == basePath and api.name == name) then
- apiList[#apiList+1] = api
- end
- end
- end
- return apiList
-end
-
---- Get API by its id
--- @param id of API
-function getAPI(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown api id ", id}))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(api))
-end
-
---- Get belongsTo relation tenant
--- @param id id of API
-function getAPITenant(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown api id ", id}))
- end
- local tenantId = api.tenantId
- local tenant = redis.getTenant(red, tenantId)
- if tenant == nil then
- request.err(404, utils.concatStrings({"Unknown tenant id ", tenantId}))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(tenant))
-end
-
---- Delete API from gateway
--- DELETE /v1/apis/<id>
-function deleteAPI()
- local id = ngx.var.api_id
- if id == nil or id == '' then
- request.err(400, "No id specified.")
- end
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local api = redis.getAPI(red, id)
- if api == nil then
- request.err(404, utils.concatStrings({"Unknown API id ", id}))
- end
- -- Delete API
- redis.deleteAPI(red, id)
- -- Delete all resources for the API
- local basePath = api.basePath:sub(2)
- for path, v in pairs(api.resources) do
- local gatewayPath = utils.concatStrings({basePath, path})
- gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
- resources.deleteResource(red, gatewayPath, api.tenantId)
- end
- redis.close(red)
- request.success(200, {})
-end
-
-return _M;
diff --git a/scripts/lua/management/lib/apis.lua b/scripts/lua/management/lib/apis.lua
new file mode 100644
index 0000000..d09fef4
--- /dev/null
+++ b/scripts/lua/management/lib/apis.lua
@@ -0,0 +1,169 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module apis
+-- Module for querying APIs
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local resources = require "management/lib/resources"
+
+local MANAGEDURL_HOST = os.getenv("PUBLIC_MANAGEDURL_HOST")
+MANAGEDURL_HOST = (MANAGEDURL_HOST ~= nil and MANAGEDURL_HOST ~= '') and MANAGEDURL_HOST or "0.0.0.0"
+local MANAGEDURL_PORT = os.getenv("PUBLIC_MANAGEDURL_PORT")
+MANAGEDURL_PORT = (MANAGEDURL_PORT ~= nil and MANAGEDURL_PORT ~= '') and MANAGEDURL_PORT or "8080"
+
+local _M = {}
+
+--- Get all APIs in redis
+-- @param red redis client
+-- @param queryParams object containing optional query parameters
+function _M.getAllAPIs(red, queryParams)
+ local apis = redis.getAllAPIs(red)
+ local apiList
+ if next(queryParams) ~= nil then
+ apiList = filterAPIs(apis, queryParams);
+ end
+ if apiList == nil then
+ apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ apiList[#apiList+1] = cjson.decode(v)
+ end
+ end
+ end
+ return apiList
+end
+
+--- Get API by its id
+-- @param red redis client
+-- @param id of API
+function _M.getAPI(red, id)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown api id ", id}))
+ end
+ return api
+end
+
+--- Get belongsTo relation tenant
+-- @param red redis client
+-- @param id id of API
+function _M.getAPITenant(red, id)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown api id ", id}))
+ end
+ local tenantId = api.tenantId
+ local tenant = redis.getTenant(red, tenantId)
+ if tenant == nil then
+ request.err(404, utils.concatStrings({"Unknown tenant id ", tenantId}))
+ end
+ return tenant
+end
+
+--- Add API to redis
+-- @param red redis client
+-- @param decoded JSON body as a lua table
+-- @param existingAPI optional existing API for updates
+function _M.addAPI(red, decoded, existingAPI)
+ -- Format basePath
+ local basePath = decoded.basePath:sub(1,1) == '/' and decoded.basePath:sub(2) or decoded.basePath
+ basePath = basePath:sub(-1) == '/' and basePath:sub(1, -2) or basePath
+ -- Create managedUrl object
+ local uuid = existingAPI ~= nil and existingAPI.id or utils.uuid()
+ local managedUrl = utils.concatStrings({"http://", MANAGEDURL_HOST, ":", MANAGEDURL_PORT, "/api/", decoded.tenantId})
+ if basePath:sub(1,1) ~= '' then
+ managedUrl = utils.concatStrings({managedUrl, "/", basePath})
+ end
+ local tenantObj = redis.getTenant(red, decoded.tenantId)
+ local managedUrlObj = {
+ id = uuid,
+ name = decoded.name,
+ basePath = utils.concatStrings({"/", basePath}),
+ tenantId = decoded.tenantId,
+ tenantNamespace = tenantObj.namespace,
+ tenantInstance = tenantObj.instance,
+ cors = decoded.cors,
+ resources = decoded.resources,
+ managedUrl = managedUrl
+ }
+ -- Add API object to redis
+ managedUrlObj = redis.addAPI(red, uuid, managedUrlObj, existingAPI)
+ -- Add resources to redis
+ for path, resource in pairs(decoded.resources) do
+ local gatewayPath = utils.concatStrings({basePath, path})
+ gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
+ resource.apiId = uuid
+ resources.addResource(red, resource, gatewayPath, tenantObj)
+ end
+ return managedUrlObj
+end
+
+--- Delete API from gateway
+-- @param red redis client
+-- @param id id of API to delete
+function _M.deleteAPI(red, id)
+ local api = redis.getAPI(red, id)
+ if api == nil then
+ request.err(404, utils.concatStrings({"Unknown API id ", id}))
+ end
+ -- Delete API
+ redis.deleteAPI(red, id)
+ -- Delete all resources for the API
+ local basePath = api.basePath:sub(2)
+ for path in pairs(api.resources) do
+ local gatewayPath = utils.concatStrings({basePath, path})
+ gatewayPath = (gatewayPath:sub(1,1) == '/') and gatewayPath:sub(2) or gatewayPath
+ resources.deleteResource(red, gatewayPath, api.tenantId)
+ end
+ return {}
+end
+
+--- Filter APIs based on query parameters
+-- @param apis list of apis
+-- @param queryParams query parameters to filter tenants
+function filterAPIs(apis, queryParams)
+ local basePath = queryParams['filter[where][basePath]']
+ basePath = basePath == nil and queryParams['basePath'] or basePath
+ local name = queryParams['filter[where][name]']
+ name = name == nil and queryParams['title'] or name
+ -- missing or invalid query parameters
+ if (basePath == nil and name == nil) then
+ return nil
+ end
+ -- filter tenants
+ local apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ local api = cjson.decode(v)
+ if (basePath ~= nil and name == nil and api.basePath == basePath) or
+ (name ~= nil and basePath == nil and api.name == name) or
+ (basePath ~= nil and name ~= nil and api.basePath == basePath and api.name == name) then
+ apiList[#apiList+1] = api
+ end
+ end
+ end
+ return apiList
+end
+
+return _M
\ No newline at end of file
diff --git a/scripts/lua/management/resources.lua b/scripts/lua/management/lib/resources.lua
similarity index 100%
rename from scripts/lua/management/resources.lua
rename to scripts/lua/management/lib/resources.lua
diff --git a/scripts/lua/management/lib/swagger.lua b/scripts/lua/management/lib/swagger.lua
new file mode 100644
index 0000000..90f24ed
--- /dev/null
+++ b/scripts/lua/management/lib/swagger.lua
@@ -0,0 +1,187 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module swagger
+-- Module for parsing swagger file
+
+local _M = {}
+
+-- Convert passed-in swagger body to valid lua table
+-- @param swagger swagger file to parse
+function _M.parseSwagger(swagger)
+ local backends = parseBackends(swagger)
+ local policies = parsePolicies(swagger)
+ local security = parseSecurity(swagger)
+ local decoded = {
+ name = swagger.info.title,
+ basePath = swagger.basePath,
+ resources = {}
+ }
+ for path, verbObj in pairs(swagger.paths) do
+ decoded.resources[path] = { operations = {} }
+ for verb, value in pairs(verbObj) do
+ decoded.resources[path].operations[verb] = {}
+ local verbObj = decoded.resources[path].operations[verb]
+ local backend = (backends["all"] ~= nil) and backends["all"] or backends[value.operationId]
+ verbObj.backendUrl = backend.backendUrl
+ verbObj.backendMethod = (backend.backendMethod == 'keep') and verb or backend.backendMethod
+ verbObj.policies = policies
+ verbObj.security = security
+ end
+ end
+ return decoded
+end
+
+--- Parse backendUrl and backendMethod
+-- @param swagger swagger file to parse
+function parseBackends(swagger)
+ local configObj = swagger["x-gateway-configuration"]
+ configObj = (configObj == nil) and swagger["x-ibm-configuration"] or configObj
+ if configObj ~= nil then
+ for _, obj in pairs(configObj.assembly.execute) do
+ for policy, v in pairs(obj) do
+ local res = {}
+ if policy == "operation-switch" then
+ local caseObj = v.case
+ for _, case in pairs(caseObj) do
+ for _, op in pairs(case.operations) do
+ res[op] = {
+ backendUrl = case.execute[1]["proxy"]["target-url"],
+ backendMethod = case.execute[1]["proxy"].verb
+ }
+ end
+ end
+ return res
+ end
+ if policy == "proxy" then
+ res["all"] = {
+ backendUrl = v["target-url"],
+ backendMethod = v.verb
+ }
+ return res
+ end
+ end
+ end
+ end
+end
+
+--- Parse policies in swagger
+-- @param swagger swagger file to parse
+function parsePolicies(swagger)
+ local policies = {}
+ -- parse rate limit
+ policies = parseRateLimit(swagger, policies)
+ policies = parseRequestMapping(swagger, policies)
+ return policies
+end
+
+--- Parse rate limit
+function parseRateLimit(swagger, policies)
+ local rlObj = swagger["x-gateway-rate-limit"]
+ rlObj = (rlObj == nil) and swagger["x-ibm-rate-limit"] or rlObj
+ if rlObj ~= nil then
+ rlObj = rlObj[1]
+ if rlObj.unit == "second" then
+ rlObj.unit = 1
+ elseif rlObj.unit == "minute" then
+ rlObj.unit = 60
+ elseif rlObj.unit == "hour" then
+ rlObj.unit = 3600
+ elseif rlObj.unit == "day" then
+ rlObj.unit = 86400
+ else
+ rlObj.unit = 60 -- default to minute
+ end
+ policies[#policies+1] = {
+ type = "rateLimit",
+ value = {
+ interval = rlObj.unit * rlObj.units,
+ rate = rlObj.rate,
+ scope = "api",
+ subscription = "true"
+ }
+ }
+ end
+ return policies
+end
+
+--- Parse request mapping
+function parseRequestMapping(swagger, policies)
+ local valueList = {}
+ if swagger["x-ibm-configuration"] ~= nil then
+ for _, obj in pairs(swagger["x-ibm-configuration"].assembly.execute) do
+ for policy, v in pairs(obj) do
+ if policy == "set-variable" then
+ for _, actionObj in pairs(v.actions) do
+ local fromValue = actionObj.value
+ local toParsedArray = {string.match(actionObj.set, "([^.]+).([^.]+).([^.]+)") }
+ local toName = toParsedArray[3]
+ local toLocation = toParsedArray[2]
+ toLocation = toLocation == "headers" and "header" or toLocation
+ valueList[#valueList+1] = {
+ action = "insert",
+ from = {
+ value = fromValue
+ },
+ to = {
+ name = toName,
+ location = toLocation
+ }
+ }
+ end
+ end
+ end
+ end
+ end
+ if next(valueList) ~= nil then
+ policies[#policies+1] ={
+ type = "reqMapping",
+ value = valueList
+ }
+ end
+ return policies
+end
+
+--- Parse security in swagger
+-- @param swagger swagger file to parse
+function parseSecurity(swagger)
+ local security = {}
+ if swagger["securityDefinitions"] ~= nil then
+ local secObject = swagger["securityDefinitions"]
+ for key, sec in pairs(secObject) do
+ if sec.type == 'apiKey' then
+ security[#security+1] = {
+ type = sec.type,
+ scope = "api",
+ header = sec.name
+ }
+ elseif sec.type == 'oauth2' then
+ security[#security+1] = {
+ type = sec.type,
+ scope = "api",
+ provider = key
+ }
+ end
+ end
+ end
+ return security
+end
+
+return _M
\ No newline at end of file
diff --git a/scripts/lua/management/lib/tenants.lua b/scripts/lua/management/lib/tenants.lua
new file mode 100644
index 0000000..97098db
--- /dev/null
+++ b/scripts/lua/management/lib/tenants.lua
@@ -0,0 +1,162 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module tenants
+-- Management interface for tenants for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local apis = require "management/lib/apis"
+
+local _M = {};
+
+function _M.addTenant(red, decoded, existingTenant)
+ -- Return tenant object
+ local uuid = existingTenant ~= nil and existingTenant.id or utils.uuid()
+ local tenantObj = {
+ id = uuid,
+ namespace = decoded.namespace,
+ instance = decoded.instance
+ }
+ tenantObj = redis.addTenant(red, uuid, tenantObj)
+ return cjson.decode(tenantObj)
+end
+
+--- Get all tenants in redis
+-- @param red redis client
+-- @param queryParams object containing optional query parameters
+function _M.getAllTenants(red, queryParams)
+ local tenants = redis.getAllTenants(red)
+ local tenantList
+ if next(queryParams) ~= nil then
+ tenantList = filterTenants(tenants, queryParams);
+ end
+ if tenantList == nil then
+ tenantList = {}
+ for k, v in pairs(tenants) do
+ if k%2 == 0 then
+ tenantList[#tenantList+1] = cjson.decode(v)
+ end
+ end
+ end
+ return tenantList
+end
+
+--- Filter tenants based on query parameters
+-- @param tenants list of tenants
+-- @param queryParams query parameters to filter tenants
+function filterTenants(tenants, queryParams)
+ local namespace = queryParams['filter[where][namespace]']
+ local instance = queryParams['filter[where][instance]']
+ -- missing or invalid query parameters
+ if (namespace == nil and instance == nil) or (instance ~= nil and namespace == nil) then
+ return nil
+ end
+ -- filter tenants
+ local tenantList = {}
+ for k, v in pairs(tenants) do
+ if k%2 == 0 then
+ local tenant = cjson.decode(v)
+ if (namespace ~= nil and instance == nil and tenant.namespace == namespace) or
+ (namespace ~= nil and instance ~= nil and tenant.namespace == namespace and tenant.instance == instance) then
+ tenantList[#tenantList+1] = tenant
+ end
+ end
+ end
+ return tenantList
+end
+
+--- Get tenant by its id
+-- @param red redis client
+-- @param id tenant id
+function _M.getTenant(red, id)
+ local tenant = redis.getTenant(red, id)
+ if tenant == nil then
+ request.err(404, utils.concatStrings({"Unknown tenant id ", id }))
+ end
+ return tenant
+end
+
+--- Get APIs associated with tenant
+-- @param red redis client
+-- @param id tenant id
+-- @param queryParams object containing optional query parameters
+function _M.getTenantAPIs(red, id, queryParams)
+ local apis = redis.getAllAPIs(red)
+ local apiList
+ if next(queryParams) ~= nil then
+ apiList = filterTenantAPIs(id, apis, queryParams);
+ end
+ if apiList == nil then
+ apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ local decoded = cjson.decode(v)
+ if decoded.tenantId == id then
+ apiList[#apiList+1] = decoded
+ end
+ end
+ end
+ end
+ return apiList
+end
+
+--- Filter apis based on query paramters
+-- @param queryParams query parameters to filter apis
+function filterTenantAPIs(id, apis, queryParams)
+ local basePath = queryParams['filter[where][basePath]']
+ basePath = basePath == nil and queryParams['basePath'] or basePath
+ local name = queryParams['filter[where][name]']
+ name = name == nil and queryParams['title'] or name
+ -- missing query parameters
+ if (basePath == nil and name == nil)then
+ return nil
+ end
+ -- filter apis
+ local apiList = {}
+ for k, v in pairs(apis) do
+ if k%2 == 0 then
+ local api = cjson.decode(v)
+ if api.tenantId == id and
+ ((basePath ~= nil and name == nil and api.basePath == basePath) or
+ (name ~= nil and basePath == nil and api.name == name) or
+ (basePath ~= nil and name ~= nil and api.basePath == basePath and api.name == name)) then
+ apiList[#apiList+1] = api
+ end
+ end
+ end
+ return apiList
+end
+
+--- Delete tenant from gateway
+-- @param red redis client
+-- @param id id of tenant to delete
+function _M.deleteTenant(red, id)
+ local tenantAPIs = _M.getTenantAPIs(red, id, {})
+ for _, v in pairs(tenantAPIs) do
+ apis.deleteAPI(red, v.id)
+ end
+ redis.deleteTenant(red, id)
+ return {}
+end
+
+return _M
diff --git a/scripts/lua/management/lib/validation.lua b/scripts/lua/management/lib/validation.lua
new file mode 100644
index 0000000..17af88a
--- /dev/null
+++ b/scripts/lua/management/lib/validation.lua
@@ -0,0 +1,155 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module validation
+-- Module for validating api body
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+
+local _M = {}
+
+function _M.validate(red, decoded)
+ local fields = {"name", "basePath", "tenantId", "resources"}
+ for _, v in pairs(fields) do
+ local res, err = isValid(red, v, decoded[v])
+ if res == false then
+ return err
+ end
+ end
+ return nil
+end
+
+--- Check JSON body fields for errors
+-- @param red Redis client instance
+-- @param field name of field
+-- @param object field object
+function isValid(red, field, object)
+ -- Check that field exists in body
+ if not object then
+ return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", field, "' in request body."}) }
+ end
+ -- Additional check for basePath
+ if field == "basePath" then
+ local basePath = object
+ if string.match(basePath, "'") then
+ return false, { statusCode = 400, message = "basePath contains illegal character \"'\"." }
+ end
+ end
+ -- Additional check for tenantId
+ if field == "tenantId" then
+ local tenant = redis.getTenant(red, object)
+ if tenant == nil then
+ return false, { statusCode = 404, message = utils.concatStrings({"Unknown tenant id ", object }) }
+ end
+ end
+ if field == "resources" then
+ local res, err = checkResources(object)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+ -- All error checks passed
+ return true
+end
+
+--- Error checking for resources
+-- @param resources resources object
+function checkResources(resources)
+ if next(resources) == nil then
+ return false, { statusCode = 400, message = "Empty resources object." }
+ end
+ for path, resource in pairs(resources) do
+ -- Check resource path for illegal characters
+ if string.match(path, "'") then
+ return false, { statusCode = 400, message = "resource path contains illegal character \"'\"." }
+ end
+ -- Check that resource path begins with slash
+ if path:sub(1,1) ~= '/' then
+ return false, { statusCode = 400, message = "Resource path must begin with '/'." }
+ end
+ -- Check operations object
+ local res, err = checkOperations(resource.operations)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+end
+
+--- Error checking for operations
+-- @param operations operations object
+function checkOperations(operations)
+ if not operations or next(operations) == nil then
+ return false, { statusCode = 400, message = "Missing or empty field 'operations' or in resource path object." }
+ end
+ local allowedVerbs = {GET=true, POST=true, PUT=true, DELETE=true, PATCH=true, HEAD=true, OPTIONS=true}
+ for verb, verbObj in pairs(operations) do
+ if allowedVerbs[verb:upper()] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"Resource verb '", verb, "' not supported."}) }
+ end
+ -- Check required fields
+ local requiredFields = {"backendMethod", "backendUrl"}
+ for k, v in pairs(requiredFields) do
+ if verbObj[v] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"Missing field '", v, "' for '", verb, "' operation."}) }
+ end
+ if v == "backendMethod" then
+ local backendMethod = verbObj[v]
+ if allowedVerbs[backendMethod:upper()] == nil then
+ return false, { statusCode = 400, message = utils.concatStrings({"backendMethod '", backendMethod, "' not supported."}) }
+ end
+ end
+ end
+ -- Check optional fields
+ local res, err = checkOptionalPolicies(verbObj.policies, verbObj.security)
+ if res ~= nil and res == false then
+ return res, err
+ end
+ end
+end
+
+--- Error checking for policies and security
+-- @param policies policies object
+-- @param security security object
+function checkOptionalPolicies(policies, security)
+ if policies then
+ for _, v in pairs(policies) do
+ local validTypes = {"reqMapping", "rateLimit", "backendRouting"}
+ if (v.type == nil or v.value == nil) then
+ return false, { statusCode = 400, message = "Missing field in policy object. Need \"type\" and \"value\"." }
+ elseif utils.tableContains(validTypes, v.type) == false then
+ return false, { statusCode = 400, message = "Invalid type in policy object. Valid: " .. cjson.encode(validTypes) }
+ end
+ end
+ end
+ if security then
+ for _, sec in ipairs(security) do
+ local validScopes = {"tenant", "api", "resource"}
+ if (sec.type == nil or sec.scope == nil) then
+ return false, { statusCode = 400, message = "Missing field in security object. Need \"type\" and \"scope\"." }
+ elseif utils.tableContains(validScopes, sec.scope) == false then
+ return false, { statusCode = 400, message = "Invalid scope in security object. Valid: " .. cjson.encode(validScopes) }
+ end
+ end
+ end
+end
+
+return _M
\ No newline at end of file
diff --git a/scripts/lua/management/routes/apis.lua b/scripts/lua/management/routes/apis.lua
new file mode 100644
index 0000000..a50431a
--- /dev/null
+++ b/scripts/lua/management/routes/apis.lua
@@ -0,0 +1,198 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module apis
+-- Management interface for apis for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local apis = require "management/lib/apis"
+local tenants = require "management/lib/tenants"
+local swagger = require "management/lib/swagger"
+local validation = require "management/lib/validation"
+
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local _M = {}
+
+--- Request handler for routing API calls appropriately
+function _M.requestHandler()
+ local requestMethod = ngx.req.get_method()
+ ngx.header.content_type = "application/json; charset=utf-8"
+ if requestMethod == "GET" then
+ getAPIs()
+ elseif requestMethod == 'POST' or requestMethod == 'PUT' then
+ addAPI()
+ elseif requestMethod == "DELETE" then
+ deleteAPI()
+ else
+ request.err(400, "Invalid verb.")
+ end
+end
+
+function getAPIs()
+ local queryParams = ngx.req.get_uri_args()
+ local id = ngx.var.api_id
+ local version = ngx.var.version
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ if id == '' then
+ local apiList
+ if version == 'v1' then
+ apiList = apis.getAllAPIs(red, queryParams)
+ elseif version == 'v2' then
+ local tenantId = ngx.var.tenantId
+ local v2ApiList = {}
+ apiList = tenants.getTenantAPIs(red, tenantId, queryParams)
+ for _, api in pairs(apiList) do
+ v2ApiList[#v2ApiList+1] = {
+ id = api.id,
+ managedUrl = api.managedUrl,
+ open_api_doc = redis.getSwagger(red, api.id)
+ }
+ end
+ apiList = v2ApiList
+ end
+ apiList = (next(apiList) == nil) and "[]" or cjson.encode(apiList)
+ redis.close(red)
+ request.success(200, apiList)
+ else
+ local query = ngx.var.query
+ if query ~= '' then
+ if query ~= "tenant" then
+ redis.close(red)
+ request.err(400, "Invalid request")
+ else
+ local tenant = apis.getAPITenant(red, id)
+ tenant = cjson.encode(tenant)
+ redis.close(red)
+ request.success(200, tenant)
+ end
+ else
+ local api = apis.getAPI(red, id)
+ if version == 'v1' then
+ redis.close(red)
+ api = cjson.encode(api)
+ request.success(200, api)
+ elseif version == 'v2' then
+ local returnObj = {
+ id = api.id,
+ managedUrl = api.managedUrl,
+ open_api_doc = redis.getSwagger(red, api.id)
+ }
+ redis.close(red)
+ request.success(200, cjson.encode(returnObj))
+ end
+ end
+ end
+end
+
+function addAPI()
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local id = ngx.var.api_id
+ local existingAPI = checkForExistingAPI(red, id)
+ ngx.req.read_body()
+ local args = ngx.req.get_body_data()
+ if not args then
+ redis.close(red)
+ request.err(400, "Missing request body")
+ end
+ -- Convert json into Lua table
+ local version = ngx.var.version
+ local decoded
+ local swaggerTable;
+ if version == 'v1' then
+ decoded = cjson.decode(args)
+ elseif version == 'v2' then
+ swaggerTable = cjson.decode(args)
+ decoded = swagger.parseSwagger(swaggerTable)
+ local tenantId = ngx.var.tenantId
+ local tenant = {
+ namespace = tenantId,
+ instance = ''
+ }
+ tenants.addTenant(red, tenant, {id = tenantId})
+ decoded.tenantId = tenantId
+ end
+ -- Check for api id in JSON body
+ if existingAPI == nil and decoded.id ~= nil then
+ existingAPI = redis.getAPI(red, decoded.id)
+ if existingAPI == nil then
+ redis.close(red)
+ request.err(404, utils.concatStrings({"Unknown API id ", decoded.id}))
+ end
+ end
+ local err = validation.validate(red, decoded)
+ if err ~= nil then
+ redis.close(red)
+ request.err(err.statusCode, err.message)
+ end
+ local managedUrlObj = apis.addAPI(red, decoded, existingAPI)
+ if version == 'v1' then
+ redis.close(red)
+ managedUrlObj = cjson.encode(managedUrlObj)
+ request.success(200, managedUrlObj)
+ elseif version == 'v2' then
+ redis.addSwagger(red, managedUrlObj.id, swaggerTable)
+ local returnObj = {
+ id = managedUrlObj.id,
+ managedUrl = managedUrlObj.managedUrl,
+ open_api_doc = swaggerTable
+ }
+ redis.close(red)
+ request.success(200, cjson.encode(returnObj))
+ end
+end
+
+function deleteAPI()
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local id = ngx.var.api_id
+ if id == nil or id == '' then
+ redis.close(red)
+ request.err(400, "No id specified.")
+ end
+ apis.deleteAPI(red, id)
+ local version = ngx.var.version
+ if version == 'v2' then
+ redis.deleteSwagger(red, id)
+ end
+ redis.close(red)
+ request.success(204)
+end
+
+--- Check for api id from uri and use existing API if it already exists in redis
+-- @param red Redis client instance
+-- @param id API id to check
+function checkForExistingAPI(red, id)
+ local existing
+ if id ~= nil and id ~= '' then
+ existing = redis.getAPI(red, id)
+ if existing == nil then
+ redis.close(red)
+ request.err(404, utils.concatStrings({"Unknown API id ", id}))
+ end
+ end
+ return existing
+end
+
+return _M;
diff --git a/scripts/lua/management/subscriptions.lua b/scripts/lua/management/routes/subscriptions.lua
similarity index 100%
rename from scripts/lua/management/subscriptions.lua
rename to scripts/lua/management/routes/subscriptions.lua
diff --git a/scripts/lua/management/routes/tenants.lua b/scripts/lua/management/routes/tenants.lua
new file mode 100644
index 0000000..1eec6f0
--- /dev/null
+++ b/scripts/lua/management/routes/tenants.lua
@@ -0,0 +1,151 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--- @module tenants
+-- Management interface for tenants for the gateway
+
+local cjson = require "cjson"
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+local tenants = require "management/lib/tenants"
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local _M = {};
+
+--- Request handler for routing tenant calls appropriately
+function _M.requestHandler()
+ local requestMethod = ngx.req.get_method()
+ ngx.header.content_type = "application/json; charset=utf-8"
+ if requestMethod == "GET" then
+ getTenants()
+ elseif requestMethod == "PUT" or requestMethod == "POST" then
+ addTenant()
+ elseif requestMethod == "DELETE" then
+ deleteTenant()
+ else
+ request.err(400, "Invalid verb.")
+ end
+end
+
+function addTenant()
+ -- Open connection to redis or use one from connection pool
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ -- Check for tenant id and use existingTenant if it already exists in redis
+ local existingTenant = checkForExistingTenant(red)
+ -- Read in the PUT JSON Body
+ ngx.req.read_body()
+ local args = ngx.req.get_body_data()
+ if not args then
+ redis.close(red)
+ request.err(400, "Missing request body")
+ end
+ -- Convert json into Lua table
+ local decoded = cjson.decode(args)
+ -- Check for tenant id in JSON body
+ if existingTenant == nil and decoded.id ~= nil then
+ existingTenant = redis.getTenant(red, decoded.id)
+ if existingTenant == nil then
+ redis.close(red)
+ request.err(404, utils.concatStrings({"Unknown Tenant id ", decoded.id}))
+ end
+ end
+ -- Error checking
+ local res, err = utils.tableContainsAll(decoded, {"namespace", "instance"})
+ if res == false then
+ redis.close(red)
+ request.err(err.statusCode, err.message)
+ end
+ -- Return tenant object
+ local tenantObj = tenants.addTenant(red, decoded, existingTenant)
+ tenantObj = cjson.encode(tenantObj)
+ redis.close(red)
+ request.success(200, tenantObj)
+end
+
+--- Check for tenant id from uri and use existing tenant if it already exists in redis
+-- @param red Redis client instance
+function checkForExistingTenant(red)
+ local id = ngx.var.tenant_id
+ local existing
+ -- Get object from redis
+ if id ~= nil and id ~= '' then
+ existing = redis.getTenant(red, id)
+ if existing == nil then
+ redis.close(red)
+ request.err(404, utils.concatStrings({"Unknown Tenant id ", id}))
+ end
+ end
+ return existing
+end
+
+--- Get one or all tenants from the gateway
+-- GET /v1/tenants
+function getTenants()
+ local queryParams = ngx.req.get_uri_args()
+ local id = ngx.var.tenant_id
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ if id == '' then
+ local tenantList = tenants.getAllTenants(red, queryParams)
+ tenantList = (next(tenantList) == nil) and "[]" or cjson.encode(tenantList)
+ redis.close(red)
+ request.success(200, tenantList)
+ else
+ local query = ngx.var.query
+ if query ~= '' then
+ if query ~= "apis" then
+ redis.close(red)
+ request.err(400, "Invalid request")
+ else
+ local apis = tenants.getTenantAPIs(red, id, queryParams)
+ apis = cjson.encode(apis)
+ redis.close(red)
+ request.success(200, apis)
+ end
+ else
+ local tenant = tenants.getTenant(red, id)
+ tenant = cjson.encode(tenant)
+ redis.close(red)
+ request.success(200, tenant)
+ end
+ end
+end
+
+--- Delete tenant from gateway
+-- DELETE /v1/tenants/<id>
+function deleteTenant()
+ local id = ngx.var.tenant_id
+ if id == nil or id == '' then
+ request.err(400, "No id specified.")
+ end
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local tenant = redis.getTenant(red, id)
+ if tenant == nil then
+ redis.close(red)
+ request.err(404, utils.concatStrings({"Unknown tenant id ", id}))
+ end
+ tenants.deleteTenant(red, id)
+ redis.close(red)
+ request.success(200, cjson.encode({}))
+end
+
+return _M
diff --git a/scripts/lua/management/tenants.lua b/scripts/lua/management/tenants.lua
deleted file mode 100644
index eb29e39..0000000
--- a/scripts/lua/management/tenants.lua
+++ /dev/null
@@ -1,261 +0,0 @@
--- Copyright (c) 2016 IBM. All rights reserved.
---
--- Permission is hereby granted, free of charge, to any person obtaining a
--- copy of this software and associated documentation files (the "Software"),
--- to deal in the Software without restriction, including without limitation
--- the rights to use, copy, modify, merge, publish, distribute, sublicense,
--- and/or sell copies of the Software, and to permit persons to whom the
--- Software is furnished to do so, subject to the following conditions:
---
--- The above copyright notice and this permission notice shall be included in
--- all copies or substantial portions of the Software.
---
--- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
--- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
--- DEALINGS IN THE SOFTWARE.
-
---- @module tenants
--- Management interface for tenants for the gateway
-
-local cjson = require "cjson"
-local redis = require "lib/redis"
-local utils = require "lib/utils"
-local request = require "lib/request"
-local REDIS_HOST = os.getenv("REDIS_HOST")
-local REDIS_PORT = os.getenv("REDIS_PORT")
-local REDIS_PASS = os.getenv("REDIS_PASS")
-
-local _M = {};
-
---- Request handler for routing tenant calls appropriately
-function _M.requestHandler()
- local requestMethod = ngx.req.get_method()
- if requestMethod == "GET" then
- getTenants()
- elseif requestMethod == "PUT" then
- addTenant()
- elseif requestMethod == "POST" then
- addTenant()
- elseif requestMethod == "DELETE" then
- deleteTenant()
- else
- ngx.status = 400
- ngx.say("Invalid verb")
- end
-end
-
---- Add a tenant to the Gateway
--- PUT /v1/tenants
--- body:
--- {
--- "namespace": *(String) tenant namespace
--- "instance": *(String) tenant instance
--- }
-function addTenant()
- -- Open connection to redis or use one from connection pool
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- -- Check for tenant id and use existingTenant if it already exists in redis
- local existingTenant = checkForExistingTenant(red)
- -- Read in the PUT JSON Body
- ngx.req.read_body()
- local args = ngx.req.get_body_data()
- if not args then
- request.err(400, "Missing request body")
- end
- -- Convert json into Lua table
- local decoded = cjson.decode(args)
- -- Check for tenant id in JSON body
- if existingTenant == nil and decoded.id ~= nil then
- existingTenant = redis.getTenant(red, decoded.id)
- if existingTenant == nil then
- request.err(404, utils.concatStrings({"Unknown Tenant id ", decoded.id}))
- end
- end
- -- Error checking
- local res, err = utils.tableContainsAll(decoded, {"namespace", "instance"})
- if res == false then
- request.err(err.statusCode, err.message)
- end
- -- Return tenant object
- local uuid = existingTenant ~= nil and existingTenant.id or utils.uuid()
- local tenantObj = {
- id = uuid,
- namespace = decoded.namespace,
- instance = decoded.instance
- }
- tenantObj = redis.addTenant(red, uuid, tenantObj)
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, tenantObj)
-end
-
---- Check for tenant id from uri and use existing tenant if it already exists in redis
--- @param red Redis client instance
-function checkForExistingTenant(red)
- local id = ngx.var.tenant_id
- local existing
- -- Get object from redis
- if id ~= nil and id ~= '' then
- existing = redis.getTenant(red, id)
- if existing == nil then
- request.err(404, utils.concatStrings({"Unknown Tenant id ", id}))
- end
- end
- return existing
-end
-
---- Get one or all tenants from the gateway
--- GET /v1/tenants
-function getTenants()
- local queryParams = ngx.req.get_uri_args()
- local id = ngx.var.tenant_id
- if id == nil or id == '' then
- getAllTenants(queryParams)
- else
- local query = ngx.var.query
- if (query ~= nil and query ~= '') then
- if query ~= "apis" then
- request.err(400, "Invalid request")
- else
- getTenantAPIs(id, queryParams)
- end
- else
- getTenant(id)
- end
- end
-end
-
---- Get all tenants in redis
--- @param queryParams object containing optional query parameters
-function getAllTenants(queryParams)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local tenants = redis.getAllTenants(red)
- redis.close(red)
- local tenantList
- if next(queryParams) ~= nil then
- tenantList = filterTenants(tenants, queryParams);
- end
- if tenantList == nil then
- tenantList = {}
- for k, v in pairs(tenants) do
- if k%2 == 0 then
- tenantList[#tenantList+1] = cjson.decode(v)
- end
- end
- end
- ngx.header.content_type = "application/json; charset=utf-8"
- tenantList = (next(tenantList) == nil) and "[]" or cjson.encode(tenantList)
- request.success(200, tenantList)
-end
-
---- Filter tenants based on query parameters
--- @param tenants list of tenants
--- @param queryParams query parameters to filter tenants
-function filterTenants(tenants, queryParams)
- local namespace = queryParams['filter[where][namespace]']
- local instance = queryParams['filter[where][instance]']
- -- missing or invalid query parameters
- if (namespace == nil and instance == nil) or (instance ~= nil and namespace == nil) then
- return nil
- end
- -- filter tenants
- local tenantList = {}
- for k, v in pairs(tenants) do
- if k%2 == 0 then
- local tenant = cjson.decode(v)
- if (namespace ~= nil and instance == nil and tenant.namespace == namespace) or
- (namespace ~= nil and instance ~= nil and tenant.namespace == namespace and tenant.instance == instance) then
- tenantList[#tenantList+1] = tenant
- end
- end
- end
- return tenantList
-end
-
---- Get tenant by its id
--- @param id tenant id
-function getTenant(id)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local tenant = redis.getTenant(red, id)
- if tenant == nil then
- request.err(404, utils.concatStrings({"Unknown tenant id ", id }))
- end
- redis.close(red)
- ngx.header.content_type = "application/json; charset=utf-8"
- request.success(200, cjson.encode(tenant))
-end
-
---- Get APIs associated with tenant
--- @param id tenant id
--- @param queryParams object containing optional query parameters
-function getTenantAPIs(id, queryParams)
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local apis = redis.getAllAPIs(red)
- redis.close(red)
- local apiList
- if next(queryParams) ~= nil then
- apiList = filterTenantAPIs(id, apis, queryParams);
- end
- if apiList == nil then
- apiList = {}
- for k, v in pairs(apis) do
- if k%2 == 0 then
- local decoded = cjson.decode(v)
- if decoded.tenantId == id then
- apiList[#apiList+1] = decoded
- end
- end
- end
- end
- ngx.header.content_type = "application/json; charset=utf-8"
- apiList = (next(apiList) == nil) and "[]" or cjson.encode(apiList)
- request.success(200, apiList)
-end
-
---- Filter apis based on query paramters
--- @param queryParams query parameters to filter apis
-function filterTenantAPIs(id, apis, queryParams)
- local basePath = queryParams['filter[where][basePath]']
- local name = queryParams['filter[where][name]']
- -- missing query parameters
- if (basePath == nil and name == nil)then
- return nil
- end
- -- filter apis
- local apiList = {}
- for k, v in pairs(apis) do
- if k%2 == 0 then
- local api = cjson.decode(v)
- if api.tenantId == id and
- ((basePath ~= nil and name == nil and api.basePath == basePath) or
- (name ~= nil and basePath == nil and api.name == name) or
- (basePath ~= nil and name ~= nil and api.basePath == basePath and api.name == name)) then
- apiList[#apiList+1] = api
- end
- end
- end
- return apiList
-end
-
---- Delete tenant from gateway
--- DELETE /v1/tenants/<id>
-function deleteTenant()
- local id = ngx.var.tenant_id
- if id == nil or id == '' then
- request.err(400, "No id specified.")
- end
- local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local tenant = redis.getTenant(red, id)
- if tenant == nil then
- request.err(404, utils.concatStrings({"Unknown tenant id ", id}))
- end
- redis.deleteTenant(red, id)
- redis.close(red)
- request.success(200, {})
-end
-
-return _M
diff --git a/scripts/lua/policies/rateLimit.lua b/scripts/lua/policies/rateLimit.lua
index 71a763b..8da8783 100644
--- a/scripts/lua/policies/rateLimit.lua
+++ b/scripts/lua/policies/rateLimit.lua
@@ -29,6 +29,7 @@
local utils = require "lib/utils"
local logger = require "lib/logger"
local request = require "lib/request"
+local redis = require "lib/redis"
local _M = {}
@@ -39,14 +40,19 @@
local i = 60 / obj.interval
local r = i * obj.rate
r = utils.concatStrings({tostring(r), 'r/m'})
+ local tenantId = ngx.var.tenant
+ local gatewayPath = ngx.var.gatewayPath
local k
-- Check scope
if obj.scope == 'tenant' then
- k = utils.concatStrings({"tenant:", ngx.var.tenant})
+ k = utils.concatStrings({"tenant:", tenantId})
elseif obj.scope == 'api' then
- k = utils.concatStrings({"tenant:", ngx.var.tenant, ":api:", ngx.var.apiId})
+ local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
+ local apiId = redis.resourceToApi(red, utils.concatStrings({'resources:', tenantId, ':', gatewayPath}))
+ k = utils.concatStrings({"tenant:", tenantId, ":api:", apiId})
+ redis.close(red)
elseif obj.scope == 'resource' then
- k = utils.concatStrings({"tenant:", ngx.var.tenant, ":resource:", ngx.var.gatewayPath})
+ k = utils.concatStrings({"tenant:", tenantId, ":resource:", gatewayPath})
end
-- Check subscription
if obj.subscription ~= nil and obj.subscription == true and apiKey ~= nil then
diff --git a/scripts/lua/policies/security/apiKey.lua b/scripts/lua/policies/security/apiKey.lua
index 689262e..361b438 100644
--- a/scripts/lua/policies/security/apiKey.lua
+++ b/scripts/lua/policies/security/apiKey.lua
@@ -76,7 +76,7 @@
local header = (securityObj.header == nil) and 'x-api-key' or securityObj.header
local apiKey = ngx.var[utils.concatStrings({'http_', header}):gsub("-", "_")]
if not apiKey then
- request.err(401, utils.concatStrings({'API key header "', header, '" is required.'}))
+ request.err(401, 'Unauthorized')
return nil
end
if securityObj.hashed then
@@ -84,7 +84,7 @@
end
local ok = validate(red, tenant, gatewayPath, apiId, scope, header, apiKey)
if not ok then
- request.err(401, 'Invalid API key.')
+ request.err(401, 'Invalid API key')
return nil
end
return apiKey
diff --git a/scripts/lua/routing.lua b/scripts/lua/routing.lua
index f4d54ae..bfcc50a 100644
--- a/scripts/lua/routing.lua
+++ b/scripts/lua/routing.lua
@@ -41,12 +41,15 @@
function _M.processCall()
-- Get resource object from redis
local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 10000)
- local resourceKeys = redis.getAllResourceKeys(red, ngx.var.tenant)
- local redisKey = _M.findRedisKey(resourceKeys, ngx.var.tenant, ngx.var.gatewayPath)
+ local tenantId = ngx.var.tenant
+ local gatewayPath = ngx.var.gatewayPath
+ local resourceKeys = redis.getAllResourceKeys(red, tenantId)
+ local redisKey = _M.findRedisKey(resourceKeys, tenantId, gatewayPath)
if redisKey == nil then
request.err(404, 'Not found.')
end
local obj = cjson.decode(redis.getResource(red, redisKey, "resources"))
+ redis.close(red)
ngx.var.tenantNamespace = obj.tenantNamespace
ngx.var.tenantInstance = obj.tenantInstance
for verb, opFields in pairs(obj.operations) do
@@ -54,7 +57,7 @@
-- Check if auth is required
local key
if (opFields.security) then
- for k, sec in ipairs(opFields.security) do
+ for _, sec in ipairs(opFields.security) do
local result = utils.concatStrings({key, security.process(sec)})
if key == nil then
key = result -- use the key from the first policy.
diff --git a/tests/install-deps.sh b/tests/install-deps.sh
index b5d07d3..d629df3 100755
--- a/tests/install-deps.sh
+++ b/tests/install-deps.sh
@@ -11,3 +11,4 @@
luarocks install --tree=lua_modules sha1
luarocks install --tree=lua_modules md5
luarocks install --tree=lua_modules net-url
+luarocks install --tree=lua_modules luafilesystem
\ No newline at end of file
diff --git a/tests/scripts/lua/lib/request.lua b/tests/scripts/lua/lib/request.lua
index c7aadd4..f09785c 100644
--- a/tests/scripts/lua/lib/request.lua
+++ b/tests/scripts/lua/lib/request.lua
@@ -20,6 +20,7 @@
local fakengx = require 'fakengx'
local request = require 'lib/request'
+local cjson = require 'cjson'
describe('Testing Request module', function()
before_each(function()
@@ -29,8 +30,10 @@
it('should return correct error response', function()
local code = 500
local msg = 'Internal server error\n'
- request.err(code, msg)
- assert.are.equal('Error: ' .. msg .. '\n', ngx._body)
+ request.err(code ,msg)
+ local expected = cjson.encode{status = code, message = 'Error: ' .. msg}
+ local actual = ngx._body
+ assert.are.same(expected .. '\n', actual)
assert.are.equal(code, ngx._exit)
end)
diff --git a/tests/scripts/lua/management/examples/example1.json b/tests/scripts/lua/management/examples/example1.json
new file mode 100644
index 0000000..c6a6fe7
--- /dev/null
+++ b/tests/scripts/lua/management/examples/example1.json
@@ -0,0 +1,86 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "version": "1.0",
+ "title": "Hello World API"
+ },
+ "basePath": "/native",
+ "schemes": [
+ "https"
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "paths": {
+ "/hello": {
+ "get": {
+ "description": "Returns a greeting to the user!",
+ "parameters": [
+ {
+ "name": "user",
+ "in": "path",
+ "type": "string",
+ "required": true,
+ "description": "The name of the user to greet."
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Invalid characters in \"user\" were provided."
+ }
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "client_id": {
+ "type": "apiKey",
+ "name": "X-Api-Key",
+ "in": "header"
+ },
+ "google": {
+ "type": "oauth2",
+ "flow": "tokenIntrospect",
+ "x-tokenintrospect": {
+ "url": "https://www.googleapis.com/oauth2/v3/tokeninfo"
+ }
+ }
+ },
+ "security": [
+ {
+ "client_id": [],
+ "google": []
+ }
+ ],
+ "x-gateway-rate-limit": [
+ {
+ "unit": "minute",
+ "units": 3,
+ "rate": 100
+ }
+ ],
+ "x-gateway-configuration": {
+ "cors": {
+ "enabled": true
+ },
+ "assembly": {
+ "execute": [
+ {
+ "proxy": {
+ "target-url": "https://appconnect.mybluemix.net",
+ "verb": "keep"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/scripts/lua/management/examples/example2.json b/tests/scripts/lua/management/examples/example2.json
new file mode 100644
index 0000000..79b5bdc
--- /dev/null
+++ b/tests/scripts/lua/management/examples/example2.json
@@ -0,0 +1,136 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "version": "1.0",
+ "title": "Hello World API"
+ },
+ "basePath": "/whisk",
+ "schemes": [
+ "https"
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "paths": {
+ "/hello": {
+ "get": {
+ "operationId": "getHello",
+ "description": "Returns a greeting to the user!",
+ "x-openwhisk": {
+ "namespace": "greend@us.ibm.com_dev",
+ "package": "demo",
+ "action": "hello",
+ "url": "https://openwhisk.ng.bluemix.net/api/some/action/path.http"
+ },
+ "parameters": [
+ {
+ "name": "user",
+ "in": "path",
+ "type": "string",
+ "required": true,
+ "description": "The name of the user to greet."
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Invalid characters in \"user\" were provided."
+ }
+ }
+ },
+ "post": {
+ "operationId": "postHello",
+ "description": null,
+ "x-openwhisk": {
+ "package": "demo",
+ "action": "createUser",
+ "url": "https://openwhisk.ng.bluemix.net/api/user@us.ibm.com/demo/createuser"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "client_id": {
+ "type": "apiKey",
+ "name": "X-Api-Key",
+ "in": "header"
+ },
+ "google": {
+ "type": "oauth2",
+ "flow": "tokenIntrospect",
+ "x-tokenintrospect": {
+ "url": "https://www.googleapis.com/oauth2/v3/tokeninfo"
+ }
+ }
+ },
+ "security": [
+ {
+ "client_id": [],
+ "client_secret": []
+ }
+ ],
+ "x-ibm-rate-limit": [
+ {
+ "unit": "minute",
+ "units": 3,
+ "rate": 100
+ }
+ ],
+ "x-ibm-configuration": {
+ "assembly": {
+ "execute": [
+ {
+ "set-variable": {
+ "actions": [
+ {
+ "set": "message.headers.Authorization",
+ "value": "Basic xxx"
+ }
+ ]
+ }
+ },
+ {
+ "operation-switch": {
+ "case": [
+ {
+ "operations": [
+ "getHello"
+ ],
+ "execute": [
+ {
+ "proxy": {
+ "target-url": "https://openwhisk.ng.bluemix.net/api/some/action/path.http",
+ "verb": "keep"
+ }
+ }
+ ]
+ },
+ {
+ "operations": [
+ "postHello"
+ ],
+ "execute": [
+ {
+ "proxy": {
+ "target-url": "https://openwhisk.ng.bluemix.net/api/user@us.ibm.com/demo/createuser",
+ "verb": "keep"
+ }
+ }
+ ]
+ }
+ ],
+ "otherwise": []
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/scripts/lua/management/lib/swagger.lua b/tests/scripts/lua/management/lib/swagger.lua
new file mode 100644
index 0000000..b82bb0c
--- /dev/null
+++ b/tests/scripts/lua/management/lib/swagger.lua
@@ -0,0 +1,183 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+local cjson = require 'cjson'
+local lfs = require 'lfs'
+local swagger = require 'management/lib/swagger'
+local exampleBasePath = lfs.currentdir() .. '/scripts/lua/management/examples/'
+
+describe('Testing v2 management interface', function()
+ it('should parse native swagger correctly', function()
+ local expected = cjson.decode([[
+ {
+ "basePath": "/native",
+ "name": "Hello World API",
+ "resources": {
+ "/hello": {
+ "operations": {
+ "get": {
+ "backendUrl": "https://appconnect.mybluemix.net",
+ "policies": [
+ {
+ "type": "rateLimit",
+ "value": {
+ "scope": "api",
+ "subscription": "true",
+ "interval": 180,
+ "rate": 100
+ }
+ }
+ ],
+ "security": [
+ {
+ "type": "oauth2",
+ "provider": "google",
+ "scope": "api"
+ },
+ {
+ "type": "apiKey",
+ "header": "X-Api-Key",
+ "scope": "api"
+ }
+ ],
+ "backendMethod": "get"
+ }
+ }
+ }
+ }
+ }
+ ]])
+ local jsonPath = exampleBasePath .. 'example1.json'
+ local jsonTable = loadJsonTable(jsonPath)
+ local actual = swagger.parseSwagger(jsonTable)
+ assert.are.same(expected, actual)
+ end)
+
+ it('should parse whisk swagger correctly', function()
+ local expected = cjson.decode([[
+ {
+ "basePath": "/whisk",
+ "name": "Hello World API",
+ "resources": {
+ "/hello": {
+ "operations": {
+ "post": {
+ "backendUrl": "https://openwhisk.ng.bluemix.net/api/user@us.ibm.com/demo/createuser",
+ "policies": [
+ {
+ "type": "rateLimit",
+ "value": {
+ "scope": "api",
+ "subscription": "true",
+ "interval": 180,
+ "rate": 100
+ }
+ },
+ {
+ "type": "reqMapping",
+ "value": [{
+ "from": {
+ "value": "Basic xxx"
+ },
+ "to": {
+ "name": "Authorization",
+ "location": "header"
+ },
+ "action": "insert"
+ }]
+ }
+ ],
+ "security": [
+ {
+ "type": "oauth2",
+ "provider": "google",
+ "scope": "api"
+ },
+ {
+ "type": "apiKey",
+ "header": "X-Api-Key",
+ "scope": "api"
+ }
+ ],
+ "backendMethod": "post"
+ },
+ "get": {
+ "backendUrl": "https://openwhisk.ng.bluemix.net/api/some/action/path.http",
+ "policies": [
+ {
+ "type": "rateLimit",
+ "value": {
+ "scope": "api",
+ "subscription": "true",
+ "interval": 180,
+ "rate": 100
+ }
+ },
+ {
+ "type": "reqMapping",
+ "value": [{
+ "from": {
+ "value": "Basic xxx"
+ },
+ "to": {
+ "name": "Authorization",
+ "location": "header"
+ },
+ "action": "insert"
+ }]
+ }
+ ],
+ "security": [
+ {
+ "type": "oauth2",
+ "provider": "google",
+ "scope": "api"
+ },
+ {
+ "type": "apiKey",
+ "header": "X-Api-Key",
+ "scope": "api"
+ }
+ ],
+ "backendMethod": "get"
+ }
+ }
+ }
+ }
+ }
+ ]])
+ local jsonPath = exampleBasePath .. 'example2.json'
+ local jsonTable = loadJsonTable(jsonPath)
+ local actual = swagger.parseSwagger(jsonTable)
+ assert.are.same(expected.resources["/hello"].operations.post.policies[2].value, actual.resources["/hello"].operations.post.policies[2].value)
+ end)
+end)
+
+function loadJsonTable(path)
+ local contents
+ local file = io.open(path, "r" )
+ if file then
+ contents = file:read("*a")
+ local decoded = cjson.decode(contents)
+ io.close( file )
+ return decoded
+ end
+ return nil
+end
\ No newline at end of file