fix(config): Use config get() and find secrets using one big RegExp instead
Signed-off-by: Harry Hogg <hhogg@spotify.com>
This commit is contained in:
@@ -472,9 +472,6 @@ export type ReadUrlResponse = {
|
||||
etag?: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type RedactionMap = Record<string, string>;
|
||||
|
||||
// @public
|
||||
export function requestLoggingHandler(logger?: Logger_2): RequestHandler;
|
||||
|
||||
@@ -543,7 +540,7 @@ export type ServiceBuilder = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function setRedactionMap(newRedactionMap: RedactionMap): void;
|
||||
export function setRedactionList(redactionList: string[]): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function setRootLogger(newLogger: winston.Logger): void;
|
||||
|
||||
@@ -18,39 +18,36 @@ import { resolve as resolvePath } from 'path';
|
||||
import parseArgs from 'minimist';
|
||||
import { Logger } from 'winston';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { loadConfigSchema, loadConfig } from '@backstage/config-loader';
|
||||
import {
|
||||
loadConfigSchema,
|
||||
loadConfig,
|
||||
ConfigSchema,
|
||||
} from '@backstage/config-loader';
|
||||
import { AppConfig, Config, ConfigReader, JsonValue } from '@backstage/config';
|
||||
|
||||
import { setRedactionMap } from './logging';
|
||||
import { setRedactionList } from './logging';
|
||||
|
||||
// Fetch the schema and get all the secrets to pass to the rootLogger for redaction
|
||||
const updateRedactionMap = async (configs: AppConfig[], logger: Logger) => {
|
||||
// Consider all packages in the monorepo when loading in config
|
||||
const { Project } = require('@lerna/project');
|
||||
const project = new Project();
|
||||
const packages = await project.getPackages();
|
||||
const localPackageNames = packages.map((p: any) => p.name);
|
||||
|
||||
const schema = await loadConfigSchema({ dependencies: localPackageNames });
|
||||
const updateRedactionMap = (
|
||||
schema: ConfigSchema,
|
||||
configs: AppConfig[],
|
||||
logger: Logger,
|
||||
) => {
|
||||
const secretAppConfigs = schema.process(configs, { visibility: ['secret'] });
|
||||
const secretConfig = ConfigReader.fromConfigs(secretAppConfigs);
|
||||
const configMap = secretConfig.getMap();
|
||||
const values = new Set<string>();
|
||||
const data = secretConfig.get();
|
||||
|
||||
JSON.parse(
|
||||
JSON.stringify(data),
|
||||
(_, v) => typeof v === 'string' && values.add(v),
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`${
|
||||
Object.keys(configMap).length
|
||||
} secrets found in the config which will be redacted`,
|
||||
`${values.size} secrets found in the config which will be redacted`,
|
||||
);
|
||||
|
||||
setRedactionMap(
|
||||
Object.entries(configMap).reduce<Record<any, string>>(
|
||||
(map, [key, value]) => {
|
||||
map[value] = key;
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
setRootLoggerRedactionList(Array.from(values));
|
||||
};
|
||||
|
||||
export class ObservableConfigProxy implements Config {
|
||||
@@ -120,9 +117,6 @@ export class ObservableConfigProxy implements Config {
|
||||
get<T = JsonValue>(key?: string): T {
|
||||
return this.select(true).get(key);
|
||||
}
|
||||
getMap() {
|
||||
return this.config.getMap();
|
||||
}
|
||||
getOptional<T = JsonValue>(key?: string): T | undefined {
|
||||
return this.select(false)?.getOptional(key);
|
||||
}
|
||||
@@ -185,6 +179,9 @@ export async function loadBackendConfig(options: {
|
||||
const args = parseArgs(options.argv);
|
||||
const configPaths: string[] = [args.config ?? []].flat();
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
dependencies: ['@backstage/backend-common'],
|
||||
});
|
||||
const config = new ObservableConfigProxy(options.logger);
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
|
||||
@@ -19,10 +19,8 @@ import { LoggerOptions } from 'winston';
|
||||
import { coloredFormat } from './formats';
|
||||
|
||||
/** @public */
|
||||
export type RedactionMap = Record<string, string>;
|
||||
|
||||
let rootLogger: winston.Logger;
|
||||
let redactionMap: RedactionMap;
|
||||
let redactionRegExp: RegExp;
|
||||
|
||||
/** @public */
|
||||
export function getRootLogger(): winston.Logger {
|
||||
@@ -35,8 +33,8 @@ export function setRootLogger(newLogger: winston.Logger) {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function setRedactionMap(newRedactionMap: RedactionMap) {
|
||||
redactionMap = newRedactionMap;
|
||||
export function setRedactionList(redactionList: string[]) {
|
||||
redactionRegExp = new RegExp(`(${redactionList.join('|')})`, 'g');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,11 +44,9 @@ export function setRedactionMap(newRedactionMap: RedactionMap) {
|
||||
function redactLogLine(info: winston.Logform.TransformableInfo) {
|
||||
// TODO(hhogg): The logger is created before the config is loaded,
|
||||
// because the logger is needed in the config loader. There is a risk of
|
||||
// a secret being logged out during the config loading stage 🤷♂️
|
||||
if (redactionMap) {
|
||||
Object.entries(redactionMap || {}).forEach(([key, value]) => {
|
||||
info.message = info.message.replace(new RegExp(key, 'g'), `{{${value}}}`);
|
||||
});
|
||||
// a secret being logged out during the config loading stage.
|
||||
if (redactionRegExp) {
|
||||
info.message = info.message.replace(redactionRegExp, '[REDACTED]');
|
||||
}
|
||||
|
||||
return info;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import { JsonValue, JsonObject } from '@backstage/types';
|
||||
import { AppConfig, Config } from './types';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import merge from 'lodash/merge';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
|
||||
// Update the same pattern in config-loader package if this is changed
|
||||
@@ -119,28 +118,6 @@ export class ConfigReader implements Config {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
getMap() {
|
||||
const map: Record<string, any> = {};
|
||||
|
||||
const flatten = (data: JsonValue, path = '') => {
|
||||
if (isObject(data)) {
|
||||
Object.entries(data).forEach(([key, value]: [string, any]) =>
|
||||
flatten(value, `${path}${path ? '.' : ''}${key}`),
|
||||
);
|
||||
} else if (Array.isArray(data)) {
|
||||
data.forEach((value, key) => {
|
||||
flatten(value, `${path}[${key}]`);
|
||||
});
|
||||
} else {
|
||||
map[path] = data;
|
||||
}
|
||||
};
|
||||
|
||||
flatten(merge({}, this.fallback?.data, this.data));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
getOptional<T = JsonValue>(key?: string): T | undefined {
|
||||
const value = this.readValue(key);
|
||||
const fallbackValue = this.fallback?.getOptional<T>(key);
|
||||
|
||||
@@ -65,12 +65,6 @@ export type Config = {
|
||||
*/
|
||||
keys(): string[];
|
||||
|
||||
/**
|
||||
* Returns a flattened map of the config with the full path to the keys and
|
||||
* the config value as the value.
|
||||
*/
|
||||
getMap(): Record<string, JsonPrimitive>;
|
||||
|
||||
/**
|
||||
* Same as `getOptional`, but will throw an error if there's no value for the given key.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user