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:
Harry Hogg
2021-09-30 13:31:07 +01:00
parent 54decb6266
commit 12428bf338
5 changed files with 30 additions and 69 deletions
+1 -4
View File
@@ -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;
+23 -26
View File
@@ -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;
-23
View File
@@ -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);
-6
View File
@@ -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.
*/