fix: ensure workspace and account uuid when generating token (#9246)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-06-16 02:02:25 +07:00
committed by GitHub
parent 35dcda4364
commit 7c5d76ccc1
8 changed files with 132 additions and 76 deletions
+4 -2
View File
@@ -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'
+1 -1
View File
@@ -35,7 +35,7 @@ prepare()
jest.setTimeout(500000)
class TestWorkspaceManager extends WorkspaceManager {
public async getWorkspaceInfo (token?: string): Promise<WorkspaceInfoWithStatus | undefined> {
public async getWorkspaceInfo (ctx: MeasureContext, token?: string): Promise<WorkspaceInfoWithStatus | undefined> {
const decodedToken = decodeToken(token ?? '')
return {
uuid: decodedToken.workspace,
+28 -26
View File
@@ -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<WorkspaceInfoWithStatus | undefined> {
public async getWorkspaceInfo (ctx: MeasureContext, token?: string): Promise<WorkspaceInfoWithStatus | undefined> {
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<string | undefined> {
@@ -200,9 +203,8 @@ export class WorkspaceManager {
): Promise<WorkspaceIndexer | undefined> {
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 })
+8 -2
View File
@@ -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<void>((resolve) => {
clearTo = setTimeout(resolve, 4000)
@@ -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<string, any> = { 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')
})
})
+4 -2
View File
@@ -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": {
+79
View File
@@ -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'
})
})
})
+8
View File
@@ -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<string, string>,
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()