From 3fc24d7a30fc7248b88ebf2d9ea10e20df7febe9 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 4 Jan 2024 15:09:04 -0600 Subject: [PATCH 01/21] Adding POC Results for auth-layer, this could be used as the beginnig of a long term final solution. Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/Dockerfile | 17 ++ envoy-auth-layer/README.md | 159 ++++++++++++++++++ .../configs/envoy-auth-pg-db.yaml | 55 ++++++ .../configs/envoy-auth-redis.yaml | 55 ++++++ envoy-auth-layer/docker-compose.yaml | 16 ++ .../filters/JsonValidationFilter.lua | 18 ++ .../filters/TokenVerificationPgFilter.lua | 92 ++++++++++ .../filters/TokenVerificationRedisFilter.lua | 69 ++++++++ 8 files changed, 481 insertions(+) create mode 100644 envoy-auth-layer/Dockerfile create mode 100644 envoy-auth-layer/README.md create mode 100644 envoy-auth-layer/configs/envoy-auth-pg-db.yaml create mode 100644 envoy-auth-layer/configs/envoy-auth-redis.yaml create mode 100644 envoy-auth-layer/docker-compose.yaml create mode 100644 envoy-auth-layer/filters/JsonValidationFilter.lua create mode 100644 envoy-auth-layer/filters/TokenVerificationPgFilter.lua create mode 100644 envoy-auth-layer/filters/TokenVerificationRedisFilter.lua diff --git a/envoy-auth-layer/Dockerfile b/envoy-auth-layer/Dockerfile new file mode 100644 index 0000000..686fa70 --- /dev/null +++ b/envoy-auth-layer/Dockerfile @@ -0,0 +1,17 @@ +FROM envoyproxy/envoy:v1.28-latest +# copy envoy.yaml to the container +#COPY envoy.yaml /etc/envoy/envoy.yaml +#RUN chmod go+r /etc/envoy/envoy.yaml + +# Install Lua and Luarocks +RUN apt-get update && apt-get install -y lua5.1 luarocks git + +# Install PostgreSQL client and development headers, required for luasql-postgres +RUN apt-get install -y postgresql-client libpq-dev + +# Install Lua modules +RUN luarocks install lua-cjson +RUN luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql/ + +# Install Redis Lua module +RUN luarocks install redis-lua diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md new file mode 100644 index 0000000..f1066b8 --- /dev/null +++ b/envoy-auth-layer/README.md @@ -0,0 +1,159 @@ +# Readme + +This is a POC auth-layer 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 (auth layer) + +```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; + +``` + +## 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 +3. Payload Params Extraction +3. Token Validation (using both Postgres or Redis) +4. 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 + +### Postgres +``` +docker run --name postgres-envoy-test -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres +``` + +Run init script to create the database and the table + +``` +docker exec -it postgres-envoy-test bash + +psql -U postgres + +CREATE DATABASE thegraphauth; +\c thegraphauth; + +CREATE SCHEMA auth + AUTHORIZATION postgres; + +CREATE TABLE IF NOT EXISTS auth.permissions +( + id integer, + token character varying(255) NOT NULL, + method character varying(50) NOT NULL, + param_name character varying(50) NOT NULL +); + +INSERT INTO auth.permissions( + id, token, method, param_name) + VALUES (1, 'Bearer 12345', 'deploy_subgraph', 'test'); + + +``` + +### Redis + +``` +docker run --name redis-envoy-test -p 6379:6379 -d redis +``` + +Run init script to insert token example on redis: + +``` +docker exec -it redis-envoy-test bash + +redis-cli SET "permissions:Bearer 12345:deploy_subgraph:test" "true" +``` + + +## Usage + +### Build the image + +```bash + +docker build -t envoy-auth-layer . + +``` + +### Configure the environment + +Add Postgres or Redis credentials to the .env file + +``` +# Postgres +DB_USER=postgres +DB_PASSWORD=mysecretpassword +DB_HOST=host.docker.internal +DB_PORT=5432 +DB_NAME=thegraphauth +# Redis +REDIS_HOST=host.docker.internal +``` + +### Configure the details of the service to be proxied on the envoy.yaml file + +```yaml + + address: host.docker.internal + port_value: 8020 + +``` + + +### Run the container + +Change the `envoy` configuration file to execute on the command property of the `docker-compose.yaml`, for the desired DB Store of choice. + +**For Postgres:** +``` +command: -c /configs/envoy-auth-pg-db.yaml + +``` +**For Redis:** +``` +command: -c /configs/envoy-auth-redis.yaml +``` + +**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" + } +}' + +``` diff --git a/envoy-auth-layer/configs/envoy-auth-pg-db.yaml b/envoy-auth-layer/configs/envoy-auth-pg-db.yaml new file mode 100644 index 0000000..14c6d6e --- /dev/null +++ b/envoy-auth-layer/configs/envoy-auth-pg-db.yaml @@ -0,0 +1,55 @@ +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/JsonValidationFilter.lua + + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + filename: /filters/TokenVerificationPgFilter.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 \ No newline at end of file diff --git a/envoy-auth-layer/configs/envoy-auth-redis.yaml b/envoy-auth-layer/configs/envoy-auth-redis.yaml new file mode 100644 index 0000000..c356ecd --- /dev/null +++ b/envoy-auth-layer/configs/envoy-auth-redis.yaml @@ -0,0 +1,55 @@ +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/JsonValidationFilter.lua + + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + filename: /filters/TokenVerificationRedisFilter.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 \ No newline at end of file diff --git a/envoy-auth-layer/docker-compose.yaml b/envoy-auth-layer/docker-compose.yaml new file mode 100644 index 0000000..bf4a6bb --- /dev/null +++ b/envoy-auth-layer/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3' + +services: + envoy: + image: envoy-auth-layer:latest + command: -c /configs/envoy-auth-redis.yaml + env_file: + - .env + volumes: + - ./configs/:/configs/ + - ./filters/:/filters/ + ports: + - "9901:9901" + - "10000:10000" + stdin_open: true + tty: true diff --git a/envoy-auth-layer/filters/JsonValidationFilter.lua b/envoy-auth-layer/filters/JsonValidationFilter.lua new file mode 100644 index 0000000..6fa78e6 --- /dev/null +++ b/envoy-auth-layer/filters/JsonValidationFilter.lua @@ -0,0 +1,18 @@ +local cjson = require "cjson" + +function envoy_on_request(request_handle) + -- Read the request body + local body = request_handle:body() + local json_body = body:getBytes(0, body:length()) + + -- Try to parse the JSON body using cjson + local status, parsed_body = pcall(cjson.decode, json_body) + + if not status then + -- If parsing failed, return a 400 Bad Request response + request_handle:respond({[":status"] = "400"}, "Invalid JSON body") + end + + -- If the script reaches here, it means the request is authorized + -- The request will continue processing +end \ No newline at end of file diff --git a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua new file mode 100644 index 0000000..c621560 --- /dev/null +++ b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua @@ -0,0 +1,92 @@ +local cjson = require("cjson") +local luasql = require("luasql.postgres") + +local function extractToken(request_handle) + -- Adjust the header name to match your token header + return request_handle:headers():get("Authorization") +end + +local function parseJsonBody(body) + local success, jsonBody = pcall(cjson.decode, body) + if not success then + return nil, nil, "JSON parsing failed" + end + return jsonBody.method, jsonBody.params and jsonBody.params.name, nil +end + +local function escapeLiteral(conn, literal) + -- Use the connection's escape method to safely escape literals + return conn:escape(literal) +end + +local function getDbConnection() + local env = luasql.postgres() + local db_user = os.getenv("DB_USER") or "postgres" + local db_password = os.getenv("DB_PASSWORD") or "" + local db_name = os.getenv("DB_NAME") or "thegraphauth" + local db_host = os.getenv("DB_HOST") or "host.docker.internal" + local db_port = tonumber(os.getenv("DB_PORT")) or 5432 + + return env:connect(db_name, db_user, db_password, db_host, db_port) +end + +local function checkTokenPermissions(token, method, paramName) + local conn, err = getDbConnection() + if not conn then + return false, "Database connection error: " .. err + end + + -- Escape parameters + token = escapeLiteral(conn, token) + method = escapeLiteral(conn, method) + paramName = escapeLiteral(conn, paramName) + + -- Build and execute the query + local query = string.format("SELECT * FROM auth.permissions WHERE token = '%s' AND method = '%s' AND param_name = '%s'", token, method, paramName) + local cursor, error = conn:execute(query) + + if not cursor then + conn:close() + return false, "Database error: " .. error + end + + local result = cursor:numrows() > 0 + cursor:close() + conn:close() + + return result, nil +end + +function envoy_on_request(request_handle) + local token = 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, paramName, parseError = parseJsonBody(body) + if parseError then + request_handle:respond({[":status"] = "400"}, parseError) + return + end + + if not method or not paramName then + request_handle:respond({[":status"] = "400"}, "Invalid request body") + return + end + + local hasPermission, permissionError = checkTokenPermissions(token, method, paramName) + if permissionError then + request_handle:logErr(permissionError) + request_handle:respond({[":status"] = "500"}, "Internal Server Error") + return + end + + if not hasPermission then + request_handle:respond({[":status"] = "401"}, "Unauthorized") + return + end + + -- The request is authorized and processing continues +end diff --git a/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua b/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua new file mode 100644 index 0000000..8ab0a8b --- /dev/null +++ b/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua @@ -0,0 +1,69 @@ +local cjson = require("cjson") +local redis = require 'redis' + +local redisHost = os.getenv("REDIS_HOST") or "host.docker.internal" +local client = redis.connect(redisHost, 6379) + +local function extractToken(request_handle) + -- Adjust the header name to match your token header + return request_handle:headers():get("Authorization") +end + +local function parseJsonBody(body) + local success, jsonBody = pcall(cjson.decode, body) + if not success then + return nil, nil, "JSON parsing failed" + end + return jsonBody.method, jsonBody.params and jsonBody.params.name, nil +end + +local function checkTokenPermissions(token, method, paramName) + -- Construct a unique key based on token, method, and param_name + -- The key format should match how you've stored the data in Redis + local key = "permissions:" .. token .. ":" .. method .. ":" .. paramName + + -- Query Redis for the key + local result = client:get(key) + + if result then + -- Assuming a truthy result means permissions exist + return true + else + -- No result found, or permissions do not exist + return false + end +end + +function envoy_on_request(request_handle) + local token = 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, paramName, parseError = parseJsonBody(body) + if parseError then + request_handle:respond({[":status"] = "400"}, parseError) + return + end + + if not method or not paramName then + request_handle:respond({[":status"] = "400"}, "Invalid request body") + return + end + + local hasPermission, permissionError = checkTokenPermissions(token, method, paramName) + if permissionError then + request_handle:logErr(permissionError) + request_handle:respond({[":status"] = "500"}, "Internal Server Error") + return + end + + if not hasPermission then + request_handle:respond({[":status"] = "401"}, "Unauthorized") + return + end + + -- The request is authorized and processing continues +end From a44d6f99c8cf4480f32eb4c72b6ffc1e5d8ce1e1 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 4 Jan 2024 15:11:54 -0600 Subject: [PATCH 02/21] Adding white lines EOF Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/configs/envoy-auth-pg-db.yaml | 2 +- envoy-auth-layer/configs/envoy-auth-redis.yaml | 2 +- envoy-auth-layer/filters/JsonValidationFilter.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/envoy-auth-layer/configs/envoy-auth-pg-db.yaml b/envoy-auth-layer/configs/envoy-auth-pg-db.yaml index 14c6d6e..306587d 100644 --- a/envoy-auth-layer/configs/envoy-auth-pg-db.yaml +++ b/envoy-auth-layer/configs/envoy-auth-pg-db.yaml @@ -52,4 +52,4 @@ static_resources: address: socket_address: address: host.docker.internal - port_value: 8020 \ No newline at end of file + port_value: 8020 diff --git a/envoy-auth-layer/configs/envoy-auth-redis.yaml b/envoy-auth-layer/configs/envoy-auth-redis.yaml index c356ecd..02255c1 100644 --- a/envoy-auth-layer/configs/envoy-auth-redis.yaml +++ b/envoy-auth-layer/configs/envoy-auth-redis.yaml @@ -52,4 +52,4 @@ static_resources: address: socket_address: address: host.docker.internal - port_value: 8020 \ No newline at end of file + port_value: 8020 diff --git a/envoy-auth-layer/filters/JsonValidationFilter.lua b/envoy-auth-layer/filters/JsonValidationFilter.lua index 6fa78e6..5e612a4 100644 --- a/envoy-auth-layer/filters/JsonValidationFilter.lua +++ b/envoy-auth-layer/filters/JsonValidationFilter.lua @@ -15,4 +15,4 @@ function envoy_on_request(request_handle) -- If the script reaches here, it means the request is authorized -- The request will continue processing -end \ No newline at end of file +end From 9e2ee759c24f722de9e8463970f7e464a4fb6f77 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 4 Jan 2024 15:14:18 -0600 Subject: [PATCH 03/21] Add example.env Signed-off-by: Alfredo Gutierrez --- example.env | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 example.env diff --git a/example.env b/example.env new file mode 100644 index 0000000..9fe5c76 --- /dev/null +++ b/example.env @@ -0,0 +1,8 @@ +# Postgres +DB_USER=postgres +DB_PASSWORD=mysecretpassword +DB_HOST=host.docker.internal +DB_PORT=5432 +DB_NAME=thegraphauth +# Redis +REDIS_HOST=host.docker.internal \ No newline at end of file From ef95661a56d598232fb3aab4c5661707a2522bd0 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 4 Jan 2024 15:15:05 -0600 Subject: [PATCH 04/21] Add example.env Signed-off-by: Alfredo Gutierrez --- example.env => envoy-auth-layer/example.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example.env => envoy-auth-layer/example.env (100%) diff --git a/example.env b/envoy-auth-layer/example.env similarity index 100% rename from example.env rename to envoy-auth-layer/example.env From 64c4643f5008e6a3247479aa264b17befecdec10 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 4 Jan 2024 15:39:30 -0600 Subject: [PATCH 05/21] improvements on docs Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/README.md | 18 ++++++++++++++---- envoy-auth-layer/example.env | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md index f1066b8..10aa1fc 100644 --- a/envoy-auth-layer/README.md +++ b/envoy-auth-layer/README.md @@ -108,19 +108,29 @@ DB_NAME=thegraphauth REDIS_HOST=host.docker.internal ``` -### Configure the details of the service to be proxied on the envoy.yaml file +### Configure the details of the service to be proxied on the envoy.yam +Either `envoy-auth-pg-db.yaml` or `envoy-auth-redis.yaml` file, 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 -Change the `envoy` configuration file to execute on the command property of the `docker-compose.yaml`, for the desired DB Store of choice. +Change the `envoy` configuration file to execute on the command property of the `docker-compose.yaml`, for the desired one (either `envoy-auth-pg-db.yaml` or `envoy-auth-redis.yaml`). **For Postgres:** ``` diff --git a/envoy-auth-layer/example.env b/envoy-auth-layer/example.env index 9fe5c76..6adf630 100644 --- a/envoy-auth-layer/example.env +++ b/envoy-auth-layer/example.env @@ -5,4 +5,4 @@ DB_HOST=host.docker.internal DB_PORT=5432 DB_NAME=thegraphauth # Redis -REDIS_HOST=host.docker.internal \ No newline at end of file +REDIS_HOST=host.docker.internal From f24cce59cfda7b2c931fd1d9530a2adc3fb0358f Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 18 Jan 2024 14:46:43 -0600 Subject: [PATCH 06/21] Removed some commented lines, addded licence header to filters scripts and improved a little on the docs instructions, also moved the copy of the filters from the mapping volume on the docker-compose to the Dockerfile, so those files can be copied and included with the image at build time Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/Dockerfile | 6 +++--- envoy-auth-layer/README.md | 5 ++++- envoy-auth-layer/docker-compose.yaml | 3 +-- .../filters/JsonValidationFilter.lua | 20 +++++++++++++++++++ .../filters/TokenVerificationPgFilter.lua | 20 +++++++++++++++++++ .../filters/TokenVerificationRedisFilter.lua | 20 +++++++++++++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/envoy-auth-layer/Dockerfile b/envoy-auth-layer/Dockerfile index 686fa70..136e0a8 100644 --- a/envoy-auth-layer/Dockerfile +++ b/envoy-auth-layer/Dockerfile @@ -1,7 +1,7 @@ FROM envoyproxy/envoy:v1.28-latest -# copy envoy.yaml to the container -#COPY envoy.yaml /etc/envoy/envoy.yaml -#RUN chmod go+r /etc/envoy/envoy.yaml + +# Copy the Filter Scripts +COPY /filters/ /filters/ # Install Lua and Luarocks RUN apt-get update && apt-get install -y lua5.1 luarocks git diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md index 10aa1fc..a1e9922 100644 --- a/envoy-auth-layer/README.md +++ b/envoy-auth-layer/README.md @@ -63,8 +63,11 @@ CREATE TABLE IF NOT EXISTS auth.permissions INSERT INTO auth.permissions( id, token, method, param_name) - VALUES (1, 'Bearer 12345', 'deploy_subgraph', 'test'); + VALUES (1, 'Bearer 12345', 'subgraph_create', 'test'); +INSERT INTO auth.permissions( + id, token, method, param_name) + VALUES (1, 'Bearer 12345', 'subgraph_deploy', 'test'); ``` diff --git a/envoy-auth-layer/docker-compose.yaml b/envoy-auth-layer/docker-compose.yaml index bf4a6bb..d13b852 100644 --- a/envoy-auth-layer/docker-compose.yaml +++ b/envoy-auth-layer/docker-compose.yaml @@ -3,12 +3,11 @@ version: '3' services: envoy: image: envoy-auth-layer:latest - command: -c /configs/envoy-auth-redis.yaml + command: -c /configs/envoy-auth-pg-db.yaml env_file: - .env volumes: - ./configs/:/configs/ - - ./filters/:/filters/ ports: - "9901:9901" - "10000:10000" diff --git a/envoy-auth-layer/filters/JsonValidationFilter.lua b/envoy-auth-layer/filters/JsonValidationFilter.lua index 5e612a4..88d9b27 100644 --- a/envoy-auth-layer/filters/JsonValidationFilter.lua +++ b/envoy-auth-layer/filters/JsonValidationFilter.lua @@ -1,3 +1,23 @@ +/*- + * + * Hedera JSON RPC Relay + * + * 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 cjson = require "cjson" function envoy_on_request(request_handle) diff --git a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua index c621560..0a7f6bb 100644 --- a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua +++ b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua @@ -1,3 +1,23 @@ +/*- + * + * Hedera JSON RPC Relay + * + * 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 cjson = require("cjson") local luasql = require("luasql.postgres") diff --git a/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua b/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua index 8ab0a8b..224edfc 100644 --- a/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua +++ b/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua @@ -1,3 +1,23 @@ +/*- + * + * Hedera JSON RPC Relay + * + * 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 cjson = require("cjson") local redis = require 'redis' From 7351e43b8a67af3133da74ddef9b2d9182886845 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 22 Jan 2024 16:06:43 -0600 Subject: [PATCH 07/21] Clean up redis POC, and leaved only PG Example so we can continue work on productization Signed-off-by: Alfredo Gutierrez --- .../configs/envoy-auth-redis.yaml | 55 ------------ ...{envoy-auth-pg-db.yaml => envoy-auth.yaml} | 0 .../filters/JsonValidationFilter.lua | 38 -------- .../filters/TokenVerificationRedisFilter.lua | 89 ------------------- 4 files changed, 182 deletions(-) delete mode 100644 envoy-auth-layer/configs/envoy-auth-redis.yaml rename envoy-auth-layer/configs/{envoy-auth-pg-db.yaml => envoy-auth.yaml} (100%) delete mode 100644 envoy-auth-layer/filters/JsonValidationFilter.lua delete mode 100644 envoy-auth-layer/filters/TokenVerificationRedisFilter.lua diff --git a/envoy-auth-layer/configs/envoy-auth-redis.yaml b/envoy-auth-layer/configs/envoy-auth-redis.yaml deleted file mode 100644 index 02255c1..0000000 --- a/envoy-auth-layer/configs/envoy-auth-redis.yaml +++ /dev/null @@ -1,55 +0,0 @@ -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/JsonValidationFilter.lua - - - name: envoy.filters.http.lua - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua - default_source_code: - filename: /filters/TokenVerificationRedisFilter.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/envoy-auth-layer/configs/envoy-auth-pg-db.yaml b/envoy-auth-layer/configs/envoy-auth.yaml similarity index 100% rename from envoy-auth-layer/configs/envoy-auth-pg-db.yaml rename to envoy-auth-layer/configs/envoy-auth.yaml diff --git a/envoy-auth-layer/filters/JsonValidationFilter.lua b/envoy-auth-layer/filters/JsonValidationFilter.lua deleted file mode 100644 index 88d9b27..0000000 --- a/envoy-auth-layer/filters/JsonValidationFilter.lua +++ /dev/null @@ -1,38 +0,0 @@ -/*- - * - * Hedera JSON RPC Relay - * - * 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 cjson = require "cjson" - -function envoy_on_request(request_handle) - -- Read the request body - local body = request_handle:body() - local json_body = body:getBytes(0, body:length()) - - -- Try to parse the JSON body using cjson - local status, parsed_body = pcall(cjson.decode, json_body) - - if not status then - -- If parsing failed, return a 400 Bad Request response - request_handle:respond({[":status"] = "400"}, "Invalid JSON body") - end - - -- If the script reaches here, it means the request is authorized - -- The request will continue processing -end diff --git a/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua b/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua deleted file mode 100644 index 224edfc..0000000 --- a/envoy-auth-layer/filters/TokenVerificationRedisFilter.lua +++ /dev/null @@ -1,89 +0,0 @@ -/*- - * - * Hedera JSON RPC Relay - * - * 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 cjson = require("cjson") -local redis = require 'redis' - -local redisHost = os.getenv("REDIS_HOST") or "host.docker.internal" -local client = redis.connect(redisHost, 6379) - -local function extractToken(request_handle) - -- Adjust the header name to match your token header - return request_handle:headers():get("Authorization") -end - -local function parseJsonBody(body) - local success, jsonBody = pcall(cjson.decode, body) - if not success then - return nil, nil, "JSON parsing failed" - end - return jsonBody.method, jsonBody.params and jsonBody.params.name, nil -end - -local function checkTokenPermissions(token, method, paramName) - -- Construct a unique key based on token, method, and param_name - -- The key format should match how you've stored the data in Redis - local key = "permissions:" .. token .. ":" .. method .. ":" .. paramName - - -- Query Redis for the key - local result = client:get(key) - - if result then - -- Assuming a truthy result means permissions exist - return true - else - -- No result found, or permissions do not exist - return false - end -end - -function envoy_on_request(request_handle) - local token = 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, paramName, parseError = parseJsonBody(body) - if parseError then - request_handle:respond({[":status"] = "400"}, parseError) - return - end - - if not method or not paramName then - request_handle:respond({[":status"] = "400"}, "Invalid request body") - return - end - - local hasPermission, permissionError = checkTokenPermissions(token, method, paramName) - if permissionError then - request_handle:logErr(permissionError) - request_handle:respond({[":status"] = "500"}, "Internal Server Error") - return - end - - if not hasPermission then - request_handle:respond({[":status"] = "401"}, "Unauthorized") - return - end - - -- The request is authorized and processing continues -end From b717b7518969a0bf78ccfb251aacf76340c7631e Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 22 Jan 2024 16:07:27 -0600 Subject: [PATCH 08/21] Missing changes to complete cleanup Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/README.md | 44 ++++------------------------ envoy-auth-layer/docker-compose.yaml | 2 +- 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md index a1e9922..db557a6 100644 --- a/envoy-auth-layer/README.md +++ b/envoy-auth-layer/README.md @@ -1,6 +1,6 @@ # Readme -This is a POC auth-layer for Hedera-The-Graph implementation that will allow a node operator to publish a secured `admin port` of the-graph deployment for hedera. +This is a token verification auth-layer for Hedera-The-Graph implementation that will allows 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 (auth layer) @@ -27,9 +27,10 @@ This is an implementation of EnvoyProxy filters for authentication and authoriza 1. JSON Validation 2. Token Extraction -3. Payload Params Extraction -3. Token Validation (using both Postgres or Redis) -4. Proxy Routing Configuration (using EnvoyProxy itself) +3. Token Hashing +4. Payload Params Extraction +5. Token Validation using Postgres +6. Proxy Routing Configuration (using EnvoyProxy itself) it includes a Dockerfile for building the image and a docker-compose file for running the container. @@ -68,24 +69,8 @@ INSERT INTO auth.permissions( INSERT INTO auth.permissions( id, token, method, param_name) VALUES (1, 'Bearer 12345', 'subgraph_deploy', 'test'); - -``` - -### Redis - -``` -docker run --name redis-envoy-test -p 6379:6379 -d redis ``` -Run init script to insert token example on redis: - -``` -docker exec -it redis-envoy-test bash - -redis-cli SET "permissions:Bearer 12345:deploy_subgraph:test" "true" -``` - - ## Usage ### Build the image @@ -107,12 +92,10 @@ DB_PASSWORD=mysecretpassword DB_HOST=host.docker.internal DB_PORT=5432 DB_NAME=thegraphauth -# Redis -REDIS_HOST=host.docker.internal ``` ### Configure the details of the service to be proxied on the envoy.yam -Either `envoy-auth-pg-db.yaml` or `envoy-auth-redis.yaml` file, by default will be proxying/relaying the request to address: `host.docker.internal` and port `8020` +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: @@ -133,30 +116,16 @@ Either `envoy-auth-pg-db.yaml` or `envoy-auth-redis.yaml` file, by default will ### Run the container -Change the `envoy` configuration file to execute on the command property of the `docker-compose.yaml`, for the desired one (either `envoy-auth-pg-db.yaml` or `envoy-auth-redis.yaml`). - -**For Postgres:** -``` -command: -c /configs/envoy-auth-pg-db.yaml - -``` -**For Redis:** -``` -command: -c /configs/envoy-auth-redis.yaml -``` **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' \ @@ -168,5 +137,4 @@ curl --location 'http://localhost:10000' \ "name": "test" } }' - ``` diff --git a/envoy-auth-layer/docker-compose.yaml b/envoy-auth-layer/docker-compose.yaml index d13b852..2c5fbf7 100644 --- a/envoy-auth-layer/docker-compose.yaml +++ b/envoy-auth-layer/docker-compose.yaml @@ -3,7 +3,7 @@ version: '3' services: envoy: image: envoy-auth-layer:latest - command: -c /configs/envoy-auth-pg-db.yaml + command: -c /configs/envoy-auth.yaml env_file: - .env volumes: From dabf8c9ae3b574aa0a3f5ab2ed4b0353e63c3da5 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 22 Jan 2024 16:08:22 -0600 Subject: [PATCH 09/21] cleanup of dockerfile to remove installation of redis module, since will no longer be needed. Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/envoy-auth-layer/Dockerfile b/envoy-auth-layer/Dockerfile index 136e0a8..5e09830 100644 --- a/envoy-auth-layer/Dockerfile +++ b/envoy-auth-layer/Dockerfile @@ -12,6 +12,3 @@ RUN apt-get install -y postgresql-client libpq-dev # Install Lua modules RUN luarocks install lua-cjson RUN luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql/ - -# Install Redis Lua module -RUN luarocks install redis-lua From 00d5f1f8ab5ab3a0024d026606e74bd3843a4958 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 22 Jan 2024 19:21:09 -0600 Subject: [PATCH 10/21] Removed Json HTTP Filter script and config, since is possible to do it all on the same script. Added SHA-256 function to hash received token before comparing it to db. Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/Dockerfile | 2 + envoy-auth-layer/configs/envoy-auth.yaml | 6 -- envoy-auth-layer/docker-compose.yaml | 1 + .../filters/TokenVerificationPgFilter.lua | 60 ++++++++++++------- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/envoy-auth-layer/Dockerfile b/envoy-auth-layer/Dockerfile index 5e09830..f5a04f9 100644 --- a/envoy-auth-layer/Dockerfile +++ b/envoy-auth-layer/Dockerfile @@ -12,3 +12,5 @@ RUN apt-get install -y postgresql-client libpq-dev # Install Lua modules RUN luarocks install lua-cjson RUN luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql/ + +RUN luarocks install sha2 diff --git a/envoy-auth-layer/configs/envoy-auth.yaml b/envoy-auth-layer/configs/envoy-auth.yaml index 306587d..1ecd8f1 100644 --- a/envoy-auth-layer/configs/envoy-auth.yaml +++ b/envoy-auth-layer/configs/envoy-auth.yaml @@ -12,12 +12,6 @@ static_resources: "@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/JsonValidationFilter.lua - - name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua diff --git a/envoy-auth-layer/docker-compose.yaml b/envoy-auth-layer/docker-compose.yaml index 2c5fbf7..1d4e147 100644 --- a/envoy-auth-layer/docker-compose.yaml +++ b/envoy-auth-layer/docker-compose.yaml @@ -8,6 +8,7 @@ services: - .env volumes: - ./configs/:/configs/ + - ./filters/:/filters/ ports: - "9901:9901" - "10000:10000" diff --git a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua index 0a7f6bb..6b55e54 100644 --- a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua +++ b/envoy-auth-layer/filters/TokenVerificationPgFilter.lua @@ -1,31 +1,42 @@ -/*- - * - * Hedera JSON RPC Relay - * - * 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. - * - */ +-- 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 cjson = require("cjson") local luasql = require("luasql.postgres") +local sha256 = require("sha2") + local function extractToken(request_handle) -- Adjust the header name to match your token header return request_handle:headers():get("Authorization") end +local function to_hex(str) + return (str:gsub('.', function (c) + return string.format('%02x', string.byte(c)) + end)) +end + +local function sha256_hash(input) + local hash = sha256.sha256(input) + return to_hex(hash) +end + + local function parseJsonBody(body) local success, jsonBody = pcall(cjson.decode, body) if not success then @@ -62,7 +73,7 @@ local function checkTokenPermissions(token, method, paramName) paramName = escapeLiteral(conn, paramName) -- Build and execute the query - local query = string.format("SELECT * FROM auth.permissions WHERE token = '%s' AND method = '%s' AND param_name = '%s'", token, method, paramName) + local query = string.format("SELECT-- FROM auth.permissions WHERE token = '%s' AND method = '%s' AND param_name = '%s'", token, method, paramName) local cursor, error = conn:execute(query) if not cursor then @@ -79,7 +90,12 @@ end function envoy_on_request(request_handle) local token = extractToken(request_handle) - if not token then + local hashed_token = sha256_hash(token) + + print("Token: " .. token) + print("Hashed token: " .. hashed_token) + + if not hashed_token then request_handle:respond({[":status"] = "401"}, "No token provided") return end @@ -96,7 +112,7 @@ function envoy_on_request(request_handle) return end - local hasPermission, permissionError = checkTokenPermissions(token, method, paramName) + local hasPermission, permissionError = checkTokenPermissions(hashed_token, method, paramName) if permissionError then request_handle:logErr(permissionError) request_handle:respond({[":status"] = "500"}, "Internal Server Error") From 5d5ba6061f121911871d23df1a99997dc4aaad7f Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Wed, 24 Jan 2024 16:00:44 -0600 Subject: [PATCH 11/21] Improvements and further cleanup Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/README.md | 8 ++--- envoy-auth-layer/configs/envoy-auth.yaml | 6 ++-- ...Filter.lua => TokenVerificationFilter.lua} | 32 ++++++++++++------- 3 files changed, 28 insertions(+), 18 deletions(-) rename envoy-auth-layer/filters/{TokenVerificationPgFilter.lua => TokenVerificationFilter.lua} (79%) diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md index db557a6..db2dddc 100644 --- a/envoy-auth-layer/README.md +++ b/envoy-auth-layer/README.md @@ -54,12 +54,12 @@ CREATE DATABASE thegraphauth; CREATE SCHEMA auth AUTHORIZATION postgres; -CREATE TABLE IF NOT EXISTS auth.permissions +CREATE TABLE IF NOT EXISTS auth.subgraph_token ( id integer, - token character varying(255) NOT NULL, - method character varying(50) NOT NULL, - param_name character varying(50) NOT NULL + email character varying(255) NOT NULL, + subgraph_name character varying(255) NOT NULL, + token_hash character varying(65) NOT NULL ); INSERT INTO auth.permissions( diff --git a/envoy-auth-layer/configs/envoy-auth.yaml b/envoy-auth-layer/configs/envoy-auth.yaml index 1ecd8f1..7a15c07 100644 --- a/envoy-auth-layer/configs/envoy-auth.yaml +++ b/envoy-auth-layer/configs/envoy-auth.yaml @@ -16,7 +16,7 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua default_source_code: - filename: /filters/TokenVerificationPgFilter.lua + filename: /filters/TokenVerificationFilter.lua - name: envoy.filters.http.router typed_config: @@ -32,8 +32,8 @@ static_resources: name: ":method" exact_match: "POST" route: - cluster: local_service - + cluster: local_service + clusters: - name: local_service connect_timeout: 5s diff --git a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua b/envoy-auth-layer/filters/TokenVerificationFilter.lua similarity index 79% rename from envoy-auth-layer/filters/TokenVerificationPgFilter.lua rename to envoy-auth-layer/filters/TokenVerificationFilter.lua index 6b55e54..0568331 100644 --- a/envoy-auth-layer/filters/TokenVerificationPgFilter.lua +++ b/envoy-auth-layer/filters/TokenVerificationFilter.lua @@ -20,8 +20,7 @@ local luasql = require("luasql.postgres") local sha256 = require("sha2") -local function extractToken(request_handle) - -- Adjust the header name to match your token header +local function extractToken(request_handle) return request_handle:headers():get("Authorization") end @@ -36,11 +35,10 @@ local function sha256_hash(input) return to_hex(hash) end - local function parseJsonBody(body) local success, jsonBody = pcall(cjson.decode, body) if not success then - return nil, nil, "JSON parsing failed" + return nil, nil, "INVALID JSON BODY" end return jsonBody.method, jsonBody.params and jsonBody.params.name, nil end @@ -61,7 +59,15 @@ local function getDbConnection() return env:connect(db_name, db_user, db_password, db_host, db_port) end -local function checkTokenPermissions(token, method, paramName) +local function verifyValidMethod(method) + if((method == "subgraph_deploy") or (method == "subgraph_create") or (method == "subgraph_remove")) then + return true, nil + end + + return false, "Invalid method" +end + +local function checkTokenPermissions(token, subgraphName) local conn, err = getDbConnection() if not conn then return false, "Database connection error: " .. err @@ -69,11 +75,10 @@ local function checkTokenPermissions(token, method, paramName) -- Escape parameters token = escapeLiteral(conn, token) - method = escapeLiteral(conn, method) - paramName = escapeLiteral(conn, paramName) + subgraphName = escapeLiteral(conn, subgraphName) -- Build and execute the query - local query = string.format("SELECT-- FROM auth.permissions WHERE token = '%s' AND method = '%s' AND param_name = '%s'", token, method, paramName) + local query = string.format("SELECT-- FROM auth.subgraph_token WHERE token_hash = '%s' AND subgraph_name = '%s'", token, subgraphName) local cursor, error = conn:execute(query) if not cursor then @@ -101,18 +106,23 @@ function envoy_on_request(request_handle) end local body = request_handle:body():getBytes(0, request_handle:body():length()) - local method, paramName, parseError = parseJsonBody(body) + local method, subgraphName, parseError = parseJsonBody(body) if parseError then request_handle:respond({[":status"] = "400"}, parseError) return end - if not method or not paramName then + if not method or not subgraphName then request_handle:respond({[":status"] = "400"}, "Invalid request body") return end - local hasPermission, permissionError = checkTokenPermissions(hashed_token, method, paramName) + if not verifyValidMethod(method) then + request_handle:respond({[":status"] = "400"}, "Invalid method") + return + end + + local hasPermission, permissionError = checkTokenPermissions(hashed_token, subgraphName) if permissionError then request_handle:logErr(permissionError) request_handle:respond({[":status"] = "500"}, "Internal Server Error") From 8f6ddf058c01fa9d4b4006759adbb1f6a39c9918 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Wed, 7 Feb 2024 12:57:54 -0600 Subject: [PATCH 12/21] Refactor and re-did the TokenVerificationFilter to use an OAuth token instrospection endpoint instead of checking it agasint postgres, added validations and descriptive errors. Cleanup Dockerfile to only include the needed dependencies. Updated example.env with needed env variables Updated README.md with new instructions Signed-off-by: Alfredo Gutierrez --- envoy-auth-layer/Dockerfile | 7 +- envoy-auth-layer/README.md | 87 +++++---- envoy-auth-layer/example.env | 12 +- .../filters/TokenVerificationFilter.lua | 178 ++++++++++++------ 4 files changed, 176 insertions(+), 108 deletions(-) diff --git a/envoy-auth-layer/Dockerfile b/envoy-auth-layer/Dockerfile index f5a04f9..b7e3620 100644 --- a/envoy-auth-layer/Dockerfile +++ b/envoy-auth-layer/Dockerfile @@ -6,11 +6,8 @@ COPY /filters/ /filters/ # Install Lua and Luarocks RUN apt-get update && apt-get install -y lua5.1 luarocks git -# Install PostgreSQL client and development headers, required for luasql-postgres -RUN apt-get install -y postgresql-client libpq-dev - # Install Lua modules RUN luarocks install lua-cjson -RUN luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql/ -RUN luarocks install sha2 +# Install http socket module +RUN luarocks install luasocket diff --git a/envoy-auth-layer/README.md b/envoy-auth-layer/README.md index db2dddc..2076090 100644 --- a/envoy-auth-layer/README.md +++ b/envoy-auth-layer/README.md @@ -29,47 +29,53 @@ This is an implementation of EnvoyProxy filters for authentication and authoriza 2. Token Extraction 3. Token Hashing 4. Payload Params Extraction -5. Token Validation using Postgres +5. Token Validation using JWT 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 -### Postgres -``` -docker run --name postgres-envoy-test -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres -``` - -Run init script to create the database and the table - -``` -docker exec -it postgres-envoy-test bash - -psql -U postgres - -CREATE DATABASE thegraphauth; -\c thegraphauth; - -CREATE SCHEMA auth - AUTHORIZATION postgres; - -CREATE TABLE IF NOT EXISTS auth.subgraph_token -( - id integer, - email character varying(255) NOT NULL, - subgraph_name character varying(255) NOT NULL, - token_hash character varying(65) NOT NULL -); - -INSERT INTO auth.permissions( - id, token, method, param_name) - VALUES (1, 'Bearer 12345', 'subgraph_create', 'test'); - -INSERT INTO auth.permissions( - id, token, method, param_name) - VALUES (1, 'Bearer 12345', 'subgraph_deploy', 'test'); -``` +### 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: + + ```json + { + "iss": "http://host.docker.internal:8080/realms/HederaTheGraph", + "resource_access": { + "htg-auth-layer": { + "roles": [ + "subgraph_create", + "subgraph_deploy" + ] + } + }, + "subgraph_access": "", + "email_verified": true, + "active": true, + "email": "user1@gmail.com", + "client_id": "htg-auth-layer" + } + ``` + + +### Configure KeyCloak if using it as the token server +1. For local testing you can install it using a docker container +2. Create a realm for the Hedera-The-Graph +3. Create a client for the auth-layer +4. Create a client scope for the auth-layer. +5. Map UserAttribute "subgraph_access" to the client scope. +6. Create custom roles for that client: `subgraph_create` and `subgraph_deploy` +7. Create a user and assign the roles to the user, set password and verify email. +8. Add user attribute to the user "subgraph_access" and set the value to the subgraph names that the user can access. (CSV, ie: "subgraph1,subgraph2") +9. Get a Token using the `/token` endpoint and use it for testing the auth-layer. +10. Validate the token using the `/token/introspection` endpoint. ## Usage @@ -86,12 +92,11 @@ docker build -t envoy-auth-layer . Add Postgres or Redis credentials to the .env file ``` -# Postgres -DB_USER=postgres -DB_PASSWORD=mysecretpassword -DB_HOST=host.docker.internal -DB_PORT=5432 -DB_NAME=thegraphauth +# 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 diff --git a/envoy-auth-layer/example.env b/envoy-auth-layer/example.env index 6adf630..7d6f95f 100644 --- a/envoy-auth-layer/example.env +++ b/envoy-auth-layer/example.env @@ -1,8 +1,4 @@ -# Postgres -DB_USER=postgres -DB_PASSWORD=mysecretpassword -DB_HOST=host.docker.internal -DB_PORT=5432 -DB_NAME=thegraphauth -# Redis -REDIS_HOST=host.docker.internal +# OAuth +CLIENT_ID=htg-auth-layer +CLIENT_SECRET=0cyYtDVVbVvaZjrDViiw4p2kegTy9Q5X +TOKEN_INTROSPECTION_URL=http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect \ No newline at end of file diff --git a/envoy-auth-layer/filters/TokenVerificationFilter.lua b/envoy-auth-layer/filters/TokenVerificationFilter.lua index 0568331..384492b 100644 --- a/envoy-auth-layer/filters/TokenVerificationFilter.lua +++ b/envoy-auth-layer/filters/TokenVerificationFilter.lua @@ -16,23 +16,26 @@ local cjson = require("cjson") -local luasql = require("luasql.postgres") -local sha256 = require("sha2") +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) - return request_handle:headers():get("Authorization") -end -local function to_hex(str) - return (str:gsub('.', function (c) - return string.format('%02x', string.byte(c)) - end)) -end -local function sha256_hash(input) - local hash = sha256.sha256(input) - return to_hex(hash) +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) @@ -43,22 +46,6 @@ local function parseJsonBody(body) return jsonBody.method, jsonBody.params and jsonBody.params.name, nil end -local function escapeLiteral(conn, literal) - -- Use the connection's escape method to safely escape literals - return conn:escape(literal) -end - -local function getDbConnection() - local env = luasql.postgres() - local db_user = os.getenv("DB_USER") or "postgres" - local db_password = os.getenv("DB_PASSWORD") or "" - local db_name = os.getenv("DB_NAME") or "thegraphauth" - local db_host = os.getenv("DB_HOST") or "host.docker.internal" - local db_port = tonumber(os.getenv("DB_PORT")) or 5432 - - return env:connect(db_name, db_user, db_password, db_host, db_port) -end - local function verifyValidMethod(method) if((method == "subgraph_deploy") or (method == "subgraph_create") or (method == "subgraph_remove")) then return true, nil @@ -67,40 +54,121 @@ local function verifyValidMethod(method) return false, "Invalid method" end -local function checkTokenPermissions(token, subgraphName) - local conn, err = getDbConnection() - if not conn then - return false, "Database connection error: " .. err +-- 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 - -- Escape parameters - token = escapeLiteral(conn, token) - subgraphName = escapeLiteral(conn, subgraphName) - - -- Build and execute the query - local query = string.format("SELECT-- FROM auth.subgraph_token WHERE token_hash = '%s' AND subgraph_name = '%s'", token, subgraphName) - local cursor, error = conn:execute(query) +-- 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 - if not cursor then - conn:close() - return false, "Database error: " .. error +-- Function to check if the "method" parameter is included in roles +local function checkMethodInRoles(result, method) + local roles = result.resource_access[clientId].roles + if roles then + return contains(roles, method) end + return false +end - local result = cursor:numrows() > 0 - cursor:close() - conn:close() +-- 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 resource roles collection. + if not result.resource_access[clientId] then + return false, "Client roles not found in token" + end + + -- 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) + + print("Token introspection successful for user: " .. result.email) + -- Set the token user for logging purposes + tokenUser = result.email + + 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 result, nil + 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 = extractToken(request_handle) - local hashed_token = sha256_hash(token) - print("Token: " .. token) - print("Hashed token: " .. hashed_token) - - if not hashed_token then + if not token then request_handle:respond({[":status"] = "401"}, "No token provided") return end @@ -122,10 +190,11 @@ function envoy_on_request(request_handle) return end - local hasPermission, permissionError = checkTokenPermissions(hashed_token, subgraphName) + local hasPermission, permissionError = checkTokenPermissions(token, subgraphName, method) + if permissionError then request_handle:logErr(permissionError) - request_handle:respond({[":status"] = "500"}, "Internal Server Error") + request_handle:respond({[":status"] = "401"}, permissionError) return end @@ -134,5 +203,6 @@ function envoy_on_request(request_handle) return end + print("Token is authorized for method: ".. method .. " and subgraph: " .. subgraphName.. " by the user".. tokenUser) -- The request is authorized and processing continues end From 03904bccfd16a6a3986271ccc80d1dca4ec54fbc Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 7 Mar 2024 15:55:44 -0600 Subject: [PATCH 13/21] moved files to more descriptive project folder Signed-off-by: Alfredo Gutierrez --- {envoy-auth-layer => auth-layer-proxy}/Dockerfile | 0 {envoy-auth-layer => auth-layer-proxy}/README.md | 0 {envoy-auth-layer => auth-layer-proxy}/configs/envoy-auth.yaml | 0 {envoy-auth-layer => auth-layer-proxy}/docker-compose.yaml | 0 {envoy-auth-layer => auth-layer-proxy}/example.env | 0 .../filters/TokenVerificationFilter.lua | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {envoy-auth-layer => auth-layer-proxy}/Dockerfile (100%) rename {envoy-auth-layer => auth-layer-proxy}/README.md (100%) rename {envoy-auth-layer => auth-layer-proxy}/configs/envoy-auth.yaml (100%) rename {envoy-auth-layer => auth-layer-proxy}/docker-compose.yaml (100%) rename {envoy-auth-layer => auth-layer-proxy}/example.env (100%) rename {envoy-auth-layer => auth-layer-proxy}/filters/TokenVerificationFilter.lua (100%) diff --git a/envoy-auth-layer/Dockerfile b/auth-layer-proxy/Dockerfile similarity index 100% rename from envoy-auth-layer/Dockerfile rename to auth-layer-proxy/Dockerfile diff --git a/envoy-auth-layer/README.md b/auth-layer-proxy/README.md similarity index 100% rename from envoy-auth-layer/README.md rename to auth-layer-proxy/README.md diff --git a/envoy-auth-layer/configs/envoy-auth.yaml b/auth-layer-proxy/configs/envoy-auth.yaml similarity index 100% rename from envoy-auth-layer/configs/envoy-auth.yaml rename to auth-layer-proxy/configs/envoy-auth.yaml diff --git a/envoy-auth-layer/docker-compose.yaml b/auth-layer-proxy/docker-compose.yaml similarity index 100% rename from envoy-auth-layer/docker-compose.yaml rename to auth-layer-proxy/docker-compose.yaml diff --git a/envoy-auth-layer/example.env b/auth-layer-proxy/example.env similarity index 100% rename from envoy-auth-layer/example.env rename to auth-layer-proxy/example.env diff --git a/envoy-auth-layer/filters/TokenVerificationFilter.lua b/auth-layer-proxy/filters/TokenVerificationFilter.lua similarity index 100% rename from envoy-auth-layer/filters/TokenVerificationFilter.lua rename to auth-layer-proxy/filters/TokenVerificationFilter.lua From bacc19eeb666d922abc68670af7c7da3720485c3 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 7 Mar 2024 17:23:40 -0600 Subject: [PATCH 14/21] improvements to README, example.env and Lua Filter Signed-off-by: Alfredo Gutierrez --- auth-layer-proxy/README.md | 20 +++++-------------- auth-layer-proxy/example.env | 2 +- .../filters/TokenVerificationFilter.lua | 20 ++++++++----------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/auth-layer-proxy/README.md b/auth-layer-proxy/README.md index 2076090..77bdf92 100644 --- a/auth-layer-proxy/README.md +++ b/auth-layer-proxy/README.md @@ -1,8 +1,8 @@ # Readme -This is a token verification auth-layer for Hedera-The-Graph implementation that will allows a node operator to publish a secured `admin port` of the-graph deployment for hedera. +This is a token verification auth-layer-proxy for Hedera-The-Graph implementation that will allows 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 (auth layer) +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 --- @@ -20,6 +20,7 @@ flowchart LR linkStyle 4 stroke:#00ff00,stroke-width:2px; ``` +More information on the **Authorization Layer** can be found [here](link) ## Overview @@ -64,18 +65,7 @@ Make sure that the access token has the following claims: } ``` - -### Configure KeyCloak if using it as the token server -1. For local testing you can install it using a docker container -2. Create a realm for the Hedera-The-Graph -3. Create a client for the auth-layer -4. Create a client scope for the auth-layer. -5. Map UserAttribute "subgraph_access" to the client scope. -6. Create custom roles for that client: `subgraph_create` and `subgraph_deploy` -7. Create a user and assign the roles to the user, set password and verify email. -8. Add user attribute to the user "subgraph_access" and set the value to the subgraph names that the user can access. (CSV, ie: "subgraph1,subgraph2") -9. Get a Token using the `/token` endpoint and use it for testing the auth-layer. -10. Validate the token using the `/token/introspection` endpoint. +For instructions on how to set-up the Auth Provider using KeyCloak, refer to the `Auth-Layer-Server` [README](link) ## Usage @@ -83,7 +73,7 @@ Make sure that the access token has the following claims: ```bash -docker build -t envoy-auth-layer . +docker build -t envoy-auth-proxy . ``` diff --git a/auth-layer-proxy/example.env b/auth-layer-proxy/example.env index 7d6f95f..46622ae 100644 --- a/auth-layer-proxy/example.env +++ b/auth-layer-proxy/example.env @@ -1,4 +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 \ No newline at end of file +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 index 384492b..62307bc 100644 --- a/auth-layer-proxy/filters/TokenVerificationFilter.lua +++ b/auth-layer-proxy/filters/TokenVerificationFilter.lua @@ -24,8 +24,6 @@ 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") @@ -47,7 +45,12 @@ local function parseJsonBody(body) end local function verifyValidMethod(method) - if((method == "subgraph_deploy") or (method == "subgraph_create") or (method == "subgraph_remove")) then + 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 @@ -80,7 +83,7 @@ end -- Function to check if the "method" parameter is included in roles local function checkMethodInRoles(result, method) - local roles = result.resource_access[clientId].roles + local roles = result.realm_access.roles if roles then return contains(roles, method) end @@ -124,11 +127,6 @@ local function checkTokenPermissions(token, subgraphName, method) -- Check if the token is active if result.active then - -- check if the token claims has resource roles collection. - if not result.resource_access[clientId] then - return false, "Client roles not found in token" - end - -- check if the token claims has subgraph_access claim. if not result.subgraph_access then return false, "subgraph_access claim not found in token" @@ -137,9 +135,9 @@ local function checkTokenPermissions(token, subgraphName, method) local subgraphAccessGranted = checkSubgraphAccessClaim(result, subgraphName) local methodAccessGranted = checkMethodInRoles(result, method) - print("Token introspection successful for user: " .. result.email) -- 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 @@ -162,8 +160,6 @@ local function checkTokenPermissions(token, subgraphName, method) 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 = extractToken(request_handle) From b1b937359e18fe8f4d8f253c8cdab68c0a854893 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 21 Mar 2024 20:51:15 -0600 Subject: [PATCH 15/21] Adding UnitTests coverage for filter/TokenVerificationFilter.lua Added GHA WF for running unit tests as part of CI pipeline Improvements to Readme with instructions on how to run tests Signed-off-by: Alfredo Gutierrez --- .github/workflows/proxy-tests.yml | 58 ++++++ .gitignore | 4 + auth-layer-proxy/README.md | 33 ++++ .../filters/TokenVerificationFilter.lua | 36 ++-- auth-layer-proxy/tests/.luacov | 3 + auth-layer-proxy/tests/test.lua | 17 ++ auth-layer-proxy/tests/testMocks.lua | 76 ++++++++ auth-layer-proxy/tests/testUtils.lua | 22 +++ .../tests/test_checkTokenPermissions.lua | 89 +++++++++ .../tests/test_envoy_on_request.lua | 170 ++++++++++++++++++ auth-layer-proxy/tests/test_extractToken.lua | 57 ++++++ auth-layer-proxy/tests/test_parseJsonBody.lua | 113 ++++++++++++ .../tests/test_verifyValidMethod.lua | 38 ++++ 13 files changed, 703 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/proxy-tests.yml create mode 100644 auth-layer-proxy/tests/.luacov create mode 100644 auth-layer-proxy/tests/test.lua create mode 100644 auth-layer-proxy/tests/testMocks.lua create mode 100644 auth-layer-proxy/tests/testUtils.lua create mode 100644 auth-layer-proxy/tests/test_checkTokenPermissions.lua create mode 100644 auth-layer-proxy/tests/test_envoy_on_request.lua create mode 100644 auth-layer-proxy/tests/test_extractToken.lua create mode 100644 auth-layer-proxy/tests/test_parseJsonBody.lua create mode 100644 auth-layer-proxy/tests/test_verifyValidMethod.lua diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml new file mode 100644 index 0000000..e436fc4 --- /dev/null +++ b/.github/workflows/proxy-tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: + pull_request: + branches: [ main, release/**] + push: + branches: [ main, release/*] + tags: [ v* ] + +env: + ACTIONS_RUNTIME_TOKEN: fake-token + + +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: Print Current Directory + run: pwd && ls -la + working-directory: auth-layer-proxy/tests + + - 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/README.md b/auth-layer-proxy/README.md index 77bdf92..c04d2fd 100644 --- a/auth-layer-proxy/README.md +++ b/auth-layer-proxy/README.md @@ -133,3 +133,36 @@ curl --location 'http://localhost:10000' \ } }' ``` + + +## 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/filters/TokenVerificationFilter.lua b/auth-layer-proxy/filters/TokenVerificationFilter.lua index 62307bc..94d5460 100644 --- a/auth-layer-proxy/filters/TokenVerificationFilter.lua +++ b/auth-layer-proxy/filters/TokenVerificationFilter.lua @@ -14,6 +14,7 @@ -- 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") @@ -96,7 +97,7 @@ local tokenUser = ""; local function checkTokenPermissions(token, subgraphName, method) -- Prepare the HTTP request body - local requestBody = "token=" .. token .. "&client_id=" .. clientId .. "&client_secret=" .. clientSecret + local requestBody = "token=" .. token .. "&client_id=" .. clientId .. "&client_secret=" .. clientSecret -- Prepare the HTTP request headers local headers = { @@ -108,13 +109,13 @@ local function checkTokenPermissions(token, subgraphName, method) local responseBody = {} -- Perform the HTTP POST request - local response, statusCode, responseHeaders, statusText = http.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 @@ -162,7 +163,7 @@ end -- This function is called for each request, and is the entry point for the filter function envoy_on_request(request_handle) - local token = extractToken(request_handle) + local token = TokenVerificationFilter.extractToken(request_handle) if not token then request_handle:respond({[":status"] = "401"}, "No token provided") @@ -170,7 +171,7 @@ function envoy_on_request(request_handle) end local body = request_handle:body():getBytes(0, request_handle:body():length()) - local method, subgraphName, parseError = parseJsonBody(body) + local method, subgraphName, parseError = TokenVerificationFilter.parseJsonBody(body) if parseError then request_handle:respond({[":status"] = "400"}, parseError) return @@ -181,24 +182,33 @@ function envoy_on_request(request_handle) return end - if not verifyValidMethod(method) then + if not TokenVerificationFilter.verifyValidMethod(method) then request_handle:respond({[":status"] = "400"}, "Invalid method") return end - local hasPermission, permissionError = checkTokenPermissions(token, subgraphName, method) + local hasPermission, permissionError = TokenVerificationFilter.checkTokenPermissions(token, subgraphName, method) - if permissionError then + if not hasPermission or permissionError then request_handle:logErr(permissionError) request_handle:respond({[":status"] = "401"}, permissionError) return end - if not hasPermission then - request_handle:respond({[":status"] = "401"}, "Unauthorized") - 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..100c055 --- /dev/null +++ b/auth-layer-proxy/tests/.luacov @@ -0,0 +1,3 @@ +modules = { + ["TokenVerificationFilter"] = "../filters/TokenVerificationFilter.lua", +} \ No newline at end of file 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..539d81d --- /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 \ No newline at end of file diff --git a/auth-layer-proxy/tests/testUtils.lua b/auth-layer-proxy/tests/testUtils.lua new file mode 100644 index 0000000..4b11a0a --- /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 \ No newline at end of file diff --git a/auth-layer-proxy/tests/test_checkTokenPermissions.lua b/auth-layer-proxy/tests/test_checkTokenPermissions.lua new file mode 100644 index 0000000..8b18d7e --- /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 \ No newline at end of file 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..9bcbaf9 --- /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 \ No newline at end of file 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..21b3396 --- /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 \ No newline at end of file From 2d21d652e6590c0f7151bc15d92a133f25f823bf Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 21 Mar 2024 20:52:21 -0600 Subject: [PATCH 16/21] removed dummy env variable used for testing Signed-off-by: Alfredo Gutierrez --- .github/workflows/proxy-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index e436fc4..6576695 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -7,10 +7,6 @@ on: branches: [ main, release/*] tags: [ v* ] -env: - ACTIONS_RUNTIME_TOKEN: fake-token - - jobs: proxy-tests: runs-on: ubuntu-latest From ff60c453fe3f53cd7a6f84921f1fdad5741daced Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 21 Mar 2024 20:53:20 -0600 Subject: [PATCH 17/21] removed print directory step used for debug of GHA WF Signed-off-by: Alfredo Gutierrez --- .github/workflows/proxy-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index 6576695..cdab67f 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -36,10 +36,6 @@ jobs: - name: Install luasocket run: luarocks install luasocket - - - name: Print Current Directory - run: pwd && ls -la - working-directory: auth-layer-proxy/tests - name: Run tests run: lua test.lua From f4613e96428140b7cbc64d7deab9b94739ecf1c6 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 21 Mar 2024 20:56:43 -0600 Subject: [PATCH 18/21] adding blank lines EOF where missing Signed-off-by: Alfredo Gutierrez --- auth-layer-proxy/tests/.luacov | 2 +- auth-layer-proxy/tests/testMocks.lua | 2 +- auth-layer-proxy/tests/testUtils.lua | 2 +- auth-layer-proxy/tests/test_checkTokenPermissions.lua | 2 +- auth-layer-proxy/tests/test_envoy_on_request.lua | 2 +- auth-layer-proxy/tests/test_verifyValidMethod.lua | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/auth-layer-proxy/tests/.luacov b/auth-layer-proxy/tests/.luacov index 100c055..436e035 100644 --- a/auth-layer-proxy/tests/.luacov +++ b/auth-layer-proxy/tests/.luacov @@ -1,3 +1,3 @@ modules = { ["TokenVerificationFilter"] = "../filters/TokenVerificationFilter.lua", -} \ No newline at end of file +} diff --git a/auth-layer-proxy/tests/testMocks.lua b/auth-layer-proxy/tests/testMocks.lua index 539d81d..94273e7 100644 --- a/auth-layer-proxy/tests/testMocks.lua +++ b/auth-layer-proxy/tests/testMocks.lua @@ -73,4 +73,4 @@ end local testMocks = {} testMocks.mockHttpRequest = mockHttpRequest testMocks.mockGetEnv = mockGetEnv -return testMocks \ No newline at end of file +return testMocks diff --git a/auth-layer-proxy/tests/testUtils.lua b/auth-layer-proxy/tests/testUtils.lua index 4b11a0a..fc9eb38 100644 --- a/auth-layer-proxy/tests/testUtils.lua +++ b/auth-layer-proxy/tests/testUtils.lua @@ -19,4 +19,4 @@ testUtils.printGreen = printGreen testUtils.printRed = printRed testUtils.wrapTextInRed = wrapTextInRed -return testUtils \ No newline at end of file +return testUtils diff --git a/auth-layer-proxy/tests/test_checkTokenPermissions.lua b/auth-layer-proxy/tests/test_checkTokenPermissions.lua index 8b18d7e..6543002 100644 --- a/auth-layer-proxy/tests/test_checkTokenPermissions.lua +++ b/auth-layer-proxy/tests/test_checkTokenPermissions.lua @@ -86,4 +86,4 @@ test_checkTokenPermissions.test_checkTokenPermissions_emailNotVerified = test_ch test_checkTokenPermissions.test_checkTokenPermissions_invalidMethod = test_checkTokenPermissions_invalidMethod test_checkTokenPermissions.test_checkTokenPermissions_noPermissionForMethod = test_checkTokenPermissions_noPermissionForMethod -return test_checkTokenPermissions \ No newline at end of file +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 index 9bcbaf9..df6fee4 100644 --- a/auth-layer-proxy/tests/test_envoy_on_request.lua +++ b/auth-layer-proxy/tests/test_envoy_on_request.lua @@ -167,4 +167,4 @@ 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 \ No newline at end of file +return test_envoy_on_request diff --git a/auth-layer-proxy/tests/test_verifyValidMethod.lua b/auth-layer-proxy/tests/test_verifyValidMethod.lua index 21b3396..2649c68 100644 --- a/auth-layer-proxy/tests/test_verifyValidMethod.lua +++ b/auth-layer-proxy/tests/test_verifyValidMethod.lua @@ -35,4 +35,4 @@ local test_verifyValidMethod = {} test_verifyValidMethod.test_valid_methods = test_valid_methods test_verifyValidMethod.test_invalid_method = test_invalid_method -return test_verifyValidMethod \ No newline at end of file +return test_verifyValidMethod From 600a2da6aaff82e97e8a55904c564e0e94197275 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 25 Mar 2024 12:40:41 -0600 Subject: [PATCH 19/21] Improvements on Documentation Signed-off-by: Alfredo Gutierrez --- auth-layer-proxy/README.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/auth-layer-proxy/README.md b/auth-layer-proxy/README.md index c04d2fd..1c03a61 100644 --- a/auth-layer-proxy/README.md +++ b/auth-layer-proxy/README.md @@ -20,7 +20,7 @@ flowchart LR linkStyle 4 stroke:#00ff00,stroke-width:2px; ``` -More information on the **Authorization Layer** can be found [here](link) +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 @@ -48,15 +48,32 @@ Make sure that the access token has the following claims: ```json { + "exp": 1711427468, + "iat": 1711391468, + "jti": "2fab170f-beb1-4821-acb4-ac19a71c9abe", "iss": "http://host.docker.internal:8080/realms/HederaTheGraph", - "resource_access": { - "htg-auth-layer": { - "roles": [ - "subgraph_create", - "subgraph_deploy" - ] - } + "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, From fff4e0e7a058e13846b420c565d2faa35bc817ec Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Mon, 25 Mar 2024 13:18:34 -0600 Subject: [PATCH 20/21] Improvements on Documentation Signed-off-by: Alfredo Gutierrez --- auth-layer-proxy/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/auth-layer-proxy/README.md b/auth-layer-proxy/README.md index 1c03a61..5ab9d95 100644 --- a/auth-layer-proxy/README.md +++ b/auth-layer-proxy/README.md @@ -28,9 +28,8 @@ This is an implementation of EnvoyProxy filters for authentication and authoriza 1. JSON Validation 2. Token Extraction -3. Token Hashing 4. Payload Params Extraction -5. Token Validation using JWT +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. From 8b43fa0ae6eee3a1aaf74619d15ebc3bbde7503e Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Thu, 28 Mar 2024 17:02:47 -0600 Subject: [PATCH 21/21] improvements on documentation and test name on GHA WF Signed-off-by: Alfredo Gutierrez --- .github/workflows/proxy-tests.yml | 2 +- auth-layer-proxy/README.md | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index cdab67f..d27c9db 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Auth Layer Proxy Tests on: pull_request: diff --git a/auth-layer-proxy/README.md b/auth-layer-proxy/README.md index 5ab9d95..d4e6dc3 100644 --- a/auth-layer-proxy/README.md +++ b/auth-layer-proxy/README.md @@ -1,6 +1,6 @@ # Readme -This is a token verification auth-layer-proxy for Hedera-The-Graph implementation that will allows a node operator to publish a secured `admin port` of the-graph deployment for hedera. +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. @@ -44,6 +44,11 @@ So make sure to have a token server running that is previously configured with a ### 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 { @@ -81,7 +86,7 @@ Make sure that the access token has the following claims: } ``` -For instructions on how to set-up the Auth Provider using KeyCloak, refer to the `Auth-Layer-Server` [README](link) +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