From 36e4bef21bbf20059068031d3069b2c77f962818 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Tue, 26 Apr 2022 22:20:20 +0500 Subject: [PATCH] feat: Remove attachments from public access Closes #219 --- Dockerfile | 2 +- docker-compose.yml | 2 +- server/.gitignore | 8 ++- .../attachments/download-thumbnail.js | 67 ++++++++++++++++++ .../api/controllers/attachments/download.js | 69 +++++++++++++++++++ server/api/models/Attachment.js | 4 +- server/config/custom.js | 2 +- server/config/policies.js | 11 --- server/config/routes.js | 10 +++ .../{public => private}/attachments/.gitkeep | 0 10 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 server/api/controllers/attachments/download-thumbnail.js create mode 100644 server/api/controllers/attachments/download.js rename server/{public => private}/attachments/.gitkeep (100%) diff --git a/Dockerfile b/Dockerfile index 062221c0..f6e6520f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,7 +57,7 @@ COPY --from=client-builder /app/build/index.html views VOLUME /app/public/user-avatars VOLUME /app/public/project-background-images -VOLUME /app/public/attachments +VOLUME /app/private/attachments EXPOSE 1337 diff --git a/docker-compose.yml b/docker-compose.yml index 4e72e69f..0c813fc6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: volumes: - user-avatars:/app/public/user-avatars - project-background-images:/app/public/project-background-images - - attachments:/app/public/attachments + - attachments:/app/private/attachments ports: - 3000:1337 environment: diff --git a/server/.gitignore b/server/.gitignore index 39fabc19..fa265718 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -135,9 +135,11 @@ public/user-avatars/* !public/project-background-images public/project-background-images/* !public/project-background-images/.gitkeep -!public/attachments -public/attachments/* -!public/attachments/.gitkeep + +private/* +!private/attachments +private/attachments/* +!private/attachments/.gitkeep views/* !views/.gitkeep diff --git a/server/api/controllers/attachments/download-thumbnail.js b/server/api/controllers/attachments/download-thumbnail.js new file mode 100644 index 00000000..00c6f6d9 --- /dev/null +++ b/server/api/controllers/attachments/download-thumbnail.js @@ -0,0 +1,67 @@ +const fs = require('fs'); +const path = require('path'); + +const Errors = { + ATTACHMENT_NOT_FOUND: { + attachmentNotFound: 'Attachment not found', + }, +}; + +module.exports = { + inputs: { + id: { + type: 'string', + regex: /^[0-9]+$/, + required: true, + }, + filename: { + type: 'string', + required: true, + }, + }, + + exits: { + attachmentNotFound: { + responseType: 'notFound', + }, + }, + + async fn(inputs, exits) { + const { currentUser } = this.req; + + const { attachment, card, project } = await sails.helpers.attachments + .getProjectPath(inputs.id) + .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND); + + const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); + + if (!isBoardMember) { + const isProjectManager = await sails.helpers.users.isProjectManager( + currentUser.id, + project.id, + ); + + if (!isProjectManager) { + throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden + } + } + + if (!attachment.isImage) { + throw Errors.ATTACHMENT_NOT_FOUND; + } + + const filePath = path.join( + sails.config.custom.attachmentsPath, + attachment.dirname, + 'thumbnails', + inputs.filename, + ); + + if (!fs.existsSync(filePath)) { + throw Errors.ATTACHMENT_NOT_FOUND; + } + + this.res.setHeader('Content-Disposition', `inline; ${inputs.filename}`); + return exits.success(fs.createReadStream(filePath)); + }, +}; diff --git a/server/api/controllers/attachments/download.js b/server/api/controllers/attachments/download.js new file mode 100644 index 00000000..4c7d466e --- /dev/null +++ b/server/api/controllers/attachments/download.js @@ -0,0 +1,69 @@ +const fs = require('fs'); +const path = require('path'); + +const Errors = { + ATTACHMENT_NOT_FOUND: { + attachmentNotFound: 'Attachment not found', + }, +}; + +module.exports = { + inputs: { + id: { + type: 'string', + regex: /^[0-9]+$/, + required: true, + }, + filename: { + type: 'string', + required: true, + }, + }, + + exits: { + attachmentNotFound: { + responseType: 'notFound', + }, + }, + + async fn(inputs, exits) { + const { currentUser } = this.req; + + const { attachment, card, project } = await sails.helpers.attachments + .getProjectPath(inputs.id) + .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND); + + const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId); + + if (!isBoardMember) { + const isProjectManager = await sails.helpers.users.isProjectManager( + currentUser.id, + project.id, + ); + + if (!isProjectManager) { + throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden + } + } + + const filePath = path.join( + sails.config.custom.attachmentsPath, + attachment.dirname, + attachment.filename, + ); + + if (!fs.existsSync(filePath)) { + throw Errors.ATTACHMENT_NOT_FOUND; + } + + let contentDisposition; + if (attachment.isImage || path.extname(attachment.filename) === '.pdf') { + contentDisposition = 'inline'; + } else { + contentDisposition = `attachment; ${inputs.filename}`; + } + + this.res.setHeader('Content-Disposition', contentDisposition); + return exits.success(fs.createReadStream(filePath)); + }, +}; diff --git a/server/api/models/Attachment.js b/server/api/models/Attachment.js index dd1b649a..0a0f3185 100644 --- a/server/api/models/Attachment.js +++ b/server/api/models/Attachment.js @@ -52,9 +52,9 @@ module.exports = { customToJSON() { return { ..._.omit(this, ['dirname', 'filename', 'isImage']), - url: `${sails.config.custom.attachmentsUrl}/${this.dirname}/${this.filename}`, + url: `${sails.config.custom.attachmentsUrl}/${this.id}/download/${this.filename}`, coverUrl: this.isImage - ? `${sails.config.custom.attachmentsUrl}/${this.dirname}/thumbnails/cover-256.jpg` + ? `${sails.config.custom.attachmentsUrl}/${this.id}/download/thumbnails/cover-256.jpg` : null, }; }, diff --git a/server/config/custom.js b/server/config/custom.js index 1cea3b05..8d47ae74 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -26,6 +26,6 @@ module.exports.custom = { projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'), projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`, - attachmentsPath: path.join(sails.config.paths.public, 'attachments'), + attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'), attachmentsUrl: `${process.env.BASE_URL}/attachments`, }; diff --git a/server/config/policies.js b/server/config/policies.js index 9a329aa4..9095e72a 100644 --- a/server/config/policies.js +++ b/server/config/policies.js @@ -18,21 +18,10 @@ module.exports.policies = { '*': 'is-authenticated', - // 'users/index': ['is-authenticated', 'is-admin'], 'users/create': ['is-authenticated', 'is-admin'], 'users/delete': ['is-authenticated', 'is-admin'], 'projects/create': ['is-authenticated', 'is-admin'], - // 'projects/update': ['is-authenticated', 'is-admin'], - // 'projects/update-background-image': ['is-authenticated', 'is-admin'], - // 'projects/delete': ['is-authenticated', 'is-admin'], - - // 'project-memberships/create': ['is-authenticated', 'is-admin'], - // 'project-memberships/delete': ['is-authenticated', 'is-admin'], - - // 'boards/create': ['is-authenticated', 'is-admin'], - // 'boards/update': ['is-authenticated', 'is-admin'], - // 'boards/delete': ['is-authenticated', 'is-admin'], 'access-tokens/create': true, }; diff --git a/server/config/routes.js b/server/config/routes.js index 922251f8..1bcd4db7 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -75,6 +75,16 @@ module.exports.routes = { 'GET /api/notifications/:id': 'notifications/show', 'PATCH /api/notifications/:ids': 'notifications/update', + 'GET /attachments/:id/download/:filename': { + action: 'attachments/download', + skipAssets: false, + }, + + 'GET /attachments/:id/download/thumbnails/:filename': { + action: 'attachments/download-thumbnail', + skipAssets: false, + }, + 'GET /*': { view: 'index', skipAssets: true, diff --git a/server/public/attachments/.gitkeep b/server/private/attachments/.gitkeep similarity index 100% rename from server/public/attachments/.gitkeep rename to server/private/attachments/.gitkeep