diff --git a/README.md b/README.md
index 15e84638f2..6fa6cf4ab4 100644
--- a/README.md
+++ b/README.md
@@ -392,7 +392,7 @@ The following table lists all route groups covered by `routeAllowList` with exam
| Purchase validation | `validate_purchase` | `validate_purchase` |
> [!NOTE]
-> File upload, file download, and file metadata routes are not covered by `routeAllowList`. File upload access is controlled via the `fileUpload` option.
+> File routes are not covered by `routeAllowList`. File upload access is controlled via the `fileUpload` option. File download and metadata access is controlled via the `fileDownload` option.
## Email Verification and Password Reset
diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js
index 4220215a10..8bfeb3799c 100644
--- a/resources/buildConfigDefinitions.js
+++ b/resources/buildConfigDefinitions.js
@@ -15,6 +15,7 @@ const parsers = require('../src/Options/parsers');
const nestedOptionTypes = [
'CustomPagesOptions',
'DatabaseOptions',
+ 'FileDownloadOptions',
'FileUploadOptions',
'IdempotencyOptions',
'Object',
@@ -34,6 +35,7 @@ const nestedOptionEnvPrefix = {
DatabaseOptionsClientMetadata: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_',
CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_',
DatabaseOptions: 'PARSE_SERVER_DATABASE_',
+ FileDownloadOptions: 'PARSE_SERVER_FILE_DOWNLOAD_',
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
diff --git a/spec/FileDownload.spec.js b/spec/FileDownload.spec.js
new file mode 100644
index 0000000000..5010f032ee
--- /dev/null
+++ b/spec/FileDownload.spec.js
@@ -0,0 +1,282 @@
+'use strict';
+
+describe('fileDownload', () => {
+ describe('config validation', () => {
+ it('should default all flags to true when fileDownload is undefined', async () => {
+ await reconfigureServer({ fileDownload: undefined });
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ expect(config.fileDownload.enableForAnonymousUser).toBe(true);
+ expect(config.fileDownload.enableForAuthenticatedUser).toBe(true);
+ expect(config.fileDownload.enableForPublic).toBe(true);
+ });
+
+ it('should accept valid boolean values', async () => {
+ await reconfigureServer({
+ fileDownload: {
+ enableForAnonymousUser: false,
+ enableForAuthenticatedUser: false,
+ enableForPublic: false,
+ },
+ });
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ expect(config.fileDownload.enableForAnonymousUser).toBe(false);
+ expect(config.fileDownload.enableForAuthenticatedUser).toBe(false);
+ expect(config.fileDownload.enableForPublic).toBe(false);
+ });
+
+ it('should reject non-object values', async () => {
+ for (const value of ['string', 123, true, []]) {
+ await expectAsync(reconfigureServer({ fileDownload: value })).toBeRejected();
+ }
+ });
+
+ it('should reject non-boolean flag values', async () => {
+ await expectAsync(
+ reconfigureServer({ fileDownload: { enableForAnonymousUser: 'yes' } })
+ ).toBeRejected();
+ await expectAsync(
+ reconfigureServer({ fileDownload: { enableForAuthenticatedUser: 1 } })
+ ).toBeRejected();
+ await expectAsync(
+ reconfigureServer({ fileDownload: { enableForPublic: null } })
+ ).toBeRejected();
+ });
+ });
+
+ describe('permissions', () => {
+ async function uploadTestFile() {
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'Content-Type': 'text/plain',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-Master-Key': 'test',
+ },
+ method: 'POST',
+ url: 'http://localhost:8378/1/files/test.txt',
+ body: 'hello world',
+ });
+ return res.data;
+ }
+
+ it('should allow public download by default', async () => {
+ await reconfigureServer();
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ const res = await request({
+ method: 'GET',
+ url: file.url,
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it('should block public download when enableForPublic is false', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForPublic: false },
+ });
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ try {
+ await request({
+ method: 'GET',
+ url: file.url,
+ });
+ fail('should have thrown');
+ } catch (e) {
+ expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
+ }
+ });
+
+ it('should allow authenticated user download when enableForAuthenticatedUser is true', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForPublic: false, enableForAuthenticatedUser: true },
+ });
+ const file = await uploadTestFile();
+ const user = new Parse.User();
+ user.set('username', 'testuser');
+ user.set('password', 'testpass');
+ await user.signUp();
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'X-Parse-Session-Token': user.getSessionToken(),
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it('should block authenticated user download when enableForAuthenticatedUser is false', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForAuthenticatedUser: false },
+ });
+ const file = await uploadTestFile();
+ const user = new Parse.User();
+ user.set('username', 'testuser');
+ user.set('password', 'testpass');
+ await user.signUp();
+ const request = require('../lib/request');
+ try {
+ await request({
+ headers: {
+ 'X-Parse-Session-Token': user.getSessionToken(),
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ fail('should have thrown');
+ } catch (e) {
+ expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
+ }
+ });
+
+ it('should block anonymous user download when enableForAnonymousUser is false', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForAnonymousUser: false },
+ });
+ const file = await uploadTestFile();
+ const user = await Parse.AnonymousUtils.logIn();
+ const request = require('../lib/request');
+ try {
+ await request({
+ headers: {
+ 'X-Parse-Session-Token': user.getSessionToken(),
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ fail('should have thrown');
+ } catch (e) {
+ expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
+ }
+ });
+
+ it('should allow anonymous user download when enableForAnonymousUser is true', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForAnonymousUser: true, enableForPublic: false },
+ });
+ const file = await uploadTestFile();
+ const user = await Parse.AnonymousUtils.logIn();
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'X-Parse-Session-Token': user.getSessionToken(),
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it('should allow master key to bypass all restrictions', async () => {
+ await reconfigureServer({
+ fileDownload: {
+ enableForAnonymousUser: false,
+ enableForAuthenticatedUser: false,
+ enableForPublic: false,
+ },
+ });
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'X-Parse-Master-Key': 'test',
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it('should block metadata endpoint when download is disabled for public', async () => {
+ await reconfigureServer({
+ fileDownload: { enableForPublic: false },
+ });
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ // The file URL is like http://localhost:8378/1/files/test/abc_test.txt
+ // The metadata URL replaces /files/APPID/ with /files/APPID/metadata/
+ const url = new URL(file.url);
+ const pathParts = url.pathname.split('/');
+ // pathParts: ['', '1', 'files', 'test', 'abc_test.txt']
+ const appIdIndex = pathParts.indexOf('files') + 1;
+ pathParts.splice(appIdIndex + 1, 0, 'metadata');
+ url.pathname = pathParts.join('/');
+ try {
+ await request({
+ method: 'GET',
+ url: url.toString(),
+ });
+ fail('should have thrown');
+ } catch (e) {
+ expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
+ }
+ });
+
+ it('should block all downloads when all flags are false', async () => {
+ await reconfigureServer({
+ fileDownload: {
+ enableForAnonymousUser: false,
+ enableForAuthenticatedUser: false,
+ enableForPublic: false,
+ },
+ });
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ try {
+ await request({
+ method: 'GET',
+ url: file.url,
+ });
+ fail('should have thrown');
+ } catch (e) {
+ expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
+ }
+ });
+
+ it('should allow maintenance key to bypass download restrictions', async () => {
+ await reconfigureServer({
+ fileDownload: {
+ enableForAnonymousUser: false,
+ enableForAuthenticatedUser: false,
+ enableForPublic: false,
+ },
+ });
+ const file = await uploadTestFile();
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'X-Parse-Maintenance-Key': 'testing',
+ },
+ method: 'GET',
+ url: file.url,
+ });
+ expect(res.status).toBe(200);
+ });
+
+ it('should allow maintenance key to bypass upload restrictions', async () => {
+ await reconfigureServer({
+ fileUpload: {
+ enableForAnonymousUser: false,
+ enableForAuthenticatedUser: false,
+ enableForPublic: false,
+ },
+ });
+ const request = require('../lib/request');
+ const res = await request({
+ headers: {
+ 'Content-Type': 'text/plain',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-Maintenance-Key': 'testing',
+ },
+ method: 'POST',
+ url: 'http://localhost:8378/1/files/test.txt',
+ body: 'hello world',
+ });
+ expect(res.data.url).toBeDefined();
+ });
+ });
+});
diff --git a/src/Config.js b/src/Config.js
index 9cc3cf970a..c5a6a6593e 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -12,6 +12,7 @@ import { version } from '../package.json';
import {
AccountLockoutOptions,
DatabaseOptions,
+ FileDownloadOptions,
FileUploadOptions,
IdempotencyOptions,
LiveQueryOptions,
@@ -130,6 +131,7 @@ export class Config {
allowHeaders,
idempotencyOptions,
fileUpload,
+ fileDownload,
pages,
security,
enforcePrivateUsers,
@@ -157,6 +159,11 @@ export class Config {
this.validateAccountLockoutPolicy(accountLockout);
this.validatePasswordPolicy(passwordPolicy);
this.validateFileUploadOptions(fileUpload);
+ if (fileDownload == null) {
+ fileDownload = {};
+ arguments[0].fileDownload = fileDownload;
+ }
+ this.validateFileDownloadOptions(fileDownload);
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
throw 'revokeSessionOnPasswordReset must be a boolean value';
@@ -586,6 +593,34 @@ export class Config {
}
}
+ static validateFileDownloadOptions(fileDownload) {
+ try {
+ if (fileDownload == null || typeof fileDownload !== 'object' || Array.isArray(fileDownload)) {
+ throw 'fileDownload must be an object value.';
+ }
+ } catch (e) {
+ if (e instanceof ReferenceError) {
+ return;
+ }
+ throw e;
+ }
+ if (fileDownload.enableForAnonymousUser === undefined) {
+ fileDownload.enableForAnonymousUser = FileDownloadOptions.enableForAnonymousUser.default;
+ } else if (typeof fileDownload.enableForAnonymousUser !== 'boolean') {
+ throw 'fileDownload.enableForAnonymousUser must be a boolean value.';
+ }
+ if (fileDownload.enableForPublic === undefined) {
+ fileDownload.enableForPublic = FileDownloadOptions.enableForPublic.default;
+ } else if (typeof fileDownload.enableForPublic !== 'boolean') {
+ throw 'fileDownload.enableForPublic must be a boolean value.';
+ }
+ if (fileDownload.enableForAuthenticatedUser === undefined) {
+ fileDownload.enableForAuthenticatedUser = FileDownloadOptions.enableForAuthenticatedUser.default;
+ } else if (typeof fileDownload.enableForAuthenticatedUser !== 'boolean') {
+ throw 'fileDownload.enableForAuthenticatedUser must be a boolean value.';
+ }
+ }
+
static validateIps(field, masterKeyIps) {
for (let ip of masterKeyIps) {
if (ip.includes('/')) {
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 35fa836304..8c4f7e4a89 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -272,6 +272,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: false,
},
+ fileDownload: {
+ env: 'PARSE_SERVER_FILE_DOWNLOAD_OPTIONS',
+ help: 'Options for file downloads',
+ action: parsers.objectParser,
+ type: 'FileDownloadOptions',
+ default: {},
+ },
fileKey: {
env: 'PARSE_SERVER_FILE_KEY',
help: 'Key for your files',
@@ -1113,6 +1120,26 @@ module.exports.FileUploadOptions = {
],
},
};
+module.exports.FileDownloadOptions = {
+ enableForAnonymousUser: {
+ env: 'PARSE_SERVER_FILE_DOWNLOAD_ENABLE_FOR_ANONYMOUS_USER',
+ help: 'Is true if file download should be allowed for anonymous users.',
+ action: parsers.booleanParser,
+ default: true,
+ },
+ enableForAuthenticatedUser: {
+ env: 'PARSE_SERVER_FILE_DOWNLOAD_ENABLE_FOR_AUTHENTICATED_USER',
+ help: 'Is true if file download should be allowed for authenticated users.',
+ action: parsers.booleanParser,
+ default: true,
+ },
+ enableForPublic: {
+ env: 'PARSE_SERVER_FILE_DOWNLOAD_ENABLE_FOR_PUBLIC',
+ help: 'Is true if file download should be allowed for anyone, regardless of user authentication.',
+ action: parsers.booleanParser,
+ default: true,
+ },
+};
/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */
module.exports.LogLevel = {
debug: {
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 1e7a5b661b..09f9ef1852 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -52,6 +52,7 @@
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
* @property {Boolean} extendSessionOnUse Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.
+ * @property {FileDownloadOptions} fileDownload Options for file downloads
* @property {String} fileKey Key for your files
* @property {Adapter} filesAdapter Adapter module for the files sub-system
* @property {FileUploadOptions} fileUpload Options for file uploads
@@ -259,6 +260,13 @@
* @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.
The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser's local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.
Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"]`.
*/
+/**
+ * @interface FileDownloadOptions
+ * @property {Boolean} enableForAnonymousUser Is true if file download should be allowed for anonymous users.
+ * @property {Boolean} enableForAuthenticatedUser Is true if file download should be allowed for authenticated users.
+ * @property {Boolean} enableForPublic Is true if file download should be allowed for anyone, regardless of user authentication.
+ */
+
/**
* @interface LogLevel
* @property {StringLiteral} debug Debug level
diff --git a/src/Options/index.js b/src/Options/index.js
index 2ecc8479f1..e495b6fe79 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -355,6 +355,10 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_FILE_UPLOAD_OPTIONS
:DEFAULT: {} */
fileUpload: ?FileUploadOptions;
+ /* Options for file downloads
+ :ENV: PARSE_SERVER_FILE_DOWNLOAD_OPTIONS
+ :DEFAULT: {} */
+ fileDownload: ?FileDownloadOptions;
/* Full path to your GraphQL custom schema.graphql file */
graphQLSchema: ?string;
/* Mounts the GraphQL endpoint
@@ -698,6 +702,18 @@ export interface FileUploadOptions {
allowedFileUrlDomains: ?(string[]);
}
+export interface FileDownloadOptions {
+ /* Is true if file download should be allowed for anonymous users.
+ :DEFAULT: true */
+ enableForAnonymousUser: ?boolean;
+ /* Is true if file download should be allowed for authenticated users.
+ :DEFAULT: true */
+ enableForAuthenticatedUser: ?boolean;
+ /* Is true if file download should be allowed for anyone, regardless of user authentication.
+ :DEFAULT: true */
+ enableForPublic: ?boolean;
+}
+
/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */
export interface LogLevel {
/* Error level - highest priority */
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index 4563e87178..df6f710135 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -5,7 +5,6 @@ import Config from '../Config';
import logger from '../logger';
const triggers = require('../triggers');
const Utils = require('../Utils');
-const auth = require('../Auth');
import { Readable } from 'stream';
import { createSanitizedHttpError } from '../Error';
@@ -89,9 +88,27 @@ export const RESERVED_DIRECTORY_SEGMENTS = ['metadata'];
export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) {
var router = express.Router();
+ // Lightweight info initializer so handleParseSession can resolve session tokens.
+ // Unlike POST/DELETE routes, GET file routes skip handleParseHeaders (which
+ // normally sets req.info) because those requests may not carry Parse headers.
+ const initInfo = (req, res, next) => {
+ if (!req.info) {
+ const sessionToken = req.get('X-Parse-Session-Token');
+ req.info = {
+ sessionToken,
+ installationId: req.get('X-Parse-Installation-Id'),
+ };
+ // If no session token and no auth yet (public access), set a minimal
+ // auth object so handleParseSession skips session resolution.
+ if (!sessionToken && !req.auth) {
+ req.auth = { isMaster: false };
+ }
+ }
+ next();
+ };
// Metadata route must come before the catch-all GET route
- router.get('/files/:appId/metadata/*filepath', this.metadataHandler);
- router.get('/files/:appId/*filepath', this.getHandler);
+ router.get('/files/:appId/metadata/*filepath', initInfo, Middlewares.handleParseSession, this.metadataHandler);
+ router.get('/files/:appId/*filepath', initInfo, Middlewares.handleParseSession, this.getHandler);
router.post('/files', function (req, res, next) {
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename not provided.'));
@@ -121,22 +138,6 @@ export class FilesRouter {
return Array.isArray(parts) ? parts.join('/') : parts;
}
- static async _resolveAuth(req, config) {
- const sessionToken = req.get('X-Parse-Session-Token');
- if (!sessionToken) {
- return null;
- }
- try {
- return await auth.getAuthForSessionToken({
- config,
- sessionToken,
- installationId: req.get('X-Parse-Installation-Id'),
- });
- } catch {
- return null;
- }
- }
-
static validateDirectory(directory) {
if (typeof directory !== 'string') {
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Directory must be a string.');
@@ -179,6 +180,34 @@ export class FilesRouter {
return null;
}
+ static _validateFileDownload(req, config) {
+ const isMaster = req.auth?.isMaster;
+ const isMaintenance = req.auth?.isMaintenance;
+ if (isMaster || isMaintenance) {
+ return;
+ }
+ const user = req.auth?.user;
+ const isLinked = user && Parse.AnonymousUtils.isLinked(user);
+ if (!config.fileDownload.enableForAnonymousUser && isLinked) {
+ throw new Parse.Error(
+ Parse.Error.OPERATION_FORBIDDEN,
+ 'File download by anonymous user is disabled.'
+ );
+ }
+ if (!config.fileDownload.enableForAuthenticatedUser && !isLinked && user) {
+ throw new Parse.Error(
+ Parse.Error.OPERATION_FORBIDDEN,
+ 'File download by authenticated user is disabled.'
+ );
+ }
+ if (!config.fileDownload.enableForPublic && !user) {
+ throw new Parse.Error(
+ Parse.Error.OPERATION_FORBIDDEN,
+ 'File download by public is disabled.'
+ );
+ }
+ }
+
async getHandler(req, res) {
const config = Config.get(req.params.appId);
if (!config) {
@@ -188,13 +217,15 @@ export class FilesRouter {
return;
}
+ FilesRouter._validateFileDownload(req, config);
+
let filename = FilesRouter._getFilenameFromParams(req);
try {
const filesController = config.filesController;
const mime = (await import('mime')).default;
let contentType = mime.getType(filename);
let file = new Parse.File(filename, { base64: '' }, contentType);
- const fileAuth = await FilesRouter._resolveAuth(req, config);
+ const fileAuth = req.auth;
const triggerResult = await triggers.maybeRunFileTrigger(
triggers.Types.beforeFind,
{ file },
@@ -344,27 +375,30 @@ export class FilesRouter {
return;
}
const config = req.config;
- const user = req.auth.user;
const isMaster = req.auth.isMaster;
- const isLinked = user && Parse.AnonymousUtils.isLinked(user);
- if (!isMaster && !config.fileUpload.enableForAnonymousUser && isLinked) {
- next(
- new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
- );
- return;
- }
- if (!isMaster && !config.fileUpload.enableForAuthenticatedUser && !isLinked && user) {
- next(
- new Parse.Error(
- Parse.Error.FILE_SAVE_ERROR,
- 'File upload by authenticated user is disabled.'
- )
- );
- return;
- }
- if (!isMaster && !config.fileUpload.enableForPublic && !user) {
- next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.'));
- return;
+ const isMaintenance = req.auth.isMaintenance;
+ if (!isMaster && !isMaintenance) {
+ const user = req.auth.user;
+ const isLinked = user && Parse.AnonymousUtils.isLinked(user);
+ if (!config.fileUpload.enableForAnonymousUser && isLinked) {
+ next(
+ new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
+ );
+ return;
+ }
+ if (!config.fileUpload.enableForAuthenticatedUser && !isLinked && user) {
+ next(
+ new Parse.Error(
+ Parse.Error.FILE_SAVE_ERROR,
+ 'File upload by authenticated user is disabled.'
+ )
+ );
+ return;
+ }
+ if (!config.fileUpload.enableForPublic && !user) {
+ next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.'));
+ return;
+ }
}
const filesController = config.filesController;
const { filename } = req.params;
@@ -760,10 +794,11 @@ export class FilesRouter {
res.json({});
return;
}
+ FilesRouter._validateFileDownload(req, config);
const { filesController } = config;
let filename = FilesRouter._getFilenameFromParams(req);
const file = new Parse.File(filename, { base64: '' });
- const fileAuth = await FilesRouter._resolveAuth(req, config);
+ const fileAuth = req.auth;
const triggerResult = await triggers.maybeRunFileTrigger(
triggers.Types.beforeFind,
{ file },