Skip to content

Commit

Permalink
Add support for master key clients to create user sessions (#7406)
Browse files Browse the repository at this point in the history
* 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 <git@gormanfletcher.com>
Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 4, 2021
1 parent 754c127 commit 129f7bf
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 43 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,16 @@ jobs:
include:
- name: PostgreSQL 11, PostGIS 3.0
POSTGRES_IMAGE: postgis/postgis:11-3.0
NODE_VERSION: 14.17.0
- name: PostgreSQL 11, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:11-3.1
NODE_VERSION: 14.17.0
- name: PostgreSQL 12, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:12-3.1
NODE_VERSION: 14.17.0
- name: PostgreSQL 13, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:13-3.1
NODE_VERSION: 14.17.0
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
Expand All @@ -199,12 +203,13 @@ jobs:
env:
PARSE_SERVER_TEST_DB: postgres
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
NODE_VERSION: ${{ matrix.NODE_VERSION }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 10
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
with:
node-version: 10
node-version: ${{ matrix.NODE_VERSION }}
- name: Cache Node.js modules
uses: actions/cache@v2
with:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ ___
- 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)
- 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)
- 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)
- 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)

### Other Changes
- 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)
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@
"jsdoc-babel": "0.5.0",
"lint-staged": "10.2.3",
"madge": "4.0.2",
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "4.8.1",
"mongodb-version-list": "1.0.0",
"node-fetch": "2.6.1",
Expand Down
128 changes: 128 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4032,3 +4032,131 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () {
expect(user.get('authData')).toEqual({ custom: { id: 'linkedID' } });
});
});

describe('login as other user', () => {
it('allows creating a session for another user with the master key', async done => {
await Parse.User.signUp('some_user', 'some_password');
const userId = Parse.User.current().id;
await Parse.User.logOut();

try {
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/loginAs',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
body: {
userId,
},
});

expect(response.data.sessionToken).toBeDefined();
} catch (err) {
fail(`no request should fail: ${JSON.stringify(err)}`);
done();
}

const sessionsQuery = new Parse.Query(Parse.Session);
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
expect(sessionsAfterRequest.length).toBe(1);

done();
});

it('rejects creating a session for another user if the user does not exist', async done => {
try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/loginAs',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
body: {
userId: 'bogus-user',
},
});

fail('Request should fail without a valid user ID');
done();
} catch (err) {
expect(err.data.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(err.data.error).toBe('user not found');
}

const sessionsQuery = new Parse.Query(Parse.Session);
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
expect(sessionsAfterRequest.length).toBe(0);

done();
});

it('rejects creating a session for another user with invalid parameters', async done => {
const invalidUserIds = [undefined, null, ''];

for (const invalidUserId of invalidUserIds) {
try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/loginAs',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
body: {
userId: invalidUserId,
},
});

fail('Request should fail without a valid user ID');
done();
} catch (err) {
expect(err.data.code).toBe(Parse.Error.INVALID_VALUE);
expect(err.data.error).toBe('userId must not be empty, null, or undefined');
}

const sessionsQuery = new Parse.Query(Parse.Session);
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
expect(sessionsAfterRequest.length).toBe(0);
}

done();
});

it('rejects creating a session for another user without the master key', async done => {
await Parse.User.signUp('some_user', 'some_password');
const userId = Parse.User.current().id;
await Parse.User.logOut();

try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/loginAs',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
body: {
userId,
},
});

fail('Request should fail without the master key');
done();
} catch (err) {
expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(err.data.error).toBe('master key is required');
}

const sessionsQuery = new Parse.Query(Parse.Session);
const sessionsAfterRequest = await sessionsQuery.find({ useMasterKey: true });
expect(sessionsAfterRequest.length).toBe(0);

done();
});
});
60 changes: 34 additions & 26 deletions spec/SecurityCheck.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ describe('Security Check', () => {
await reconfigureServer(config);
}

const securityRequest = (options) => request(Object.assign({
url: securityUrl,
headers: {
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Application-Id': Parse.applicationId,
},
followRedirects: false,
}, options)).catch(e => e);
const securityRequest = options =>
request(
Object.assign(
{
url: securityUrl,
headers: {
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Application-Id': Parse.applicationId,
},
followRedirects: false,
},
options
)
).catch(e => e);

beforeEach(async () => {
groupName = 'Example Group Name';
Expand All @@ -41,7 +47,7 @@ describe('Security Check', () => {
solution: 'TestSolution',
check: () => {
return true;
}
},
});
checkFail = new Check({
group: 'TestGroup',
Expand All @@ -50,14 +56,14 @@ describe('Security Check', () => {
solution: 'TestSolution',
check: () => {
throw 'Fail';
}
},
});
Group = class Group extends CheckGroup {
setName() {
return groupName;
}
setChecks() {
return [ checkSuccess, checkFail ];
return [checkSuccess, checkFail];
}
};
config = {
Expand Down Expand Up @@ -154,7 +160,7 @@ describe('Security Check', () => {
title: 'string',
warning: 'string',
solution: 'string',
check: () => {}
check: () => {},
},
{
group: 'string',
Expand Down Expand Up @@ -203,7 +209,9 @@ describe('Security Check', () => {
title: 'string',
warning: 'string',
solution: 'string',
check: () => { throw 'error' },
check: () => {
throw 'error';
},
});
expect(check._checkState == CheckState.none);
check.run();
Expand Down Expand Up @@ -277,7 +285,7 @@ describe('Security Check', () => {
});

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

it('reports correct default syntax version 1.0.0', async () => {
const checkGroups = [ Group ];
const checkGroups = [Group];
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
const report = await runner.run();
expect(report).toEqual({
report: {
version: "1.0.0",
state: "fail",
version: '1.0.0',
state: 'fail',
groups: [
{
name: "Example Group Name",
state: "fail",
name: 'Example Group Name',
state: 'fail',
checks: [
{
title: "TestTitleSuccess",
state: "success",
title: 'TestTitleSuccess',
state: 'success',
},
{
title: "TestTitleFail",
state: "fail",
warning: "TestWarning",
solution: "TestSolution",
title: 'TestTitleFail',
state: 'fail',
warning: 'TestWarning',
solution: 'TestSolution',
},
],
},
Expand All @@ -319,7 +327,7 @@ describe('Security Check', () => {
it('logs report', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn').and.callThrough();
const checkGroups = [ Group ];
const checkGroups = [Group];
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
const report = await runner.run();
const titles = report.report.groups.flatMap(group => group.checks.map(check => check.title));
Expand Down
Loading

0 comments on commit 129f7bf

Please sign in to comment.