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

FileUpload options for Server Config #7071

Merged
merged 16 commits into from
Dec 17, 2020
13 changes: 4 additions & 9 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function getENVPrefix(iface) {
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_'
}
if (options[iface.id.name]) {
return options[iface.id.name]
Expand Down Expand Up @@ -163,14 +164,8 @@ function parseDefaultValue(elt, value, t) {
if (type == 'NumberOrBoolean') {
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
}
if (type == 'CustomPagesOptions') {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
});
literalValue = t.objectExpression(props);
}
if (type == 'IdempotencyOptions') {
const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions'];
if (literalTypes.includes(type)) {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
Expand Down
121 changes: 121 additions & 0 deletions spec/ParseFile.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,4 +860,125 @@ describe('Parse.File testing', () => {
});
});
});
describe('file upload restrictions', () => {
it('can reject file upload with unspecified', async () => {
await reconfigureServer({
fileUpload: {},
});
try {
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save();
fail('should not have been able to save file.');
} catch (e) {
expect(e.code).toBe(130);
expect(e.message).toBe('Public file upload is not enabled.');
}
});
it('disable file upload', async () => {
await reconfigureServer({
fileUpload: {
enabledForPublic: false,
enabledForAnonymousUser: false,
enabledForAuthenticatedUser: false,
},
});
try {
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save();
fail('should not have been able to save file.');
} catch (e) {
expect(e.code).toBe(130);
expect(e.message).toBe('Public file upload is not enabled.');
}
});
it('disable for public', async () => {
dblythy marked this conversation as resolved.
Show resolved Hide resolved
await reconfigureServer({
fileUpload: {
enabledForPublic: false,
},
});
try {
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save();
fail('should not have been able to save file.');
} catch (e) {
expect(e.code).toBe(130);
expect(e.message).toBe('Public file upload is not enabled.');
}
});

it('disable for public allow user', async () => {
await reconfigureServer({
fileUpload: {
enabledForPublic: false,
},
});
try {
const user = await Parse.User.signUp('myUser', 'password');
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save({ sessionToken: user.getSessionToken() });
} catch (e) {
fail('should have allowed file to save.');
}
});

it('disable for anonymous', async () => {
await reconfigureServer({
fileUpload: {
enabledForAnonymousUser: false,
},
});
try {
const user = await Parse.AnonymousUtils.logIn();
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save({ sessionToken: user.getSessionToken() });
fail('should not have been able to save file.');
} catch (e) {
expect(e.code).toBe(130);
expect(e.message).toBe('Anonymous file upload is not enabled.');
}
});

it('enable for anonymous', async () => {
await reconfigureServer({
fileUpload: {
enabledForPublic: false,
enabledForAnonymousUser: true,
},
});
try {
const user = await Parse.AnonymousUtils.logIn();
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save({ sessionToken: user.getSessionToken() });
} catch (e) {
fail('should have allowed file to save.');
}
});

it('enable for anonymous but not authenticated', async () => {
await reconfigureServer({
fileUpload: {
enabledForPublic: false,
enabledForAnonymousUser: true,
enabledForAuthenticatedUser: false,
},
});
try {
const user = await Parse.AnonymousUtils.logIn();
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save({ sessionToken: user.getSessionToken() });
} catch (e) {
fail('should have allowed file to save.');
}
try {
const user = await Parse.User.signUp('myUser', 'password');
const file = new Parse.File('hello.txt', data, 'text/plain');
await file.save({ sessionToken: user.getSessionToken() });
fail('should have not allowed file to save.');
} catch (e) {
expect(e.code).toBe(130);
expect(e.message).toBe('Authenticated file upload is not enabled.');
}
});
});
});
3 changes: 3 additions & 0 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ const defaultConfiguration = {
fileKey: 'test',
silent,
logLevel,
fileUpload: {
enabledForPublic: true,
},
push: {
android: {
senderId: 'yolo',
Expand Down
26 changes: 26 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ module.exports.ParseServerOptions = {
help: 'Adapter module for the files sub-system',
action: parsers.moduleOrObjectParser,
},
fileUpload: {
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
help: 'Options for file uploads',
action: parsers.objectParser,
},
graphQLPath: {
env: 'PARSE_SERVER_GRAPHQL_PATH',
help: 'Mount path for the GraphQL endpoint, defaults to /graphql',
Expand Down Expand Up @@ -600,3 +605,24 @@ module.exports.PasswordPolicyOptions = {
help: 'a RegExp object or a regex string representing the pattern to enforce',
},
};
module.exports.FileUploadOptions = {
enabledForAnonymousUser: {
env: 'PARSE_SERVER_FILE_UPLOAD_ENABLED_FOR_ANONYMOUS_USER',
help: 'File upload is enabled for Anonymous Users.',
action: parsers.booleanParser,
default: false,
},
enabledForAuthenticatedUser: {
env: 'PARSE_SERVER_FILE_UPLOAD_ENABLED_FOR_AUTHENTICATED_USER',
help: 'File upload is enabled for authenticated users.',
action: parsers.booleanParser,
default: true,
},
enabledForPublic: {
env: 'PARSE_SERVER_FILE_UPLOAD_ENABLED_FOR_PUBLIC',
help:
'File upload is enabled for anyone with access to the Parse Server file upload endpoint, regardless of user authentication.',
action: parsers.booleanParser,
default: false,
},
};
8 changes: 8 additions & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true
* @property {String} fileKey Key for your files
* @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
* @property {FileUploadOptions} fileUpload Options for file uploads
* @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql
* @property {String} graphQLSchema Full path to your GraphQL custom schema.graphql file
* @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0
Expand Down Expand Up @@ -137,3 +138,10 @@
* @property {Function} validatorCallback a callback function to be invoked to validate the password
* @property {String} validatorPattern a RegExp object or a regex string representing the pattern to enforce
*/

/**
* @interface FileUploadOptions
* @property {Boolean} enabledForAnonymousUser File upload is enabled for Anonymous Users.
dblythy marked this conversation as resolved.
Show resolved Hide resolved
* @property {Boolean} enabledForAuthenticatedUser File upload is enabled for authenticated users.
* @property {Boolean} enabledForPublic File upload is enabled for anyone with access to the Parse Server file upload endpoint, regardless of user authentication.
*/
15 changes: 15 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS
:DEFAULT: false */
idempotencyOptions: ?IdempotencyOptions;
/* Options for file uploads
:ENV: PARSE_SERVER_FILE_UPLOAD_OPTIONS */
fileUpload: ?FileUploadOptions;
/* Full path to your GraphQL custom schema.graphql file */
graphQLSchema: ?string;
/* Mounts the GraphQL endpoint
Expand Down Expand Up @@ -315,3 +318,15 @@ export interface PasswordPolicyOptions {
/* resend token if it's still valid */
resetTokenReuseIfValid: ?boolean;
}

export interface FileUploadOptions {
/* File upload is enabled for Anonymous Users.
:DEFAULT: false */
enabledForAnonymousUser: ?boolean;
/* File upload is enabled for anyone with access to the Parse Server file upload endpoint, regardless of user authentication.
:DEFAULT: false */
enabledForPublic: ?boolean;
/* File upload is enabled for authenticated users.
:DEFAULT: true */
enabledForAuthenticatedUser: ?boolean;
}
23 changes: 23 additions & 0 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ export class FilesRouter {

async createHandler(req, res, next) {
const config = req.config;
if (
dblythy marked this conversation as resolved.
Show resolved Hide resolved
!req.config.fileUpload.enabledForAnonymousUser &&
req.auth.user &&
Parse.AnonymousUtils.isLinked(req.auth.user)
) {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Anonymous file upload is not enabled.'));
dblythy marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (
!req.config.fileUpload.enabledForAuthenticatedUser &&
req.config.fileUpload.enabledForAuthenticatedUser != null &&
dblythy marked this conversation as resolved.
Show resolved Hide resolved
req.auth.user &&
!Parse.AnonymousUtils.isLinked(req.auth.user)
) {
next(
dblythy marked this conversation as resolved.
Show resolved Hide resolved
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Authenticated file upload is not enabled.')
);
return;
}
if (!req.config.fileUpload.enabledForPublic && !req.auth.user) {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Public file upload is not enabled.'));
return;
}
const filesController = config.filesController;
const { filename } = req.params;
const contentType = req.get('Content-type');
Expand Down