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

Add support for master key clients to create user sessions #7406

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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