feat: Access the internal scope of Parse Server using the new maintenanceKey; 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)
This commit is contained in:
10
README.md
10
README.md
@@ -60,6 +60,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
|||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Basic Options](#basic-options)
|
- [Basic Options](#basic-options)
|
||||||
- [Client Key Options](#client-key-options)
|
- [Client Key Options](#client-key-options)
|
||||||
|
- [Access Scopes](#access-scopes)
|
||||||
- [Email Verification and Password Reset](#email-verification-and-password-reset)
|
- [Email Verification and Password Reset](#email-verification-and-password-reset)
|
||||||
- [Password and Account Policy](#password-and-account-policy)
|
- [Password and Account Policy](#password-and-account-policy)
|
||||||
- [Custom Routes](#custom-routes)
|
- [Custom Routes](#custom-routes)
|
||||||
@@ -357,6 +358,15 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
|||||||
* `restAPIKey`
|
* `restAPIKey`
|
||||||
* `dotNetKey`
|
* `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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const Auth = require('../lib/Auth');
|
||||||
const Config = require('../lib/Config');
|
const Config = require('../lib/Config');
|
||||||
const request = require('../lib/request');
|
const request = require('../lib/request');
|
||||||
|
|
||||||
@@ -262,9 +263,14 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
return config.database.find('_User', {
|
return config.database.find(
|
||||||
|
'_User',
|
||||||
|
{
|
||||||
username: 'sets_email_verify_token_expires_at',
|
username: 'sets_email_verify_token_expires_at',
|
||||||
});
|
},
|
||||||
|
{},
|
||||||
|
Auth.maintenance(config)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(results => {
|
.then(results => {
|
||||||
expect(results.length).toBe(1);
|
expect(results.length).toBe(1);
|
||||||
@@ -499,7 +505,12 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
return config.database
|
return config.database
|
||||||
.find('_User', { username: 'newEmailVerifyTokenOnEmailReset' })
|
.find(
|
||||||
|
'_User',
|
||||||
|
{ username: 'newEmailVerifyTokenOnEmailReset' },
|
||||||
|
{},
|
||||||
|
Auth.maintenance(config)
|
||||||
|
)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
return results[0];
|
return results[0];
|
||||||
});
|
});
|
||||||
@@ -582,7 +593,7 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
// query for this user again
|
// query for this user again
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
return config.database
|
return config.database
|
||||||
.find('_User', { username: 'resends_verification_token' })
|
.find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config))
|
||||||
.then(results => {
|
.then(results => {
|
||||||
return results[0];
|
return results[0];
|
||||||
});
|
});
|
||||||
@@ -599,6 +610,7 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
jfail(error);
|
jfail(error);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,6 +162,22 @@ describe('middlewares', () => {
|
|||||||
expect(fakeReq.auth.isMaster).toBe(false);
|
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 () => {
|
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCache.put(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
const Auth = require('../lib/Auth');
|
||||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||||
const Config = require('../lib/Config');
|
const Config = require('../lib/Config');
|
||||||
const validatorFail = () => {
|
const validatorFail = () => {
|
||||||
@@ -977,6 +978,7 @@ describe('ParseLiveQuery', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await reconfigureServer({
|
await reconfigureServer({
|
||||||
|
maintenanceKey: 'test2',
|
||||||
liveQuery: {
|
liveQuery: {
|
||||||
classNames: [Parse.User],
|
classNames: [Parse.User],
|
||||||
},
|
},
|
||||||
@@ -998,9 +1000,14 @@ describe('ParseLiveQuery', function () {
|
|||||||
.signUp()
|
.signUp()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
return config.database.find('_User', {
|
return config.database.find(
|
||||||
|
'_User',
|
||||||
|
{
|
||||||
username: 'zxcv',
|
username: 'zxcv',
|
||||||
});
|
},
|
||||||
|
{},
|
||||||
|
Auth.maintenance(config)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(async results => {
|
.then(async results => {
|
||||||
const foundUser = results[0];
|
const foundUser = results[0];
|
||||||
|
|||||||
@@ -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 = {
|
const emailAdapter = {
|
||||||
sendVerificationEmail: () => {},
|
sendVerificationEmail: () => {},
|
||||||
sendPasswordResetEmail: () => Promise.resolve(),
|
sendPasswordResetEmail: () => Promise.resolve(),
|
||||||
sendMail: () => Promise.resolve(),
|
sendMail: () => Promise.resolve(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = new Parse.User();
|
const user = new Parse.User();
|
||||||
user.set({
|
user.set({
|
||||||
username: 'hello',
|
username: 'hello',
|
||||||
password: 'world',
|
password: 'world',
|
||||||
email: 'test@email.com',
|
email: 'test@email.com',
|
||||||
});
|
});
|
||||||
|
await reconfigureServer({
|
||||||
reconfigureServer({
|
|
||||||
appName: 'unused',
|
appName: 'unused',
|
||||||
verifyUserEmails: true,
|
verifyUserEmails: true,
|
||||||
emailAdapter: emailAdapter,
|
emailAdapter: emailAdapter,
|
||||||
publicServerURL: 'http://localhost:8378/1',
|
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();
|
||||||
|
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.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'test@example.com',
|
||||||
|
});
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'unused',
|
||||||
|
maintenanceKey: 'test2',
|
||||||
|
verifyUserEmails: true,
|
||||||
|
emailVerifyTokenValidityDuration: 5,
|
||||||
|
accountLockout: {
|
||||||
|
duration: 1,
|
||||||
|
threshold: 1,
|
||||||
|
},
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
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 => {
|
it('should revoke sessions when setting paswword with masterKey (#3289)', done => {
|
||||||
|
|||||||
@@ -1677,12 +1677,19 @@ describe('Password Policy: ', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
|
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 user = new Parse.User();
|
||||||
const query = new Parse.Query(Parse.User);
|
const query = new Parse.Query(Parse.User);
|
||||||
|
|
||||||
await reconfigureServer({
|
await reconfigureServer({
|
||||||
appName: 'passwordPolicy',
|
appName: 'passwordPolicy',
|
||||||
verifyUserEmails: false,
|
verifyUserEmails: false,
|
||||||
|
maintenanceKey: 'test2',
|
||||||
passwordPolicy: {
|
passwordPolicy: {
|
||||||
maxPasswordHistory: 1,
|
maxPasswordHistory: 1,
|
||||||
},
|
},
|
||||||
@@ -1696,15 +1703,28 @@ describe('Password Policy: ', () => {
|
|||||||
user.setPassword('user2');
|
user.setPassword('user2');
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
const result1 = await query.get(user.id, { useMasterKey: true });
|
const user1 = await query.get(user.id, { useMasterKey: true });
|
||||||
expect(result1.get('_password_history').length).toBe(1);
|
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');
|
user.setPassword('user3');
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
const result2 = await query.get(user.id, { useMasterKey: true });
|
const result2 = await request({
|
||||||
expect(result2.get('_password_history').length).toBe(1);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const publicServerURL = 'http://localhost:8378/1';
|
|||||||
describe('Regex Vulnerabilities', function () {
|
describe('Regex Vulnerabilities', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await reconfigureServer({
|
await reconfigureServer({
|
||||||
|
maintenanceKey: 'test2',
|
||||||
verifyUserEmails: true,
|
verifyUserEmails: true,
|
||||||
emailAdapter,
|
emailAdapter,
|
||||||
appName,
|
appName,
|
||||||
@@ -98,11 +99,20 @@ describe('Regex Vulnerabilities', function () {
|
|||||||
|
|
||||||
it('should work with plain token', async function () {
|
it('should work with plain token', async function () {
|
||||||
expect(this.user.get('emailVerified')).toEqual(false);
|
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
|
// It should work
|
||||||
await request({
|
await request({
|
||||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${this.user.get(
|
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`,
|
||||||
'_email_verify_token'
|
|
||||||
)}`,
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
await this.user.fetch({ useMasterKey: true });
|
await this.user.fetch({ useMasterKey: true });
|
||||||
@@ -164,8 +174,18 @@ describe('Regex Vulnerabilities', function () {
|
|||||||
email: 'someemail@somedomain.com',
|
email: 'someemail@somedomain.com',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
await this.user.fetch({ useMasterKey: true });
|
const current = await request({
|
||||||
const token = this.user.get('_perishable_token');
|
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({
|
const passwordResetResponse = await request({
|
||||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ const defaultConfiguration = {
|
|||||||
enableForAnonymousUser: true,
|
enableForAnonymousUser: true,
|
||||||
enableForAuthenticatedUser: true,
|
enableForAuthenticatedUser: true,
|
||||||
},
|
},
|
||||||
masterKeyIps: ['127.0.0.1'],
|
|
||||||
push: {
|
push: {
|
||||||
android: {
|
android: {
|
||||||
senderId: 'yolo',
|
senderId: 'yolo',
|
||||||
|
|||||||
@@ -859,6 +859,15 @@ describe('read-only masterKey', () => {
|
|||||||
await reconfigureServer();
|
await reconfigureServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw when masterKey and maintenanceKey are the same', async () => {
|
||||||
|
await expectAsync(
|
||||||
|
reconfigureServer({
|
||||||
|
masterKey: 'yolo',
|
||||||
|
maintenanceKey: 'yolo',
|
||||||
|
})
|
||||||
|
).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different'));
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw when trying to create RestWrite', () => {
|
it('should throw when trying to create RestWrite', () => {
|
||||||
const config = Config.get('test');
|
const config = Config.get('test');
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
|||||||
13
src/Auth.js
13
src/Auth.js
@@ -11,6 +11,7 @@ function Auth({
|
|||||||
config,
|
config,
|
||||||
cacheController = undefined,
|
cacheController = undefined,
|
||||||
isMaster = false,
|
isMaster = false,
|
||||||
|
isMaintenance = false,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
user,
|
user,
|
||||||
installationId,
|
installationId,
|
||||||
@@ -19,6 +20,7 @@ function Auth({
|
|||||||
this.cacheController = cacheController || (config && config.cacheController);
|
this.cacheController = cacheController || (config && config.cacheController);
|
||||||
this.installationId = installationId;
|
this.installationId = installationId;
|
||||||
this.isMaster = isMaster;
|
this.isMaster = isMaster;
|
||||||
|
this.isMaintenance = isMaintenance;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.isReadOnly = isReadOnly;
|
this.isReadOnly = isReadOnly;
|
||||||
|
|
||||||
@@ -35,6 +37,9 @@ Auth.prototype.isUnauthenticated = function () {
|
|||||||
if (this.isMaster) {
|
if (this.isMaster) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (this.isMaintenance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -46,6 +51,11 @@ function master(config) {
|
|||||||
return new Auth({ config, isMaster: true });
|
return new Auth({ config, isMaster: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A helper to get a maintenance-level Auth object
|
||||||
|
function maintenance(config) {
|
||||||
|
return new Auth({ config, isMaintenance: true });
|
||||||
|
}
|
||||||
|
|
||||||
// A helper to get a master-level Auth object
|
// A helper to get a master-level Auth object
|
||||||
function readOnly(config) {
|
function readOnly(config) {
|
||||||
return new Auth({ config, isMaster: true, isReadOnly: true });
|
return new Auth({ config, isMaster: true, isReadOnly: true });
|
||||||
@@ -149,7 +159,7 @@ var getAuthForLegacySessionToken = function ({ config, sessionToken, installatio
|
|||||||
|
|
||||||
// Returns a promise that resolves to an array of role names
|
// Returns a promise that resolves to an array of role names
|
||||||
Auth.prototype.getUserRoles = function () {
|
Auth.prototype.getUserRoles = function () {
|
||||||
if (this.isMaster || !this.user) {
|
if (this.isMaster || this.isMaintenance || !this.user) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
if (this.fetchedRoles) {
|
if (this.fetchedRoles) {
|
||||||
@@ -493,6 +503,7 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
Auth,
|
Auth,
|
||||||
master,
|
master,
|
||||||
|
maintenance,
|
||||||
nobody,
|
nobody,
|
||||||
readOnly,
|
readOnly,
|
||||||
getAuthForSessionToken,
|
getAuthForSessionToken,
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export class Config {
|
|||||||
passwordPolicy,
|
passwordPolicy,
|
||||||
masterKeyIps,
|
masterKeyIps,
|
||||||
masterKey,
|
masterKey,
|
||||||
|
maintenanceKey,
|
||||||
|
maintenanceKeyIps,
|
||||||
readOnlyMasterKey,
|
readOnlyMasterKey,
|
||||||
allowHeaders,
|
allowHeaders,
|
||||||
idempotencyOptions,
|
idempotencyOptions,
|
||||||
@@ -91,6 +93,10 @@ export class Config {
|
|||||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (masterKey === maintenanceKey) {
|
||||||
|
throw new Error('masterKey and maintenanceKey should be different');
|
||||||
|
}
|
||||||
|
|
||||||
const emailAdapter = userController.adapter;
|
const emailAdapter = userController.adapter;
|
||||||
if (verifyUserEmails) {
|
if (verifyUserEmails) {
|
||||||
this.validateEmailConfiguration({
|
this.validateEmailConfiguration({
|
||||||
@@ -116,7 +122,8 @@ export class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
||||||
this.validateMasterKeyIps(masterKeyIps);
|
this.validateIps('masterKeyIps', masterKeyIps);
|
||||||
|
this.validateIps('maintenanceKeyIps', maintenanceKeyIps);
|
||||||
this.validateDefaultLimit(defaultLimit);
|
this.validateDefaultLimit(defaultLimit);
|
||||||
this.validateMaxLimit(maxLimit);
|
this.validateMaxLimit(maxLimit);
|
||||||
this.validateAllowHeaders(allowHeaders);
|
this.validateAllowHeaders(allowHeaders);
|
||||||
@@ -440,13 +447,13 @@ export class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateMasterKeyIps(masterKeyIps) {
|
static validateIps(field, masterKeyIps) {
|
||||||
for (let ip of masterKeyIps) {
|
for (let ip of masterKeyIps) {
|
||||||
if (ip.includes('/')) {
|
if (ip.includes('/')) {
|
||||||
ip = ip.split('/')[0];
|
ip = ip.split('/')[0];
|
||||||
}
|
}
|
||||||
if (!net.isIP(ip)) {
|
if (!net.isIP(ip)) {
|
||||||
throw `The Parse Server option "masterKeyIps" contains an invalid IP address "${ip}".`;
|
throw `The Parse Server option "${field}" contains an invalid IP address "${ip}".`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,14 +68,22 @@ const specialMasterQueryKeys = [
|
|||||||
'_password_history',
|
'_password_history',
|
||||||
];
|
];
|
||||||
|
|
||||||
const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
|
const validateQuery = (
|
||||||
|
query: any,
|
||||||
|
isMaster: boolean,
|
||||||
|
isMaintenance: boolean,
|
||||||
|
update: boolean
|
||||||
|
): void => {
|
||||||
|
if (isMaintenance) {
|
||||||
|
isMaster = true;
|
||||||
|
}
|
||||||
if (query.ACL) {
|
if (query.ACL) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.$or) {
|
if (query.$or) {
|
||||||
if (query.$or instanceof Array) {
|
if (query.$or instanceof Array) {
|
||||||
query.$or.forEach(value => validateQuery(value, isMaster, update));
|
query.$or.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||||
} else {
|
} else {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
|
||||||
}
|
}
|
||||||
@@ -83,7 +91,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void =>
|
|||||||
|
|
||||||
if (query.$and) {
|
if (query.$and) {
|
||||||
if (query.$and instanceof Array) {
|
if (query.$and instanceof Array) {
|
||||||
query.$and.forEach(value => validateQuery(value, isMaster, update));
|
query.$and.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||||
} else {
|
} else {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
|
||||||
}
|
}
|
||||||
@@ -91,7 +99,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void =>
|
|||||||
|
|
||||||
if (query.$nor) {
|
if (query.$nor) {
|
||||||
if (query.$nor instanceof Array && query.$nor.length > 0) {
|
if (query.$nor instanceof Array && query.$nor.length > 0) {
|
||||||
query.$nor.forEach(value => validateQuery(value, isMaster, update));
|
query.$nor.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||||
} else {
|
} else {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.INVALID_QUERY,
|
Parse.Error.INVALID_QUERY,
|
||||||
@@ -124,6 +132,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void =>
|
|||||||
// Filters out any data that shouldn't be on this REST-formatted object.
|
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||||
const filterSensitiveData = (
|
const filterSensitiveData = (
|
||||||
isMaster: boolean,
|
isMaster: boolean,
|
||||||
|
isMaintenance: boolean,
|
||||||
aclGroup: any[],
|
aclGroup: any[],
|
||||||
auth: any,
|
auth: any,
|
||||||
operation: any,
|
operation: any,
|
||||||
@@ -195,6 +204,15 @@ const filterSensitiveData = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isUserClass = className === '_User';
|
const isUserClass = className === '_User';
|
||||||
|
if (isUserClass) {
|
||||||
|
object.password = object._hashed_password;
|
||||||
|
delete object._hashed_password;
|
||||||
|
delete object.sessionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaintenance) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
/* special treat for the user class: don't filter protectedFields if currently loggedin user is
|
/* special treat for the user class: don't filter protectedFields if currently loggedin user is
|
||||||
the retrieved user */
|
the retrieved user */
|
||||||
@@ -208,22 +226,13 @@ const filterSensitiveData = (
|
|||||||
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
|
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserClass) {
|
|
||||||
object.password = object._hashed_password;
|
|
||||||
delete object._hashed_password;
|
|
||||||
delete object.sessionToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMaster) {
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
for (const key in object) {
|
for (const key in object) {
|
||||||
if (key.charAt(0) === '_') {
|
if (key.charAt(0) === '_') {
|
||||||
delete object[key];
|
delete object[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUserClass) {
|
if (!isUserClass || isMaster) {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +448,8 @@ class DatabaseController {
|
|||||||
className: string,
|
className: string,
|
||||||
object: any,
|
object: any,
|
||||||
query: any,
|
query: any,
|
||||||
runOptions: QueryOptions
|
runOptions: QueryOptions,
|
||||||
|
maintenance: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
let schema;
|
let schema;
|
||||||
const acl = runOptions.acl;
|
const acl = runOptions.acl;
|
||||||
@@ -454,7 +464,7 @@ class DatabaseController {
|
|||||||
return this.canAddField(schema, className, object, aclGroup, runOptions);
|
return this.canAddField(schema, className, object, aclGroup, runOptions);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return schema.validateObject(className, object, query);
|
return schema.validateObject(className, object, query, maintenance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +522,7 @@ class DatabaseController {
|
|||||||
if (acl) {
|
if (acl) {
|
||||||
query = addWriteACL(query, acl);
|
query = addWriteACL(query, acl);
|
||||||
}
|
}
|
||||||
validateQuery(query, isMaster, true);
|
validateQuery(query, isMaster, false, true);
|
||||||
return schemaController
|
return schemaController
|
||||||
.getOneSchema(className, true)
|
.getOneSchema(className, true)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -758,7 +768,7 @@ class DatabaseController {
|
|||||||
if (acl) {
|
if (acl) {
|
||||||
query = addWriteACL(query, acl);
|
query = addWriteACL(query, acl);
|
||||||
}
|
}
|
||||||
validateQuery(query, isMaster, false);
|
validateQuery(query, isMaster, false, false);
|
||||||
return schemaController
|
return schemaController
|
||||||
.getOneSchema(className)
|
.getOneSchema(className)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -1151,7 +1161,8 @@ class DatabaseController {
|
|||||||
auth: any = {},
|
auth: any = {},
|
||||||
validSchemaController: SchemaController.SchemaController
|
validSchemaController: SchemaController.SchemaController
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const isMaster = acl === undefined;
|
const isMaintenance = auth.isMaintenance;
|
||||||
|
const isMaster = acl === undefined || isMaintenance;
|
||||||
const aclGroup = acl || [];
|
const aclGroup = acl || [];
|
||||||
op =
|
op =
|
||||||
op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
||||||
@@ -1253,7 +1264,7 @@ class DatabaseController {
|
|||||||
query = addReadACL(query, aclGroup);
|
query = addReadACL(query, aclGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateQuery(query, isMaster, false);
|
validateQuery(query, isMaster, isMaintenance, false);
|
||||||
if (count) {
|
if (count) {
|
||||||
if (!classExists) {
|
if (!classExists) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1296,6 +1307,7 @@ class DatabaseController {
|
|||||||
object = untransformObjectACL(object);
|
object = untransformObjectACL(object);
|
||||||
return filterSensitiveData(
|
return filterSensitiveData(
|
||||||
isMaster,
|
isMaster,
|
||||||
|
isMaintenance,
|
||||||
aclGroup,
|
aclGroup,
|
||||||
auth,
|
auth,
|
||||||
op,
|
op,
|
||||||
@@ -1813,8 +1825,8 @@ class DatabaseController {
|
|||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _validateQuery: (any, boolean, boolean) => void;
|
static _validateQuery: (any, boolean, boolean, boolean) => void;
|
||||||
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
|
static filterSensitiveData: (boolean, boolean, any[], any, any, any, string, any[], any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = DatabaseController;
|
module.exports = DatabaseController;
|
||||||
|
|||||||
@@ -1071,14 +1071,19 @@ export default class SchemaController {
|
|||||||
className: string,
|
className: string,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
type: string | SchemaField,
|
type: string | SchemaField,
|
||||||
isValidation?: boolean
|
isValidation?: boolean,
|
||||||
|
maintenance?: boolean
|
||||||
) {
|
) {
|
||||||
if (fieldName.indexOf('.') > 0) {
|
if (fieldName.indexOf('.') > 0) {
|
||||||
// subdocument key (x.y) => ok if x is of type 'object'
|
// subdocument key (x.y) => ok if x is of type 'object'
|
||||||
fieldName = fieldName.split('.')[0];
|
fieldName = fieldName.split('.')[0];
|
||||||
type = 'Object';
|
type = 'Object';
|
||||||
}
|
}
|
||||||
if (!fieldNameIsValid(fieldName, className)) {
|
let fieldNameToValidate = `${fieldName}`;
|
||||||
|
if (maintenance && fieldNameToValidate.charAt(0) === '_') {
|
||||||
|
fieldNameToValidate = fieldNameToValidate.substring(1);
|
||||||
|
}
|
||||||
|
if (!fieldNameIsValid(fieldNameToValidate, className)) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,7 +1233,7 @@ export default class SchemaController {
|
|||||||
// Validates an object provided in REST format.
|
// Validates an object provided in REST format.
|
||||||
// Returns a promise that resolves to the new schema if this object is
|
// Returns a promise that resolves to the new schema if this object is
|
||||||
// valid.
|
// valid.
|
||||||
async validateObject(className: string, object: any, query: any) {
|
async validateObject(className: string, object: any, query: any, maintenance: boolean) {
|
||||||
let geocount = 0;
|
let geocount = 0;
|
||||||
const schema = await this.enforceClassExists(className);
|
const schema = await this.enforceClassExists(className);
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -1258,7 +1263,7 @@ export default class SchemaController {
|
|||||||
// Every object has ACL implicitly.
|
// Every object has ACL implicitly.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
promises.push(schema.enforceFieldExists(className, fieldName, expected, true));
|
promises.push(schema.enforceFieldExists(className, fieldName, expected, true, maintenance));
|
||||||
}
|
}
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
const enforceFields = results.filter(result => !!result);
|
const enforceFields = results.filter(result => !!result);
|
||||||
|
|||||||
@@ -69,20 +69,17 @@ export class UserController extends AdaptableController {
|
|||||||
|
|
||||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||||
}
|
}
|
||||||
const masterAuth = Auth.master(this.config);
|
const maintenanceAuth = Auth.maintenance(this.config);
|
||||||
var findUserForEmailVerification = new RestQuery(
|
var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
|
||||||
this.config,
|
username,
|
||||||
Auth.master(this.config),
|
});
|
||||||
'_User',
|
|
||||||
{ username: username }
|
|
||||||
);
|
|
||||||
return findUserForEmailVerification.execute().then(result => {
|
return findUserForEmailVerification.execute().then(result => {
|
||||||
if (result.results.length && result.results[0].emailVerified) {
|
if (result.results.length && result.results[0].emailVerified) {
|
||||||
return Promise.resolve(result.results.length[0]);
|
return Promise.resolve(result.results.length[0]);
|
||||||
} else if (result.results.length) {
|
} else if (result.results.length) {
|
||||||
query.objectId = result.results[0].objectId;
|
query.objectId = result.results[0].objectId;
|
||||||
}
|
}
|
||||||
return rest.update(this.config, masterAuth, '_User', query, updateFields);
|
return rest.update(this.config, maintenanceAuth, '_User', query, updateFields);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +91,8 @@ export class UserController extends AdaptableController {
|
|||||||
username: username,
|
username: username,
|
||||||
_perishable_token: token,
|
_perishable_token: token,
|
||||||
},
|
},
|
||||||
{ limit: 1 }
|
{ limit: 1 },
|
||||||
|
Auth.maintenance(this.config)
|
||||||
)
|
)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (results.length != 1) {
|
if (results.length != 1) {
|
||||||
@@ -228,7 +226,8 @@ export class UserController extends AdaptableController {
|
|||||||
{ username: email, email: { $exists: false }, _perishable_token: { $exists: true } },
|
{ username: email, email: { $exists: false }, _perishable_token: { $exists: true } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ limit: 1 }
|
{ limit: 1 },
|
||||||
|
Auth.maintenance(this.config)
|
||||||
);
|
);
|
||||||
if (results.length == 1) {
|
if (results.length == 1) {
|
||||||
let expiresDate = results[0]._perishable_token_expires_at;
|
let expiresDate = results[0]._perishable_token_expires_at;
|
||||||
|
|||||||
@@ -625,6 +625,7 @@ class ParseLiveQueryServer {
|
|||||||
}
|
}
|
||||||
return DatabaseController.filterSensitiveData(
|
return DatabaseController.filterSensitiveData(
|
||||||
client.hasMasterKey,
|
client.hasMasterKey,
|
||||||
|
false,
|
||||||
aclGroup,
|
aclGroup,
|
||||||
clientAuth,
|
clientAuth,
|
||||||
op,
|
op,
|
||||||
|
|||||||
@@ -300,6 +300,19 @@ module.exports.ParseServerOptions = {
|
|||||||
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
|
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
|
||||||
default: './logs',
|
default: './logs',
|
||||||
},
|
},
|
||||||
|
maintenanceKey: {
|
||||||
|
env: 'PARSE_SERVER_MAINTENANCE_KEY',
|
||||||
|
help:
|
||||||
|
'(Optional) The maintenance key is used for modifying internal fields of Parse Server.<br><br>\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
maintenanceKeyIps: {
|
||||||
|
env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS',
|
||||||
|
help:
|
||||||
|
"(Optional) Restricts the use of maintenance key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.",
|
||||||
|
action: parsers.arrayParser,
|
||||||
|
default: ['127.0.0.1', '::1'],
|
||||||
|
},
|
||||||
masterKey: {
|
masterKey: {
|
||||||
env: 'PARSE_SERVER_MASTER_KEY',
|
env: 'PARSE_SERVER_MASTER_KEY',
|
||||||
help: 'Your Parse Master Key',
|
help: 'Your Parse Master Key',
|
||||||
@@ -308,7 +321,7 @@ module.exports.ParseServerOptions = {
|
|||||||
masterKeyIps: {
|
masterKeyIps: {
|
||||||
env: 'PARSE_SERVER_MASTER_KEY_IPS',
|
env: 'PARSE_SERVER_MASTER_KEY_IPS',
|
||||||
help:
|
help:
|
||||||
"(Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.",
|
"(Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.",
|
||||||
action: parsers.arrayParser,
|
action: parsers.arrayParser,
|
||||||
default: ['127.0.0.1', '::1'],
|
default: ['127.0.0.1', '::1'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,8 +58,10 @@
|
|||||||
* @property {String} logLevel Sets the level for logs
|
* @property {String} logLevel Sets the level for logs
|
||||||
* @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
|
* @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
|
||||||
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
|
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
|
||||||
|
* @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal fields of Parse Server.<br><br>⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.
|
||||||
|
* @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.
|
||||||
* @property {String} masterKey Your Parse Master Key
|
* @property {String} masterKey Your Parse Master Key
|
||||||
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
||||||
* @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited
|
* @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited
|
||||||
* @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)
|
* @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)
|
||||||
* @property {String} maxUploadSize Max file size for uploads, defaults to 20mb
|
* @property {String} maxUploadSize Max file size for uploads, defaults to 20mb
|
||||||
|
|||||||
@@ -46,12 +46,17 @@ export interface ParseServerOptions {
|
|||||||
appId: string;
|
appId: string;
|
||||||
/* Your Parse Master Key */
|
/* Your Parse Master Key */
|
||||||
masterKey: string;
|
masterKey: string;
|
||||||
|
/* (Optional) The maintenance key is used for modifying internal fields of Parse Server.<br><br>⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */
|
||||||
|
maintenanceKey: string;
|
||||||
/* URL to your parse server with http:// or https://.
|
/* URL to your parse server with http:// or https://.
|
||||||
:ENV: PARSE_SERVER_URL */
|
:ENV: PARSE_SERVER_URL */
|
||||||
serverURL: string;
|
serverURL: string;
|
||||||
/* (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
/* (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
||||||
:DEFAULT: ["127.0.0.1","::1"] */
|
:DEFAULT: ["127.0.0.1","::1"] */
|
||||||
masterKeyIps: ?(string[]);
|
masterKeyIps: ?(string[]);
|
||||||
|
/* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.
|
||||||
|
:DEFAULT: ["127.0.0.1","::1"] */
|
||||||
|
maintenanceKeyIps: ?(string[]);
|
||||||
/* Sets the app name */
|
/* Sets the app name */
|
||||||
appName: ?string;
|
appName: ?string;
|
||||||
/* Add headers to Access-Control-Allow-Headers */
|
/* Add headers to Access-Control-Allow-Headers */
|
||||||
|
|||||||
@@ -513,10 +513,6 @@ function injectDefaults(options: ParseServerOptions) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
options.masterKeyIps = Array.from(
|
|
||||||
new Set(options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Those can't be tested as it requires a subprocess
|
// Those can't be tested as it requires a subprocess
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ RestWrite.prototype.execute = function () {
|
|||||||
|
|
||||||
// Uses the Auth object to get the list of roles, adds the user id
|
// Uses the Auth object to get the list of roles, adds the user id
|
||||||
RestWrite.prototype.getUserAndRoleACL = function () {
|
RestWrite.prototype.getUserAndRoleACL = function () {
|
||||||
if (this.auth.isMaster) {
|
if (this.auth.isMaster || this.auth.isMaintenance) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +187,7 @@ RestWrite.prototype.validateClientClassCreation = function () {
|
|||||||
if (
|
if (
|
||||||
this.config.allowClientClassCreation === false &&
|
this.config.allowClientClassCreation === false &&
|
||||||
!this.auth.isMaster &&
|
!this.auth.isMaster &&
|
||||||
|
!this.auth.isMaintenance &&
|
||||||
SchemaController.systemClasses.indexOf(this.className) === -1
|
SchemaController.systemClasses.indexOf(this.className) === -1
|
||||||
) {
|
) {
|
||||||
return this.config.database
|
return this.config.database
|
||||||
@@ -211,7 +212,8 @@ RestWrite.prototype.validateSchema = function () {
|
|||||||
this.className,
|
this.className,
|
||||||
this.data,
|
this.data,
|
||||||
this.query,
|
this.query,
|
||||||
this.runOptions
|
this.runOptions,
|
||||||
|
this.auth.isMaintenance
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -434,7 +436,7 @@ RestWrite.prototype.validateAuthData = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RestWrite.prototype.filteredObjectsByACL = function (objects) {
|
RestWrite.prototype.filteredObjectsByACL = function (objects) {
|
||||||
if (this.auth.isMaster) {
|
if (this.auth.isMaster || this.auth.isMaintenance) {
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
return objects.filter(object => {
|
return objects.filter(object => {
|
||||||
@@ -605,7 +607,7 @@ RestWrite.prototype.transformUser = function () {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.auth.isMaster && 'emailVerified' in this.data) {
|
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
|
||||||
const error = `Clients aren't allowed to manually update email verification.`;
|
const error = `Clients aren't allowed to manually update email verification.`;
|
||||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||||
}
|
}
|
||||||
@@ -640,7 +642,7 @@ RestWrite.prototype.transformUser = function () {
|
|||||||
if (this.query) {
|
if (this.query) {
|
||||||
this.storage['clearSessions'] = true;
|
this.storage['clearSessions'] = true;
|
||||||
// Generate a new session only if the user requested
|
// Generate a new session only if the user requested
|
||||||
if (!this.auth.isMaster) {
|
if (!this.auth.isMaster && !this.auth.isMaintenance) {
|
||||||
this.storage['generateNewSession'] = true;
|
this.storage['generateNewSession'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,7 +815,8 @@ RestWrite.prototype._validatePasswordHistory = function () {
|
|||||||
.find(
|
.find(
|
||||||
'_User',
|
'_User',
|
||||||
{ objectId: this.objectId() },
|
{ objectId: this.objectId() },
|
||||||
{ keys: ['_password_history', '_hashed_password'] }
|
{ keys: ['_password_history', '_hashed_password'] },
|
||||||
|
Auth.maintenance(this.config)
|
||||||
)
|
)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (results.length != 1) {
|
if (results.length != 1) {
|
||||||
@@ -1015,7 +1018,7 @@ RestWrite.prototype.handleSession = function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.auth.user && !this.auth.isMaster) {
|
if (!this.auth.user && !this.auth.isMaster && !this.auth.isMaintenance) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.');
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,7 +1051,7 @@ RestWrite.prototype.handleSession = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.query && !this.auth.isMaster) {
|
if (!this.query && !this.auth.isMaster && !this.auth.isMaintenance) {
|
||||||
const additionalSessionData = {};
|
const additionalSessionData = {};
|
||||||
for (var key in this.data) {
|
for (var key in this.data) {
|
||||||
if (key === 'objectId' || key === 'user') {
|
if (key === 'objectId' || key === 'user') {
|
||||||
@@ -1115,7 +1118,7 @@ RestWrite.prototype.handleInstallation = function () {
|
|||||||
let installationId = this.data.installationId;
|
let installationId = this.data.installationId;
|
||||||
|
|
||||||
// If data.installationId is not set and we're not master, we can lookup in auth
|
// If data.installationId is not set and we're not master, we can lookup in auth
|
||||||
if (!installationId && !this.auth.isMaster) {
|
if (!installationId && !this.auth.isMaster && !this.auth.isMaintenance) {
|
||||||
installationId = this.auth.installationId;
|
installationId = this.auth.installationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,7 +1382,12 @@ RestWrite.prototype.runDatabaseOperation = function () {
|
|||||||
if (this.query) {
|
if (this.query) {
|
||||||
// Force the user to not lockout
|
// Force the user to not lockout
|
||||||
// Matched with parse.com
|
// Matched with parse.com
|
||||||
if (this.className === '_User' && this.data.ACL && this.auth.isMaster !== true) {
|
if (
|
||||||
|
this.className === '_User' &&
|
||||||
|
this.data.ACL &&
|
||||||
|
this.auth.isMaster !== true &&
|
||||||
|
this.auth.isMaintenance !== true
|
||||||
|
) {
|
||||||
this.data.ACL[this.query.objectId] = { read: true, write: true };
|
this.data.ACL[this.query.objectId] = { read: true, write: true };
|
||||||
}
|
}
|
||||||
// update password timestamp if user password is being changed
|
// update password timestamp if user password is being changed
|
||||||
@@ -1406,7 +1414,8 @@ RestWrite.prototype.runDatabaseOperation = function () {
|
|||||||
.find(
|
.find(
|
||||||
'_User',
|
'_User',
|
||||||
{ objectId: this.objectId() },
|
{ objectId: this.objectId() },
|
||||||
{ keys: ['_password_history', '_hashed_password'] }
|
{ keys: ['_password_history', '_hashed_password'] },
|
||||||
|
Auth.maintenance(this.config)
|
||||||
)
|
)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (results.length != 1) {
|
if (results.length != 1) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
query = { $or: [{ username }, { email: username }] };
|
query = { $or: [{ username }, { email: username }] };
|
||||||
}
|
}
|
||||||
return req.config.database
|
return req.config.database
|
||||||
.find('_User', query)
|
.find('_User', query, {}, Auth.maintenance(req.config))
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
appId: req.get('X-Parse-Application-Id'),
|
appId: req.get('X-Parse-Application-Id'),
|
||||||
sessionToken: req.get('X-Parse-Session-Token'),
|
sessionToken: req.get('X-Parse-Session-Token'),
|
||||||
masterKey: req.get('X-Parse-Master-Key'),
|
masterKey: req.get('X-Parse-Master-Key'),
|
||||||
|
maintenanceKey: req.get('X-Parse-Maintenance-Key'),
|
||||||
installationId: req.get('X-Parse-Installation-Id'),
|
installationId: req.get('X-Parse-Installation-Id'),
|
||||||
clientKey: req.get('X-Parse-Client-Key'),
|
clientKey: req.get('X-Parse-Client-Key'),
|
||||||
javascriptKey: req.get('X-Parse-Javascript-Key'),
|
javascriptKey: req.get('X-Parse-Javascript-Key'),
|
||||||
@@ -177,6 +178,24 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
req.config.ip = clientIp;
|
req.config.ip = clientIp;
|
||||||
req.info = info;
|
req.info = info;
|
||||||
|
|
||||||
|
const isMaintenance =
|
||||||
|
req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey;
|
||||||
|
if (isMaintenance) {
|
||||||
|
if (ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) {
|
||||||
|
req.auth = new auth.Auth({
|
||||||
|
config: req.config,
|
||||||
|
installationId: info.installationId,
|
||||||
|
isMaintenance: true,
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const log = req.config?.loggerController || defaultLogger;
|
||||||
|
log.error(
|
||||||
|
`Request using maintenance key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'maintenanceKeyIps'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let isMaster = info.masterKey === req.config.masterKey;
|
let isMaster = info.masterKey === req.config.masterKey;
|
||||||
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
|
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
|
||||||
const log = req.config?.loggerController || defaultLogger;
|
const log = req.config?.loggerController || defaultLogger;
|
||||||
|
|||||||
21
src/rest.js
21
src/rest.js
@@ -111,7 +111,7 @@ function del(config, auth, className, objectId, context) {
|
|||||||
if (response && response.results && response.results.length) {
|
if (response && response.results && response.results.length) {
|
||||||
const firstResult = response.results[0];
|
const firstResult = response.results[0];
|
||||||
firstResult.className = className;
|
firstResult.className = className;
|
||||||
if (className === '_Session' && !auth.isMaster) {
|
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
|
||||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ function del(config, auth, className, objectId, context) {
|
|||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!auth.isMaster) {
|
if (!auth.isMaster && !auth.isMaintenance) {
|
||||||
return auth.getUserRoles();
|
return auth.getUserRoles();
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -144,7 +144,7 @@ function del(config, auth, className, objectId, context) {
|
|||||||
.then(s => {
|
.then(s => {
|
||||||
schemaController = s;
|
schemaController = s;
|
||||||
const options = {};
|
const options = {};
|
||||||
if (!auth.isMaster) {
|
if (!auth.isMaster && !auth.isMaintenance) {
|
||||||
options.acl = ['*'];
|
options.acl = ['*'];
|
||||||
if (auth.user) {
|
if (auth.user) {
|
||||||
options.acl.push(auth.user.id);
|
options.acl.push(auth.user.id);
|
||||||
@@ -237,7 +237,12 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
|
|||||||
|
|
||||||
function handleSessionMissingError(error, className, auth) {
|
function handleSessionMissingError(error, className, auth) {
|
||||||
// If we're trying to update a user without / with bad session token
|
// If we're trying to update a user without / with bad session token
|
||||||
if (className === '_User' && error.code === Parse.Error.OBJECT_NOT_FOUND && !auth.isMaster) {
|
if (
|
||||||
|
className === '_User' &&
|
||||||
|
error.code === Parse.Error.OBJECT_NOT_FOUND &&
|
||||||
|
!auth.isMaster &&
|
||||||
|
!auth.isMaintenance
|
||||||
|
) {
|
||||||
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
|
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -253,7 +258,7 @@ const classesWithMasterOnlyAccess = [
|
|||||||
];
|
];
|
||||||
// Disallowing access to the _Role collection except by master key
|
// Disallowing access to the _Role collection except by master key
|
||||||
function enforceRoleSecurity(method, className, auth) {
|
function enforceRoleSecurity(method, className, auth) {
|
||||||
if (className === '_Installation' && !auth.isMaster) {
|
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
|
||||||
if (method === 'delete' || method === 'find') {
|
if (method === 'delete' || method === 'find') {
|
||||||
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
||||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||||
@@ -261,7 +266,11 @@ function enforceRoleSecurity(method, className, auth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//all volatileClasses are masterKey only
|
//all volatileClasses are masterKey only
|
||||||
if (classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster) {
|
if (
|
||||||
|
classesWithMasterOnlyAccess.indexOf(className) >= 0 &&
|
||||||
|
!auth.isMaster &&
|
||||||
|
!auth.isMaintenance
|
||||||
|
) {
|
||||||
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
||||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user