Skip to content

Commit 35ae973

Browse files
committed
disable by default
1 parent 0ca8e36 commit 35ae973

File tree

8 files changed

+217
-75
lines changed

8 files changed

+217
-75
lines changed

resources/buildConfigDefinitions.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ function getENVPrefix(iface) {
5555
if (iface.id.name === 'IdempotencyOptions') {
5656
return 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_';
5757
}
58+
if (iface.id.name === 'DashboardOptions') {
59+
return 'PARSE_SERVER_DASHBOARD_OPTIONS_';
60+
}
5861
}
5962

6063
function processProperty(property, iface) {
@@ -180,6 +183,13 @@ function parseDefaultValue(elt, value, t) {
180183
});
181184
literalValue = t.objectExpression(props);
182185
}
186+
if (type == 'DashboardOptions') {
187+
const object = parsers.objectParser(value);
188+
const props = Object.keys(object).map((key) => {
189+
return t.objectProperty(key, object[value]);
190+
});
191+
literalValue = t.objectExpression(props);
192+
}
183193
if (type == 'ProtectedFields') {
184194
const prop = t.objectProperty(
185195
t.stringLiteral('_User'), t.objectPattern([

spec/CloudCode.spec.js

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ describe('Cloud Code', () => {
5050
};
5151
it('can load cloud code file from dashboard', async done => {
5252
const cloudDir = './spec/cloud/cloudCodeAbsoluteFile.js';
53-
await reconfigureServer({ cloud: cloudDir });
53+
await reconfigureServer({
54+
cloud: cloudDir,
55+
dashboardOptions: {
56+
cloudFileView: true,
57+
},
58+
});
5459
const options = Object.assign({}, masterKeyOptions, {
5560
method: 'GET',
5661
url: Parse.serverURL + '/releases/latest',
@@ -75,7 +80,12 @@ describe('Cloud Code', () => {
7580

7681
it('can load multiple cloud code files from dashboard', async done => {
7782
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
78-
await reconfigureServer({ cloud: cloudDir });
83+
await reconfigureServer({
84+
cloud: cloudDir,
85+
dashboardOptions: {
86+
cloudFileView: true,
87+
},
88+
});
7989
const options = Object.assign({}, masterKeyOptions, {
8090
method: 'GET',
8191
url: Parse.serverURL + '/releases/latest',
@@ -95,9 +105,126 @@ describe('Cloud Code', () => {
95105
});
96106
});
97107

108+
it('can server info for for file options', async () => {
109+
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
110+
await reconfigureServer({
111+
cloud: cloudDir,
112+
});
113+
const options = Object.assign({}, masterKeyOptions, {
114+
method: 'GET',
115+
url: Parse.serverURL + '/serverInfo',
116+
});
117+
let { data } = await request(options);
118+
expect(data).not.toBe(null);
119+
expect(data.features).not.toBe(null);
120+
expect(data.features.cloudCode).not.toBe(null);
121+
expect(data.features.cloudCode.viewCode).toBe(false);
122+
expect(data.features.cloudCode.editCode).toBe(false);
123+
124+
await reconfigureServer({
125+
cloud: cloudDir,
126+
dashboardOptions: {
127+
cloudFileView: true,
128+
},
129+
});
130+
data = (await request(options)).data;
131+
expect(data).not.toBe(null);
132+
expect(data.features).not.toBe(null);
133+
expect(data.features.cloudCode).not.toBe(null);
134+
expect(data.features.cloudCode.viewCode).toBe(true);
135+
expect(data.features.cloudCode.editCode).toBe(false);
136+
await reconfigureServer({
137+
cloud: cloudDir,
138+
dashboardOptions: {
139+
cloudFileView: true,
140+
cloudFileEdit: true,
141+
},
142+
});
143+
data = (await request(options)).data;
144+
expect(data).not.toBe(null);
145+
expect(data.features).not.toBe(null);
146+
expect(data.features.cloudCode).not.toBe(null);
147+
expect(data.features.cloudCode.viewCode).toBe(true);
148+
expect(data.features.cloudCode.editCode).toBe(true);
149+
});
150+
151+
it('cannot view or edit cloud files by default', async () => {
152+
const options = Object.assign({}, masterKeyOptions, {
153+
method: 'GET',
154+
url: Parse.serverURL + '/releases/latest',
155+
});
156+
try {
157+
await request(options);
158+
fail('should not have been able to get cloud files');
159+
} catch (e) {
160+
expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}');
161+
}
162+
try {
163+
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
164+
await request(options);
165+
fail('should not have been able to get cloud files');
166+
} catch (e) {
167+
expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}');
168+
}
169+
try {
170+
options.method = 'POST';
171+
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
172+
options.body = {
173+
data: 'new file text',
174+
};
175+
await request(options);
176+
fail('should not have been able to get cloud files');
177+
} catch (e) {
178+
expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}');
179+
}
180+
});
181+
182+
it('can view cloud code file from dashboard', async () => {
183+
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
184+
await reconfigureServer({
185+
cloud: cloudDir,
186+
dashboardOptions: {
187+
cloudFileView: true,
188+
},
189+
});
190+
const options = Object.assign({}, masterKeyOptions, {
191+
method: 'GET',
192+
url: Parse.serverURL + '/releases/latest',
193+
});
194+
let res = await request(options);
195+
expect(Array.isArray(res.data)).toBe(true);
196+
const first = res.data[0];
197+
expect(first.userFiles).toBeDefined();
198+
expect(first.checksums).toBeDefined();
199+
expect(first.userFiles).toContain(cloudDir);
200+
expect(first.checksums).toContain(cloudDir);
201+
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
202+
res = await request(options);
203+
let response = res.data;
204+
expect(response).toContain(`require('./cloudCodeAbsoluteFile.js`);
205+
response = response + '\nconst additionalData;\n';
206+
options.method = 'POST';
207+
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
208+
options.body = {
209+
data: response,
210+
};
211+
try {
212+
await request(options);
213+
fail('should have failed to save');
214+
} catch (e) {
215+
expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}');
216+
}
217+
});
218+
98219
it('can edit cloud code file from dashboard', async done => {
99220
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
100-
await reconfigureServer({ cloud: cloudDir });
221+
await reconfigureServer({
222+
cloud: cloudDir,
223+
dashboardOptions: {
224+
cloudFileView: true,
225+
cloudFileEdit: true,
226+
},
227+
});
101228
const options = Object.assign({}, masterKeyOptions, {
102229
method: 'GET',
103230
url: Parse.serverURL + '/releases/latest',

spec/GridFSBucketStorageAdapter.spec.js

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
4444
await expectMissingFile(encryptedAdapter, 'myFileName');
4545
const originalString = 'abcdefghi';
4646
await encryptedAdapter.createFile('myFileName', originalString);
47-
const unencryptedResult = await unencryptedAdapter.getFileData(
48-
'myFileName'
49-
);
47+
const unencryptedResult = await unencryptedAdapter.getFileData('myFileName');
5048
expect(unencryptedResult.toString('utf8')).not.toBe(originalString);
5149
const encryptedResult = await encryptedAdapter.getFileData('myFileName');
5250
expect(encryptedResult.toString('utf8')).toBe(originalString);
@@ -71,10 +69,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
7169
const unencryptedResult2 = await unencryptedAdapter.getFileData(fileName2);
7270
expect(unencryptedResult2.toString('utf8')).toBe(data2);
7371
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
74-
const {
75-
rotated,
76-
notRotated,
77-
} = await encryptedAdapter.rotateEncryptionKey();
72+
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey();
7873
expect(rotated.length).toEqual(2);
7974
expect(
8075
rotated.filter(function (value) {
@@ -101,30 +96,18 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
10196

10297
it('should rotate key of all old encrypted GridFS files to encrypted files', async () => {
10398
const oldEncryptionKey = 'oldKeyThatILoved';
104-
const oldEncryptedAdapter = new GridFSBucketAdapter(
105-
databaseURI,
106-
{},
107-
oldEncryptionKey
108-
);
109-
const encryptedAdapter = new GridFSBucketAdapter(
110-
databaseURI,
111-
{},
112-
'newKeyThatILove'
113-
);
99+
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
100+
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
114101
const fileName1 = 'file1.txt';
115102
const data1 = 'hello world';
116103
const fileName2 = 'file2.txt';
117104
const data2 = 'hello new world';
118105
//Store unecrypted files
119106
await oldEncryptedAdapter.createFile(fileName1, data1);
120-
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(
121-
fileName1
122-
);
107+
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
123108
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
124109
await oldEncryptedAdapter.createFile(fileName2, data2);
125-
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(
126-
fileName2
127-
);
110+
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
128111
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
129112
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
130113
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({
@@ -170,32 +153,21 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
170153

171154
it('should rotate key of all old encrypted GridFS files to unencrypted files', async () => {
172155
const oldEncryptionKey = 'oldKeyThatILoved';
173-
const oldEncryptedAdapter = new GridFSBucketAdapter(
174-
databaseURI,
175-
{},
176-
oldEncryptionKey
177-
);
156+
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
178157
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
179158
const fileName1 = 'file1.txt';
180159
const data1 = 'hello world';
181160
const fileName2 = 'file2.txt';
182161
const data2 = 'hello new world';
183162
//Store unecrypted files
184163
await oldEncryptedAdapter.createFile(fileName1, data1);
185-
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(
186-
fileName1
187-
);
164+
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
188165
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
189166
await oldEncryptedAdapter.createFile(fileName2, data2);
190-
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(
191-
fileName2
192-
);
167+
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
193168
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
194169
//Check if unEncrypted adapter can read data and make sure it's not the same as oldEncrypted adapter
195-
const {
196-
rotated,
197-
notRotated,
198-
} = await unEncryptedAdapter.rotateEncryptionKey({
170+
const { rotated, notRotated } = await unEncryptedAdapter.rotateEncryptionKey({
199171
oldKey: oldEncryptionKey,
200172
});
201173
expect(rotated.length).toEqual(2);
@@ -238,31 +210,19 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
238210

239211
it('should only encrypt specified fileNames', async () => {
240212
const oldEncryptionKey = 'oldKeyThatILoved';
241-
const oldEncryptedAdapter = new GridFSBucketAdapter(
242-
databaseURI,
243-
{},
244-
oldEncryptionKey
245-
);
246-
const encryptedAdapter = new GridFSBucketAdapter(
247-
databaseURI,
248-
{},
249-
'newKeyThatILove'
250-
);
213+
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
214+
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
251215
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
252216
const fileName1 = 'file1.txt';
253217
const data1 = 'hello world';
254218
const fileName2 = 'file2.txt';
255219
const data2 = 'hello new world';
256220
//Store unecrypted files
257221
await oldEncryptedAdapter.createFile(fileName1, data1);
258-
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(
259-
fileName1
260-
);
222+
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
261223
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
262224
await oldEncryptedAdapter.createFile(fileName2, data2);
263-
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(
264-
fileName2
265-
);
225+
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
266226
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
267227
//Inject unecrypted file to see if causes an issue
268228
const fileName3 = 'file3.txt';
@@ -318,31 +278,19 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
318278

319279
it("should return fileNames of those it can't encrypt with the new key", async () => {
320280
const oldEncryptionKey = 'oldKeyThatILoved';
321-
const oldEncryptedAdapter = new GridFSBucketAdapter(
322-
databaseURI,
323-
{},
324-
oldEncryptionKey
325-
);
326-
const encryptedAdapter = new GridFSBucketAdapter(
327-
databaseURI,
328-
{},
329-
'newKeyThatILove'
330-
);
281+
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
282+
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
331283
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
332284
const fileName1 = 'file1.txt';
333285
const data1 = 'hello world';
334286
const fileName2 = 'file2.txt';
335287
const data2 = 'hello new world';
336288
//Store unecrypted files
337289
await oldEncryptedAdapter.createFile(fileName1, data1);
338-
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(
339-
fileName1
340-
);
290+
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
341291
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
342292
await oldEncryptedAdapter.createFile(fileName2, data2);
343-
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(
344-
fileName2
345-
);
293+
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
346294
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
347295
//Inject unecrypted file to see if causes an issue
348296
const fileName3 = 'file3.txt';

src/Options/Definitions.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ module.exports.ParseServerOptions = {
9393
action: parsers.objectParser,
9494
default: {},
9595
},
96+
dashboardOptions: {
97+
env: 'PARSE_SERVER_DASHBOARD_OPTIONS',
98+
help:
99+
'Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.',
100+
action: parsers.objectParser,
101+
default: {},
102+
},
96103
databaseAdapter: {
97104
env: 'PARSE_SERVER_DATABASE_ADAPTER',
98105
help: 'Adapter module for the database',
@@ -545,3 +552,18 @@ module.exports.IdempotencyOptions = {
545552
default: 300,
546553
},
547554
};
555+
module.exports.DashboardOptions = {
556+
cloudFileEdit: {
557+
env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_EDIT',
558+
help:
559+
'Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent.',
560+
action: parsers.booleanParser,
561+
default: false,
562+
},
563+
cloudFileView: {
564+
env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_VIEW',
565+
help: 'Whether the Parse Dashboard can view cloud files.',
566+
action: parsers.booleanParser,
567+
default: false,
568+
},
569+
};

src/Options/docs.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length
1818
* @property {String} collectionPrefix A collection prefix for the classes
1919
* @property {CustomPagesOptions} customPages custom pages for password validation and reset
20+
* @property {DashboardOptions} dashboardOptions Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.
2021
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database
2122
* @property {Any} databaseOptions Options to pass to the mongodb client
2223
* @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres.
@@ -118,3 +119,9 @@
118119
* @property {String[]} paths An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.
119120
* @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s.
120121
*/
122+
123+
/**
124+
* @interface DashboardOptions
125+
* @property {Boolean} cloudFileEdit Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent.
126+
* @property {Boolean} cloudFileView Whether the Parse Dashboard can view cloud files.
127+
*/

0 commit comments

Comments
 (0)