Skip to content

Commit

Permalink
feat: Signed Upload URL for TUS (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos authored May 2, 2024
1 parent 8d5ad0a commit d431640
Show file tree
Hide file tree
Showing 13 changed files with 552 additions and 165 deletions.
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const build = (opts: buildOpts = {}): FastifyInstance => {
app.register(plugins.metrics({ enabledEndpoint: !isMultitenant }))
app.register(plugins.logTenantId)
app.register(plugins.logRequest({ excludeUrls: ['/status', '/metrics', '/health'] }))
app.register(routes.multiPart, { prefix: 'upload/resumable' })
app.register(routes.tus, { prefix: 'upload/resumable' })
app.register(routes.bucket, { prefix: 'bucket' })
app.register(routes.object, { prefix: 'object' })
app.register(routes.render, { prefix: 'render/image' })
Expand Down
5 changes: 0 additions & 5 deletions src/auth/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ const JWT_RSA_ALGOS: jwt.Algorithm[] = ['RS256', 'RS384', 'RS512']
const JWT_ECC_ALGOS: jwt.Algorithm[] = ['ES256', 'ES384', 'ES512']
const JWT_ED_ALGOS: jwt.Algorithm[] = ['EdDSA'] as unknown as jwt.Algorithm[] // types for EdDSA not yet updated

interface jwtInterface {
sub?: string
role?: string
}

export type SignedToken = {
url: string
transformations?: string
Expand Down
1 change: 1 addition & 0 deletions src/database/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class TenantConnection {
},
connection: {
connectionString: connectionString,
connectionTimeoutMillis: databaseConnectionTimeout,
...this.sslSettings(),
},
acquireConnectionTimeout: databaseConnectionTimeout,
Expand Down
5 changes: 2 additions & 3 deletions src/http/plugins/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fastifyPlugin from 'fastify-plugin'
import { createResponse } from '../generic-routes'
import { verifyJWT } from '../../auth'
import { getJwtSecret } from '../../database'
import { JwtPayload } from 'jsonwebtoken'
import { ERRORS } from '../../storage'

declare module 'fastify' {
interface FastifyRequest {
Expand All @@ -28,8 +28,7 @@ export const jwt = fastifyPlugin(async (fastify) => {
request.jwtPayload = payload
request.owner = payload.sub
} catch (err: any) {
request.log.error({ error: err }, 'unable to get owner')
return reply.status(400).send(createResponse(err.message, '400', err.message))
throw ERRORS.AccessDenied(err.message, err)
}
})
})
2 changes: 1 addition & 1 deletion src/http/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { default as bucket } from './bucket'
export { default as object } from './object'
export { default as render } from './render'
export { default as multiPart } from './tus'
export { default as tus } from './tus'
export { default as healthcheck } from './health'
export { default as s3 } from './s3'
export * from './admin'
9 changes: 6 additions & 3 deletions src/http/routes/object/getSignedUploadURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const successResponseSchema = {
'/object/sign/upload/avatars/folder/cat.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJhdmF0YXJzL2ZvbGRlci9jYXQucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.s7Gt8ME80iREVxPhH01ZNv8oUn4XtaWsmiQ5csiUHn4',
],
},
token: {
type: 'string',
},
},
required: ['url'],
}
Expand Down Expand Up @@ -60,15 +63,15 @@ export default async function routes(fastify: FastifyInstance) {
const objectName = request.params['*']
const owner = request.owner

const urlPath = request.url.split('?').shift()
const urlPath = `${bucketName}/${objectName}`

const signedUploadURL = await request.storage
const signedUpload = await request.storage
.from(bucketName)
.signUploadObjectUrl(objectName, urlPath as string, uploadSignedUrlExpirationTime, owner, {
upsert: request.headers['x-upsert'] === 'true',
})

return response.status(200).send({ url: signedUploadURL })
return response.status(200).send({ url: signedUpload.url, token: signedUpload.token })
}
)
}
27 changes: 4 additions & 23 deletions src/http/routes/object/uploadSignedObject.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { FastifyInstance } from 'fastify'
import { FromSchema } from 'json-schema-to-ts'
import { SignedUploadToken, verifyJWT } from '../../../auth'
import { ERRORS } from '../../../storage'
import { getJwtSecret } from '../../../database/tenant'

const uploadSignedObjectParamsSchema = {
type: 'object',
Expand Down Expand Up @@ -70,36 +67,20 @@ export default async function routes(fastify: FastifyInstance) {
async (request, response) => {
// Validate sender
const { token } = request.query

const { secret: jwtSecret } = await getJwtSecret(request.tenantId)

let payload: SignedUploadToken
try {
payload = (await verifyJWT(token, jwtSecret)) as SignedUploadToken
} catch (e) {
const err = e as Error
throw ERRORS.InvalidJWT(err)
}

const { url, exp, owner } = payload
const { bucketName } = request.params
const objectName = request.params['*']

if (url !== `${bucketName}/${objectName}`) {
throw ERRORS.InvalidSignature()
}

if (exp * 1000 < Date.now()) {
throw ERRORS.ExpiredSignature()
}
const { owner, upsert } = await request.storage
.from(bucketName)
.verifyObjectSignature(token, objectName)

const { objectMetadata, path } = await request.storage
.asSuperUser()
.from(bucketName)
.uploadNewObject(request, {
owner,
objectName,
isUpsert: payload.upsert,
isUpsert: upsert,
})

return response.status(objectMetadata?.httpStatusCode ?? 200).send({
Expand Down
Loading

0 comments on commit d431640

Please sign in to comment.