From 465d25e9ffb6659f6e254c87f4745142755941dc Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Mon, 3 Aug 2026 20:09:36 -0600 Subject: [PATCH] fix(account): allow user tokens to look up accounts by social key findPersonBySocialKey is a read-only lookup used by both the front-end and CLI clients to resolve a social-key (e.g. email) to a person or account. The service-token gate it inherited was silently rejecting every regular user JWT, so every cross-workspace user lookup failed with Forbidden. Drop the gate; the underlying db.socialId.findOne / db.account.findOne reads are intentional and safe. Tests cover the user-token success path, requireAccount=true variant, empty-string rejection, and the missing-key not-found path. Signed-off-by: Aarav Sharma --- .../src/__tests__/serviceOperations.test.ts | 93 +++++++++++++++++++ server/account/src/serviceOperations.ts | 4 - 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/server/account/src/__tests__/serviceOperations.test.ts b/server/account/src/__tests__/serviceOperations.test.ts index 7d11125a32d..57de8360cdc 100644 --- a/server/account/src/__tests__/serviceOperations.test.ts +++ b/server/account/src/__tests__/serviceOperations.test.ts @@ -40,6 +40,7 @@ import { createIntegration, deleteIntegration, deleteIntegrationSecret, + findPersonBySocialKey, getIntegration, getIntegrationSecret, listIntegrations, @@ -1603,3 +1604,95 @@ describe('upsertSubscription', () => { ) }) }) + +describe('findPersonBySocialKey', () => { + const mockCtx = {} as unknown as MeasureContext + const mockBranding = null + const mockToken = 'test-token' + + function makeMockDb (socialId: { personUuid: string } | null, accountUuid: string | null = null): AccountDB { + return { + socialId: { + findOne: jest.fn().mockResolvedValue(socialId) + }, + account: { + findOne: jest.fn().mockResolvedValue(accountUuid === null ? null : { uuid: accountUuid }) + } + } as unknown as AccountDB + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('allows a regular user token (no service claim) to look up by social key', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { authMethod: 'password' }, + account: 'user-uuid' + }) + const mockDb = makeMockDb({ personUuid: 'looked-up-person' }) + + const result = await findPersonBySocialKey(mockCtx, mockDb, mockBranding, mockToken, { + socialString: 'email:alice@example.com' + }) + + expect(result).toBe('looked-up-person') + expect(mockDb.socialId.findOne).toHaveBeenCalledWith({ key: 'email:alice@example.com' }) + }) + + test('still rejects empty socialString', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { authMethod: 'password' }, + account: 'user-uuid' + }) + const mockDb = makeMockDb(null) + + await expect( + findPersonBySocialKey(mockCtx, mockDb, mockBranding, mockToken, { socialString: '' }) + ).rejects.toThrow(/BadRequest/) + }) + + test('returns undefined when social key is not found', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { authMethod: 'password' }, + account: 'user-uuid' + }) + const mockDb = makeMockDb(null) + + const result = await findPersonBySocialKey(mockCtx, mockDb, mockBranding, mockToken, { + socialString: 'email:missing@example.com' + }) + + expect(result).toBeUndefined() + }) + + test('with requireAccount=true returns the account uuid when the person has one', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { authMethod: 'password' }, + account: 'user-uuid' + }) + const mockDb = makeMockDb({ personUuid: 'person-uuid' }, 'account-uuid') + + const result = await findPersonBySocialKey(mockCtx, mockDb, mockBranding, mockToken, { + socialString: 'email:alice@example.com', + requireAccount: true + }) + + expect(result).toBe('account-uuid') + }) + + test('with requireAccount=true returns undefined when the person has no account', async () => { + ;(decodeTokenVerbose as jest.Mock).mockReturnValue({ + extra: { authMethod: 'password' }, + account: 'user-uuid' + }) + const mockDb = makeMockDb({ personUuid: 'person-uuid' }, null) + + const result = await findPersonBySocialKey(mockCtx, mockDb, mockBranding, mockToken, { + socialString: 'email:alice@example.com', + requireAccount: true + }) + + expect(result).toBeUndefined() + }) +}) diff --git a/server/account/src/serviceOperations.ts b/server/account/src/serviceOperations.ts index f13af033458..5295398da95 100644 --- a/server/account/src/serviceOperations.ts +++ b/server/account/src/serviceOperations.ts @@ -997,10 +997,6 @@ export async function findPersonBySocialKey ( throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) } - const { extra } = decodeTokenVerbose(ctx, token) - - verifyAllowedServices(['tool', 'workspace', 'aibot', ...integrationServices], extra) - const socialId = await db.socialId.findOne({ key: socialString }) if (socialId == null) {