Skip to content

Commit de25a4b

Browse files
committed
Make keychains session-specific and register config override secrets
Keychain now supports a parent-child hierarchy. Each session creates a child keychain via Keychain.root.createChild(), ensuring session-specific secrets are scoped while remaining visible to shared loggers via upward propagation. Config override secrets are now registered on the session keychain in createServer(), fixing the gap where overrides bypassed keychain registration entirely.
1 parent d267c70 commit de25a4b

5 files changed

Lines changed: 91 additions & 15 deletions

File tree

src/common/config/parseUserConfig.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function parseUserConfig({
8686
// TODO: Separate correctly parsed user config from all other valid
8787
// arguments relevant to mongosh's args-parser.
8888
const userConfig: UserConfig = { ...parsed, ...configParseResult.data };
89-
registerKnownSecretsInRootKeychain(userConfig);
89+
registerConfigSecrets(userConfig, Keychain.root);
9090
return {
9191
parsed: userConfig,
9292
warnings,
@@ -153,9 +153,12 @@ function parseUserConfigSources<T extends typeof UserConfigSchema>({
153153
};
154154
}
155155

156-
function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
157-
const keychain = Keychain.root;
158-
156+
/**
157+
* Registers known secret fields from the given config on the provided keychain.
158+
* Called once on `Keychain.root` during initial config parsing, and again on
159+
* each session's child keychain to cover config-override secrets.
160+
*/
161+
export function registerConfigSecrets(userConfig: Partial<UserConfig>, keychain: Keychain): void {
159162
const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
160163
if (value) {
161164
keychain.register(value, kind);

src/common/keychain.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,55 @@ import type { Secret } from "mongodb-redact";
22
export type { Secret } from "mongodb-redact";
33

44
/**
5-
* This class holds the secrets of a single server. Ideally, we might want to have a keychain
6-
* per session, but right now the loggers are set up by server and are not aware of the concept
7-
* of session and this would require a bigger refactor.
5+
* Holds secrets for redaction in logging and telemetry pipelines.
86
*
9-
* Whenever we identify or create a secret (for example, Atlas login, CLI arguments...) we
10-
* should register them in the root Keychain (`Keychain.root.register`) or preferably
11-
* on the session keychain if available `this.session.keychain`.
7+
* Keychains form a parent-child hierarchy:
8+
* - `Keychain.root` stores base config secrets (registered during config parsing).
9+
* - Each session creates a child via `Keychain.root.createChild()` to hold
10+
* session-specific secrets (e.g. generated credentials, config override values).
11+
*
12+
* `allSecrets` aggregates secrets from the entire parent chain so that redaction
13+
* covers both global and session-scoped values.
14+
*
15+
* When a secret is registered on a child keychain it is also propagated to the
16+
* parent so that shared loggers (ConsoleLogger, DiskLogger) — which reference
17+
* the root keychain — can redact session-specific secrets as well.
1218
**/
1319
export class Keychain {
1420
private secrets: Secret[];
1521
private static rootKeychain: Keychain = new Keychain();
22+
private readonly parent?: Keychain;
1623

17-
constructor() {
24+
constructor(parent?: Keychain) {
1825
this.secrets = [];
26+
this.parent = parent;
1927
}
2028

2129
static get root(): Keychain {
2230
return Keychain.rootKeychain;
2331
}
2432

33+
/**
34+
* Creates a child keychain whose `allSecrets` includes this keychain's
35+
* secrets. Secrets registered on the child also propagate upward so
36+
* shared loggers that reference an ancestor still redact them.
37+
*/
38+
createChild(): Keychain {
39+
return new Keychain(this);
40+
}
41+
2542
register(value: Secret["value"], kind: Secret["kind"]): void {
2643
this.secrets.push({ value, kind });
44+
this.parent?.register(value, kind);
2745
}
2846

2947
clearAllSecrets(): void {
3048
this.secrets = [];
3149
}
3250

3351
get allSecrets(): Secret[] {
34-
return [...this.secrets];
52+
const parentSecrets = this.parent?.allSecrets ?? [];
53+
return [...parentSecrets, ...this.secrets];
3554
}
3655
}
3756

src/lib.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
export { Server, type ServerOptions, type AnyToolClass } from "./server.js";
22
export { Session, type SessionOptions } from "./common/session.js";
33
export { type UserConfig, UserConfigSchema } from "./common/config/userConfig.js";
4-
export { parseUserConfig, defaultParserOptions, type ParserOptions } from "./common/config/parseUserConfig.js";
4+
export {
5+
parseUserConfig,
6+
defaultParserOptions,
7+
registerConfigSecrets,
8+
type ParserOptions,
9+
} from "./common/config/parseUserConfig.js";
510

611
import { parseUserConfig } from "./common/config/parseUserConfig.js";
712
import type { UserConfig } from "./common/config/userConfig.js";

src/transports/base.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CompositeLogger, ConsoleLogger, DiskLogger, McpLogger } from "../common
99
import { ExportsManager } from "../common/exportsManager.js";
1010
import { DeviceId } from "../helpers/deviceId.js";
1111
import { Keychain } from "../common/keychain.js";
12+
import { registerConfigSecrets } from "../common/config/parseUserConfig.js";
1213
import { defaultCreateConnectionManager, type ConnectionManagerFactoryFn } from "../common/connectionManager.js";
1314
import {
1415
type ConnectionErrorHandler,
@@ -318,6 +319,9 @@ export abstract class TransportRunnerBase<
318319
)
319320
: undefined;
320321

322+
const sessionKeychain = Keychain.root.createChild();
323+
registerConfigSecrets(userConfig, sessionKeychain);
324+
321325
const session = new Session({
322326
userConfig,
323327
atlasLocalClient:
@@ -326,7 +330,7 @@ export abstract class TransportRunnerBase<
326330
connectionErrorHandler: sessionOptions?.connectionErrorHandler ?? this.connectionErrorHandler,
327331
exportsManager,
328332
connectionManager,
329-
keychain: Keychain.root,
333+
keychain: sessionKeychain,
330334
apiClient: sessionOptions?.apiClient ?? apiClient,
331335
});
332336

@@ -356,7 +360,7 @@ export abstract class TransportRunnerBase<
356360
// We need to create the MCP logger after the server is constructed
357361
// because it needs the server instance
358362
if (userConfig.loggers.includes("mcp")) {
359-
logger.addLogger(new McpLogger(result, Keychain.root));
363+
logger.addLogger(new McpLogger(result, sessionKeychain));
360364
}
361365

362366
return result;

tests/unit/common/keychain.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,49 @@ describe("Keychain", () => {
3333
expect(keychain.allSecrets).toEqual([{ value: "123456", kind: "password" }]);
3434
});
3535
});
36+
37+
describe("parent-child keychains", () => {
38+
it("child allSecrets includes parent secrets", () => {
39+
keychain.register("root-secret", "password");
40+
const child = keychain.createChild();
41+
child.register("child-secret", "password");
42+
43+
expect(child.allSecrets).toEqual([
44+
{ value: "root-secret", kind: "password" },
45+
// root-secret again because child.register propagated it to parent
46+
{ value: "child-secret", kind: "password" },
47+
{ value: "child-secret", kind: "password" },
48+
]);
49+
});
50+
51+
it("secrets registered on child propagate to parent", () => {
52+
const child = keychain.createChild();
53+
child.register("child-secret", "password");
54+
55+
expect(keychain.allSecrets).toContainEqual({ value: "child-secret", kind: "password" });
56+
});
57+
58+
it("clearAllSecrets on child does not affect parent", () => {
59+
keychain.register("root-secret", "password");
60+
const child = keychain.createChild();
61+
child.register("child-secret", "password");
62+
63+
child.clearAllSecrets();
64+
expect(child.allSecrets).toContainEqual({ value: "root-secret", kind: "password" });
65+
expect(child.allSecrets).toContainEqual({ value: "child-secret", kind: "password" });
66+
expect(keychain.allSecrets).toContainEqual({ value: "root-secret", kind: "password" });
67+
});
68+
69+
it("supports multi-level chains", () => {
70+
keychain.register("root-secret", "password");
71+
const child = keychain.createChild();
72+
const grandchild = child.createChild();
73+
grandchild.register("deep-secret", "password");
74+
75+
const deepSecretEntries = grandchild.allSecrets.filter((s) => s.value === "deep-secret");
76+
expect(deepSecretEntries.length).toBeGreaterThanOrEqual(1);
77+
78+
expect(keychain.allSecrets).toContainEqual({ value: "deep-secret", kind: "password" });
79+
});
80+
});
3681
});

0 commit comments

Comments
 (0)