Skip to content

Commit

Permalink
feat: allow upload signed url to overwrite files
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos committed Apr 24, 2024
1 parent 5bec8db commit 0dee8ee
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/auth/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type SignedToken = {

export type SignedUploadToken = {
owner: string | undefined
upsert: boolean
url: string
exp: number
}
Expand Down
14 changes: 13 additions & 1 deletion src/http/routes/object/getSignedUploadURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const getSignedUploadURLParamsSchema = {
required: ['bucketName', '*'],
} as const

const getSignedUploadURLHeadersSchema = {
type: 'object',
properties: {
'x-upsert': { type: 'string' },
authorization: { type: 'string' },
},
required: ['authorization'],
} as const

const successResponseSchema = {
type: 'object',
properties: {
Expand All @@ -29,6 +38,7 @@ const successResponseSchema = {
}
interface getSignedURLRequestInterface extends AuthenticatedRequest {
Params: FromSchema<typeof getSignedUploadURLParamsSchema>
Headers: FromSchema<typeof getSignedUploadURLHeadersSchema>
}

export default async function routes(fastify: FastifyInstance) {
Expand All @@ -54,7 +64,9 @@ export default async function routes(fastify: FastifyInstance) {

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

return response.status(200).send({ url: signedUploadURL })
}
Expand Down
1 change: 1 addition & 0 deletions src/http/routes/object/uploadSignedObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default async function routes(fastify: FastifyInstance) {
.uploadNewObject(request, {
owner,
objectName,
isUpsert: payload.upsert,
})

return response.status(objectMetadata?.httpStatusCode ?? 200).send({
Expand Down
27 changes: 18 additions & 9 deletions src/storage/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,15 @@ export class ObjectStorage {
* @param url
* @param expiresIn seconds
* @param owner
* @param options
*/
async signUploadObjectUrl(objectName: string, url: string, expiresIn: number, owner?: string) {
async signUploadObjectUrl(
objectName: string,
url: string,
expiresIn: number,
owner?: string,
options?: { upsert?: boolean }
) {
// check as super user if the object already exists
const found = await this.asSuperUser().findObject(objectName, 'id', {
dontErrorOnEmpty: true,
Expand All @@ -584,19 +591,21 @@ export class ObjectStorage {
}

// check if user has INSERT permissions
await this.db.testPermission((db) => {
return db.createObject({
bucket_id: this.bucketId,
name: objectName,
owner,
metadata: {},
})
await this.uploader.canUpload({
bucketId: this.bucketId,
objectName,
owner,
isUpsert: options?.upsert ?? false,
})

const urlParts = url.split('/')
const urlToSign = decodeURI(urlParts.splice(4).join('/'))
const { secret: jwtSecret } = await getJwtSecret(this.db.tenantId)
const token = await signJWT({ owner, url: urlToSign }, jwtSecret, expiresIn)
const token = await signJWT(
{ owner, url: urlToSign, upsert: Boolean(options?.upsert) },
jwtSecret,
expiresIn
)

return `/object/upload/sign/${urlToSign}?token=${token}`
}
Expand Down
67 changes: 67 additions & 0 deletions src/test/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,73 @@ describe('testing uploading with generated signed upload URL', () => {
expect(response.statusCode).toBe(400)
expect(S3Backend.prototype.uploadObject).not.toHaveBeenCalled()
})

it('will allow overwriting a file when the generating a signed upload url with x-upsert:true', async () => {
function createUpload() {
const form = new FormData()
form.append('file', fs.createReadStream(`./src/test/assets/sadcat.jpg`))
return form
}

const BUCKET_ID = 'bucket2'
const OBJECT_NAME = 'signed/sadcat-upload-signed-2.png'
const urlToSign = `${BUCKET_ID}/${OBJECT_NAME}`
const owner = '317eadce-631a-4429-a0bb-f19a7a517b4a'

// Upload a file first
const resp = await app().inject({
method: 'POST',
url: `/object/${urlToSign}`,
payload: createUpload(),
headers: {
authorization: serviceKey,
},
})

expect(resp.statusCode).toBe(200)

const jwtToken = await signJWT({ owner, url: urlToSign, upsert: true }, jwtSecret, 100)
const response = await app().inject({
method: 'PUT',
url: `/object/upload/sign/${urlToSign}?token=${jwtToken}`,
payload: createUpload(),
})
expect(response.statusCode).toBe(200)
expect(S3Backend.prototype.uploadObject).toHaveBeenCalled()
})

it('will allow not be able overwriting a file when the generating a signed upload url without x-upsert header', async () => {
function createUpload() {
const form = new FormData()
form.append('file', fs.createReadStream(`./src/test/assets/sadcat.jpg`))
return form
}

const BUCKET_ID = 'bucket2'
const OBJECT_NAME = 'signed/sadcat-upload-signed-3.png'
const urlToSign = `${BUCKET_ID}/${OBJECT_NAME}`
const owner = '317eadce-631a-4429-a0bb-f19a7a517b4a'

// Upload a file first
const resp = await app().inject({
method: 'POST',
url: `/object/${urlToSign}`,
payload: createUpload(),
headers: {
authorization: serviceKey,
},
})

expect(resp.statusCode).toBe(200)

const jwtToken = await signJWT({ owner, url: urlToSign }, jwtSecret, 100)
const response = await app().inject({
method: 'PUT',
url: `/object/upload/sign/${urlToSign}?token=${jwtToken}`,
payload: createUpload(),
})
expect(response.statusCode).toBe(400)
})
})

/**
Expand Down

0 comments on commit 0dee8ee

Please sign in to comment.