Skip to content

Commit b688cc8

Browse files
authored
Merge 5385a48 into c3da290
2 parents c3da290 + 5385a48 commit b688cc8

9 files changed

+381
-22
lines changed

package-lock.json

Lines changed: 16 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"mime": "2.5.2",
4848
"mongodb": "3.6.11",
4949
"mustache": "4.2.0",
50+
"otpauth": "7.0.6",
5051
"parse": "3.3.0",
5152
"pg-monitor": "1.4.1",
5253
"pg-promise": "10.11.0",

spec/DashboardRouter.spec.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const request = require('../lib/request');
2+
const TOTP = require('otpauth').TOTP;
3+
4+
describe('Dashboard', () => {
5+
const signup = (master, mfa) =>
6+
request({
7+
url: `${Parse.serverURL}/dashboardSignup`,
8+
headers: {
9+
'X-Parse-Application-Id': Parse.applicationId,
10+
'X-Parse-Master-Key': master ? Parse.masterKey : null,
11+
'Content-Type': 'application/json',
12+
},
13+
method: 'POST',
14+
body: {
15+
username: '[email protected]',
16+
password: 'password',
17+
mfa,
18+
mfaOptions: {
19+
algorithm: 'SHA1',
20+
period: 30,
21+
digits: 6,
22+
},
23+
features: {
24+
globalConfig: {
25+
create: true,
26+
read: true,
27+
update: true,
28+
delete: true,
29+
},
30+
hooks: {
31+
create: true,
32+
read: true,
33+
update: true,
34+
delete: true,
35+
},
36+
cloudCode: {
37+
jobs: true,
38+
},
39+
logs: {
40+
level: true,
41+
size: true,
42+
order: true,
43+
until: true,
44+
from: true,
45+
},
46+
push: {
47+
immediatePush: true,
48+
pushAudiences: true,
49+
localization: true,
50+
},
51+
schemas: {
52+
addField: true,
53+
removeField: true,
54+
addClass: true,
55+
removeClass: true,
56+
clearAllDataFromClass: true,
57+
exportClass: false,
58+
editClassLevelPermissions: true,
59+
editPointerPermissions: true,
60+
},
61+
},
62+
},
63+
followRedirects: false,
64+
}).catch(e => e);
65+
66+
const login = (password, otp) =>
67+
request({
68+
url: `${Parse.serverURL}/dashboardLogin`,
69+
headers: {
70+
'X-Parse-Application-Id': Parse.applicationId,
71+
'X-Parse-Master-Key': Parse.masterKey,
72+
'Content-Type': 'application/json',
73+
},
74+
method: 'POST',
75+
body: {
76+
username: '[email protected]',
77+
password,
78+
otp,
79+
},
80+
followRedirects: false,
81+
}).catch(e => e);
82+
it('creating a user responds with 403 without masterKey', async () => {
83+
const response = await signup();
84+
expect(response.status).toBe(403);
85+
});
86+
87+
it('creating a user responds with masterKey', async () => {
88+
const response = await signup(true);
89+
expect(response.status).toBe(201);
90+
expect(response.text).toContain('[email protected]');
91+
});
92+
93+
it('cannot query dashboard user class', async () => {
94+
const response = await signup(true);
95+
expect(response.status).toBe(201);
96+
expect(response.text).toContain('[email protected]');
97+
await expectAsync(new Parse.Query('_DashboardUser').first()).toBeRejectedWith(
98+
new Parse.Error(
99+
Parse.Error.OPERATION_FORBIDDEN,
100+
"Clients aren't allowed to perform the find operation on the _DashboardUser collection."
101+
)
102+
);
103+
});
104+
105+
it('can query dashboard user class with masterKey', async () => {
106+
const response = await signup(true);
107+
expect(response.status).toBe(201);
108+
expect(response.text).toContain('[email protected]');
109+
const [user] = await new Parse.Query('_DashboardUser').find({ useMasterKey: true });
110+
expect(user).toBeDefined();
111+
expect(user.get('password')).toBeUndefined();
112+
expect(user.get('mfaOptions')).toBeUndefined();
113+
});
114+
115+
it('dashboard can signup and then login', async () => {
116+
const response = await signup(true, true);
117+
expect(response.status).toBe(201);
118+
expect(response.text).toContain('[email protected]');
119+
const { mfaSecret } = JSON.parse(response.text);
120+
const totp = new TOTP({
121+
algorithm: 'SHA1',
122+
digits: 6,
123+
period: 30,
124+
secret: mfaSecret,
125+
});
126+
const loginResponse = await login('password', totp.generate());
127+
expect(loginResponse.status).toEqual(200);
128+
const user = JSON.parse(loginResponse.text);
129+
expect(user.username).toEqual('[email protected]');
130+
expect(user.features).toBeDefined();
131+
expect(user.features.globalConfig).toBeDefined();
132+
expect(user.features.hooks).toBeDefined();
133+
expect(user.features.cloudCode).toBeDefined();
134+
expect(user.features.logs).toBeDefined();
135+
expect(user.features.push).toBeDefined();
136+
expect(user.features.schemas).toBeDefined();
137+
});
138+
139+
it('dashboard can signup and then login without mfa', async () => {
140+
const response = await signup(true, false);
141+
expect(response.status).toBe(201);
142+
expect(response.text).toContain('[email protected]');
143+
const loginResponse = await login('password');
144+
expect(loginResponse.status).toEqual(200);
145+
const user = JSON.parse(loginResponse.text);
146+
expect(user.username).toEqual('[email protected]');
147+
expect(user.features).toBeDefined();
148+
expect(user.features.globalConfig).toBeDefined();
149+
expect(user.features.hooks).toBeDefined();
150+
expect(user.features.cloudCode).toBeDefined();
151+
expect(user.features.logs).toBeDefined();
152+
expect(user.features.push).toBeDefined();
153+
expect(user.features.schemas).toBeDefined();
154+
});
155+
156+
it('dashboard can signup and rejects login with invalid password', async () => {
157+
const response = await signup(true, false);
158+
expect(response.status).toBe(201);
159+
expect(response.text).toContain('[email protected]');
160+
const loginResponse = await login('password2');
161+
expect(loginResponse.status).toEqual(404);
162+
expect(loginResponse.text).toEqual(`{"code":101,"error":"Invalid username/password."}`);
163+
});
164+
165+
it('dashboard can signup and rejects login with invalid mfa', async () => {
166+
const response = await signup(true, true);
167+
expect(response.status).toBe(201);
168+
expect(response.text).toContain('[email protected]');
169+
const loginResponse = await login('password');
170+
expect(loginResponse.status).toEqual(400);
171+
expect(loginResponse.text).toEqual(
172+
`{"code":211,"error":"Please specify a One Time password."}`
173+
);
174+
const invalidMFAResponse = await login('password', 123456);
175+
expect(invalidMFAResponse.status).toEqual(400);
176+
expect(invalidMFAResponse.text).toEqual(`{"code":210,"error":"Invalid One Time Password."}`);
177+
});
178+
});

src/Controllers/DatabaseController.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,13 @@ class DatabaseController {
17041704
throw error;
17051705
});
17061706

1707+
await this.adapter
1708+
.ensureUniqueness('_DashboardUser', requiredUserFields, ['username'])
1709+
.catch(error => {
1710+
logger.warn('Unable to ensure uniqueness for usernames: ', error);
1711+
throw error;
1712+
});
1713+
17071714
await this.adapter
17081715
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
17091716
.catch(error => {

src/Controllers/SchemaController.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({
148148
reqId: { type: 'String' },
149149
expire: { type: 'Date' },
150150
},
151+
_DashboardUser: {
152+
objectId: { type: 'String' },
153+
username: { type: 'String' },
154+
password: { type: 'String' },
155+
mfaOptions: { type: 'Object' },
156+
features: { type: 'Object' },
157+
},
151158
});
152159

153160
const requiredColumns = Object.freeze({
@@ -168,6 +175,7 @@ const systemClasses = Object.freeze([
168175
'_JobSchedule',
169176
'_Audience',
170177
'_Idempotency',
178+
'_DashboardUser',
171179
]);
172180

173181
const volatileClasses = Object.freeze([
@@ -179,6 +187,7 @@ const volatileClasses = Object.freeze([
179187
'_JobSchedule',
180188
'_Audience',
181189
'_Idempotency',
190+
'_DashboardUser',
182191
]);
183192

184193
// Anything that start with role
@@ -648,6 +657,15 @@ const _IdempotencySchema = convertSchemaToAdapterSchema(
648657
classLevelPermissions: {},
649658
})
650659
);
660+
661+
const _DashboardUser = convertSchemaToAdapterSchema(
662+
injectDefaultSchema({
663+
className: '_DashboardUser',
664+
fields: defaultColumns._DashboardUser,
665+
classLevelPermissions: {},
666+
})
667+
);
668+
651669
const VolatileClassesSchemas = [
652670
_HooksSchema,
653671
_JobStatusSchema,
@@ -657,6 +675,7 @@ const VolatileClassesSchemas = [
657675
_GraphQLConfigSchema,
658676
_AudienceSchema,
659677
_IdempotencySchema,
678+
_DashboardUser,
660679
];
661680

662681
const dbTypeMatchesObjectType = (dbType: SchemaField | string, objectType: SchemaField) => {

src/ParseServer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PromiseRouter from './PromiseRouter';
1717
import requiredParameter from './requiredParameter';
1818
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
1919
import { ClassesRouter } from './Routers/ClassesRouter';
20+
import { DashboardRouter } from './Routers/DashboardRouter';
2021
import { FeaturesRouter } from './Routers/FeaturesRouter';
2122
import { FilesRouter } from './Routers/FilesRouter';
2223
import { FunctionsRouter } from './Routers/FunctionsRouter';
@@ -235,6 +236,7 @@ class ParseServer {
235236
new AudiencesRouter(),
236237
new AggregateRouter(),
237238
new SecurityRouter(),
239+
new DashboardRouter(),
238240
];
239241

240242
const routes = routers.reduce((memo, router) => {

src/RestQuery.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,12 @@ RestQuery.prototype.runFind = function (options = {}) {
662662
cleanResultAuthData(result);
663663
}
664664
}
665+
if (this.className === '_DashboardUser') {
666+
for (const result of results) {
667+
delete result.password;
668+
delete result.mfaOptions;
669+
}
670+
}
665671

666672
this.config.filesController.expandFilesInObject(this.config, results);
667673

0 commit comments

Comments
 (0)