UBERF-5283 Validate token in collaborator (#4517)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-02-05 15:38:18 +07:00 committed by GitHub
parent a1f64176fd
commit 697fb2732c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 77 additions and 28 deletions

1
.vscode/launch.json vendored
View File

@ -90,6 +90,7 @@
"env": {
"SECRET": "secret",
"METRICS_CONSOLE": "true",
"ACCOUNTS_URL": "http://localhost:3000",
"TRANSACTOR_URL": "ws://localhost:3333",
"UPLOAD_URL": "/files",
"MONGO_URL": "mongodb://localhost:27017",

View File

@ -101,6 +101,7 @@ services:
environment:
- COLLABORATOR_PORT=3078
- SECRET=secret
- ACCOUNTS_URL=http://account:3000
- TRANSACTOR_URL=ws://transactor:3333
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27017

View File

@ -0,0 +1,36 @@
//
// Copyright © 2024 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 { WorkspaceLoginInfo } from '@hcengineering/account'
import config from './config'
export async function getWorkspaceInfo (token: string): Promise<WorkspaceLoginInfo> {
const accountsUrl = config.AccountsUrl
const workspaceInfo = await (
await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'getWorkspaceInfo',
params: []
})
})
).json()
return workspaceInfo.result as WorkspaceLoginInfo
}

View File

@ -24,6 +24,7 @@ export interface Config {
Port: number
AccountsUrl: string
TransactorUrl: string
MongoUrl: string
UploadUrl: string
@ -38,6 +39,7 @@ const envMap: { [key in keyof Config]: string } = {
Secret: 'SECRET',
Interval: 'INTERVAL',
Port: 'COLLABORATOR_PORT',
AccountsUrl: 'ACCOUNTS_URL',
TransactorUrl: 'TRANSACTOR_URL',
MongoUrl: 'MONGO_URL',
UploadUrl: 'UPLOAD_URL',
@ -50,6 +52,7 @@ const required: Array<keyof Config> = [
'Secret',
'ServiceID',
'Port',
'AccountsUrl',
'TransactorUrl',
'MongoUrl',
'MinioEndpoint',
@ -63,6 +66,7 @@ const config: Config = (() => {
ServiceID: process.env[envMap.ServiceID] ?? 'collaborator-service',
Interval: parseInt(process.env[envMap.Interval] ?? '30000'),
Port: parseInt(process.env[envMap.Port] ?? '3078'),
AccountsUrl: process.env[envMap.AccountsUrl],
TransactorUrl: process.env[envMap.TransactorUrl],
MongoUrl: process.env[envMap.MongoUrl],
UploadUrl: process.env[envMap.UploadUrl] ?? '/files',

View File

@ -27,6 +27,7 @@ import { MongoClient } from 'mongodb'
import { WebSocket, WebSocketServer } from 'ws'
import { applyUpdate, encodeStateAsUpdate } from 'yjs'
import { getWorkspaceInfo } from './account'
import { Config } from './config'
import { Context, buildContext } from './context'
import { ActionsExtension } from './extensions/action'
@ -145,20 +146,25 @@ export async function start (
ctx.measure('authenticate', 1)
const context = buildContext(data, controller)
// verify workspace can be accessed with the token
const workspaceInfo = await getWorkspaceInfo(data.token)
// verify document name
let documentName = data.documentName
if (documentName.includes('://')) {
documentName = documentName.split('://', 2)[1]
}
// if (documentName.includes('/')) {
// const [workspace] = documentName.split('/', 2)
// if (workspace !== context.workspaceId.name) {
// throw new Error('documentName must include workspace')
// }
// } else {
// throw new Error('documentName must include workspace')
// }
if (documentName.includes('/')) {
const [workspaceUrl] = documentName.split('/', 2)
// verify workspace url in the document matches the token
if (workspaceInfo.workspace !== workspaceUrl) {
throw new Error('documentName must include workspace')
}
} else {
throw new Error('documentName must include workspace')
}
return context
},

View File

@ -23,20 +23,20 @@ import { Context } from '../context'
import { StorageAdapter } from './adapter'
interface MinioDocumentId {
workspace: string
workspaceUrl: string
minioDocumentId: string
}
function parseDocumentId (documentId: string): MinioDocumentId {
const [workspace, minioDocumentId] = documentId.split('/')
const [workspaceUrl, minioDocumentId] = documentId.split('/')
return {
workspace: workspace ?? '',
workspaceUrl: workspaceUrl ?? '',
minioDocumentId: minioDocumentId ?? ''
}
}
function isValidDocumentId (documentId: MinioDocumentId, context: Context): boolean {
return documentId.minioDocumentId !== '' // && documentId.workspace === context.workspaceId.name
function isValidDocumentId (documentId: MinioDocumentId): boolean {
return documentId.minioDocumentId !== '' && documentId.workspaceUrl !== ''
}
function maybePlatformDocumentId (documentId: string): boolean {
@ -52,9 +52,9 @@ export class MinioStorageAdapter implements StorageAdapter {
async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { workspaceId } = context
const { workspace, minioDocumentId } = parseDocumentId(documentId)
const { workspaceUrl, minioDocumentId } = parseDocumentId(documentId)
if (!isValidDocumentId({ workspace, minioDocumentId }, context)) {
if (!isValidDocumentId({ workspaceUrl, minioDocumentId })) {
console.warn('malformed document id', documentId)
return undefined
}
@ -91,9 +91,9 @@ export class MinioStorageAdapter implements StorageAdapter {
async saveDocument (documentId: string, document: YDoc, context: Context): Promise<void> {
const { clientFactory, workspaceId } = context
const { workspace, minioDocumentId } = parseDocumentId(documentId)
const { workspaceUrl, minioDocumentId } = parseDocumentId(documentId)
if (!isValidDocumentId({ workspace, minioDocumentId }, context)) {
if (!isValidDocumentId({ workspaceUrl, minioDocumentId })) {
console.warn('malformed document id', documentId)
return undefined
}

View File

@ -23,7 +23,7 @@ import { Context } from '../context'
import { StorageAdapter } from './adapter'
interface MongodbDocumentId {
workspace: string
workspaceUrl: string
objectDomain: string
objectId: string
objectAttr: string
@ -32,7 +32,7 @@ interface MongodbDocumentId {
function parseDocumentId (documentId: string): MongodbDocumentId {
const [workspace, objectDomain, objectId, objectAttr] = documentId.split('/')
return {
workspace: workspace ?? '',
workspaceUrl: workspace ?? '',
objectId: objectId ?? '',
objectDomain: objectDomain ?? '',
objectAttr: objectAttr ?? ''
@ -54,9 +54,9 @@ export class MongodbStorageAdapter implements StorageAdapter {
) {}
async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { workspace, objectId, objectDomain, objectAttr } = parseDocumentId(documentId)
const { workspaceUrl, objectId, objectDomain, objectAttr } = parseDocumentId(documentId)
if (!isValidDocumentId({ workspace, objectId, objectDomain, objectAttr }, context)) {
if (!isValidDocumentId({ workspaceUrl, objectId, objectDomain, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}

View File

@ -22,16 +22,16 @@ import { Context } from '../context'
import { StorageAdapter } from './adapter'
interface PlatformDocumentId {
workspace: string
workspaceUrl: string
objectClass: Ref<Class<Doc>>
objectId: Ref<Doc>
objectAttr: string
}
function parseDocumentId (documentId: string): PlatformDocumentId {
const [workspace, objectClass, objectId, objectAttr] = documentId.split('/')
const [workspaceUrl, objectClass, objectId, objectAttr] = documentId.split('/')
return {
workspace: workspace ?? '',
workspaceUrl: workspaceUrl ?? '',
objectClass: (objectClass ?? '') as Ref<Class<Doc>>,
objectId: (objectId ?? '') as Ref<Doc>,
objectAttr: objectAttr ?? ''
@ -53,9 +53,9 @@ export class PlatformStorageAdapter implements StorageAdapter {
async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { clientFactory } = context
const { workspace, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
const { workspaceUrl, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
if (!isValidDocumentId({ workspace, objectId, objectClass, objectAttr }, context)) {
if (!isValidDocumentId({ workspaceUrl, objectId, objectClass, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}
@ -82,9 +82,9 @@ export class PlatformStorageAdapter implements StorageAdapter {
async saveDocument (documentId: string, document: YDoc, context: Context): Promise<void> {
const { clientFactory } = context
const { workspace, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
const { workspaceUrl, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
if (!isValidDocumentId({ workspace, objectId, objectClass, objectAttr }, context)) {
if (!isValidDocumentId({ workspaceUrl, objectId, objectClass, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}

View File

@ -111,6 +111,7 @@ services:
environment:
- COLLABORATOR_PORT=3078
- SECRET=secret
- ACCOUNTS_URL=http://account:3003
- TRANSACTOR_URL=ws://transactor:3334
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27018