Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Signed Upload URL for TUS #461

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_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 Expand Up @@ -48,7 +43,7 @@

// find the first key without a kid or with the matching kid and the "oct" type
const jwk = jwks.keys.find(
(key) => (!key.kid || key.kid === header.kid) && key.kty === 'oct' && (key as any).k

Check warning on line 46 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type
)

if (!jwk) {
Expand All @@ -56,7 +51,7 @@
return secret
}

return Buffer.from((jwk as any).k, 'base64')

Check warning on line 54 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type
}

// jwt is using an asymmetric algorithm
Expand Down Expand Up @@ -89,11 +84,11 @@
jwks: { keys: { kid?: string; kty: string }[] } | null
): jwt.GetPublicKeyOrSecret {
return (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
let result: any = null

Check warning on line 87 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type

try {
result = findJWKFromHeader(header, secret, jwks)
} catch (e: any) {

Check warning on line 91 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type
callback(e)
return
}
Expand All @@ -109,9 +104,9 @@
const hasRSA = jwks.keys.find((key) => key.kty === 'RSA')
const hasECC = jwks.keys.find((key) => key.kty === 'EC')
const hasED = jwks.keys.find(
(key) => key.kty === 'OKP' && ((key as any).crv === 'Ed25519' || (key as any).crv === 'Ed448')

Check warning on line 107 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type

Check warning on line 107 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type
)
const hasHS = jwks.keys.find((key) => key.kty === 'oct' && (key as any).k)

Check warning on line 109 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Unexpected any. Specify a different type

algorithms = [
jwtAlgorithm as jwt.Algorithm,
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
Loading