From ef5473b517330a3e70f24a90f61193c93a6a2466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0ren=20SALTALI?= Date: Thu, 7 Nov 2024 15:35:52 +0300 Subject: [PATCH] feat: Add support for environment variable and secret replacement in API configuration (#66) * feat: Update wrangler configuration for serverless API gateway * feat: Add support for environment variable and secret replacement in API configuration * feat: Replace hardcoded Auth0 credentials with environment variables in API configuration --- src/api-config.json | 285 +++++++++++++++++++++++++++++++++++++ src/api-config.schema.json | 4 - src/cors.js | 5 - src/index.js | 3 + src/mapping.js | 38 ++++- src/requests.js | 1 - wrangler.toml | 71 +++++++-- 7 files changed, 384 insertions(+), 23 deletions(-) create mode 100644 src/api-config.json diff --git a/src/api-config.json b/src/api-config.json new file mode 100644 index 0000000..dc96f97 --- /dev/null +++ b/src/api-config.json @@ -0,0 +1,285 @@ +{ + "$schema": "./api-config.schema.json", + "title": "API Gateway Config", + "description": "Configuration for the Serverless API Gateway", + "servers": [ + { + "alias": "serverlessapigateway-api", + "url": "https://74ec-2a02-e0-665f-2400-4803-52e0-7bcf-8789.ngrok-free.app" + }, + { + "alias": "serverlessapigateway-api-sub", + "url": "https://4e05-2a02-e0-665f-2400-e945-4e3-409c-d532.ngrok-free.app/sub" + } + ], + "services": [ + { + "alias": "endpoint1", + "entrypoint": "./services/endpoint1" + }, + { + "alias": "endpoint2", + "entrypoint": "services/endpoint2" + }, + { + "alias": "endpoint3", + "entrypoint": "./endpoint3" + } + ], + "cors": { + "allow_origins": ["https://api1.serverlessapigateway.com", "http://api1.serverlessapigateway.com", "https://api2.serverlessapigateway.com"], + "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["*"], + "expose_headers": ["*"], + "allow_credentials": true, + "max_age": 3600 + }, + "authorizer": { + "type": "auth0", + "domain": "$env.AUTH0_DOMAIN", + "client_id": "$env.AUTH0_CLIENT_ID", + "client_secret": "$secret.AUTH0_CLIENT_SECRET", + "redirect_uri": "https://api-test.serverlessapi.com/api/v1/auth0/callback", + "jwks": "$secret.AUTH0_JWKS", + "jwks_uri": "https://serverlessapi.us.auth0.com/.well-known/jwks.json", + "scope": "openid profile email" + }, + "variables": { + "global_variable": "global_variable_value" + }, + "paths": [ + { + "method": "GET", + "path": "/api/v1/mapping", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + }, + "auth": true, + "mapping": { + "headers": { + "x-jwt-sub": "$request.jwt.sub", + "x-jwt-aud": "$request.jwt.aud", + "x-jwt-iss": "$request.jwt.iss", + "x-jwt-name": "$request.jwt.name", + "x-jwt-email": "$request.jwt.email", + "x-config-api-key": "$config.api_key", + "x-config-database-url": "$config.database-url", + "x-config-nested-config-key": "$config.nested.config.key", + "x-query-userId": "$request.query.userId", + "x-query-redirect_uri": "$request.query.redirect_uri", + "x-global-variable": "$config.global_variable" + }, + "query": { + "jwt-sub": "$request.jwt.sub", + "jwt-aud": "$request.jwt.aud", + "jwt-iss": "$request.jwt.iss", + "jwt-name": "$request.jwt.name", + "jwt-email": "$request.jwt.email", + "config-api-key": "$config.api_key", + "config-database-url": "$config.database-url", + "config-nested-config-key": "$config.nested.config.key" + } + }, + "variables": { + "api_key": "API_KEY_VALUE", + "database-url": "sqlite://db.sqlite", + "nested.config.key": "nested config value", + "global_variable": "this-not-global-variable" + } + }, + { + "method": "GET", + "path": "/api/v1/auth", + "response": { + "status": "this is authenticated GET method" + }, + "auth": true + }, + { + "method": "GET", + "path": "/api/v1/no-auth", + "response": { + "status": "this is un-authenticated GET method" + }, + "auth": false + }, + { + "method": "GET", + "path": "/api/v1/proxy", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + } + }, + { + "method": "GET", + "path": "/api/v1/proxy/{parameter}", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + } + }, + { + "method": "ANY", + "path": "/api/v1/proxy/{.+}", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + } + }, + { + "method": "ANY", + "path": "/{.+}", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + } + }, + { + "method": "GET", + "path": "/api/v1/proxy/sub", + "integration": { + "type": "http", + "server": "serverlessapigateway-api-sub" + } + }, + { + "method": "GET", + "path": "/api/v1/method", + "response": { + "status": "this is GET method" + } + }, + { + "method": "POST", + "path": "/api/v1/method", + "response": { + "status": "this is POST method" + } + }, + { + "method": "ANY", + "path": "/api/v1/method", + "response": { + "status": "this is ANY method" + } + }, + { + "method": "OPTIONS", + "path": "/api/v1/method", + "response": { + "status": "this is OPTIONS method" + } + }, + { + "method": "POST", + "path": "/api/v1/proxy", + "integration": { + "type": "http_proxy", + "server": "serverlessapigateway-api" + } + }, + { + "method": "GET", + "path": "/api/v1/health", + "response": { + "status": "ok" + } + }, + { + "method": "GET", + "path": "/api/v1/health/ready", + "response": { + "status": "ready ok" + } + }, + { + "method": "GET", + "path": "/api/v1/health/live", + "response": { + "status": "live ok" + } + }, + { + "method": "GET", + "path": "/api/v1/health/string", + "response": "string ok" + }, + { + "method": "POST", + "path": "/api/v1/health", + "response": { + "status": "ok" + } + }, + { + "method": "ANY", + "path": "/api/v1/health/any", + "response": { + "status": "ok" + } + }, + { + "method": "ANY", + "path": "/api/v1/env", + "response": { + "status": "$env.VAR_TEST_RESPONSE_TEXT" + } + }, + { + "method": "ANY", + "path": "/api/v1/secret", + "response": { + "status": "$secrets.VAR_TEST_RESPONSE_TEXT_SECRET" + } + }, + { + "method": "GET", + "path": "/api/v1/endpoint1", + "integration": { + "type": "service", + "binding": "endpoint1" + } + }, + { + "method": "GET", + "path": "/api/v1/endpoint2", + "integration": { + "type": "service", + "binding": "endpoint2" + } + }, + { + "method": "GET", + "path": "/api/v1/endpoint3", + "integration": { + "type": "service", + "binding": "endpoint3" + } + }, + { + "method": "GET", + "path": "/api/v1/auth0/callback", + "integration": { + "type": "auth0_callback" + } + }, + { + "method": "GET", + "path": "/api/v1/auth0/profile", + "integration": { + "type": "auth0_userinfo" + }, + "auth": true + }, + { + "method": "GET", + "path": "/api/v1/auth0/callback-redirect", + "integration": { + "type": "auth0_callback_redirect" + }, + "auth": false + } + ] +} diff --git a/src/api-config.schema.json b/src/api-config.schema.json index 9e70bce..b519139 100644 --- a/src/api-config.schema.json +++ b/src/api-config.schema.json @@ -166,9 +166,6 @@ "client_secret": { "type": "string" }, - "secret": { - "type": "string" - }, "redirect_uri": { "type": "string" }, @@ -187,7 +184,6 @@ "domain", "client_id", "client_secret", - "secret", "redirect_uri", "scope" ], diff --git a/src/cors.js b/src/cors.js index 7fe8846..8b060e2 100644 --- a/src/cors.js +++ b/src/cors.js @@ -1,8 +1,4 @@ function setCorsHeaders(request, response, corsConfig) { - console.log('Setting CORS headers'); - console.log('Request headers:', request); - console.log('Response headers:', response.headers); - console.log('CORS config:', corsConfig); const origin = request.headers.get('Origin'); const matchingOrigin = corsConfig.allow_origins.find((allowedOrigin) => allowedOrigin === origin); @@ -19,7 +15,6 @@ function setCorsHeaders(request, response, corsConfig) { statusText: response.statusText, headers: headers, }); - console.log('New response:', newResponse); return newResponse; } diff --git a/src/index.js b/src/index.js index 733d802..d41c75d 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,9 @@ export default { console.error('Error loading API configuration', e); return setPoweredByHeader(request, responses.configIsMissingResponse()); } + + // Replace environment variables and secrets in the API configuration + apiConfig = await ValueMapper.replaceEnvAndSecrets(apiConfig, env); // Handle CORS preflight (OPTIONS) requests directly if (apiConfig.cors && request.method === 'OPTIONS') { diff --git a/src/mapping.js b/src/mapping.js index 06a8464..146f24f 100644 --- a/src/mapping.js +++ b/src/mapping.js @@ -16,7 +16,6 @@ export class ValueMapper { incoming.configVariables, incoming.globalVariables, ); - console.log(`Resolved value for ${key}: ${resolvedValue}`); if (resolvedValue !== null) { newHeaders.set(key, resolvedValue); } @@ -82,4 +81,41 @@ export class ValueMapper { return null; } + + static async replaceEnvAndSecrets(config, env) { + // Helper function to recursively traverse the object + function traverse(obj) { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + // Recursively call traverse for nested objects + traverse(obj[key]); + } else if (typeof obj[key] === 'string') { + // Replace environment variables + if (obj[key].startsWith('$env.')) { + const varName = obj[key].substring(5); // Get the variable name + if (env[varName] === null) { + console.error(`Error: Environment variable ${varName} is null.`); + obj[key] = ''; // Replace with empty string + } else { + obj[key] = env[varName] !== undefined ? env[varName] : ''; // Replace or set to empty string + } + } + // Replace secrets + else if (obj[key].startsWith('$secrets.')) { + const secretName = obj[key].substring(9); // Get the secret name + if (env[secretName] === null) { + console.error(`Error: Secret ${secretName} is null.`); + obj[key] = ''; // Replace with empty string + } else { + obj[key] = env[secretName] !== undefined ? env[secretName] : ''; // Replace or set to empty string + } + } + } + } + } + + // Start traversing the config object + traverse(config); + return config; // Return the modified config + } } diff --git a/src/requests.js b/src/requests.js index 2481d4f..70be0ce 100644 --- a/src/requests.js +++ b/src/requests.js @@ -7,7 +7,6 @@ function createProxiedRequest(request, server, matchedPath) { // For 'http_proxy', use the original path without the matching part const matchedPathPart = matchedPath.path.replace('{.+}', ''); newPath = requestUrl.pathname.replace(matchedPathPart, '/'); - console.log('New path:', newPath); } // Create the new request with the updated URL diff --git a/wrangler.toml b/wrangler.toml index 9503b8c..b17e0e0 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,21 +1,68 @@ -name = "serverlessapigw" +name = "serverlessapigateway" main = "src/index.js" -compatibility_date = "2024-02-08" -route = { pattern = "api-test.serverlessapigw.com/*", zone_name = "serverlessapigw.com" } -logpush = false -send_metrics = true -minify = true -workers_dev = true - -kv_namespaces =[ - { binding="CONFIG", id="1863fa3d004549e4800edeebeb36c0ff", preview_id = "1989f3848bb645b090f1cc82a5eb04b1"} -] +compatibility_date = "2024-01-17" find_additional_modules = true rules = [ - { type = "ESModule", globs = ["services/*.js","services_by_link/*.js"]} + { type = "ESModule", globs = ["services/*.js"]} +] + +services = [ + { binding = "USER_SERVICE", service = "user_service" } ] [observability] enabled = true head_sampling_rate = 0.3 + +[vars] +VAR_TEST_RESPONSE_TEXT = "This respose from environment" +VAR_TEST_RESPONSE_TEXT_SECRET = "This respose from secrets" + +# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) +# Note: Use secrets to store sensitive data. +# Docs: https://developers.cloudflare.com/workers/platform/environment-variables +# [vars] +# MY_VARIABLE = "production_value" + +# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. +# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.producers]] +# binding = "MY_QUEUE" +# queue = "my-queue" + +# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.consumers]] +# queue = "my-queue" + +# Bind another Worker service. Use this binding to call another Worker without network overhead. +# Docs: https://developers.cloudflare.com/workers/platform/services +# [[services]] +# binding = "MY_SERVICE" +# service = "my-service" + +# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. +# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects +# [[durable_objects.bindings]] +# name = "MY_DURABLE_OBJECT" +# class_name = "MyDurableObject" + +# Durable Object migrations. +# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations +# [[migrations]] +# tag = "v1" +# new_classes = ["MyDurableObject"]