Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1953 joi env validation #1963

Merged
merged 23 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"tsc": "tsc",
"test:here": "node ./runTest.js",
"generate-joi": "node dist/scripts/joiGenerator.js",
"build-docs": "bash ./buildDocs.sh"
"build-docs": "bash ./buildDocs.sh",
"validate-env-variables": "node ./dist/scripts/envVarsValidator.js"
},
"nyc": {
"include": [
Expand Down
185 changes: 49 additions & 136 deletions api/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logger from "./lib/logger";
import { randomString } from "./service/hash";

import { envVarsSchema } from "envVarsSchema";
SamuelPull marked this conversation as resolved.
Show resolved Hide resolved

export interface JwtConfig {
secretOrPrivateKey: string;
Expand Down Expand Up @@ -121,133 +121,77 @@ interface Config {
silenceLoggingOnFrequentRoutes: boolean;
}

/**
* environment variables which are required by the API
* @notExported
*/
const requiredEnvVars = ["ORGANIZATION", "ORGANIZATION_VAULT_SECRET"];


const { error, value: envVars } = envVarsSchema.validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}

export const config: Config = {
organization: process.env.ORGANIZATION || "",
organizationVaultSecret: process.env.ORGANIZATION_VAULT_SECRET || "",
port: Number(process.env.PORT) || 8080,
rootSecret: process.env.ROOT_SECRET || randomString(32),
organization: envVars.ORGANIZATION,
organizationVaultSecret: envVars.ORGANIZATION_VAULT_SECRET,
port: envVars.PORT,
rootSecret: envVars.ROOT_SECRET,
// RPC is the mutlichain daemon
rpc: {
host: process.env.MULTICHAIN_RPC_HOST || "localhost",
port: Number(process.env.MULTICHAIN_RPC_PORT) || 8000,
user: process.env.MULTICHAIN_RPC_USER || "multichainrpc",
password: process.env.MULTICHAIN_RPC_PASSWORD || "s750SiJnj50yIrmwxPnEdSzpfGlTAHzhaUwgqKeb0G1j",
host: envVars.MULTICHAIN_RPC_HOST,
port: envVars.MULTICHAIN_RPC_PORT,
user: envVars.MULTICHAIN_RPC_USER,
password: envVars.MULTICHAIN_RPC_PASSWORD,
},
// Blockchain is the blockchain component of Trubudget
// It serves e.g. backup or version endpoints
blockchain: {
host: process.env.MULTICHAIN_RPC_HOST || "localhost",
port: Number(process.env.BLOCKCHAIN_PORT) || 8085,
host: envVars.MULTICHAIN_RPC_HOST,
port: envVars.BLOCKCHAIN_PORT,
},
jwt: {
secretOrPrivateKey: process.env.JWT_SECRET || randomString(32),
publicKey: process.env.JWT_PUBLIC_KEY || "",
algorithm: process.env.JWT_ALGORITHM === "RS256" ? "RS256" : "HS256",
secretOrPrivateKey: envVars.JWT_SECRET,
publicKey: envVars.JWT_PUBLIC_KEY,
algorithm: envVars.JWT_ALGORITHM,
},
npmPackageVersion: process.env.npm_package_version || "",
// Continues Integration
ciCommitSha: process.env.CI_COMMIT_SHA || "",
buildTimeStamp: process.env.BUILDTIMESTAMP || "",
documentFeatureEnabled: process.env.DOCUMENT_FEATURE_ENABLED === "true" ? true : false,
documentExternalLinksEnabled:
process.env.DOCUMENT_EXTERNAL_LINKS_ENABLED === "true" ? true : false,
documentFeatureEnabled: envVars.DOCUMENT_FEATURE_ENABLED,
documentExternalLinksEnabled: envVars.DOCUMENT_EXTERNAL_LINKS_ENABLED,
storageService: {
host: process.env.STORAGE_SERVICE_HOST || "localhost",
port: Number(process.env.STORAGE_SERVICE_PORT) || 8090,
externalUrl: process.env.STORAGE_SERVICE_EXTERNAL_URL || "",
host: envVars.STORAGE_SERVICE_HOST,
port: envVars.STORAGE_SERVICE_PORT,
externalUrl: envVars.STORAGE_SERVICE_EXTERNAL_URL,
},
emailService: {
host: process.env.EMAIL_HOST || "localhost",
port: Number(process.env.EMAIL_PORT) || 8089,
host: envVars.EMAIL_HOST,
port: envVars.EMAIL_PORT,
},
encryptionPassword:
process.env.ENCRYPTION_PASSWORD === "" ? undefined : process.env.ENCRYPTION_PASSWORD,
signingMethod: process.env.SIGNING_METHOD || "node",
nodeEnv: process.env.NODE_ENV || "production",
accessControlAllowOrigin: process.env.ACCESS_CONTROL_ALLOW_ORIGIN || "*",
rateLimit:
process.env.RATE_LIMIT === "" || isNaN(Number(process.env.RATE_LIMIT))
? undefined
: Number(process.env.RATE_LIMIT),
encryptionPassword: envVars.ENCRYPTION_PASSWORD,
signingMethod: envVars.SIGNING_METHOD,
nodeEnv: envVars.NODE_ENV,
accessControlAllowOrigin: envVars.ACCESS_CONTROL_ALLOW_ORIGIN,
rateLimit: envVars.RATE_LIMIT,
authProxy: {
enabled: process.env.AUTHPROXY_ENABLED === "true" || false,
enabled: envVars.AUTHPROXY_ENABLED,
authProxyCookie: "authorizationToken",
jwsSignature: process.env.AUTHPROXY_JWS_SIGNATURE || undefined,
jwsSignature: envVars.AUTHPROXY_JWS_SIGNATURE,
},
db: {
user: process.env.API_DB_USER || "postgres",
password: process.env.API_DB_PASSWORD || "test",
host: process.env.API_DB_HOST || "localhost",
database: process.env.API_DB_NAME || "trubudget_email_service",
port: Number(process.env.API_DB_PORT) || 5432,
ssl: process.env.API_DB_SSL === "true",
schema: process.env.API_DB_SCHEMA || "public",
user: envVars.API_DB_USER,
password: envVars.API_DB_PASSWORD,
host: envVars.API_DB_HOST,
database: envVars.API_DB_NAME,
port: envVars.API_DB_PORT,
ssl: envVars.API_DB_SSL,
schema: envVars.API_DB_SCHEMA,
},
dbType: process.env.DB_TYPE || "pg",
sqlDebug: Boolean(process.env.SQL_DEBUG) || false,
refreshTokensTable: process.env.API_REFRESH_TOKENS_TABLE || "refresh_token",
refreshTokenStorage:
process.env.REFRESH_TOKEN_STORAGE &&
["db", "memory"].includes(process.env.REFRESH_TOKEN_STORAGE)
? process.env.REFRESH_TOKEN_STORAGE
: undefined,
snapshotEventInterval: Number(process.env.SNAPSHOT_EVENT_INTERVAL) || 3,
azureMonitorConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || "",
silenceLoggingOnFrequentRoutes:
process.env.SILENCE_LOGGING_ON_FREQUENT_ROUTES === "true" || false,
};

/**
* Checks if required environment variables are set, stops the process otherwise
* @notExported
*
* @param requiredEnvVars environment variables required for the API to run
*/
function exitIfMissing(requiredEnvVars): void {
let envVarMissing = false;
requiredEnvVars.forEach((env) => {
if (!envExists(process.env, env)) envVarMissing = true;
});
if (envVarMissing) process.exit(1);
}

/**
* Checks if an environment variable is attached to the current process
* @notExported
*
* @param processEnv environment variables attached to the current process
* @param prop environment variable to check
* @param msg optional message to print out
* @returns a boolean indicating if an environment variable is attached to the current process
*/
const envExists = <T, K extends keyof T>(
processEnv: Partial<T>,
prop: K,
msg?: string,
): boolean => {
if (processEnv[prop] === undefined || processEnv[prop] === null) {
switch (prop) {
case "ORGANIZATION":
msg = "Please set ORGANIZATION to the organization this node belongs to.";
break;
case "ORGANIZATION_VAULT_SECRET":
msg =
"Please set ORGANIZATION_VAULT_SECRET to the secret key used to encrypt the organization's vault.";
break;
default:
break;
}
logger.fatal(msg || `Environment is missing required variable ${String(prop)}`);
return false;
} else {
return true;
}
dbType: envVars.DB_TYPE,
sqlDebug: envVars.SQL_DEBUG,
refreshTokensTable: envVars.API_REFRESH_TOKENS_TABLE,
refreshTokenStorage: envVars.REFRESH_TOKEN_STORAGE,
snapshotEventInterval: envVars.SNAPSHOT_EVENT_INTERVAL,
azureMonitorConnectionString: envVars.APPLICATIONINSIGHTS_CONNECTION_STRING,
silenceLoggingOnFrequentRoutes: envVars.SILENCE_LOGGING_ON_FREQUENT_ROUTES,
};

/**
Expand All @@ -257,37 +201,6 @@ const envExists = <T, K extends keyof T>(
* @notExported
*/
const getValidConfig = (): Config => {
exitIfMissing(requiredEnvVars);

// Environment Validation
const jwtSecret: string = process.env.JWT_SECRET || randomString(32);
if (jwtSecret.length < 32) {
logger.warn("Warning: the JWT secret key should be at least 32 characters long.");
}
const rootSecret: string = process.env.ROOT_SECRET || randomString(32);
if (!process.env.ROOT_SECRET) {
logger.warn(`Warning: root password not set; autogenerated to ${rootSecret}`);
}

// Document feature enabled
if (process.env.DOCUMENT_FEATURE_ENABLED === "true") {
const requiredDocEnvVars = ["STORAGE_SERVICE_EXTERNAL_URL"];
exitIfMissing(requiredDocEnvVars);
}

const jwtAlgorithm: string = process.env.JWT_ALGORITHM;
if (
!(
jwtAlgorithm === "HS256" ||
jwtAlgorithm === "RS256" ||
jwtAlgorithm === undefined ||
jwtAlgorithm === ""
)
) {
logger.fatal("JWT_ALGORITHM must be either HS256 or RS256 or empty (defaults to HS256)");
process.exit(1);
}

return config;
};
/**
Expand Down
118 changes: 118 additions & 0 deletions api/src/envVarsSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as Joi from "joi";
import { randomString } from "./service/hash";

export const envVarsSchema = Joi.object({
ORGANIZATION: Joi.string()
.min(1)
.max(100)
.required()
.note(
"In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner.",
),
PORT: Joi.number()
SamuelPull marked this conversation as resolved.
Show resolved Hide resolved
.min(0)
.max(65535)
.default(8091)
.note(
"The port used to expose the API for your installation. <br/>Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`.",
),
ORGANIZATION_VAULT_SECRET: Joi.string()
.invalid("secret")
.required()
.note(
"This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node. <br/>**Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env_example` file!",
),
ROOT_SECRET: Joi.string()
.min(8)
.required()
.default(randomString(32))
.note(
"The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console <br/>**Caution:** If you want to run TruBudget in production, make sure to set a secure root secret.",
),
MULTICHAIN_RPC_HOST: Joi.string()
.default("localhost")
.note(
"The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to.",
),
MULTICHAIN_RPC_PORT: Joi.number()
.default(8000)
.note(
"The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore)",
),
MULTICHAIN_RPC_USER: Joi.string()
.default("multichainrpc")
.note("The user used to connect to the multichain daemon."),
MULTICHAIN_RPC_PASSWORD: Joi.string()
.default("s750SiJnj50yIrmwxPnEdSzpfGlTAHzhaUwgqKeb0G1j")
.note(
"Password used by the API to connect to the blockchain. The password is set by the origin node upon start. Every beta node needs to use the same RPC password in order to be able to connect to the blockchain. <br/>**Hint:** Although the MULTICHAIN_RPC_PASSWORD is not required it is highly recommended to set an own secure one.",
),
BLOCKCHAIN_PORT: Joi.number()
.default(8085)
.note(
"The port used to expose the multichain daemon of your Trubudget blockchain installation(bc). The port used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon.",
),
SWAGGER_BASEPATH: Joi.string()
.example("/")
.forbidden()
.note(
"deprecated",
"This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately.",
),
JWT_ALGORITHM: Joi.string()
.default("HS256")
.valid("HS256", "RS256")
.note("Algorithm used for signing and verifying JWTs."),
JWT_SECRET: Joi.string()
.min(32)
SamuelPull marked this conversation as resolved.
Show resolved Hide resolved
.default(randomString(32))
.when("JWT_ALGORITHM", {
is: "RS256",
then: Joi.string().base64().required(),
})
.note(
"A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA.",
),
JWT_PUBLIC_KEY: Joi.string()
.default("")
.when("JWT_ALGORITHM", {
is: "RS256",
then: Joi.string().base64().required(),
})
.note(
"If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA.",
),
DOCUMENT_FEATURE_ENABLED: Joi.boolean().default(false),
DOCUMENT_EXTERNAL_LINKS_ENABLED: Joi.boolean().default(false),
STORAGE_SERVICE_HOST: Joi.string().default("localhost"),
STORAGE_SERVICE_PORT: Joi.number().default(8090),
STORAGE_SERVICE_EXTERNAL_URL: Joi.string().default("").when("DOCUMENT_FEATURE_ENABLED", {
is: true,
then: Joi.required(),
}),
EMAIL_HOST: Joi.string().default("localhost"),
EMAIL_PORT: Joi.number().default(8089),
ACCESS_CONTROL_ALLOW_ORIGIN: Joi.string().default("*"),
NODE_ENV: Joi.string().default("production"),
ENCRYPTION_PASSWORD: Joi.string(),
SIGNING_METHOD: Joi.string().default("node"),
RATE_LIMIT: Joi.number().allow("").empty(""),
AUTHPROXY_ENABLED: Joi.boolean().default(false),
AUTHPROXY_JWS_SIGNATURE: Joi.string(),
DB_TYPE: Joi.string().default("pg"),
SQL_DEBUG: Joi.boolean().default(false),
API_DB_USER: Joi.string().default("postgres"),
API_DB_PASSWORD: Joi.string().default("test"),
API_DB_HOST: Joi.string().default("localhost"),
API_DB_NAME: Joi.string().default("trubudget_email_service"),
API_DB_PORT: Joi.number().default(5432),
API_DB_SSL: Joi.boolean().default(false),
API_DB_SCHEMA: Joi.string().default("public"),
API_REFRESH_TOKENS_TABLE: Joi.string().default("refresh_token"),
REFRESH_TOKEN_STORAGE: Joi.string().allow("db", "memory"),
SNAPSHOT_EVENT_INTERVAL: Joi.number().default(3),
SILENCE_LOGGING_ON_FREQUENT_ROUTES: Joi.boolean().default(false),
APPLICATIONINSIGHTS_CONNECTION_STRING: Joi.string().allow(""),
})
.unknown()
.required();
Loading