fix: secure blobs (#10490)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2026-02-09 09:22:27 +07:00
committed by GitHub
parent 78bf4743c9
commit 7b133e084d
9 changed files with 86 additions and 14 deletions
+1
View File
@@ -519,6 +519,7 @@ services:
- REGION=cockroach
- QUEUE_CONFIG=${QUEUE_CONFIG}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- SECURE=true
restart: unless-stopped
hulylake:
image: hardcoreeng/hulylake
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadMultipart, uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class DatalakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class DatalakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/blob/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`)
try {
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class FrontStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class FrontStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class HulylakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class HulylakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/api/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -29,6 +29,7 @@ export interface FileStorageUploadOptions {
/** @public */
export interface FileStorage {
getFileUrl: (workspace: string, file: string, filename?: string) => string
getCookiePath: (workspace: string) => string
getFileMeta: (token: string, workspace: string, file: string) => Promise<Record<string, any>>
uploadFile: (
token: string,
+12 -8
View File
@@ -891,15 +891,19 @@ export function isSpaceClass (_class: Ref<Class<Doc>>): boolean {
}
export function setPresentationCookie (token: string, workspaceUuid: WorkspaceUuid): void {
function setToken (path: string): void {
const res =
encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-')) +
'=' +
encodeURIComponent(token) +
`; path=${path}`
document.cookie = res
const cookieName = encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-'))
const cookieValue = encodeURIComponent(token)
const storage = getMetadata(plugin.metadata.FileStorage)
if (storage !== undefined) {
let path = `/files/${workspaceUuid}`
try {
path = storage.getCookiePath(workspaceUuid)
} catch {}
const normalized = path.startsWith('/') ? path : `/${path}`
document.cookie = `${cookieName}=${cookieValue}; path=${normalized}`
}
setToken('/files/' + workspaceUuid)
}
export const upgradeDownloadProgress = writable(-1)
@@ -35,6 +35,7 @@ export interface Config {
DbUrl: string
Buckets: BucketConfig[]
CleanupInterval: number
Secure: boolean
Readonly: boolean
Cache: CacheConfig
}
@@ -86,6 +87,7 @@ const config: Config = (() => {
AccountsUrl: process.env.ACCOUNTS_URL,
DbUrl: process.env.DB_URL,
Buckets: parseBucketsConfig(process.env.BUCKETS),
Secure: process.env.SECURE === 'true',
Readonly: process.env.READONLY === 'true',
Cache: {
enabled: process.env.CACHE_ENABLED !== 'false',
@@ -38,6 +38,14 @@ export const keepAlive = (options: KeepAliveOptions): RequestHandler => {
}
}
export const withOptionalAuth = (secure: boolean): RequestHandler => {
return secure
? withAuthorization
: (req: Request, res: Response, next: NextFunction) => {
next()
}
}
export const withAdminAuthorization = (req: RequestWithAuth, res: Response, next: NextFunction): void => {
try {
const token = extractToken(req.headers)
+32 -6
View File
@@ -36,7 +36,8 @@ import {
withAuthorization,
withBlob,
withWorkspace,
withReadonly
withReadonly,
withOptionalAuth
} from './middleware'
import {
handleBlobDelete,
@@ -176,13 +177,33 @@ export async function createServer (
app.get('/blob/:workspace', withAdminAuthorization, withWorkspace, wrapRequest(ctx, 'listBlobs', handleBlobList))
app.head('/blob/:workspace/:name', withBlob, wrapRequest(ctx, 'headBlob', handleBlobHead))
app.head(
'/blob/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'headBlob', handleBlobHead)
)
app.head('/blob/:workspace/:name/:filename', withBlob, wrapRequest(ctx, 'headBlob', handleBlobHead))
app.head(
'/blob/:workspace/:name/:filename',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'headBlob', handleBlobHead)
)
app.get('/blob/:workspace/:name', withBlob, wrapRequest(ctx, 'getBlob', handleBlobGet))
app.get(
'/blob/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getBlob', handleBlobGet)
)
app.get('/blob/:workspace/:name/:filename', withBlob, wrapRequest(ctx, 'getBlob', handleBlobGet))
app.get(
'/blob/:workspace/:name/:filename',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getBlob', handleBlobGet)
)
app.delete('/blob/:workspace/:name', withAuthorization, withBlob, wrapRequest(ctx, 'deleteBlob', handleBlobDelete))
@@ -206,7 +227,12 @@ export async function createServer (
// Blob meta
app.get('/meta/:workspace/:name', withBlob, wrapRequest(ctx, 'getMeta', handleMetaGet))
app.get(
'/meta/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getMeta', handleMetaGet)
)
app.put('/meta/:workspace/:name', withAuthorization, withBlob, wrapRequest(ctx, 'putMeta', handleMetaPut))