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:
@@ -68,14 +68,22 @@ const specialMasterQueryKeys = [
|
||||
'_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) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||
}
|
||||
|
||||
if (query.$or) {
|
||||
if (query.$or instanceof Array) {
|
||||
query.$or.forEach(value => validateQuery(value, isMaster, update));
|
||||
query.$or.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||
} else {
|
||||
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 instanceof Array) {
|
||||
query.$and.forEach(value => validateQuery(value, isMaster, update));
|
||||
query.$and.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||
} else {
|
||||
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 instanceof Array && query.$nor.length > 0) {
|
||||
query.$nor.forEach(value => validateQuery(value, isMaster, update));
|
||||
query.$nor.forEach(value => validateQuery(value, isMaster, isMaintenance, update));
|
||||
} else {
|
||||
throw new Parse.Error(
|
||||
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.
|
||||
const filterSensitiveData = (
|
||||
isMaster: boolean,
|
||||
isMaintenance: boolean,
|
||||
aclGroup: any[],
|
||||
auth: any,
|
||||
operation: any,
|
||||
@@ -195,6 +204,15 @@ const filterSensitiveData = (
|
||||
}
|
||||
|
||||
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
|
||||
the retrieved user */
|
||||
@@ -208,22 +226,13 @@ const filterSensitiveData = (
|
||||
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) {
|
||||
if (key.charAt(0) === '_') {
|
||||
delete object[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUserClass) {
|
||||
if (!isUserClass || isMaster) {
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -439,7 +448,8 @@ class DatabaseController {
|
||||
className: string,
|
||||
object: any,
|
||||
query: any,
|
||||
runOptions: QueryOptions
|
||||
runOptions: QueryOptions,
|
||||
maintenance: boolean
|
||||
): Promise<boolean> {
|
||||
let schema;
|
||||
const acl = runOptions.acl;
|
||||
@@ -454,7 +464,7 @@ class DatabaseController {
|
||||
return this.canAddField(schema, className, object, aclGroup, runOptions);
|
||||
})
|
||||
.then(() => {
|
||||
return schema.validateObject(className, object, query);
|
||||
return schema.validateObject(className, object, query, maintenance);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -512,7 +522,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query, isMaster, true);
|
||||
validateQuery(query, isMaster, false, true);
|
||||
return schemaController
|
||||
.getOneSchema(className, true)
|
||||
.catch(error => {
|
||||
@@ -758,7 +768,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query, isMaster, false);
|
||||
validateQuery(query, isMaster, false, false);
|
||||
return schemaController
|
||||
.getOneSchema(className)
|
||||
.catch(error => {
|
||||
@@ -1151,7 +1161,8 @@ class DatabaseController {
|
||||
auth: any = {},
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
const isMaster = acl === undefined;
|
||||
const isMaintenance = auth.isMaintenance;
|
||||
const isMaster = acl === undefined || isMaintenance;
|
||||
const aclGroup = acl || [];
|
||||
op =
|
||||
op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
||||
@@ -1253,7 +1264,7 @@ class DatabaseController {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
}
|
||||
validateQuery(query, isMaster, false);
|
||||
validateQuery(query, isMaster, isMaintenance, false);
|
||||
if (count) {
|
||||
if (!classExists) {
|
||||
return 0;
|
||||
@@ -1296,6 +1307,7 @@ class DatabaseController {
|
||||
object = untransformObjectACL(object);
|
||||
return filterSensitiveData(
|
||||
isMaster,
|
||||
isMaintenance,
|
||||
aclGroup,
|
||||
auth,
|
||||
op,
|
||||
@@ -1813,8 +1825,8 @@ class DatabaseController {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
static _validateQuery: (any, boolean, boolean) => void;
|
||||
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
|
||||
static _validateQuery: (any, boolean, boolean, boolean) => void;
|
||||
static filterSensitiveData: (boolean, boolean, any[], any, any, any, string, any[], any) => void;
|
||||
}
|
||||
|
||||
module.exports = DatabaseController;
|
||||
|
||||
@@ -1071,14 +1071,19 @@ export default class SchemaController {
|
||||
className: string,
|
||||
fieldName: string,
|
||||
type: string | SchemaField,
|
||||
isValidation?: boolean
|
||||
isValidation?: boolean,
|
||||
maintenance?: boolean
|
||||
) {
|
||||
if (fieldName.indexOf('.') > 0) {
|
||||
// subdocument key (x.y) => ok if x is of type 'object'
|
||||
fieldName = fieldName.split('.')[0];
|
||||
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}.`);
|
||||
}
|
||||
|
||||
@@ -1228,7 +1233,7 @@ export default class SchemaController {
|
||||
// Validates an object provided in REST format.
|
||||
// Returns a promise that resolves to the new schema if this object is
|
||||
// valid.
|
||||
async validateObject(className: string, object: any, query: any) {
|
||||
async validateObject(className: string, object: any, query: any, maintenance: boolean) {
|
||||
let geocount = 0;
|
||||
const schema = await this.enforceClassExists(className);
|
||||
const promises = [];
|
||||
@@ -1258,7 +1263,7 @@ export default class SchemaController {
|
||||
// Every object has ACL implicitly.
|
||||
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 enforceFields = results.filter(result => !!result);
|
||||
|
||||
@@ -69,20 +69,17 @@ export class UserController extends AdaptableController {
|
||||
|
||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||
}
|
||||
const masterAuth = Auth.master(this.config);
|
||||
var findUserForEmailVerification = new RestQuery(
|
||||
this.config,
|
||||
Auth.master(this.config),
|
||||
'_User',
|
||||
{ username: username }
|
||||
);
|
||||
const maintenanceAuth = Auth.maintenance(this.config);
|
||||
var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
|
||||
username,
|
||||
});
|
||||
return findUserForEmailVerification.execute().then(result => {
|
||||
if (result.results.length && result.results[0].emailVerified) {
|
||||
return Promise.resolve(result.results.length[0]);
|
||||
} else if (result.results.length) {
|
||||
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,
|
||||
_perishable_token: token,
|
||||
},
|
||||
{ limit: 1 }
|
||||
{ limit: 1 },
|
||||
Auth.maintenance(this.config)
|
||||
)
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
@@ -228,7 +226,8 @@ export class UserController extends AdaptableController {
|
||||
{ username: email, email: { $exists: false }, _perishable_token: { $exists: true } },
|
||||
],
|
||||
},
|
||||
{ limit: 1 }
|
||||
{ limit: 1 },
|
||||
Auth.maintenance(this.config)
|
||||
);
|
||||
if (results.length == 1) {
|
||||
let expiresDate = results[0]._perishable_token_expires_at;
|
||||
|
||||
Reference in New Issue
Block a user