Merge pull request #40 from apimesh/rate-limit-group

Implement API rate limit policy
diff --git a/api-gateway-config/scripts/lua/lib/filemgmt.lua b/api-gateway-config/scripts/lua/lib/filemgmt.lua
index 2980a46..f805093 100644
--- a/api-gateway-config/scripts/lua/lib/filemgmt.lua
+++ b/api-gateway-config/scripts/lua/lib/filemgmt.lua
@@ -35,17 +35,21 @@
 -- @param resourceObj
 -- @return fileLocation location of created/updated conf file
 function _M.createResourceConf(baseConfDir, namespace, gatewayPath, resourceObj)
-  resourceObj = utils.serializeTable(cjson.decode(resourceObj))
+  local decoded = cjson.decode(resourceObj)
+  resourceObj = utils.serializeTable(decoded)
   local prefix = utils.concatStrings({"\tinclude /etc/api-gateway/conf.d/commons/common-headers.conf;\n",
                                       "\tset $upstream https://172.17.0.1;\n",
                                       "\tset $namespace ", namespace, ";\n",
-                                      "\tset $gatewayPath ", gatewayPath, ";\n\n"})
-  -- Set resource headers and mapping by calling routing.processCall()
+                                      "\tset $gatewayPath ", gatewayPath, ";\n"})
+  if decoded.apiId ~= nil then
+    prefix = utils.concatStrings({prefix, "\tset $apiId ", decoded.apiId, ";\n"})
+  end
+  -- Set route headers and mapping by calling routing.processCall()
   local outgoingResource = utils.concatStrings({"\taccess_by_lua_block {\n",
-                                             "\t\tlocal routing = require \"routing\"\n",
-                                             "\t\trouting.processCall(", resourceObj, ")\n",
-                                             "\t}\n\n",
-                                             "\tproxy_pass $upstream;\n"})
+                                                "\t\tlocal routing = require \"routing\"\n",
+                                                "\t\trouting.processCall(", resourceObj, ")\n",
+                                                "\t}\n\n",
+                                                "\tproxy_pass $upstream;\n"})
 
   -- Add to endpoint conf file
   os.execute(utils.concatStrings({"mkdir -p ", baseConfDir, namespace}))
diff --git a/api-gateway-config/scripts/lua/policies/rateLimit.lua b/api-gateway-config/scripts/lua/policies/rateLimit.lua
index 23d4836..a4b6e7b 100644
--- a/api-gateway-config/scripts/lua/policies/rateLimit.lua
+++ b/api-gateway-config/scripts/lua/policies/rateLimit.lua
@@ -31,31 +31,39 @@
 
 local _M = {}
 
-function limit(obj)
+function limit(obj, subHeader)
   local i = 60 / obj.interval
   local r = i * obj.rate
   r = utils.concatStrings({tostring(r), 'r/m'})
   local k
-   if obj.field == 'namespace' then
-     k = ngx.var.namespace
-   elseif obj.field == 'apikey' then
-     k = utils.concatStrings({ngx.var.namespace, '_', ngx.var['http_x_api_key']})
-   elseif obj.field == 'resource' then
-     k = utils.concatStrings({ngx.var.namespace, '_', ngx.var.gatewayPath})
-   end
+  -- Check scope
+  if obj.scope == 'tenant' then
+    k = utils.concatStrings({"tenant:", ngx.var.namespace})
+  elseif obj.scope == 'api' then
+    k = utils.concatStrings({"tenant:", ngx.var.namespace, ":api:", ngx.var.apiId})
+  elseif obj.scope == 'resource' then
+    k = utils.concatStrings({"tenant:", ngx.var.namespace, ":resource:", ngx.var.gatewayPath})
+  end
+  -- Check subscription
+  if obj.subscription ~= nil and obj.subscription == true and subHeader ~= nil then
+    apiKey = ngx.var[subHeader]
+    k = utils.concatStrings({k, ':subscription:', apiKey})
+  end
 
   local config = {
-     key = k,
-     zone = 'rateLimiting',
-     rate = r,
-     interval = obj.interval,
-     log_level = ngx.NOTICE,
-     rds = { host = REDIS_HOST, port = REDIS_PORT, pass = REDIS_PASS}
-   }
-  local ok = request.limit (config)
+    key = k,
+    zone = 'rateLimiting',
+    rate = r,
+    interval = obj.interval,
+    log_level = ngx.NOTICE,
+    rds = { host = REDIS_HOST, port = REDIS_PORT, pass = REDIS_PASS}
+  }
+  local ok = request.limit(config)
   if not ok then
+    ngx.status = 429
     logger.err('Rate limit exceeded. Sending 429')
-    ngx.exit(429)
+    ngx.say('Rate limit exceeded.')
+    ngx.exit(ngx.status)
   end
 end
 
diff --git a/api-gateway-config/scripts/lua/policies/security.lua b/api-gateway-config/scripts/lua/policies/security.lua
index 693f267..dce81fd 100644
--- a/api-gateway-config/scripts/lua/policies/security.lua
+++ b/api-gateway-config/scripts/lua/policies/security.lua
@@ -51,12 +51,16 @@
   local apiKey = ngx.var[h]
   if not apiKey then
     logger.err('No api-key passed. Sending 401')
-    ngx.exit(401)
+    ngx.status = 401
+    ngx.say('API key is required.')
+    ngx.exit(ngx.status)
   end
   local ok = validateAPIKey(namespace, gatewayPath, apiKey)
   if not ok then
     logger.err('api-key does not match. Sending 401')
-    ngx.exit(401)
+    ngx.status = 401
+    ngx.say('Invalid API Key.')
+    ngx.exit(ngx.status)
   end
 end
 
diff --git a/api-gateway-config/scripts/lua/routing.lua b/api-gateway-config/scripts/lua/routing.lua
index 4bce185..0ef2bd5 100644
--- a/api-gateway-config/scripts/lua/routing.lua
+++ b/api-gateway-config/scripts/lua/routing.lua
@@ -51,6 +51,7 @@
     k = string.upper(k)
     if k == verb then
       -- Check if auth is required
+      local subHeader
       if (v.security and string.lower(v.security.type) == 'apikey') then
         local h = v.security.header
         if h == nil then
@@ -58,7 +59,8 @@
         else
           h = utils.concatStrings({'http_', h})
         end
-        security.processAPIKey(h:gsub("-", "_"))
+        subHeader = h:gsub("-", "_")
+        security.processAPIKey(subHeader)
       end
       local u = url.parse(v.backendUrl)
       ngx.req.set_uri(u.path)
@@ -67,7 +69,7 @@
         setVerb(v.backendMethod)
       end
       if v.policies ~= nil then
-        parsePolicies(v.policies)
+        parsePolicies(v.policies, subHeader)
       end
       found = true
       break
@@ -80,13 +82,14 @@
 end
 
 --- Function to read the list of policies and send implementation to the correct backend
--- @param obj List of policies containing a type and value field. This function reads the type field and resources it appropriately.
-function parsePolicies(obj)
+-- @param obj List of policies containing a type and value field. This function reads the type field and routes it appropriately.
+-- @param subHeader optional subscription header
+function parsePolicies(obj, subHeader)
   for k, v in pairs (obj) do
     if v.type == 'reqMapping' then
       mapping.processMap(v.value)
     elseif v.type == 'rateLimit' then
-      rateLimit.limit(v.value)
+      rateLimit.limit(v.value, subHeader)
     end
   end
 end