Skip to content

Commit

Permalink
feat: Add support for environment variable and secret replacement in …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
irensaltali authored Nov 7, 2024
1 parent e1956c2 commit ef5473b
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 23 deletions.
285 changes: 285 additions & 0 deletions src/api-config.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
4 changes: 0 additions & 4 deletions src/api-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,6 @@
"client_secret": {
"type": "string"
},
"secret": {
"type": "string"
},
"redirect_uri": {
"type": "string"
},
Expand All @@ -187,7 +184,6 @@
"domain",
"client_id",
"client_secret",
"secret",
"redirect_uri",
"scope"
],
Expand Down
5 changes: 0 additions & 5 deletions src/cors.js
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -19,7 +15,6 @@ function setCorsHeaders(request, response, corsConfig) {
statusText: response.statusText,
headers: headers,
});
console.log('New response:', newResponse);
return newResponse;
}

Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
38 changes: 37 additions & 1 deletion src/mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
}
}
1 change: 0 additions & 1 deletion src/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ef5473b

Please sign in to comment.