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

feat: Allow read and write of internal fields with maintenanceKey #8212

Merged
merged 40 commits into from
Jan 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4bd135c
fix: prevent stripping of user fields with masterKey
dblythy Oct 6, 2022
906072d
feat: allow read and write of internal fields with masterKey
dblythy Oct 6, 2022
7e9bb67
Update ParseUser.spec.js
dblythy Oct 6, 2022
1be7ff5
fix lint
dblythy Oct 6, 2022
62c14eb
run prettier
dblythy Oct 6, 2022
5797d43
add maintenance
dblythy Oct 27, 2022
7fc8227
Update UsersRouter.js
dblythy Oct 27, 2022
9473747
Merge branch 'alpha' into fix-master
dblythy Oct 27, 2022
d06ae11
fix tests
dblythy Oct 27, 2022
7538dec
Merge branch 'fix-master' of https://github.com/dblythy/parse-server …
dblythy Oct 27, 2022
bf8c511
Merge branch 'alpha' into fix-master
dblythy Oct 27, 2022
3f14373
run lint
dblythy Oct 27, 2022
4b88000
Merge branch 'fix-master' of https://github.com/dblythy/parse-server …
dblythy Oct 27, 2022
da1f421
Update UserController.js
dblythy Oct 27, 2022
f2bcdcc
fix tests
dblythy Oct 27, 2022
9388f6a
Merge branch 'alpha' into fix-master
dblythy Nov 3, 2022
62f511d
Merge branch 'alpha' into fix-master
dblythy Nov 23, 2022
767e3a8
add ::1
dblythy Nov 23, 2022
d2c9e81
prettier
dblythy Nov 23, 2022
518dec0
Update package-lock.json
dblythy Nov 23, 2022
4814cce
Merge branch 'alpha' into fix-master
mtrezza Nov 26, 2022
2e39b18
Merge branch 'alpha' into fix-master
dblythy Nov 27, 2022
4dc463c
wip
dblythy Nov 27, 2022
e6ef840
Merge branch 'alpha' into fix-master
dblythy Dec 15, 2022
dabd501
prettier
dblythy Dec 15, 2022
179879d
add access scopes to README
mtrezza Dec 17, 2022
80ce4ed
Merge branch 'alpha' into fix-master
mtrezza Dec 17, 2022
cae5680
Merge branch 'alpha' into fix-master
dblythy Dec 22, 2022
1987968
Update Middlewares.spec.js
dblythy Dec 22, 2022
012c1ad
Merge branch 'fix-master' of https://github.com/dblythy/parse-server …
dblythy Dec 22, 2022
57abc1f
Update ParseUser.spec.js
dblythy Dec 22, 2022
82bf8a9
Update src/Options/index.js
dblythy Dec 22, 2022
c57efb1
Update src/Options/index.js
dblythy Dec 22, 2022
687893b
docs
dblythy Dec 22, 2022
5fce261
Update Middlewares.spec.js
dblythy Dec 22, 2022
4e24693
Update ParseUser.spec.js
dblythy Dec 22, 2022
60fd5d6
Merge branch 'alpha' into fix-master
dblythy Jan 8, 2023
746c978
use example domain
mtrezza Jan 8, 2023
1f199fe
Merge branch 'alpha' into fix-master
mtrezza Jan 8, 2023
43f1d0e
Merge branch 'alpha' into fix-master
mtrezza Jan 8, 2023
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
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',
mtrezza marked this conversation as resolved.
Show resolved Hide resolved
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