diff --git a/CHANGELOG.md b/CHANGELOG.md index af633100..e6bba425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ Controller v3.8 is a **greenfield** release aligned with **Edgelet**. There is * - TCP bridge background provisioning resolves agent router mode via **`RouterManager.findOne`** (fixes **`TypeError`** when **`fakeTransaction`** is used in background jobs). - TCP bridge / router provisioning **background error logging** uses pino object-first `{ err, msg, … }` so failures are no longer logged as empty errors. - Router microservice **`siteConfig.platform`** defaults to **`edgelet`** (was **`docker`**) when the agent uses the Edgelet runtime. +- Boolean env vars (`TRUST_PROXY`, `SERVER_DEV_MODE`, `DB_USE_SSL`, `VAULT_ENABLED`, `ENABLE_TELEMETRY`, and other mapped flags) are parsed consistently from Kubernetes string values (`true`/`false`, `1`/`0`) via shared **`config.getBoolean()`** — fixes startup crash when **`TRUST_PROXY=true`** was passed as a string to Express. +- Postgres/MySQL SSL reads canonical **`DB_SSL_CA`** (via config) instead of undocumented **`DB_SSL_CA_B64`**; **`database.*.useSSL`** config key honored (was **`useSsl`** typo). ### Changed diff --git a/src/config/config.yaml b/src/config/config.yaml index b07365ac..0f1e5072 100644 --- a/src/config/config.yaml +++ b/src/config/config.yaml @@ -9,7 +9,7 @@ app: server: port: 51121 # Server port number devMode: true - publicUrl: "" # Canonical external URL (CONTROLLER_PUBLIC_URL); https required in prod unless auth.insecureAllowHttp + publicUrl: "https://localhost:51121" # Canonical external URL (CONTROLLER_PUBLIC_URL); https required in prod unless auth.insecureAllowHttp trustProxy: false # Honor X-Forwarded-* when behind reverse proxy (TRUST_PROXY) webSocket: perMessageDeflate: false diff --git a/src/config/embedded-oidc.js b/src/config/embedded-oidc.js index 5a419f16..dfaf73f5 100644 --- a/src/config/embedded-oidc.js +++ b/src/config/embedded-oidc.js @@ -14,6 +14,7 @@ const { createOidcProviderAdapterFactory } = require('../data/adapters/oidc-prov const { buildUserAccessClaims } = require('../services/auth-token-service') const { loadOidcProviderTtls } = require('./auth-oidc-ttl') const { getPublicUrl, getConsoleUrl } = require('./auth-urls') +const { getTrustProxySetting } = require('./trust-proxy') const DEFAULT_CONSOLE_CLIENT_ID = 'ecn-viewer' @@ -40,11 +41,7 @@ function buildInteractionPolicy () { } function isConsoleClientEnabled () { - const envValue = process.env.AUTH_CONSOLE_CLIENT_ENABLED - if (envValue !== undefined && envValue !== null && envValue !== '') { - return envValue === 'true' || envValue === '1' - } - return config.get('auth.consoleClient.enabled', false) === true + return config.getBoolean('auth.consoleClient.enabled', false) } function getConsoleClientId () { @@ -79,14 +76,6 @@ async function ensureConfidentialClientMetadata (db) { } } -function getTrustProxySetting () { - const trustProxy = process.env.TRUST_PROXY || config.get('server.trustProxy', false) - if (trustProxy === true || trustProxy === 'true' || trustProxy === 1 || trustProxy === '1') { - return true - } - return trustProxy || false -} - async function ensureConsoleClientMetadata (db) { if (!isConsoleClientEnabled()) { return null diff --git a/src/config/index.js b/src/config/index.js index bd292048..8bd52e87 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -2,6 +2,7 @@ const nconf = require('nconf') const path = require('path') const fs = require('fs') const yaml = require('js-yaml') +const { parseBoolean } = require('./parse-boolean') class Config { constructor () { @@ -107,13 +108,23 @@ class Config { } parseEnvValue (value) { - // Handle different types - if (value === 'true') return true - if (value === 'false') return false + const bool = parseBoolean(value) + if (bool !== undefined) { + return bool + } if (!isNaN(value) && value !== '') return Number(value) return value } + getBoolean (key, defaultValue = false) { + const value = this.get(key) + if (value === undefined) { + return defaultValue + } + const parsed = parseBoolean(value) + return parsed !== undefined ? parsed : defaultValue + } + formatValue (value) { if (typeof value === 'boolean') { return value.toString() diff --git a/src/config/oidc.js b/src/config/oidc.js index c52aa363..361d228a 100644 --- a/src/config/oidc.js +++ b/src/config/oidc.js @@ -123,9 +123,7 @@ function buildKauthGrant (claims, rawToken) { } function getDiscoveryOptions () { - const allowHttp = process.env.AUTH_INSECURE_ALLOW_HTTP !== undefined - ? process.env.AUTH_INSECURE_ALLOW_HTTP === 'true' - : config.get('auth.insecureAllowHttp', false) === true + const allowHttp = config.getBoolean('auth.insecureAllowHttp', false) if (!allowHttp) { return undefined } @@ -199,7 +197,7 @@ function initOidc () { // v3.9: read TLS client-auth policy when HTTPS + requestCert enabled (server.js listener) if (!isAuthConfigured()) { - const isProduction = !config.get('server.devMode', true) + const isProduction = !config.getBoolean('server.devMode', true) if (isProduction) { const error = new Error('Auth configuration required in production mode') logger.error('Failed to initialize OIDC:', error) diff --git a/src/config/parse-boolean.js b/src/config/parse-boolean.js new file mode 100644 index 00000000..0574b0f2 --- /dev/null +++ b/src/config/parse-boolean.js @@ -0,0 +1,18 @@ +'use strict' + +function parseBoolean (value, defaultValue) { + if (value === true || value === 'true' || value === 1 || value === '1') { + return true + } + if (value === false || value === 'false' || value === 0 || value === '0') { + return false + } + if (value === undefined || value === null || value === '') { + return arguments.length >= 2 ? defaultValue : undefined + } + return arguments.length >= 2 ? defaultValue : undefined +} + +module.exports = { + parseBoolean +} diff --git a/src/config/telemetry.js b/src/config/telemetry.js index 0d5f6fb6..3e66dc32 100644 --- a/src/config/telemetry.js +++ b/src/config/telemetry.js @@ -2,6 +2,7 @@ const { NodeSDK } = require('@opentelemetry/sdk-node') const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http') const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http') const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express') +const config = require('./index') const logger = require('../logger') const sdk = new NodeSDK({ @@ -16,10 +17,13 @@ const sdk = new NodeSDK({ ] }) +function isTelemetryEnabled () { + return config.getBoolean('otel.enabled', false) +} + // Start the SDK async function startTelemetry () { - const isTelemetryEnabled = process.env.ENABLE_TELEMETRY === 'true' - if (!isTelemetryEnabled) { + if (!isTelemetryEnabled()) { logger.info('Telemetry is disabled via ENABLE_TELEMETRY environment variable') return } @@ -35,7 +39,7 @@ async function startTelemetry () { // Handle process termination process.on('SIGTERM', () => { - if (process.env.ENABLE_TELEMETRY !== 'true') return + if (!isTelemetryEnabled()) return try { sdk.shutdown() diff --git a/src/config/trust-proxy.js b/src/config/trust-proxy.js new file mode 100644 index 00000000..9ea781b2 --- /dev/null +++ b/src/config/trust-proxy.js @@ -0,0 +1,17 @@ +'use strict' + +const config = require('./index') +const { parseBoolean } = require('./parse-boolean') + +function getTrustProxySetting () { + const trustProxy = config.get('server.trustProxy', false) + const parsed = parseBoolean(trustProxy) + if (parsed !== undefined) { + return parsed + } + return trustProxy || false +} + +module.exports = { + getTrustProxySetting +} diff --git a/src/data/migrations/mysql/db_migration_mysql_v3.8.0.sql b/src/data/migrations/mysql/db_migration_mysql_v3.8.0.sql index 7cfa7ce5..edd71845 100644 --- a/src/data/migrations/mysql/db_migration_mysql_v3.8.0.sql +++ b/src/data/migrations/mysql/db_migration_mysql_v3.8.0.sql @@ -218,7 +218,6 @@ CREATE TABLE IF NOT EXISTS Microservices ( name VARCHAR(255) DEFAULT 'New Microservice', config_last_updated BIGINT, rebuild BOOLEAN DEFAULT false, - root_host_access BOOLEAN DEFAULT false, log_size BIGINT DEFAULT 0, `delete` BOOLEAN DEFAULT false, delete_with_cleanup BOOLEAN DEFAULT false, @@ -228,6 +227,9 @@ CREATE TABLE IF NOT EXISTS Microservices ( registry_id INT DEFAULT 1, iofog_uuid VARCHAR(36), application_id INT, + run_as_user TEXT, + platform TEXT, + runtime TEXT, annotations TEXT, pid_mode VARCHAR(36), ipc_mode VARCHAR(36), @@ -280,7 +282,6 @@ CREATE TABLE IF NOT EXISTS MicroserviceExtraHost ( id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, template_type TEXT, name TEXT, - public_port INT, template TEXT, `value` TEXT, microservice_uuid VARCHAR(36), @@ -300,8 +301,6 @@ CREATE TABLE IF NOT EXISTS MicroservicePorts ( port_internal INT, port_external INT, is_udp BOOLEAN, - is_public BOOLEAN, - is_proxy BOOLEAN, created_at DATETIME, updated_at DATETIME, microservice_uuid VARCHAR(36), diff --git a/src/data/migrations/postgres/db_migration_pg_v3.8.0.sql b/src/data/migrations/postgres/db_migration_pg_v3.8.0.sql index 3ceb8dff..5bc338b5 100644 --- a/src/data/migrations/postgres/db_migration_pg_v3.8.0.sql +++ b/src/data/migrations/postgres/db_migration_pg_v3.8.0.sql @@ -216,7 +216,6 @@ CREATE TABLE IF NOT EXISTS "Microservices" ( name VARCHAR(255) DEFAULT 'New Microservice', config_last_updated BIGINT, rebuild BOOLEAN DEFAULT false, - root_host_access BOOLEAN DEFAULT false, log_size BIGINT DEFAULT 0, delete BOOLEAN DEFAULT false, delete_with_cleanup BOOLEAN DEFAULT false, @@ -226,6 +225,9 @@ CREATE TABLE IF NOT EXISTS "Microservices" ( registry_id INT DEFAULT 1, iofog_uuid VARCHAR(36), application_id INT, + run_as_user TEXT, + platform TEXT, + runtime TEXT, annotations TEXT, pid_mode VARCHAR(36), ipc_mode VARCHAR(36), @@ -278,7 +280,6 @@ CREATE TABLE IF NOT EXISTS "MicroserviceExtraHost" ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL, template_type TEXT, name TEXT, - public_port INT, template TEXT, value TEXT, microservice_uuid VARCHAR(36), @@ -298,8 +299,6 @@ CREATE TABLE IF NOT EXISTS "MicroservicePorts" ( port_internal INT, port_external INT, is_udp BOOLEAN, - is_public BOOLEAN, - is_proxy BOOLEAN, created_at TIMESTAMP(0), updated_at TIMESTAMP(0), microservice_uuid VARCHAR(36), diff --git a/src/data/migrations/sqlite/db_migration_sqlite_v3.8.0.sql b/src/data/migrations/sqlite/db_migration_sqlite_v3.8.0.sql index 46ef1bd8..0e88cbcb 100644 --- a/src/data/migrations/sqlite/db_migration_sqlite_v3.8.0.sql +++ b/src/data/migrations/sqlite/db_migration_sqlite_v3.8.0.sql @@ -216,7 +216,6 @@ CREATE TABLE IF NOT EXISTS Microservices ( name VARCHAR(255) DEFAULT 'New Microservice', config_last_updated BIGINT, rebuild BOOLEAN DEFAULT false, - root_host_access BOOLEAN DEFAULT false, log_size BIGINT DEFAULT 0, `delete` BOOLEAN DEFAULT false, delete_with_cleanup BOOLEAN DEFAULT false, diff --git a/src/data/providers/mysql.js b/src/data/providers/mysql.js index 7334a84e..fe4260b6 100644 --- a/src/data/providers/mysql.js +++ b/src/data/providers/mysql.js @@ -22,9 +22,9 @@ class MySqlDatabaseProvider extends DatabaseProvider { } // Configure SSL if enabled - const useSSL = process.env.DB_USE_SSL === 'true' || mysqlConfig.useSsl === true + const useSSL = config.getBoolean('database.mysql.useSSL', false) if (useSSL) { - const caBase64 = process.env.DB_SSL_CA_B64 + const caBase64 = config.get('database.mysql.sslCA', '') const sslOptions = caBase64 ? { ca: Buffer.from(caBase64, 'base64').toString('utf-8'), @@ -51,7 +51,7 @@ class MySqlDatabaseProvider extends DatabaseProvider { // Add SSL configuration to Sequelize if enabled if (useSSL) { - const caBase64 = process.env.DB_SSL_CA_B64 + const caBase64 = config.get('database.mysql.sslCA', '') sequelizeConfig.dialectOptions.ssl = caBase64 ? { ca: Buffer.from(caBase64, 'base64').toString('utf-8'), diff --git a/src/data/providers/postgres.js b/src/data/providers/postgres.js index b7e34369..1fdbeb0f 100644 --- a/src/data/providers/postgres.js +++ b/src/data/providers/postgres.js @@ -22,9 +22,9 @@ class PostgresDatabaseProvider extends DatabaseProvider { } // Configure SSL if enabled - const useSSL = process.env.DB_USE_SSL === 'true' || postgresConfig.useSsl === true + const useSSL = config.getBoolean('database.postgres.useSSL', false) if (useSSL) { - const caBase64 = process.env.DB_SSL_CA_B64 + const caBase64 = config.get('database.postgres.sslCA', '') const sslOptions = caBase64 ? { ca: Buffer.from(caBase64, 'base64').toString('utf-8'), @@ -50,7 +50,7 @@ class PostgresDatabaseProvider extends DatabaseProvider { } // Add SSL configuration to Sequelize if enabled if (useSSL) { - const caBase64 = process.env.DB_SSL_CA_B64 + const caBase64 = config.get('database.postgres.sslCA', '') sequelizeConfig.dialectOptions.ssl = caBase64 ? { ca: Buffer.from(caBase64, 'base64').toString('utf-8'), diff --git a/src/main.js b/src/main.js index d959b2ee..38593039 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,7 @@ const fetch = require('node-fetch-npm') const isHTTPS = () => { const sslKey = config.get('server.ssl.path.key', '') - const devMode = config.get('server.devMode', false) + const devMode = config.getBoolean('server.devMode', false) const sslCert = config.get('server.ssl.path.cert', '') return !devMode && sslKey && sslCert } diff --git a/src/middlewares/auth-rate-limit-middleware.js b/src/middlewares/auth-rate-limit-middleware.js index 46d65233..ce902eda 100644 --- a/src/middlewares/auth-rate-limit-middleware.js +++ b/src/middlewares/auth-rate-limit-middleware.js @@ -17,7 +17,7 @@ const rateLimits = new Map() function getRateLimitConfig () { return { - enabled: config.get('auth.rateLimit.enabled', true), + enabled: config.getBoolean('auth.rateLimit.enabled', true), maxRequests: config.get('auth.rateLimit.maxRequestsPerWindow', 60), windowMs: config.get('auth.rateLimit.windowMs', 60000) } diff --git a/src/middlewares/event-audit-middleware.js b/src/middlewares/event-audit-middleware.js index f48a3944..4aee3f6f 100644 --- a/src/middlewares/event-audit-middleware.js +++ b/src/middlewares/event-audit-middleware.js @@ -22,7 +22,7 @@ function eventAuditMiddleware (req, res, next) { // Check if auditing is enabled (reads from YAML or env var) // Use config.get() which properly parses boolean strings from env vars - const auditEnabled = config.get('settings.eventAuditEnabled', true) + const auditEnabled = config.getBoolean('settings.eventAuditEnabled', true) if (!auditEnabled) { return next() } diff --git a/src/routes/capabilities.js b/src/routes/capabilities.js index f2d5723e..dc1fdea7 100644 --- a/src/routes/capabilities.js +++ b/src/routes/capabilities.js @@ -23,7 +23,7 @@ module.exports = [ // Add rbacMiddleware.protect middleware to protect the route await rbacMiddleware.protect()(req, res, async () => { - if (config.get('nats.enabled')) { + if (config.getBoolean('nats.enabled', false)) { res.sendStatus(204) return } diff --git a/src/server.js b/src/server.js index e07126ad..49051d03 100755 --- a/src/server.js +++ b/src/server.js @@ -34,6 +34,7 @@ initialize().then(() => { resolveSessionSecret } = require('./config/auth-session-store.js') const { getPublicUrl, getConsoleUrl } = require('./config/auth-urls.js') + const { getTrustProxySetting } = require('./config/trust-proxy.js') function resolveConsolePath () { if (process.env.EDGEOPS_CONSOLE_PATH) { @@ -49,20 +50,20 @@ initialize().then(() => { const consoleApp = express() const app = express() - const trustProxy = process.env.TRUST_PROXY || config.get('server.trustProxy', false) + const trustProxy = getTrustProxySetting() if (trustProxy) { app.set('trust proxy', trustProxy === true ? 1 : trustProxy) consoleApp.set('trust proxy', trustProxy === true ? 1 : trustProxy) } function validateProductionPublicUrl () { - const devMode = process.env.DEV_MODE || config.get('server.devMode', true) + const devMode = config.getBoolean('server.devMode', true) if (devMode) { return } const publicUrl = process.env.CONTROLLER_PUBLIC_URL || config.get('server.publicUrl') - const insecureAllowHttp = config.get('auth.insecureAllowHttp', false) + const insecureAllowHttp = config.getBoolean('auth.insecureAllowHttp', false) if (!publicUrl) { throw new Error('CONTROLLER_PUBLIC_URL is required in production mode') @@ -82,8 +83,8 @@ initialize().then(() => { validateProductionPublicUrl() - const devMode = process.env.DEV_MODE || config.get('server.devMode', true) - const insecureAllowHttp = config.get('auth.insecureAllowHttp', false) + const devMode = config.getBoolean('server.devMode', true) + const insecureAllowHttp = config.getBoolean('auth.insecureAllowHttp', false) const consoleURLForCors = getConsoleUrl() app.use(cors({ diff --git a/src/services/agent-service.js b/src/services/agent-service.js index e458e085..d4695387 100644 --- a/src/services/agent-service.js +++ b/src/services/agent-service.js @@ -610,7 +610,7 @@ async function _checkMicroservicesFogType (fog, archId, transaction) { } const getControllerCA = async function (fog, transaction) { - const devMode = process.env.DEV_MODE || config.get('server.devMode') + const devMode = config.getBoolean('server.devMode', false) const sslCert = process.env.SSL_CERT || config.get('server.ssl.path.cert') const intermedKey = process.env.INTERMEDIATE_CERT || config.get('server.ssl.path.intermediateCert') const sslCertBase64 = config.get('server.ssl.base64.cert') diff --git a/src/services/auth-bootstrap-service.js b/src/services/auth-bootstrap-service.js index fd19b467..c70bb1cf 100644 --- a/src/services/auth-bootstrap-service.js +++ b/src/services/auth-bootstrap-service.js @@ -38,7 +38,7 @@ function getBootstrapConfig () { return { username: (process.env.OIDC_BOOTSTRAP_ADMIN_USERNAME || config.get('auth.bootstrap.username') || '').trim(), passwordRef: process.env.OIDC_BOOTSTRAP_ADMIN_PASSWORD || config.get('auth.bootstrap.password') || '', - allowBootstrapLog: config.get('auth.insecureAllowBootstrapLog', false) === true + allowBootstrapLog: config.getBoolean('auth.insecureAllowBootstrapLog', false) } } diff --git a/src/services/event-service.js b/src/services/event-service.js index 580381fe..a6c31a1e 100644 --- a/src/services/event-service.js +++ b/src/services/event-service.js @@ -352,7 +352,7 @@ async function createEvent (eventData, transaction) { async function createHttpEvent (req, res, startTime) { // Check if auditing is enabled // Use config.get() which properly parses boolean strings from env vars - const auditEnabled = config.get('settings.eventAuditEnabled', true) + const auditEnabled = config.getBoolean('settings.eventAuditEnabled', true) if (!auditEnabled) { return } @@ -367,7 +367,7 @@ async function createHttpEvent (req, res, startTime) { return } - const captureIp = config.get('settings.eventCaptureIpAddress', true) + const captureIp = config.getBoolean('settings.eventCaptureIpAddress', true) const endpointType = req.path.startsWith('/api/v3/agent/') ? 'agent' : 'user' const actorId = extractActorId(req) const resourceType = extractResourceType(req.path) @@ -404,12 +404,12 @@ async function createHttpEvent (req, res, startTime) { async function createWsConnectEvent (connectionData) { // Check if auditing is enabled // Use config.get() which properly parses boolean strings from env vars - const auditEnabled = config.get('settings.eventAuditEnabled', true) + const auditEnabled = config.getBoolean('settings.eventAuditEnabled', true) if (!auditEnabled) { return } - const captureIp = config.get('settings.eventCaptureIpAddress', true) + const captureIp = config.getBoolean('settings.eventCaptureIpAddress', true) const endpointType = connectionData.endpointType || 'user' // Sanitize path to remove sensitive query parameters (e.g., token) const sanitizedPath = sanitizeEndpointPath(connectionData.path) @@ -445,12 +445,12 @@ async function createWsConnectEvent (connectionData) { async function createWsDisconnectEvent (connectionData) { // Check if auditing is enabled // Use config.get() which properly parses boolean strings from env vars - const auditEnabled = config.get('settings.eventAuditEnabled', true) + const auditEnabled = config.getBoolean('settings.eventAuditEnabled', true) if (!auditEnabled) { return } - const captureIp = config.get('settings.eventCaptureIpAddress', true) + const captureIp = config.getBoolean('settings.eventCaptureIpAddress', true) const endpointType = connectionData.endpointType || 'user' // Sanitize path to remove sensitive query parameters (e.g., token) const sanitizedPath = sanitizeEndpointPath(connectionData.path) @@ -631,7 +631,7 @@ async function deleteEvents (params = {}, context = {}, transaction) { setImmediate(async () => { try { // Use config.get() which properly parses boolean strings from env vars - const captureIp = config.get('settings.eventCaptureIpAddress', true) + const captureIp = config.getBoolean('settings.eventCaptureIpAddress', true) const endpointType = request.path && request.path.startsWith('/api/v3/agent/') ? 'agent' : 'user' const actorId = extractActorId(request) diff --git a/src/services/iofog-service.js b/src/services/iofog-service.js index d0637491..c0b99c98 100644 --- a/src/services/iofog-service.js +++ b/src/services/iofog-service.js @@ -1095,7 +1095,7 @@ async function generateProvisioningKeyEndPoint (fogData, isCLI, transaction) { const provisioningKeyData = await refreshProvisionKeyForFog(fogData.uuid, transaction) - const devMode = process.env.DEV_MODE || config.get('server.devMode') + const devMode = config.getBoolean('server.devMode', false) const sslCert = process.env.SSL_CERT || config.get('server.ssl.path.cert') const intermedKey = process.env.INTERMEDIATE_CERT || config.get('server.ssl.path.intermediateCert') const sslCertBase64 = config.get('server.ssl.base64.cert') diff --git a/src/vault/vault-manager.js b/src/vault/vault-manager.js index 843df2f2..e0f52625 100644 --- a/src/vault/vault-manager.js +++ b/src/vault/vault-manager.js @@ -149,7 +149,7 @@ class VaultManager { */ async initialize () { // Check if vault is enabled using env var or config - const vaultEnabled = process.env.VAULT_ENABLED === 'true' || config.get('vault.enabled', false) + const vaultEnabled = config.getBoolean('vault.enabled', false) // If vault is not enabled, use internal encryption if (!vaultEnabled) { diff --git a/test/src/config/embedded-oidc.test.js b/test/src/config/embedded-oidc.test.js index a5ec1f55..dca7aa77 100644 --- a/test/src/config/embedded-oidc.test.js +++ b/test/src/config/embedded-oidc.test.js @@ -140,7 +140,7 @@ describe('Embedded OIDC issuer', () => { CONTROLLER_PUBLIC_URL: $publicUrl, AUTH_CONSOLE_CLIENT_ENABLED: 'true', OIDC_CONSOLE_CLIENT_ID: 'ecn-viewer' - }) + }, { sandbox: $sandbox }) const embeddedOidc = reloadEmbeddedOidcModule() const app = express() diff --git a/test/src/config/oidc.test.js b/test/src/config/oidc.test.js index 36ff8cca..bad73a21 100644 --- a/test/src/config/oidc.test.js +++ b/test/src/config/oidc.test.js @@ -42,7 +42,7 @@ describe('OIDC config', () => { describe('isAuthConfigured()', () => { it('returns false when embedded mode has no public URL', () => { - applyOidcEnv({}) + applyOidcEnv({}, { sandbox: $sandbox }) const oidc = reloadOidcModule() expect(oidc.isAuthConfigured()).to.equal(false) }) @@ -62,7 +62,7 @@ describe('OIDC config', () => { describe('initOidc() without auth config', () => { it('initializes without bearer validation when auth is not configured in dev mode', async () => { - applyOidcEnv({}) + applyOidcEnv({}, { sandbox: $sandbox }) const oidc = reloadOidcModule() oidc.initOidc() @@ -78,18 +78,13 @@ describe('OIDC config', () => { }) describe('initOidc() production mode', () => { - beforeEach(() => { - const originalGet = config.get.bind(config) - $sandbox.stub(config, 'get').callsFake((key, defaultValue) => { - if (key === 'server.devMode') { - return false + it('throws when auth is not configured', () => { + applyOidcEnv({}, { + sandbox: $sandbox, + configExtras: { + getBoolean: { 'server.devMode': false } } - return originalGet(key, defaultValue) }) - }) - - it('throws when auth is not configured', () => { - applyOidcEnv({}) const oidc = reloadOidcModule() expect(() => oidc.initOidc()).to.throw('Auth configuration required in production mode') }) diff --git a/test/src/config/parse-boolean.test.js b/test/src/config/parse-boolean.test.js new file mode 100644 index 00000000..0021be7e --- /dev/null +++ b/test/src/config/parse-boolean.test.js @@ -0,0 +1,36 @@ +'use strict' + +const { expect } = require('chai') +const { parseBoolean } = require('../../../src/config/parse-boolean') + +describe('parse-boolean', () => { + it('parses true-like values', () => { + expect(parseBoolean(true)).to.equal(true) + expect(parseBoolean('true')).to.equal(true) + expect(parseBoolean(1)).to.equal(true) + expect(parseBoolean('1')).to.equal(true) + }) + + it('parses false-like values', () => { + expect(parseBoolean(false)).to.equal(false) + expect(parseBoolean('false')).to.equal(false) + expect(parseBoolean(0)).to.equal(false) + expect(parseBoolean('0')).to.equal(false) + }) + + it('returns default for empty values when provided', () => { + expect(parseBoolean(undefined, true)).to.equal(true) + expect(parseBoolean(null, false)).to.equal(false) + expect(parseBoolean('', true)).to.equal(true) + }) + + it('returns undefined for non-boolean values without default', () => { + expect(parseBoolean('loopback')).to.equal(undefined) + expect(parseBoolean(2)).to.equal(undefined) + }) + + it('returns default for non-boolean values when default provided', () => { + expect(parseBoolean('loopback', false)).to.equal(false) + expect(parseBoolean(2, true)).to.equal(true) + }) +}) diff --git a/test/src/config/trust-proxy.test.js b/test/src/config/trust-proxy.test.js new file mode 100644 index 00000000..10920d3a --- /dev/null +++ b/test/src/config/trust-proxy.test.js @@ -0,0 +1,83 @@ +'use strict' + +const { expect } = require('chai') +const config = require('../../../src/config') + +describe('config.getBoolean', () => { + let originalGet + + beforeEach(() => { + originalGet = config.get.bind(config) + delete require.cache[require.resolve('../../../src/config/trust-proxy')] + }) + + afterEach(() => { + config.get = originalGet + delete require.cache[require.resolve('../../../src/config/trust-proxy')] + }) + + it('returns true for string "true" config values', () => { + config.get = (key, defaultValue) => { + if (key === 'server.trustProxy') return 'true' + return originalGet(key, defaultValue) + } + expect(config.getBoolean('server.trustProxy', false)).to.equal(true) + }) + + it('returns false for string "false" config values', () => { + config.get = (key, defaultValue) => { + if (key === 'server.trustProxy') return 'false' + return originalGet(key, defaultValue) + } + expect(config.getBoolean('server.trustProxy', true)).to.equal(false) + }) + + it('returns true for string "1" config values', () => { + config.get = (key, defaultValue) => { + if (key === 'server.devMode') return '1' + return originalGet(key, defaultValue) + } + expect(config.getBoolean('server.devMode', false)).to.equal(true) + }) +}) + +describe('trust-proxy', () => { + let originalGet + + beforeEach(() => { + originalGet = config.get.bind(config) + delete require.cache[require.resolve('../../../src/config/trust-proxy')] + }) + + afterEach(() => { + config.get = originalGet + delete require.cache[require.resolve('../../../src/config/trust-proxy')] + }) + + it('returns true when trust proxy config is the string "true"', () => { + config.get = (key, defaultValue) => { + if (key === 'server.trustProxy') return 'true' + return originalGet(key, defaultValue) + } + const { getTrustProxySetting } = require('../../../src/config/trust-proxy') + expect(getTrustProxySetting()).to.equal(true) + }) + + it('returns false when trust proxy config is the string "false"', () => { + config.get = (key, defaultValue) => { + if (key === 'server.trustProxy') return 'false' + return originalGet(key, defaultValue) + } + const { getTrustProxySetting } = require('../../../src/config/trust-proxy') + expect(getTrustProxySetting()).to.equal(false) + }) + + it('passes through hop count from config', () => { + config.get = (key, defaultValue) => { + if (key === 'server.trustProxy') return 2 + return originalGet(key, defaultValue) + } + const { getTrustProxySetting } = require('../../../src/config/trust-proxy') + expect(getTrustProxySetting()).to.equal(2) + }) +}) diff --git a/test/src/services/user-service-oidc.test.js b/test/src/services/user-service-oidc.test.js index 3089a3cc..86da74bc 100644 --- a/test/src/services/user-service-oidc.test.js +++ b/test/src/services/user-service-oidc.test.js @@ -133,7 +133,7 @@ describe('User service OIDC', () => { describe('without auth config', () => { beforeEach(() => { - applyOidcEnv({}) + applyOidcEnv({}, { sandbox: $sandbox }) reloadOidcModule() }) diff --git a/test/support/oidc-test-helpers.js b/test/support/oidc-test-helpers.js index fbc5cabe..922f72f7 100644 --- a/test/support/oidc-test-helpers.js +++ b/test/support/oidc-test-helpers.js @@ -1,3 +1,5 @@ +const { parseBoolean } = require('../../src/config/parse-boolean') + const OIDC_ENV_KEYS = [ 'AUTH_MODE', 'CONTROLLER_PUBLIC_URL', @@ -29,7 +31,86 @@ function restoreOidcEnv (snapshot) { } } -function applyOidcEnv (env = {}) { +function resolveOidcConfigOverrides (env = {}) { + const overrides = {} + const booleanOverrides = {} + + for (const key of OIDC_ENV_KEYS) { + const value = env[key] + const isCleared = value === undefined || value === null + + switch (key) { + case 'CONTROLLER_PUBLIC_URL': + overrides['server.publicUrl'] = isCleared ? '' : value + break + case 'AUTH_MODE': + if (!isCleared) { + overrides['auth.mode'] = value + } + break + case 'OIDC_ISSUER_URL': + overrides['auth.issuerUrl'] = isCleared ? '' : value + break + case 'OIDC_CLIENT_ID': + overrides['auth.client.id'] = isCleared ? '' : value + break + case 'OIDC_CLIENT_SECRET': + overrides['auth.client.secret'] = isCleared ? '' : value + break + case 'OIDC_CONSOLE_CLIENT_ID': + if (!isCleared) { + overrides['auth.consoleClient.id'] = value + overrides['auth.consoleClient'] = value + } + break + case 'AUTH_CONSOLE_CLIENT_ENABLED': + booleanOverrides['auth.consoleClient.enabled'] = isCleared + ? false + : parseBoolean(value, false) + break + case 'AUTH_INSECURE_ALLOW_HTTP': + booleanOverrides['auth.insecureAllowHttp'] = isCleared + ? false + : parseBoolean(value, false) + break + default: + break + } + } + + return { overrides, booleanOverrides } +} + +function installOidcConfigStubs (sandbox, env = {}, extras = {}) { + const config = require('../../src/config') + const originalGet = config.get.bind(config) + const originalGetBoolean = config.getBoolean.bind(config) + const { overrides, booleanOverrides } = resolveOidcConfigOverrides(env) + const extraGet = extras.get || {} + const extraGetBoolean = extras.getBoolean || {} + + sandbox.stub(config, 'get').callsFake((key, defaultValue) => { + if (Object.prototype.hasOwnProperty.call(extraGet, key)) { + return extraGet[key] + } + if (Object.prototype.hasOwnProperty.call(overrides, key)) { + return overrides[key] + } + return originalGet(key, defaultValue) + }) + + sandbox.stub(config, 'getBoolean').callsFake((key, defaultValue = false) => { + if (Object.prototype.hasOwnProperty.call(extraGetBoolean, key)) { + return extraGetBoolean[key] + } + if (Object.prototype.hasOwnProperty.call(booleanOverrides, key)) { + return booleanOverrides[key] + } + return originalGetBoolean(key, defaultValue) + }) +} + +function applyOidcEnv (env = {}, options = {}) { for (const key of OIDC_ENV_KEYS) { if (env[key] === undefined || env[key] === null) { delete process.env[key] @@ -37,6 +118,10 @@ function applyOidcEnv (env = {}) { process.env[key] = env[key] } } + + if (options.sandbox) { + installOidcConfigStubs(options.sandbox, env, options.configExtras || {}) + } } function reloadOidcModule () { @@ -75,6 +160,8 @@ module.exports = { OIDC_ENV_KEYS, snapshotOidcEnv, restoreOidcEnv, + resolveOidcConfigOverrides, + installOidcConfigStubs, applyOidcEnv, reloadOidcModule, runMiddleware