diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml new file mode 100644 index 0000000..d27c9db --- /dev/null +++ b/.github/workflows/proxy-tests.yml @@ -0,0 +1,50 @@ +name: Auth Layer Proxy Tests + +on: + pull_request: + branches: [ main, release/**] + push: + branches: [ main, release/*] + tags: [ v* ] + +jobs: + proxy-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Lua + uses: leafo/gh-actions-lua@v8 + with: + luaVersion: '5.3' + + - name: Install LuaRocks + uses: leafo/gh-actions-luarocks@v4 + + - name: Install lunatest + run: luarocks install lunatest + + - name: Install luacov + run: luarocks install luacov + + - name: Install luacov-console + run: luarocks install luacov-console + + - name: Install cjson + run: luarocks install lua-cjson + + - name: Install luasocket + run: luarocks install luasocket + + - name: Run tests + run: lua test.lua + working-directory: auth-layer-proxy/tests + + - name: Generate coverage report + run: luacov + working-directory: auth-layer-proxy/tests + + - name: Generate Console Report + run: luacov-console ../filters/ && luacov-console ../filters/ -s && luacov-console ../filters/ -s > coverage.txt + working-directory: auth-layer-proxy/tests diff --git a/.gitignore b/.gitignore index 52eb608..84f9dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,7 @@ charts/auth-layer-server/Chart.lock # DS_Store .DS_Store +auth-layer-proxy/tests/coverage.txt +auth-layer-proxy/tests/luacov.report.out +auth-layer-proxy/tests/luacov.report.out.index +auth-layer-proxy/tests/luacov.stats.out diff --git a/auth-layer-proxy/Dockerfile b/auth-layer-proxy/Dockerfile new file mode 100644 index 0000000..b7e3620 --- /dev/null +++ b/auth-layer-proxy/Dockerfile @@ -0,0 +1,13 @@ +FROM envoyproxy/envoy:v1.28-latest + +# Copy the Filter Scripts +COPY /filters/ /filters/ + +# Install Lua and Luarocks +RUN apt-get update && apt-get install -y lua5.1 luarocks git + +# Install Lua modules +RUN luarocks install lua-cjson + +# Install http socket module +RUN luarocks install luasocket diff --git a/auth-layer-proxy/README.md b/auth-layer-proxy/README.md new file mode 100644 index 0000000..d4e6dc3 --- /dev/null +++ b/auth-layer-proxy/README.md @@ -0,0 +1,189 @@ +# Readme + +This is a token verification auth-layer-proxy for Hedera-The-Graph implementation that will allow a node operator to publish a secured `admin port` of the-graph deployment for Hedera. + +Uses EnvoyProxy as a reverse proxy that handles the token verification. The token is verified using the OAuth 2.0 token server and the token claims are validated for the required roles and subgraph access. + +```mermaid +--- +title: Overall Architecture +--- +flowchart LR + A[Graph Cli] --->|1. Create / Deploy Subgraph| B(Auth-Layer) + B ---> | 4. Relay Request on Admin Port| F(TheGraph Service) + B --> | 3. 401 Unauthorized | A + F ---> | 5. response| B --> | 6. response| A + B <--> | 2. Check Token| db[(Database)] + + linkStyle 2 stroke:#ff0000,stroke-width:2px; + linkStyle 3 stroke:#00ff00,stroke-width:2px; + linkStyle 4 stroke:#00ff00,stroke-width:2px; + +``` +More information on the **Authorization Layer** can be found [here](https://github.com/hashgraph/hedera-the-graph/blob/main/docs/design/auth-layer.md) + +## Overview + +This is an implementation of EnvoyProxy filters for authentication and authorization. It is a custom config with http filters using Lua scripts for performing the following actions: + +1. JSON Validation +2. Token Extraction +4. Payload Params Extraction +5. Token Validation using JWT and Instrospect endpoint +6. Proxy Routing Configuration (using EnvoyProxy itself) + +it includes a Dockerfile for building the image and a docker-compose file for running the container. + +## Pre-requisites + +### OAUTH 2.0 Token Server +This auth-token validation proxy layer relies on an OAuth 2.0 token server for token issuace and validation. The token server should be able to issue and validate the token using the `client_id` and `client_secret` provided in the `.env` file. + +So make sure to have a token server running that is previously configured with a Client ID and Client Secret, and the `/token` and `/token/introspection` endpoints are accessible. + +### Token structure + +Make sure that the access token has the following claims: +- realm_access.roles: A list of roles that the user has. The roles are used to determine the access level of the user. +- subgraph_access: A list of subgraph names that the user has access to. The subgraph names are used to determine the access level of the user. +- active: A boolean value that indicates if the user is active or not. +- email_verified: A boolean value that indicates if the user's email is verified or not. +- email: The email of the user. + + ```json + { + "exp": 1711427468, + "iat": 1711391468, + "jti": "2fab170f-beb1-4821-acb4-ac19a71c9abe", + "iss": "http://host.docker.internal:8080/realms/HederaTheGraph", + "aud": "account", + "sub": "f60ffb03-d17f-4aa2-a34a-2c4891059c3c", + "typ": "Bearer", + "azp": "htg-auth-layer", + "session_state": "79fbb78a-3279-463e-8ee0-6ab37e06bcc2", + "acr": "1", + "allowed-origins": [ + "/*" + ], + "realm_access": { + "roles": [ + "default-roles-hederathegraph", + "subgraph_remove", + "subgraph_create", + "subgraph_deploy", + "offline_access", + "uma_authorization", + "subgraph_resume", + "subgraph_pause" + ] + }, + "scope": "profile subgraph_access email", + "subgraph_access": "", + "email_verified": true, + "active": true, + "email": "user1@gmail.com", + "client_id": "htg-auth-layer" + } + ``` + +For instructions on how to set-up the Auth Provider using KeyCloak, refer to the `Auth-Layer-Server` [README](https://github.com/hashgraph/hedera-the-graph/tree/main/charts/auth-layer-server) + +## Usage + +### Build the image + +```bash + +docker build -t envoy-auth-proxy . + +``` + +### Configure the environment + +Add Postgres or Redis credentials to the .env file + +``` +# OAuth +CLIENT_ID= +CLIENT_SECRET= +TOKEN_INTROSPECTION_URL=http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect + +``` + +### Configure the details of the service to be proxied on the envoy.yam +Edit `envoy-auth.yaml` file with config needs, by default will be proxying/relaying the request to address: `host.docker.internal` and port `8020` + +```yaml + clusters: + - name: local_service + connect_timeout: 5s + type: LOGICAL_DNS + load_assignment: + cluster_name: local_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: host.docker.internal + port_value: 8020 +``` + + +### Run the container + + +**Start the container:** + +```bash +docker-compose up +``` + +### Test the service + +```bash +curl --location 'http://localhost:10000' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer 12345' \ +--data '{ + "jsonrpc": "2.0", + "id": "2", + "method": "deploy_subgraph", + "params": { + "name": "test" + } +}' +``` + + +## Testing + +The tests are written using lunatest and can be run using the following command: + +Make sure to have installed the following prerequisites: +1. lua +2. luarocks + +Install the following luarocks packages: + +```bash +luarocks install lua-cjson +luarocks install luasocket +luarocks install lunatest +luarocks install luacov +luarocks install luacov-console +``` + +Open a terminal and navigate to the folder containing the `tests` folder and run the following command: + +```bash +lua test.lua +``` + +to show the coverage report, run the following command: + +```bash +luacov +luacov-console ../filters/ +luacov-console ../filters/ -s +``` \ No newline at end of file diff --git a/auth-layer-proxy/configs/envoy-auth.yaml b/auth-layer-proxy/configs/envoy-auth.yaml new file mode 100644 index 0000000..7a15c07 --- /dev/null +++ b/auth-layer-proxy/configs/envoy-auth.yaml @@ -0,0 +1,49 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: edge + http_filters: + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + filename: /filters/TokenVerificationFilter.lua + + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + virtual_hosts: + - name: all_domains + domains: ["*"] + routes: + - match: + prefix: "/" + headers: + name: ":method" + exact_match: "POST" + route: + cluster: local_service + + clusters: + - name: local_service + connect_timeout: 5s + type: LOGICAL_DNS + load_assignment: + cluster_name: local_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: host.docker.internal + port_value: 8020 diff --git a/auth-layer-proxy/docker-compose.yaml b/auth-layer-proxy/docker-compose.yaml new file mode 100644 index 0000000..1d4e147 --- /dev/null +++ b/auth-layer-proxy/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3' + +services: + envoy: + image: envoy-auth-layer:latest + command: -c /configs/envoy-auth.yaml + env_file: + - .env + volumes: + - ./configs/:/configs/ + - ./filters/:/filters/ + ports: + - "9901:9901" + - "10000:10000" + stdin_open: true + tty: true diff --git a/auth-layer-proxy/example.env b/auth-layer-proxy/example.env new file mode 100644 index 0000000..46622ae --- /dev/null +++ b/auth-layer-proxy/example.env @@ -0,0 +1,4 @@ +# OAuth +CLIENT_ID=htg-auth-layer +CLIENT_SECRET=0cyYtDVVbVvaZjrDViiw4p2kegTy9Q5X +TOKEN_INTROSPECTION_URL=http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect diff --git a/auth-layer-proxy/filters/TokenVerificationFilter.lua b/auth-layer-proxy/filters/TokenVerificationFilter.lua new file mode 100644 index 0000000..94d5460 --- /dev/null +++ b/auth-layer-proxy/filters/TokenVerificationFilter.lua @@ -0,0 +1,214 @@ +-- Hedera-The-Graph +-- +-- Copyright (C) 2024 Hedera Hashgraph, LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local TokenVerificationFilter = {} + +local cjson = require("cjson") +local http = require("socket.http") +local ltn12 = require("ltn12") + +-- Configuration: Replace these with your actual details +local introspectionUrl = os.getenv("TOKEN_INTROSPECTION_URL") or nil +local clientId = os.getenv("CLIENT_ID") or nil +local clientSecret = os.getenv("CLIENT_SECRET") or nil + +local function extractToken(request_handle) + + local authHeader = request_handle:headers():get("Authorization") + if not authHeader then + return nil + end + + -- Remove the "Bearer " prefix from the token + return authHeader:gsub("Bearer%s+", "") + +end + +local function parseJsonBody(body) + local success, jsonBody = pcall(cjson.decode, body) + if not success then + return nil, nil, "INVALID JSON BODY" + end + return jsonBody.method, jsonBody.params and jsonBody.params.name, nil +end + +local function verifyValidMethod(method) + if( (method == "subgraph_deploy") or + (method == "subgraph_create") or + (method == "subgraph_remove") or + (method == "subgraph_pause") or + (method == "subgraph_resume") ) + then + return true, nil + end + + return false, "Invalid method" +end + +-- Function to check if a value exists in a table +local function contains(table, value) + for _, v in ipairs(table) do + if v == value then + return true + end + end + return false +end + +-- Function to check if subgraphName is present in the "subgraph_access" claim +local function checkSubgraphAccessClaim(result, subgraphName) + local subgraphAccessClaim = result.subgraph_access + if subgraphAccessClaim then + local subgraphList = {} + -- Split the comma-separated string into a table + for value in subgraphAccessClaim:gmatch("[^,]+") do + table.insert(subgraphList, value) + end + return contains(subgraphList, subgraphName) + end + return false +end + +-- Function to check if the "method" parameter is included in roles +local function checkMethodInRoles(result, method) + local roles = result.realm_access.roles + if roles then + return contains(roles, method) + end + return false +end + +-- Variable to store the user associated with the token +local tokenUser = ""; + +local function checkTokenPermissions(token, subgraphName, method) + + -- Prepare the HTTP request body + local requestBody = "token=" .. token .. "&client_id=" .. clientId .. "&client_secret=" .. clientSecret + + -- Prepare the HTTP request headers + local headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + ["Content-Length"] = string.len(requestBody) + } + + -- Prepare a table to collect the response + local responseBody = {} + + -- Perform the HTTP POST request + local response, statusCode, responseHeaders, statusText = http.request({ + method = "POST", + url = introspectionUrl, + headers = headers, + source = ltn12.source.string(requestBody), + sink = ltn12.sink.table(responseBody) + }) + + -- Check if the request was successful + if statusCode == 200 then + -- Concatenate the response body table into a string + responseBody = table.concat(responseBody) + + -- Parse the JSON response + local result = cjson.decode(responseBody) + + -- Check if the token is active + if result.active then + + -- check if the token claims has subgraph_access claim. + if not result.subgraph_access then + return false, "subgraph_access claim not found in token" + end + + local subgraphAccessGranted = checkSubgraphAccessClaim(result, subgraphName) + local methodAccessGranted = checkMethodInRoles(result, method) + + -- Set the token user for logging purposes + tokenUser = result.email + print("Token verification successful for user: " .. tokenUser .. ", subgraph: " .. subgraphName .. " and method: " .. method) + + if not result.email_verified then + return false, "Email not verified for user: " .. tokenUser + end + + if not subgraphAccessGranted then + return false, "Subgraph access not granted for '" .. subgraphName .. "'" + end + + if not methodAccessGranted then + return false, "Access denied for method: " .. method + end + else + return false, "Token is invalid or expired." + end + else + return false, "Failed to introspect token. Status: " .. tostring(statusCode) + end + + return true, nil +end + +-- This function is called for each request, and is the entry point for the filter +function envoy_on_request(request_handle) + local token = TokenVerificationFilter.extractToken(request_handle) + + if not token then + request_handle:respond({[":status"] = "401"}, "No token provided") + return + end + + local body = request_handle:body():getBytes(0, request_handle:body():length()) + local method, subgraphName, parseError = TokenVerificationFilter.parseJsonBody(body) + if parseError then + request_handle:respond({[":status"] = "400"}, parseError) + return + end + + if not method or not subgraphName then + request_handle:respond({[":status"] = "400"}, "Invalid request body") + return + end + + if not TokenVerificationFilter.verifyValidMethod(method) then + request_handle:respond({[":status"] = "400"}, "Invalid method") + return + end + + local hasPermission, permissionError = TokenVerificationFilter.checkTokenPermissions(token, subgraphName, method) + + if not hasPermission or permissionError then + request_handle:logErr(permissionError) + request_handle:respond({[":status"] = "401"}, permissionError) + return + end + + print("Token is authorized for method: ".. method .. " and subgraph: " .. subgraphName.. " by the user".. tokenUser) + -- The request is authorized and processing continues +end + +print("TokenVerificationFilter loaded") + +-- Return the filter as a module for test purposes +TokenVerificationFilter.extractToken = extractToken +TokenVerificationFilter.parseJsonBody = parseJsonBody +TokenVerificationFilter.verifyValidMethod = verifyValidMethod +TokenVerificationFilter.checkTokenPermissions = checkTokenPermissions +TokenVerificationFilter.envoy_on_request = envoy_on_request + +-- need to export the http module also, to be able to mock it. +TokenVerificationFilter.http = http + +return TokenVerificationFilter diff --git a/auth-layer-proxy/tests/.luacov b/auth-layer-proxy/tests/.luacov new file mode 100644 index 0000000..436e035 --- /dev/null +++ b/auth-layer-proxy/tests/.luacov @@ -0,0 +1,3 @@ +modules = { + ["TokenVerificationFilter"] = "../filters/TokenVerificationFilter.lua", +} diff --git a/auth-layer-proxy/tests/test.lua b/auth-layer-proxy/tests/test.lua new file mode 100644 index 0000000..5845681 --- /dev/null +++ b/auth-layer-proxy/tests/test.lua @@ -0,0 +1,17 @@ +pcall(require, "luacov") --measure code coverage, if luacov is present +local lunatest = require "lunatest" + + +print '==============================' +print('Tests of auth-layer-proxy') +print("Tests are being executed on current directory: " .. os.getenv("PWD")) +print '==============================' + +lunatest.suite("test_checkTokenPermissions") +lunatest.suite("test_extractToken") +lunatest.suite("test_parseJsonBody") +lunatest.suite("test_verifyValidMethod") +lunatest.suite("test_envoy_on_request") + + +lunatest.run() diff --git a/auth-layer-proxy/tests/testMocks.lua b/auth-layer-proxy/tests/testMocks.lua new file mode 100644 index 0000000..94273e7 --- /dev/null +++ b/auth-layer-proxy/tests/testMocks.lua @@ -0,0 +1,76 @@ +local cjson = require("cjson") + +-- Mock for HTTP request +local function mockHttpRequest(params) + + local requestBody = params.source() + local token, clientId, clientSecret = requestBody:match("token=(.*)&client_id=(.*)&client_secret=(.*)") + + -- You can adjust the response based on the input parameters for different test cases + if token == "validToken" then + statusCode = 200 + response = cjson.encode({ + active = true, + subgraph_access = "subgraph1,subgraph2", + email = "user@example.com", + email_verified = true, + ["realm_access"] = { -- Use square brackets and quotes for the key + roles = { -- Use equals sign for assignment + "subgraph_create", + "subgraph_deploy", + "subgraph_resume", + "subgraph_pause" + } + } + }) + params.sink(response) + return 1, statusCode, nil, nil + elseif token == "emailNotVerified" then + statusCode = 200 + response = cjson.encode({ + active = true, + subgraph_access = "subgraph1,subgraph2", + email = "test@email.com", + email_verified = false, + ["realm_access"] = { -- Use square brackets and quotes for the key + roles = { -- Use equals sign for assignment + "subgraph_remove", + "subgraph_create", + "subgraph_deploy", + "subgraph_resume", + "subgraph_pause" + } + } + }) + params.sink(response) + return 1, statusCode, nil, nil + + elseif token == "invalidToken" then + statusCode = 200 + response = cjson.encode({ + active = false + }) + params.sink(response) + return 1, statusCode, nil, nil + else + -- Simulate a failed HTTP request + statusCode = 500 + statusText = "Internal Server Error" + return 1, statusCode, statusText, nil + end +end + +local function mockGetEnv(envVarName) + local envVars = { + CLIENT_ID = "htg-auth_layer", + CLIENT_SECRET = "wEGsZafep01LKNPkJhiOMQSmgAGAMWUi", + TOKEN_INTROSPECTION_URL = "http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect" + } + return envVars[envVarName] +end + +-- export module +local testMocks = {} +testMocks.mockHttpRequest = mockHttpRequest +testMocks.mockGetEnv = mockGetEnv +return testMocks diff --git a/auth-layer-proxy/tests/testUtils.lua b/auth-layer-proxy/tests/testUtils.lua new file mode 100644 index 0000000..fc9eb38 --- /dev/null +++ b/auth-layer-proxy/tests/testUtils.lua @@ -0,0 +1,22 @@ +testUtils = {} + +function printGreen(text) + print("\27[32m" .. text .. "\27[0m") +end + +-- Function to print text in red +function printRed(text) + print("\27[31m" .. text .. "\27[0m") +end + +function wrapTextInRed(text) + return "\27[31m" .. text .. "\27[0m" +end + + +-- export functions +testUtils.printGreen = printGreen +testUtils.printRed = printRed +testUtils.wrapTextInRed = wrapTextInRed + +return testUtils diff --git a/auth-layer-proxy/tests/test_checkTokenPermissions.lua b/auth-layer-proxy/tests/test_checkTokenPermissions.lua new file mode 100644 index 0000000..6543002 --- /dev/null +++ b/auth-layer-proxy/tests/test_checkTokenPermissions.lua @@ -0,0 +1,89 @@ +local lunatest = package.loaded.lunatest +local assert_true, assert_false, assert_equal = lunatest.assert_true, lunatest.assert_false, lunatest.assert_equal + + + +-- Load common test utilities +local testUtils = require("testUtils") +local mocks = require("testMocks") + +-- Save the original os.getenv +local originalGetEnv = os.getenv + +-- Test Setup: Replace os.getenv with the mock +os.getenv = mocks.mockGetEnv + +-- Load the module to be tested, assuming our working directory is /tests +package.path = package.path .. ";../filters/?.lua" -- we need this since the module to test is not on the same folder +local tokenVerificationFilter = require("TokenVerificationFilter") -- The Lua file to test +local checkTokenPermissions = tokenVerificationFilter.checkTokenPermissions -- The function to test + +-- Replace the http.request with the mock +tokenVerificationFilter.http.request = mocks.mockHttpRequest + +-- Test case 1: Successful token verification with all claims present +local function test_checkTokenPermissions_success() + local result, errorMessage = checkTokenPermissions("validToken", "subgraph1", "subgraph_deploy") + assert_true(result, testUtils.wrapTextInRed("checkTokenPermissions should succeed when all conditions are met. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 1: Successful token verification with all claims present") +end + +-- Test case 2: Token is invalid or expired +local function test_checkTokenPermissions_invalidToken() + local result, errorMessage = checkTokenPermissions("invalidToken", "subgraph1", "subgraph_deploy") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the token is invalid or expired. [FAIL] ✗")) + assert_equal("Token is invalid or expired.", errorMessage, "Error message should indicate invalid or expired token") + testUtils.printGreen("✓ [PASS] Test case 2: Token is invalid or expired") +end + +-- Test case 3: Failed to introspect token (simulate HTTP request failure) +local function test_checkTokenPermissions_httpRequestFailure() + local result, errorMessage = checkTokenPermissions("anyTokenCausingFailure", "subgraph1", "subgraph_deploy") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the HTTP request fails. [FAIL] ✗")) + assert(errorMessage:find("Failed to introspect token"), "Error message should indicate a failure in token introspection due to HTTP request failure [FAIL] ✗") + testUtils.printGreen("✓ [PASS] Test case 3: Failed to introspect token (simulate HTTP request failure)") +end + +-- Test Case 4: token is active but does not have the required permission to subgraph +local function test_checkTokenPermissions_noPermission() + local result, errorMessage = checkTokenPermissions("validToken", "subgraph3", "subgraph_create") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the token does not have the required permission. [FAIL] ✗")) + assert_equal("Subgraph access not granted for 'subgraph3'", errorMessage, testUtils.wrapTextInRed("Error message should indicate unauthorized access [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 4: token is active but does not have the required permission to subgraph") +end + +-- Test Case 5: Token is active and valid but does not have email verified +local function test_checkTokenPermissions_emailNotVerified() + local result, errorMessage = checkTokenPermissions("emailNotVerified", "subgraph1", "subgraph_deploy") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the token does not have the required permission. [FAIL] ✗")) + assert_equal("Email not verified for user: test@email.com", errorMessage, testUtils.wrapTextInRed("Error message should indicate unauthorized access [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 5: Token is active and valid but does not have email verified") +end + +-- Test Case 6: Token is active and valid but method is not Valid +local function test_checkTokenPermissions_invalidMethod() + local result, errorMessage = checkTokenPermissions("validToken", "subgraph1", "invalid_method") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the token does not have the required permission. [FAIL] ✗")) + assert_equal("Access denied for method: invalid_method", errorMessage, testUtils.wrapTextInRed("Error message should indicate unauthorized access [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 6: Token is active and valid but method is not Valid") +end + +-- Test Case 7: Token is active and valid but does not have permission for the method +local function test_checkTokenPermissions_noPermissionForMethod() + local result, errorMessage = checkTokenPermissions("validToken", "subgraph1", "subgraph_remove") + assert_false(result, testUtils.wrapTextInRed("checkTokenPermissions should fail when the token does not have the required permission. [FAIL] ✗")) + assert_equal("Access denied for method: subgraph_remove", errorMessage, testUtils.wrapTextInRed("Error message should indicate unauthorized access [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 7: Token is active and valid but does not have permission for the method") +end + +-- @module suite-checkTokenPermissions +local test_checkTokenPermissions = {} +test_checkTokenPermissions.test_checkTokenPermissions_success = test_checkTokenPermissions_success +test_checkTokenPermissions.test_checkTokenPermissions_invalidToken = test_checkTokenPermissions_invalidToken +test_checkTokenPermissions.test_checkTokenPermissions_httpRequestFailure = test_checkTokenPermissions_httpRequestFailure +test_checkTokenPermissions.test_checkTokenPermissions_noPermission = test_checkTokenPermissions_noPermission +test_checkTokenPermissions.test_checkTokenPermissions_emailNotVerified = test_checkTokenPermissions_emailNotVerified +test_checkTokenPermissions.test_checkTokenPermissions_invalidMethod = test_checkTokenPermissions_invalidMethod +test_checkTokenPermissions.test_checkTokenPermissions_noPermissionForMethod = test_checkTokenPermissions_noPermissionForMethod + +return test_checkTokenPermissions diff --git a/auth-layer-proxy/tests/test_envoy_on_request.lua b/auth-layer-proxy/tests/test_envoy_on_request.lua new file mode 100644 index 0000000..df6fee4 --- /dev/null +++ b/auth-layer-proxy/tests/test_envoy_on_request.lua @@ -0,0 +1,170 @@ +local lunatest = package.loaded.lunatest +local assert_true, assert_false, assert_equal = lunatest.assert_true, lunatest.assert_false, lunatest.assert_equal + + + +-- Load common test utilities +local testUtils = require("testUtils") +local mocks = require("testMocks") + +-- Save the original os.getenv +local originalGetEnv = os.getenv + +-- Test Setup: Replace os.getenv with the mock +os.getenv = mocks.mockGetEnv + +-- Load the module to be tested, assuming our working directory is /tests +package.path = package.path .. ";../filters/?.lua" -- we need this since the module to test is not on the same folder +local tokenVerificationFilter = require("TokenVerificationFilter") -- The Lua file to test +local envoy_on_request = tokenVerificationFilter.envoy_on_request -- The function to test + +-- Mock request_handle object +local request_handle = { + body = function() + return { + length = function() return 0 end, + getBytes = function(start, finish) return "" end + } + end, + respond = function(handler, statusCode, message) + + response = { + statusCode = statusCode[":status"], + message = message + } + end, + logErr = function(error_message) + -- Capture the error log for assertion + error_log = error_message + end, + +} + + +-- Test case 1: No token provided, should return 401 +local function test_noTokenProvided() + + -- Mock extractToken to return nil + tokenVerificationFilter.extractToken = function(request_handle) + return nil + end + tokenVerificationFilter.envoy_on_request(request_handle) + + assert_equal(response.statusCode, "401", testUtils.wrapTextInRed("No token provided should return 401. [FAIL] ✗")) + assert_equal(response.message, "No token provided", testUtils.wrapTextInRed("No token provided should return correct error message. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 1: No token provided") +end + +-- Test case 2: Invalid JSON, should return 400 +local function test_parseError() + + -- Mock extractToken to return "token" + tokenVerificationFilter.extractToken = function(request_handle) + return "token" + end + + -- Mock parseJsonBody + tokenVerificationFilter.parseJsonBody = function(body) + return nil, nil, "INVALID JSON BODY" + end + + + tokenVerificationFilter.envoy_on_request(request_handle) + + assert_equal(response.statusCode, "400", testUtils.wrapTextInRed("Invalid JSON should return 400. [FAIL] ✗")) + assert_equal(response.message, "INVALID JSON BODY", testUtils.wrapTextInRed("Invalid JSON should return correct error message. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 2: Invalid JSON") + +end + +-- Test case 3: Invalid Method, should return 400 +local function test_invalidMethod() + -- Mock extractToken to return "token" + tokenVerificationFilter.extractToken = function(request_handle) + return "token" + end + + -- Mock parseJsonBody + tokenVerificationFilter.parseJsonBody = function(body) + return "subgraph1", "subgraph_deploy", nil + end + + -- Mock verifyValidMethod to return true + tokenVerificationFilter.verifyValidMethod = function(method) + return false + end + + tokenVerificationFilter.envoy_on_request(request_handle) + + assert_equal(response.statusCode, "400", testUtils.wrapTextInRed("Invalid method should return 400. [FAIL] ✗")) + assert_equal(response.message, "Invalid method", testUtils.wrapTextInRed("Invalid method should return correct error message. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 3: Invalid method") +end + +-- Test case 4: hasPermission is false, should return 401 +local function test_noPermission() + -- Mock extractToken to return "token" + tokenVerificationFilter.extractToken = function(request_handle) + return "token" + end + + -- Mock parseJsonBody + tokenVerificationFilter.parseJsonBody = function(body) + return "subgraph1", "subgraph_deploy", nil + end + + -- Mock verifyValidMethod to return true + tokenVerificationFilter.verifyValidMethod = function(method) + return true + end + + -- Mock checkTokenPermissions to return false + tokenVerificationFilter.checkTokenPermissions = function(token, subgraphName, method) + return false, "No permission" + end + + tokenVerificationFilter.envoy_on_request(request_handle) + + assert_equal(response.statusCode, "401", testUtils.wrapTextInRed("No permission, should return 401. [FAIL] ✗")) + assert_equal(response.message, "No permission", testUtils.wrapTextInRed("No permission should return correct error message. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 4: No permission") +end + + +-- Test case 5: hasPermission is true, should return 200 +local function test_hasPermission() + -- Mock extractToken to return "validToken" + tokenVerificationFilter.extractToken = function(request_handle) + return "validToken" + end + + -- Mock parseJsonBody + tokenVerificationFilter.parseJsonBody = function(body) + return "subgraph1", "subgraph_deploy", nil + end + + -- Mock verifyValidMethod to return true + tokenVerificationFilter.verifyValidMethod = function(method) + return true + end + + -- Mock checkTokenPermissions to return true + tokenVerificationFilter.checkTokenPermissions = function(token, subgraphName, method) + return true, nil + end + tokenVerificationFilter.envoy_on_request(request_handle) + testUtils.printGreen("✓ [PASS] Test case 5: Has permission") + +end + + +-- @module suite-envoy_on_request +local test_envoy_on_request = {} + +test_envoy_on_request.test_noTokenProvided = test_noTokenProvided +test_envoy_on_request.test_parseError = test_parseError +test_envoy_on_request.test_invalidMethod = test_invalidMethod +test_envoy_on_request.test_noPermission = test_noPermission +test_envoy_on_request.test_hasPermission = test_hasPermission + +return test_envoy_on_request diff --git a/auth-layer-proxy/tests/test_extractToken.lua b/auth-layer-proxy/tests/test_extractToken.lua new file mode 100644 index 0000000..6c639d3 --- /dev/null +++ b/auth-layer-proxy/tests/test_extractToken.lua @@ -0,0 +1,57 @@ +local lunatest = package.loaded.lunatest +local assert_true, assert_false = lunatest.assert_true, lunatest.assert_false +-- Load the module to be tested +package.path = package.path .. ";../filters/?.lua" +-- Load common test utilities +local testUtils = require("testUtils") + +local token_verification = require("TokenVerificationFilter") -- The Lua file to test + +-- Refined mock request_handle object +local function mockRequestHandle(authHeader) + return { + headers = function() + return { + get = function(_, headerName) + if headerName == "Authorization" then + return authHeader + end + end + } + end + } +end + +-- Test case 1: Authorization header present +local function test_extractToken_withAuthHeader() + local request_handle = mockRequestHandle("Bearer abc123") + local token = token_verification.extractToken(request_handle) + assert_true(token == "abc123", testUtils.wrapTextInRed("extractToken should return the correct token. [FAIL] ✗")) + testUtils.printGreen("✓ [PASS] Test case 1: Authorization header present ") +end + +-- Test case 2: Authorization header present, but without "Bearer +local function test_extractToken_withAuthHeaderWithoutBearerKeyword() + --print("\n") + local request_handle = mockRequestHandle("abc123") + local token = token_verification.extractToken(request_handle) + assert(token == "abc123", "extractToken should return the correct token. [FAIL] ✗") + testUtils.printGreen("✓ [PASS] Test case 2: Authorization header present, but without Bearer") +end + +-- Test case 3: Authorization header absent +local function test_extractToken_withoutAuthHeader() + local request_handle = mockRequestHandle(nil) + local token = token_verification.extractToken(request_handle) + assert(token == nil, "extractToken should return nil when Authorization header is absent. [FAIL] ✗") + testUtils.printGreen("✓ [PASS] Test case 3: Authorization header absent") +end + +-- @module suite-extractToken +local test_extractToken = {} + +test_extractToken.test_extractToken_withAuthHeader = test_extractToken_withAuthHeader +test_extractToken.test_extractToken_withAuthHeaderWithoutBearerKeyword = test_extractToken_withAuthHeaderWithoutBearerKeyword +test_extractToken.test_extractToken_withoutAuthHeader = test_extractToken_withoutAuthHeader + +return test_extractToken diff --git a/auth-layer-proxy/tests/test_parseJsonBody.lua b/auth-layer-proxy/tests/test_parseJsonBody.lua new file mode 100644 index 0000000..651ec3a --- /dev/null +++ b/auth-layer-proxy/tests/test_parseJsonBody.lua @@ -0,0 +1,113 @@ +local lunatest = package.loaded.lunatest +local assert_equal = lunatest.assert_equal +local assert_nil = lunatest.assert_nil +local assert_not_nil = lunatest.assert_not_nil +-- Load the module to be tested +package.path = package.path .. ";../filters/?.lua" +-- Load common test utilities +local testUtils = require("testUtils") +local tokenVerificationFilter = require("TokenVerificationFilter") -- The Lua file to test +local parseJsonBody = tokenVerificationFilter.parseJsonBody + +-- Test for valid JSON input +local function test_valid_json() + local body = '{"method": "greet", "params": {"name": "Lua"}}' + local method, name, error = parseJsonBody(body) + assert_equal("greet", method) + assert_equal("Lua", name) + assert_nil(error) + testUtils.printGreen("✓ [PASS] Test for valid JSON input") +end + +-- Test for invalid JSON input +local function test_invalid_json() + local body = '{"method": "greet", "params": {"name": "Lua"' -- Missing closing bracket + local method, name, error = parseJsonBody(body) + assert_nil(method) + assert_nil(name) + assert_not_nil(error) + assert_equal("INVALID JSON BODY", error) + + testUtils.printGreen("✓ [PASS] Test for invalid JSON input") +end + +-- Test for JSON without params +local function test_json_without_params() + local body = '{"method": "goodbye"}' + local method, name, error = parseJsonBody(body) + assert_equal("goodbye", method) + assert_nil(name) + assert_nil(error) + + testUtils.printGreen("✓ [PASS] Test for JSON without params") +end + +-- Test for JSON without method +local function test_json_without_method() + local body = '{"params": {"name": "Lua"}}' + local method, name, error = parseJsonBody(body) + assert_nil(method) + assert_not_nil(name) + assert_nil(error) + + testUtils.printGreen("✓ [PASS] Test for JSON without method") +end + +-- Test for empty JSON +local function test_empty_json() + local body = '{}' + local method, name, error = parseJsonBody(body) + assert_nil(method) + assert_nil(name) + assert_nil(error) + + testUtils.printGreen("✓ [PASS] Test for empty JSON") +end + +-- Test for empty body +local function test_empty_body() + local body = '' + local method, name, error = parseJsonBody(body) + assert_nil(method) + assert_nil(name) + assert_not_nil(error) + assert_equal("INVALID JSON BODY", error) + + testUtils.printGreen("✓ [PASS] Test for empty body") +end + +-- Test for nil body +local function test_nil_body() + local method, name, error = parseJsonBody(nil) + assert_nil(method) + assert_nil(name) + assert_not_nil(error) + assert_equal("INVALID JSON BODY", error) + + testUtils.printGreen("✓ [PASS] Test for nil body") +end + +-- Test for non-JSON body +local function test_non_json_body() + local body = "This is not a JSON body" + local method, name, error = parseJsonBody(body) + assert_nil(method) + assert_nil(name) + assert_not_nil(error) + assert_equal("INVALID JSON BODY", error) + + testUtils.printGreen("✓ [PASS] Test for non-JSON body") +end + +-- export suite +local test_parseJsonBody = {} +test_parseJsonBody.test_valid_json = test_valid_json +test_parseJsonBody.test_invalid_json = test_invalid_json +test_parseJsonBody.test_json_without_params = test_json_without_params +test_parseJsonBody.test_json_without_method = test_json_without_method +test_parseJsonBody.test_empty_json = test_empty_json +test_parseJsonBody.test_empty_body = test_empty_body +test_parseJsonBody.test_nil_body = test_nil_body +test_parseJsonBody.test_non_json_body = test_non_json_body + +return test_parseJsonBody diff --git a/auth-layer-proxy/tests/test_verifyValidMethod.lua b/auth-layer-proxy/tests/test_verifyValidMethod.lua new file mode 100644 index 0000000..2649c68 --- /dev/null +++ b/auth-layer-proxy/tests/test_verifyValidMethod.lua @@ -0,0 +1,38 @@ +local lunatest = package.loaded.lunatest +local assert_true, assert_false, assert_equal = lunatest.assert_true, lunatest.assert_false, lunatest.assert_equal + +-- Load the module to be tested +package.path = package.path .. ";../filters/?.lua" +-- Load common test utilities +local testUtils = require("testUtils") +local tokenVerificationFilter = require("TokenVerificationFilter") -- The Lua file to test +local verifyValidMethod = tokenVerificationFilter.verifyValidMethod + +-- Test for valid methods +local function test_valid_methods() + local validMethods = {"subgraph_deploy", "subgraph_create", "subgraph_remove", "subgraph_pause", "subgraph_resume"} + for _, method in ipairs(validMethods) do + local result, err = verifyValidMethod(method) + assert_true(result, method .. " should be considered valid") + assert_equal(err, nil, method .. " should not return an error") + end + + testUtils.printGreen("✓ [PASS] Test for valid methods") +end + +-- Test for an invalid method +local function test_invalid_method() + local result, err = verifyValidMethod("invalid_method") + assert_false(result, "Invalid method should not be considered valid") + assert_equal(err, "Invalid method", "Invalid method should return a specific error message") + + testUtils.printGreen("✓ [PASS] Test for invalid method") +end + + +-- export suite +local test_verifyValidMethod = {} +test_verifyValidMethod.test_valid_methods = test_valid_methods +test_verifyValidMethod.test_invalid_method = test_invalid_method + +return test_verifyValidMethod