Skip to content

Commit

Permalink
feat: Access the internal scope of Parse Server using the new `mainte…
Browse files Browse the repository at this point in the history
…nanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) (#8212)

BREAKING CHANGE: Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212)
  • Loading branch information
dblythy authored Jan 8, 2023
1 parent 3d57072 commit f3bcc93
Show file tree
Hide file tree
Showing 23 changed files with 371 additions and 102 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
- [Configuration](#configuration)
- [Basic Options](#basic-options)
- [Client Key Options](#client-key-options)
- [Access Scopes](#access-scopes)
- [Email Verification and Password Reset](#email-verification-and-password-reset)
- [Password and Account Policy](#password-and-account-policy)
- [Custom Routes](#custom-routes)
Expand Down Expand Up @@ -357,6 +358,15 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `restAPIKey`
* `dotNetKey`

## Access Scopes

| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key |
|----------------|---------------|-------------|------------------------|---------------------|
| Internal | r/w | r/w | no | `maintenanceKey` |
| Master | -/- | r/w | no | `masterKey` |
| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` |
| Session | -/- | r/w | yes | `sessionToken` |

## Email Verification and Password Reset

Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
Expand Down
22 changes: 17 additions & 5 deletions spec/EmailVerificationToken.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const Auth = require('../lib/Auth');
const Config = require('../lib/Config');
const request = require('../lib/request');

Expand Down Expand Up @@ -262,9 +263,14 @@ describe('Email Verification Token Expiration: ', () => {
})
.then(() => {
const config = Config.get('test');
return config.database.find('_User', {
username: 'sets_email_verify_token_expires_at',
});
return config.database.find(
'_User',
{
username: 'sets_email_verify_token_expires_at',
},
{},
Auth.maintenance(config)
);
})
.then(results => {
expect(results.length).toBe(1);
Expand Down Expand Up @@ -499,7 +505,12 @@ describe('Email Verification Token Expiration: ', () => {
.then(() => {
const config = Config.get('test');
return config.database
.find('_User', { username: 'newEmailVerifyTokenOnEmailReset' })
.find(
'_User',
{ username: 'newEmailVerifyTokenOnEmailReset' },
{},
Auth.maintenance(config)
)
.then(results => {
return results[0];
});
Expand Down Expand Up @@ -582,7 +593,7 @@ describe('Email Verification Token Expiration: ', () => {
// query for this user again
const config = Config.get('test');
return config.database
.find('_User', { username: 'resends_verification_token' })
.find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config))
.then(results => {
return results[0];
});
Expand All @@ -599,6 +610,7 @@ describe('Email Verification Token Expiration: ', () => {
done();
})
.catch(error => {
console.log(error);
jfail(error);
done();
});
Expand Down
16 changes: 16 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ describe('middlewares', () => {
expect(fakeReq.auth.isMaster).toBe(false);
});

it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
AppCache.put(fakeReq.body._ApplicationId, {
maintenanceKey: 'masterKey',
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
});
fakeReq.ip = '10.0.0.2';
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
expect(fakeReq.auth.isMaintenance).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
`Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.`
);
});

it('should succeed if the ip does belong to masterKeyIps list', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
Expand Down
13 changes: 10 additions & 3 deletions spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const Auth = require('../lib/Auth');
const UserController = require('../lib/Controllers/UserController').UserController;
const Config = require('../lib/Config');
const validatorFail = () => {
Expand Down Expand Up @@ -977,6 +978,7 @@ describe('ParseLiveQuery', function () {
};

await reconfigureServer({
maintenanceKey: 'test2',
liveQuery: {
classNames: [Parse.User],
},
Expand All @@ -998,9 +1000,14 @@ describe('ParseLiveQuery', function () {
.signUp()
.then(() => {
const config = Config.get('test');
return config.database.find('_User', {
username: 'zxcv',
});
return config.database.find(
'_User',
{
username: 'zxcv',
},
{},
Auth.maintenance(config)
);
})
.then(async results => {
const foundUser = results[0];
Expand Down
124 changes: 106 additions & 18 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3522,40 +3522,128 @@ describe('Parse.User testing', () => {
});
});

it('should not allow updates to hidden fields', done => {
it('should not allow updates to hidden fields', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve(),
};

const user = new Parse.User();
user.set({
username: 'hello',
password: 'world',
email: '[email protected]',
});
await reconfigureServer({
appName: 'unused',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});
await user.signUp();
user.set('_email_verify_token', 'bad', { ignoreValidation: true });
await expectAsync(user.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: _email_verify_token.')
);
});

reconfigureServer({
it('should allow updates to fields with maintenanceKey', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve(),
};
const user = new Parse.User();
user.set({
username: 'hello',
password: 'world',
email: '[email protected]',
});
await reconfigureServer({
appName: 'unused',
maintenanceKey: 'test2',
verifyUserEmails: true,
emailVerifyTokenValidityDuration: 5,
accountLockout: {
duration: 1,
threshold: 1,
},
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
return user.signUp();
})
.then(() => {
return Parse.User.current().set('_email_verify_token', 'bad').save();
})
.then(() => {
fail('Should not be able to update email verification token');
done();
})
.catch(err => {
expect(err).toBeDefined();
done();
});
});
await user.signUp();
for (let i = 0; i < 2; i++) {
try {
await Parse.User.logIn(user.getEmail(), 'abc');
} catch (e) {
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(
e.message === 'Invalid username/password.' ||
e.message ===
'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)'
).toBeTrue();
}
}
await Parse.User.requestPasswordReset(user.getEmail());
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'rest',
'X-Parse-Maintenance-Key': 'test2',
'Content-Type': 'application/json',
};
const userMaster = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User`,
json: true,
headers,
}).then(res => res.data.results[0]);
expect(Object.keys(userMaster).sort()).toEqual(
[
'ACL',
'_account_lockout_expires_at',
'_email_verify_token',
'_email_verify_token_expires_at',
'_failed_login_count',
'_perishable_token',
'createdAt',
'email',
'emailVerified',
'objectId',
'updatedAt',
'username',
].sort()
);
const toSet = {
_account_lockout_expires_at: new Date(),
_email_verify_token: 'abc',
_email_verify_token_expires_at: new Date(),
_failed_login_count: 0,
_perishable_token_expires_at: new Date(),
_perishable_token: 'abc',
};
await request({
method: 'PUT',
headers,
url: Parse.serverURL + '/users/' + userMaster.objectId,
json: true,
body: toSet,
}).then(res => res.data);
const update = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User`,
json: true,
headers,
}).then(res => res.data.results[0]);
for (const key in toSet) {
const value = toSet[key];
if (update[key] && update[key].iso) {
expect(update[key].iso).toEqual(value.toISOString());
} else if (value.toISOString) {
expect(update[key]).toEqual(value.toISOString());
} else {
expect(update[key]).toEqual(value);
}
}
});

it('should revoke sessions when setting paswword with masterKey (#3289)', done => {
Expand Down
30 changes: 25 additions & 5 deletions spec/PasswordPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1677,12 +1677,19 @@ describe('Password Policy: ', () => {
});

it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'test',
'X-Parse-Maintenance-Key': 'test2',
'Content-Type': 'application/json',
};
const user = new Parse.User();
const query = new Parse.Query(Parse.User);

await reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
maintenanceKey: 'test2',
passwordPolicy: {
maxPasswordHistory: 1,
},
Expand All @@ -1696,15 +1703,28 @@ describe('Password Policy: ', () => {
user.setPassword('user2');
await user.save();

const result1 = await query.get(user.id, { useMasterKey: true });
expect(result1.get('_password_history').length).toBe(1);
const user1 = await query.get(user.id, { useMasterKey: true });
expect(user1.get('_password_history')).toBeUndefined();

const result1 = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers,
}).then(res => res.data);
expect(result1._password_history.length).toBe(1);

user.setPassword('user3');
await user.save();

const result2 = await query.get(user.id, { useMasterKey: true });
expect(result2.get('_password_history').length).toBe(1);
const result2 = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers,
}).then(res => res.data);
expect(result2._password_history.length).toBe(1);

expect(result1.get('_password_history')).not.toEqual(result2.get('_password_history'));
expect(result1._password_history).not.toEqual(result2._password_history);
});
});
30 changes: 25 additions & 5 deletions spec/RegexVulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const publicServerURL = 'http://localhost:8378/1';
describe('Regex Vulnerabilities', function () {
beforeEach(async function () {
await reconfigureServer({
maintenanceKey: 'test2',
verifyUserEmails: true,
emailAdapter,
appName,
Expand Down Expand Up @@ -98,11 +99,20 @@ describe('Regex Vulnerabilities', function () {

it('should work with plain token', async function () {
expect(this.user.get('emailVerified')).toEqual(false);
const current = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'test',
'X-Parse-Maintenance-Key': 'test2',
'Content-Type': 'application/json',
},
}).then(res => res.data);
// It should work
await request({
url: `${serverURL}/apps/test/[email protected]&token=${this.user.get(
'_email_verify_token'
)}`,
url: `${serverURL}/apps/test/[email protected]&token=${current._email_verify_token}`,
method: 'GET',
});
await this.user.fetch({ useMasterKey: true });
Expand Down Expand Up @@ -164,8 +174,18 @@ describe('Regex Vulnerabilities', function () {
email: '[email protected]',
}),
});
await this.user.fetch({ useMasterKey: true });
const token = this.user.get('_perishable_token');
const current = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'test',
'X-Parse-Maintenance-Key': 'test2',
'Content-Type': 'application/json',
},
}).then(res => res.data);
const token = current._perishable_token;
const passwordResetResponse = await request({
url: `${serverURL}/apps/test/[email protected]&token=${token}`,
method: 'GET',
Expand Down
1 change: 0 additions & 1 deletion spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ const defaultConfiguration = {
enableForAnonymousUser: true,
enableForAuthenticatedUser: true,
},
masterKeyIps: ['127.0.0.1'],
push: {
android: {
senderId: 'yolo',
Expand Down
Loading

0 comments on commit f3bcc93

Please sign in to comment.