diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 54e2eb6049..ca58da1f8c 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -4960,7 +4960,7 @@ packages: version: 0.0.0 '@rush-temp/presentation@file:projects/presentation.tgz': - resolution: {integrity: sha512-iKdFwGx+ciUZVYBDuytjG/Ort+6bkuKoIDtTUGA7X+z5YQD+neMusa4Skd9TsmsvsqMcjdxhhI0+TQpEnAjf1A==, tarball: file:projects/presentation.tgz} + resolution: {integrity: sha512-bMSkIMVw78JU9foCyHxngjWO5uPLhczp/XgPsqn+rVsQ+g4fm5WiLwEYQt504vk0DspkFc7xPjDFnMyOpwfR6w==, tarball: file:projects/presentation.tgz} version: 0.0.0 '@rush-temp/print-assets@file:projects/print-assets.tgz': @@ -5352,7 +5352,7 @@ packages: version: 0.0.0 '@rush-temp/server-token@file:projects/server-token.tgz': - resolution: {integrity: sha512-4cC2GIyVh+Wrl5bVS3x7/x+ZbYwECOSyuVVUuHjcpSsEELsA0fLflSobf6SIiyVVoIJ5aZfJSDS1orukYRYiiA==, tarball: file:projects/server-token.tgz} + resolution: {integrity: sha512-KWi8JET7wqjy0yZvPBx6LhInIBN7iXI9Af0sfQMAdKHAyxkFBAZJVY/Moun8gM22JMirucC4l0SQJklsLOZwxw==, tarball: file:projects/server-token.tgz} version: 0.0.0 '@rush-temp/server-tool@file:projects/server-tool.tgz': @@ -25974,6 +25974,7 @@ snapshots: dependencies: '@types/jest': 29.5.12 '@types/node': 22.15.29 + '@types/uuid': 8.3.4 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) eslint: 8.56.0 @@ -25986,6 +25987,7 @@ snapshots: prettier: 3.2.5 ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) typescript: 5.8.3 + uuid: 8.3.2 transitivePeerDependencies: - '@babel/core' - '@jest/types' diff --git a/pods/fulltext/src/__tests__/indexing.spec.ts b/pods/fulltext/src/__tests__/indexing.spec.ts index 7b9b99d7f7..604905696b 100644 --- a/pods/fulltext/src/__tests__/indexing.spec.ts +++ b/pods/fulltext/src/__tests__/indexing.spec.ts @@ -35,7 +35,7 @@ prepare() jest.setTimeout(500000) class TestWorkspaceManager extends WorkspaceManager { - public async getWorkspaceInfo (token?: string): Promise { + public async getWorkspaceInfo (ctx: MeasureContext, token?: string): Promise { const decodedToken = decodeToken(token ?? '') return { uuid: decodedToken.workspace, diff --git a/pods/fulltext/src/manager.ts b/pods/fulltext/src/manager.ts index 3b0393cbf0..d6fc0191ec 100644 --- a/pods/fulltext/src/manager.ts +++ b/pods/fulltext/src/manager.ts @@ -116,14 +116,15 @@ export class WorkspaceManager { for (const m of msg) { const ws = m.id as WorkspaceUuid - const indexer = await this.getIndexer( - this.ctx, - ws, - generateToken(systemAccountUuid, ws, { - service: 'fulltext' - }), - true - ) + let token: string + try { + token = generateToken(systemAccountUuid, ws, { service: 'fulltext' }) + } catch (err: any) { + this.ctx.error('Error generating token', { err, systemAccountUuid, ws }) + continue + } + + const indexer = await this.getIndexer(this.ctx, ws, token, true) await indexer?.fulltext.processDocuments(this.ctx, m.value, control) } } @@ -137,17 +138,20 @@ export class WorkspaceManager { const ws = m.id as WorkspaceUuid for (const mm of m.value) { + let token: string + try { + token = generateToken(systemAccountUuid, ws, { service: 'fulltext' }) + } catch (err: any) { + this.ctx.error('Error generating token', { err, systemAccountUuid, ws }) + continue + } + if ( mm.type === QueueWorkspaceEvent.Created || mm.type === QueueWorkspaceEvent.Restored || mm.type === QueueWorkspaceEvent.FullReindex ) { - const indexer = await this.getIndexer( - this.ctx, - ws, - generateToken(systemAccountUuid, ws, { service: 'fulltext' }), - true - ) + const indexer = await this.getIndexer(this.ctx, ws, token, true) if (indexer !== undefined) { await indexer.dropWorkspace() // TODO: Add heartbeat const classes = await indexer.getIndexClassess() @@ -161,8 +165,7 @@ export class WorkspaceManager { mm.type === QueueWorkspaceEvent.Archived || mm.type === QueueWorkspaceEvent.ClearIndex ) { - const token = generateToken(systemAccountUuid, ws, { service: 'fulltext' }) - const workspaceInfo = await this.getWorkspaceInfo(token) + const workspaceInfo = await this.getWorkspaceInfo(this.ctx, token) if (workspaceInfo !== undefined) { if (workspaceInfo.dataId != null) { await this.fulltextAdapter.clean(this.ctx, workspaceInfo.dataId as unknown as WorkspaceUuid) @@ -170,12 +173,7 @@ export class WorkspaceManager { await this.fulltextAdapter.clean(this.ctx, workspaceInfo.uuid) } } else if (mm.type === QueueWorkspaceEvent.Reindex) { - const indexer = await this.getIndexer( - this.ctx, - ws, - generateToken(systemAccountUuid, ws, { service: 'fulltext' }), - true - ) + const indexer = await this.getIndexer(this.ctx, ws, token, true) const mmd = mm as QueueWorkspaceReindexMessage await indexer?.reindex(this.ctx, mmd.domain, mmd.classes, control) } @@ -183,9 +181,14 @@ export class WorkspaceManager { } } - public async getWorkspaceInfo (token?: string): Promise { + public async getWorkspaceInfo (ctx: MeasureContext, token?: string): Promise { const accountClient = getAccountClient(token) - return await accountClient.getWorkspaceInfo(false) + try { + return await accountClient.getWorkspaceInfo(false) + } catch (err: any) { + ctx.error('Workspace not available for token', { err }) + return undefined + } } async getTransactorAPIEndpoint (token: string): Promise { @@ -200,9 +203,8 @@ export class WorkspaceManager { ): Promise { let idx = this.indexers.get(workspace) if (idx === undefined && create) { - const workspaceInfo = await this.getWorkspaceInfo(token) + const workspaceInfo = await this.getWorkspaceInfo(ctx, token) if (workspaceInfo === undefined) { - ctx.error('Workspace not available for token') return } ctx.warn('indexer created', { workspace }) diff --git a/pods/server/src/__tests__/server.test.ts b/pods/server/src/__tests__/server.test.ts index 4271a1a375..1e8f751a7d 100644 --- a/pods/server/src/__tests__/server.test.ts +++ b/pods/server/src/__tests__/server.test.ts @@ -109,7 +109,10 @@ describe('server', () => { const serverShutdown = startHttpServer(toolCtx, sessionMgr, port, opt.accountsUrl, createDummyStorageAdapter()) function connect (): WebSocket { - const token: string = generateToken('' as PersonUuid, 'latest' as WorkspaceUuid) + const token: string = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid + ) return new WebSocket(`ws://localhost:${port}/${token}`) } @@ -271,7 +274,10 @@ describe('server', () => { try { // - const token: string = generateToken('my-account-uuid' as PersonUuid, 'latest' as WorkspaceUuid) + const token: string = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid + ) let clearTo: any const timeoutPromise = new Promise((resolve) => { clearTo = setTimeout(resolve, 4000) diff --git a/server/account/src/__tests__/token.test.ts b/server/account/src/__tests__/token.test.ts deleted file mode 100644 index 04b7430714..0000000000 --- a/server/account/src/__tests__/token.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { PersonUuid, WorkspaceUuid } from '@hcengineering/core' -import { generateToken } from '@hcengineering/server-token' - -export function decodeTokenPayload (token: string): any { - try { - return JSON.parse(atob(token.split('.')[1])) - } catch (err: any) { - console.error(err) - return {} - } -} -describe('generate-tokens', () => { - it('should generate tokens', async () => { - const extra: Record = { confirmed: 'true' } - const token = generateToken('mike@some.host' as PersonUuid, '' as WorkspaceUuid, extra, 'secret') - console.log(token) - const decodedPayload = decodeTokenPayload(token) - expect(decodedPayload).toEqual({ - extra: { - confirmed: 'true' - }, - account: 'mike@some.host', - workspace: '' - }) - expect(decodedPayload.admin).not.toBe('true') - }) -}) diff --git a/server/token/package.json b/server/token/package.json index 5c7b96b112..8b217d9c4e 100644 --- a/server/token/package.json +++ b/server/token/package.json @@ -35,12 +35,14 @@ "typescript": "^5.8.3", "jest": "^29.7.0", "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" + "@types/jest": "^29.5.5", + "@types/uuid": "^8.3.1" }, "dependencies": { "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", - "jwt-simple": "^0.5.6" + "jwt-simple": "^0.5.6", + "uuid": "^8.3.2" }, "repository": "https://github.com/hcengineering/platform", "publishConfig": { diff --git a/server/token/src/__tests__/token.test.ts b/server/token/src/__tests__/token.test.ts new file mode 100644 index 0000000000..59da35e389 --- /dev/null +++ b/server/token/src/__tests__/token.test.ts @@ -0,0 +1,79 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { PersonUuid, WorkspaceUuid } from '@hcengineering/core' +import { generateToken } from '../token' + +export function decodeTokenPayload (token: string): any { + try { + return JSON.parse(atob(token.split('.')[1])) + } catch (err: any) { + console.error(err) + return {} + } +} + +describe('generateToken', () => { + it('throws TokenError for invalid account uuid', () => { + expect(() => { + generateToken('invalid-uuid' as PersonUuid, '' as WorkspaceUuid, {}, 'secret') + }).toThrow('Invalid account uuid') + }) + + it('throws TokenError for invalid workspace uuid', () => { + expect(() => { + generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, 'invalid-uuid' as WorkspaceUuid, {}, 'secret') + }).toThrow('Invalid workspace uuid') + }) + + it('generates token without extra and workspace', () => { + const token = generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, undefined, undefined, 'secret') + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: undefined + }) + }) + + it('should generate token with only required fields', () => { + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + undefined, + 'secret' + ) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) + + it('should generate token with extra fields', () => { + const extra = { service: 'test' } + const token = generateToken( + '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, + '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, + extra, + 'secret' + ) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + extra, + account: '123e4567-e89b-12d3-a456-426614174000', + workspace: '123e4567-e89b-12d3-a456-426614174001' + }) + }) +}) diff --git a/server/token/src/token.ts b/server/token/src/token.ts index 9a9bf1901d..f61522aa92 100644 --- a/server/token/src/token.ts +++ b/server/token/src/token.ts @@ -1,6 +1,7 @@ import { AccountUuid, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import { decode, encode } from 'jwt-simple' +import { validate } from 'uuid' import serverPlugin from './plugin' /** @@ -35,6 +36,13 @@ export function generateToken ( extra?: Record, secret?: string ): string { + if (!validate(accountUuid)) { + throw new TokenError('Invalid account uuid') + } + if (workspaceUuid !== undefined && !validate(workspaceUuid)) { + throw new TokenError('Invalid workspace uuid') + } + return encode( { ...(extra !== undefined ? { extra } : {}), account: accountUuid, workspace: workspaceUuid }, secret ?? getSecret()