Skip to content

Commit 129f7bf

Browse files
GormanFletchergormanfletchermtrezza
authored
Add support for master key clients to create user sessions (#7406)
* 6641: Implement support for user impersonation: master key clients can log in as any user, without access to the user's credentials, and without presuming the user already has a session * reworded changelog * rebuilt package lock * fit test * using lodash flatMap * bump to node 12 for postgres test * revert test fit * add node version to postgres CI * revert package-lock Co-authored-by: gormanfletcher <[email protected]> Co-authored-by: Manuel <[email protected]>
1 parent 754c127 commit 129f7bf

File tree

6 files changed

+249
-43
lines changed

6 files changed

+249
-43
lines changed

.github/workflows/ci.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,16 @@ jobs:
170170
include:
171171
- name: PostgreSQL 11, PostGIS 3.0
172172
POSTGRES_IMAGE: postgis/postgis:11-3.0
173+
NODE_VERSION: 14.17.0
173174
- name: PostgreSQL 11, PostGIS 3.1
174175
POSTGRES_IMAGE: postgis/postgis:11-3.1
176+
NODE_VERSION: 14.17.0
175177
- name: PostgreSQL 12, PostGIS 3.1
176178
POSTGRES_IMAGE: postgis/postgis:12-3.1
179+
NODE_VERSION: 14.17.0
177180
- name: PostgreSQL 13, PostGIS 3.1
178181
POSTGRES_IMAGE: postgis/postgis:13-3.1
182+
NODE_VERSION: 14.17.0
179183
fail-fast: false
180184
name: ${{ matrix.name }}
181185
timeout-minutes: 15
@@ -199,12 +203,13 @@ jobs:
199203
env:
200204
PARSE_SERVER_TEST_DB: postgres
201205
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
206+
NODE_VERSION: ${{ matrix.NODE_VERSION }}
202207
steps:
203208
- uses: actions/checkout@v2
204-
- name: Use Node.js 10
209+
- name: Use Node.js ${{ matrix.NODE_VERSION }}
205210
uses: actions/setup-node@v1
206211
with:
207-
node-version: 10
212+
node-version: ${{ matrix.NODE_VERSION }}
208213
- name: Cache Node.js modules
209214
uses: actions/cache@v2
210215
with:

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ ___
101101
- EXPERIMENTAL: Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification (Manuel Trezza) [#7128](https://github.com/parse-community/parse-server/pull/7128)
102102
- EXPERIMENTAL: Added custom routes to easily customize flows for password reset, email verification or build entirely new flows (Manuel Trezza) [#7231](https://github.com/parse-community/parse-server/pull/7231)
103103
- Added Deprecation Policy to govern the introduction of braking changes in a phased pattern that is more predictable for developers (Manuel Trezza) [#7199](https://github.com/parse-community/parse-server/pull/7199)
104+
- Add REST API endpoint `/loginAs` to create session of any user with master key; allows to impersonate another user. (GormanFletcher) [#7406](https://github.com/parse-community/parse-server/pull/7406)
105+
104106
### Other Changes
105107
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
106108
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@
9191
"jsdoc-babel": "0.5.0",
9292
"lint-staged": "10.2.3",
9393
"madge": "4.0.2",
94-
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
9594
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
95+
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
9696
"mongodb-runner": "4.8.1",
9797
"mongodb-version-list": "1.0.0",
9898
"node-fetch": "2.6.1",

spec/ParseUser.spec.js

+128
Original file line numberDiff line numberDiff line change
@@ -4032,3 +4032,131 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () {
40324032
expect(user.get('authData')).toEqual({ custom: { id: 'linkedID' } });
40334033
});
40344034
});
4035+
4036+
describe('login as other user', () => {
4037+
it('allows creating a session for another user with the master key', async done => {
4038+
await Parse.User.signUp('some_user', 'some_password');
4039+
const userId = Parse.User.current().id;
4040+
await Parse.User.logOut();
4041+
4042+
try {
4043+
const response = await request({
4044+
method: 'POST',
4045+
url: 'http://localhost:8378/1/loginAs',
4046+
headers: {
4047+
'X-Parse-Application-Id': Parse.applicationId,
4048+
'X-Parse-REST-API-Key': 'rest',
4049+
'X-Parse-Master-Key': 'test',
4050+
},
4051+
body: {
4052+
userId,
4053+
},
4054+
});
4055+
4056+
expect(response.data.sessionToken).toBeDefined();
4057+
} catch (err) {
4058+
fail(`no request should fail: ${JSON.stringify(err)}`);
4059+
done();
4060+
}
4061+
4062+
const sessionsQuery = new Parse.Query(Parse.Session);
4063+
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
4064+
expect(sessionsAfterRequest.length).toBe(1);
4065+
4066+
done();
4067+
});
4068+
4069+
it('rejects creating a session for another user if the user does not exist', async done => {
4070+
try {
4071+
await request({
4072+
method: 'POST',
4073+
url: 'http://localhost:8378/1/loginAs',
4074+
headers: {
4075+
'X-Parse-Application-Id': Parse.applicationId,
4076+
'X-Parse-REST-API-Key': 'rest',
4077+
'X-Parse-Master-Key': 'test',
4078+
},
4079+
body: {
4080+
userId: 'bogus-user',
4081+
},
4082+
});
4083+
4084+
fail('Request should fail without a valid user ID');
4085+
done();
4086+
} catch (err) {
4087+
expect(err.data.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
4088+
expect(err.data.error).toBe('user not found');
4089+
}
4090+
4091+
const sessionsQuery = new Parse.Query(Parse.Session);
4092+
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
4093+
expect(sessionsAfterRequest.length).toBe(0);
4094+
4095+
done();
4096+
});
4097+
4098+
it('rejects creating a session for another user with invalid parameters', async done => {
4099+
const invalidUserIds = [undefined, null, ''];
4100+
4101+
for (const invalidUserId of invalidUserIds) {
4102+
try {
4103+
await request({
4104+
method: 'POST',
4105+
url: 'http://localhost:8378/1/loginAs',
4106+
headers: {
4107+
'X-Parse-Application-Id': Parse.applicationId,
4108+
'X-Parse-REST-API-Key': 'rest',
4109+
'X-Parse-Master-Key': 'test',
4110+
},
4111+
body: {
4112+
userId: invalidUserId,
4113+
},
4114+
});
4115+
4116+
fail('Request should fail without a valid user ID');
4117+
done();
4118+
} catch (err) {
4119+
expect(err.data.code).toBe(Parse.Error.INVALID_VALUE);
4120+
expect(err.data.error).toBe('userId must not be empty, null, or undefined');
4121+
}
4122+
4123+
const sessionsQuery = new Parse.Query(Parse.Session);
4124+
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
4125+
expect(sessionsAfterRequest.length).toBe(0);
4126+
}
4127+
4128+
done();
4129+
});
4130+
4131+
it('rejects creating a session for another user without the master key', async done => {
4132+
await Parse.User.signUp('some_user', 'some_password');
4133+
const userId = Parse.User.current().id;
4134+
await Parse.User.logOut();
4135+
4136+
try {
4137+
await request({
4138+
method: 'POST',
4139+
url: 'http://localhost:8378/1/loginAs',
4140+
headers: {
4141+
'X-Parse-Application-Id': Parse.applicationId,
4142+
'X-Parse-REST-API-Key': 'rest',
4143+
},
4144+
body: {
4145+
userId,
4146+
},
4147+
});
4148+
4149+
fail('Request should fail without the master key');
4150+
done();
4151+
} catch (err) {
4152+
expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
4153+
expect(err.data.error).toBe('master key is required');
4154+
}
4155+
4156+
const sessionsQuery = new Parse.Query(Parse.Session);
4157+
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
4158+
expect(sessionsAfterRequest.length).toBe(0);
4159+
4160+
done();
4161+
});
4162+
});

spec/SecurityCheck.spec.js

+34-26
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ describe('Security Check', () => {
2323
await reconfigureServer(config);
2424
}
2525

26-
const securityRequest = (options) => request(Object.assign({
27-
url: securityUrl,
28-
headers: {
29-
'X-Parse-Master-Key': Parse.masterKey,
30-
'X-Parse-Application-Id': Parse.applicationId,
31-
},
32-
followRedirects: false,
33-
}, options)).catch(e => e);
26+
const securityRequest = options =>
27+
request(
28+
Object.assign(
29+
{
30+
url: securityUrl,
31+
headers: {
32+
'X-Parse-Master-Key': Parse.masterKey,
33+
'X-Parse-Application-Id': Parse.applicationId,
34+
},
35+
followRedirects: false,
36+
},
37+
options
38+
)
39+
).catch(e => e);
3440

3541
beforeEach(async () => {
3642
groupName = 'Example Group Name';
@@ -41,7 +47,7 @@ describe('Security Check', () => {
4147
solution: 'TestSolution',
4248
check: () => {
4349
return true;
44-
}
50+
},
4551
});
4652
checkFail = new Check({
4753
group: 'TestGroup',
@@ -50,14 +56,14 @@ describe('Security Check', () => {
5056
solution: 'TestSolution',
5157
check: () => {
5258
throw 'Fail';
53-
}
59+
},
5460
});
5561
Group = class Group extends CheckGroup {
5662
setName() {
5763
return groupName;
5864
}
5965
setChecks() {
60-
return [ checkSuccess, checkFail ];
66+
return [checkSuccess, checkFail];
6167
}
6268
};
6369
config = {
@@ -154,7 +160,7 @@ describe('Security Check', () => {
154160
title: 'string',
155161
warning: 'string',
156162
solution: 'string',
157-
check: () => {}
163+
check: () => {},
158164
},
159165
{
160166
group: 'string',
@@ -203,7 +209,9 @@ describe('Security Check', () => {
203209
title: 'string',
204210
warning: 'string',
205211
solution: 'string',
206-
check: () => { throw 'error' },
212+
check: () => {
213+
throw 'error';
214+
},
207215
});
208216
expect(check._checkState == CheckState.none);
209217
check.run();
@@ -277,7 +285,7 @@ describe('Security Check', () => {
277285
});
278286

279287
it('runs all checks of all groups', async () => {
280-
const checkGroups = [ Group, Group ];
288+
const checkGroups = [Group, Group];
281289
const runner = new CheckRunner({ checkGroups });
282290
const report = await runner.run();
283291
expect(report.report.groups[0].checks[0].state).toBe(CheckState.success);
@@ -287,27 +295,27 @@ describe('Security Check', () => {
287295
});
288296

289297
it('reports correct default syntax version 1.0.0', async () => {
290-
const checkGroups = [ Group ];
298+
const checkGroups = [Group];
291299
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
292300
const report = await runner.run();
293301
expect(report).toEqual({
294302
report: {
295-
version: "1.0.0",
296-
state: "fail",
303+
version: '1.0.0',
304+
state: 'fail',
297305
groups: [
298306
{
299-
name: "Example Group Name",
300-
state: "fail",
307+
name: 'Example Group Name',
308+
state: 'fail',
301309
checks: [
302310
{
303-
title: "TestTitleSuccess",
304-
state: "success",
311+
title: 'TestTitleSuccess',
312+
state: 'success',
305313
},
306314
{
307-
title: "TestTitleFail",
308-
state: "fail",
309-
warning: "TestWarning",
310-
solution: "TestSolution",
315+
title: 'TestTitleFail',
316+
state: 'fail',
317+
warning: 'TestWarning',
318+
solution: 'TestSolution',
311319
},
312320
],
313321
},
@@ -319,7 +327,7 @@ describe('Security Check', () => {
319327
it('logs report', async () => {
320328
const logger = require('../lib/logger').logger;
321329
const logSpy = spyOn(logger, 'warn').and.callThrough();
322-
const checkGroups = [ Group ];
330+
const checkGroups = [Group];
323331
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
324332
const report = await runner.run();
325333
const titles = report.report.groups.flatMap(group => group.checks.map(check => check.title));

0 commit comments

Comments
 (0)