From 4edd5c8eeb1361bec9a5f1c44d3b077bc98c35b7 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 17 Mar 2026 23:06:04 +0000 Subject: [PATCH] Matrix: isolate named account auth --- extensions/matrix/src/matrix/client.test.ts | 103 ++++++++++++++++++ extensions/matrix/src/matrix/client/config.ts | 61 ++++++++--- extensions/matrix/src/matrix/deps.test.ts | 4 +- 3 files changed, 148 insertions(+), 20 deletions(-) diff --git a/extensions/matrix/src/matrix/client.test.ts b/extensions/matrix/src/matrix/client.test.ts index f8d09629bd5..fc89a4944e7 100644 --- a/extensions/matrix/src/matrix/client.test.ts +++ b/extensions/matrix/src/matrix/client.test.ts @@ -262,6 +262,59 @@ describe("resolveMatrixConfig", () => { expect(resolved.userId).toBe(""); }); + it("does not inherit base or global auth secrets for non-default accounts", () => { + const cfg = { + channels: { + matrix: { + homeserver: "https://base.example.org", + accessToken: "base-token", + password: "base-pass", // pragma: allowlist secret + deviceId: "BASEDEVICE", + accounts: { + ops: { + homeserver: "https://ops.example.org", + userId: "@ops:example.org", + password: "ops-pass", // pragma: allowlist secret + }, + }, + }, + }, + } as CoreConfig; + const env = { + MATRIX_ACCESS_TOKEN: "global-token", + MATRIX_PASSWORD: "global-pass", + MATRIX_DEVICE_ID: "GLOBALDEVICE", + } as NodeJS.ProcessEnv; + + const resolved = resolveMatrixConfigForAccount(cfg, "ops", env); + expect(resolved.accessToken).toBeUndefined(); + expect(resolved.password).toBe("ops-pass"); + expect(resolved.deviceId).toBeUndefined(); + }); + + it("does not inherit a base password for non-default accounts", () => { + const cfg = { + channels: { + matrix: { + homeserver: "https://base.example.org", + password: "base-pass", // pragma: allowlist secret + accounts: { + ops: { + homeserver: "https://ops.example.org", + userId: "@ops:example.org", + }, + }, + }, + }, + } as CoreConfig; + const env = { + MATRIX_PASSWORD: "global-pass", + } as NodeJS.ProcessEnv; + + const resolved = resolveMatrixConfigForAccount(cfg, "ops", env); + expect(resolved.password).toBeUndefined(); + }); + it("rejects insecure public http Matrix homeservers", () => { expect(() => validateMatrixHomeserverUrl("http://matrix.example.org")).toThrow( "Matrix homeserver must use https:// unless it targets a private or loopback host", @@ -479,6 +532,56 @@ describe("resolveMatrixAuth", () => { expect(auth.deviceId).toBe("OPSDEVICE"); }); + it("uses named-account password auth instead of inheriting the base access token", async () => { + vi.mocked(credentialsModule.loadMatrixCredentials).mockReturnValue(null); + vi.mocked(credentialsModule.credentialsMatchConfig).mockReturnValue(false); + const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ + access_token: "ops-token", + user_id: "@ops:example.org", + device_id: "OPSDEVICE", + }); + + const cfg = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "legacy-token", + accounts: { + ops: { + homeserver: "https://matrix.example.org", + userId: "@ops:example.org", + password: "ops-pass", // pragma: allowlist secret + }, + }, + }, + }, + } as CoreConfig; + + const auth = await resolveMatrixAuth({ + cfg, + env: {} as NodeJS.ProcessEnv, + accountId: "ops", + }); + + expect(doRequestSpy).toHaveBeenCalledWith( + "POST", + "/_matrix/client/v3/login", + undefined, + expect.objectContaining({ + type: "m.login.password", + identifier: { type: "m.id.user", user: "@ops:example.org" }, + password: "ops-pass", + }), + ); + expect(auth).toMatchObject({ + accountId: "ops", + homeserver: "https://matrix.example.org", + userId: "@ops:example.org", + accessToken: "ops-token", + deviceId: "OPSDEVICE", + }); + }); + it("resolves missing whoami identity fields for token auth", async () => { const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ user_id: "@bot:example.org", diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index 2f3d11c7711..32a12d06f71 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -67,16 +67,35 @@ function resolveMatrixStringField(params: { accountValue?: string; scopedEnvValue?: string; globalEnvValue?: string; + includeBaseConfig?: boolean; + includeGlobalEnv?: boolean; }): string { return ( params.accountValue || params.scopedEnvValue || - readMatrixBaseConfigField(params.matrix, params.field) || - params.globalEnvValue || + (params.includeBaseConfig === false + ? "" + : readMatrixBaseConfigField(params.matrix, params.field)) || + (params.includeGlobalEnv === false ? "" : params.globalEnvValue) || "" ); } +function resolveMatrixAccountAuthField(params: { + matrix: ReturnType; + field: Extract; + accountValue?: string; + scopedEnvValue?: string; + globalEnvValue?: string; + isDefaultAccount: boolean; +}): string { + return resolveMatrixStringField({ + ...params, + includeBaseConfig: params.isDefaultAccount, + includeGlobalEnv: params.isDefaultAccount, + }); +} + function clampMatrixInitialSyncLimit(value: unknown): number | undefined { return typeof value === "number" ? Math.max(0, Math.floor(value)) : undefined; } @@ -248,36 +267,42 @@ export function resolveMatrixConfigForAccount( scopedEnvValue: scopedEnv.homeserver, globalEnvValue: globalEnv.homeserver, }); - const userIdSource = - accountField("userId") || - scopedEnv.userId || - (normalizedAccountId === DEFAULT_ACCOUNT_ID - ? readMatrixBaseConfigField(matrix, "userId") || globalEnv.userId || "" - : ""); - const userId = userIdSource; + const isDefaultAccount = normalizedAccountId === DEFAULT_ACCOUNT_ID; + const userId = resolveMatrixAccountAuthField({ + matrix, + field: "userId", + accountValue: accountField("userId"), + scopedEnvValue: scopedEnv.userId, + globalEnvValue: globalEnv.userId, + isDefaultAccount, + }); const accessToken = - resolveMatrixStringField({ + resolveMatrixAccountAuthField({ matrix, field: "accessToken", accountValue: accountField("accessToken"), scopedEnvValue: scopedEnv.accessToken, globalEnvValue: globalEnv.accessToken, + isDefaultAccount, }) || undefined; const password = - resolveMatrixStringField({ + resolveMatrixAccountAuthField({ matrix, field: "password", accountValue: accountField("password"), scopedEnvValue: scopedEnv.password, globalEnvValue: globalEnv.password, + isDefaultAccount, + }) || undefined; + const deviceId = + resolveMatrixAccountAuthField({ + matrix, + field: "deviceId", + accountValue: accountField("deviceId"), + scopedEnvValue: scopedEnv.deviceId, + globalEnvValue: globalEnv.deviceId, + isDefaultAccount, }) || undefined; - const deviceIdSource = - accountField("deviceId") || - scopedEnv.deviceId || - (normalizedAccountId === DEFAULT_ACCOUNT_ID - ? readMatrixBaseConfigField(matrix, "deviceId") || globalEnv.deviceId || "" - : ""); - const deviceId = deviceIdSource || undefined; const deviceName = resolveMatrixStringField({ matrix, diff --git a/extensions/matrix/src/matrix/deps.test.ts b/extensions/matrix/src/matrix/deps.test.ts index 7c5d17d1a95..c29d05d753f 100644 --- a/extensions/matrix/src/matrix/deps.test.ts +++ b/extensions/matrix/src/matrix/deps.test.ts @@ -55,7 +55,7 @@ describe("ensureMatrixCryptoRuntime", () => { it("rethrows non-crypto module errors without bootstrapping", async () => { const runCommand = vi.fn(); const requireFn = vi.fn(() => { - throw new Error("Cannot find module '@vector-im/matrix-bot-sdk'"); + throw new Error("Cannot find module 'not-the-matrix-crypto-runtime'"); }); await expect( @@ -66,7 +66,7 @@ describe("ensureMatrixCryptoRuntime", () => { resolveFn: () => "/tmp/download-lib.js", nodeExecutable: "/usr/bin/node", }), - ).rejects.toThrow("Cannot find module '@vector-im/matrix-bot-sdk'"); + ).rejects.toThrow("Cannot find module 'not-the-matrix-crypto-runtime'"); expect(runCommand).not.toHaveBeenCalled(); expect(requireFn).toHaveBeenCalledTimes(1);