diff --git a/.changeset/thirty-turtles-design.md b/.changeset/thirty-turtles-design.md new file mode 100644 index 00000000..41a85fc7 --- /dev/null +++ b/.changeset/thirty-turtles-design.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Fix Redis cache connection config to skip AUTH when `REDIS_PASSWORD` is unset diff --git a/src/cache/client.ts b/src/cache/client.ts index d9064657..de7d689b 100644 --- a/src/cache/client.ts +++ b/src/cache/client.ts @@ -4,12 +4,49 @@ import { createLogger } from '../factories/logger-factory' const logger = createLogger('cache-client') -export const getCacheConfig = (): RedisClientOptions => ({ - url: process.env.REDIS_URI - ? process.env.REDIS_URI - : `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, - password: process.env.REDIS_PASSWORD, -}) +const redactRedisUrlCredentials = (url: string): string => { + try { + const parsedUrl = new URL(url) + + if (!parsedUrl.username && !parsedUrl.password) { + return url + } + + parsedUrl.username = parsedUrl.username ? '***' : '' + parsedUrl.password = parsedUrl.password ? '***' : '' + + return parsedUrl.toString() + } catch { + return url + } +} + +export const getCacheConfig = (): RedisClientOptions => { + if (process.env.REDIS_URI) { + return { + url: process.env.REDIS_URI, + ...(process.env.REDIS_PASSWORD ? { password: process.env.REDIS_PASSWORD } : {}), + } + } + + const hasPassword = Boolean(process.env.REDIS_PASSWORD) + const host = process.env.REDIS_HOST + const port = process.env.REDIS_PORT + + if (hasPassword) { + const username = process.env.REDIS_USER ?? 'default' + + return { + url: `redis://${host}:${port}`, + username, + password: process.env.REDIS_PASSWORD, + } + } + + return { + url: `redis://${host}:${port}`, + } +} let instance: CacheClient | undefined = undefined @@ -18,7 +55,10 @@ export const getCacheClient = (): CacheClient => { const config = getCacheConfig() // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password: _, ...loggableConfig } = config - logger('config: %o', loggableConfig) + logger('config: %o', { + ...loggableConfig, + ...(loggableConfig.url ? { url: redactRedisUrlCredentials(loggableConfig.url) } : {}), + }) instance = createClient(config) } diff --git a/test/unit/cache/client.spec.ts b/test/unit/cache/client.spec.ts new file mode 100644 index 00000000..17564c1a --- /dev/null +++ b/test/unit/cache/client.spec.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai' + +import { getCacheConfig } from '../../../src/cache/client' + +describe('getCacheConfig', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = { ...originalEnv } + delete process.env.REDIS_URI + delete process.env.REDIS_USER + delete process.env.REDIS_PASSWORD + delete process.env.REDIS_HOST + delete process.env.REDIS_PORT + }) + + afterEach(() => { + process.env = originalEnv + }) + + it('builds unauthenticated redis url when REDIS_URI and REDIS_PASSWORD are unset', () => { + process.env.REDIS_HOST = 'localhost' + process.env.REDIS_PORT = '6379' + + const config = getCacheConfig() + + expect(config).to.deep.equal({ + url: 'redis://localhost:6379', + }) + }) + + it('builds authenticated redis config when REDIS_PASSWORD is set', () => { + process.env.REDIS_HOST = 'localhost' + process.env.REDIS_PORT = '6379' + process.env.REDIS_USER = 'default' + process.env.REDIS_PASSWORD = 'secret' + + const config = getCacheConfig() + + expect(config).to.deep.equal({ + url: 'redis://localhost:6379', + username: 'default', + password: 'secret', + }) + }) + + it('defaults REDIS_USER to default when REDIS_PASSWORD is set and REDIS_USER is unset', () => { + process.env.REDIS_HOST = 'localhost' + process.env.REDIS_PORT = '6379' + process.env.REDIS_PASSWORD = 'secret' + + const config = getCacheConfig() + + expect(config).to.deep.equal({ + url: 'redis://localhost:6379', + username: 'default', + password: 'secret', + }) + }) + + it('prefers REDIS_URI over host/port settings', () => { + process.env.REDIS_URI = 'redis://cache.internal:6380' + process.env.REDIS_PASSWORD = 'secret' + + const config = getCacheConfig() + + expect(config).to.deep.equal({ + url: 'redis://cache.internal:6380', + password: 'secret', + }) + }) +}) \ No newline at end of file