diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index b41c53f07c..7387d1eeba 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -24,31 +24,33 @@ const nestedOptionTypes = [ 'PasswordPolicyOptions', 'SecurityOptions', 'SchemaOptions', + 'LogLevels', ]; /** The prefix of environment variables for nested options. */ const nestedOptionEnvPrefix = { - 'AccountLockoutOptions': 'PARSE_SERVER_ACCOUNT_LOCKOUT_', - 'CustomPagesOptions': 'PARSE_SERVER_CUSTOM_PAGES_', - 'DatabaseOptions': 'PARSE_SERVER_DATABASE_', - 'FileUploadOptions': 'PARSE_SERVER_FILE_UPLOAD_', - 'IdempotencyOptions': 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', - 'LiveQueryOptions': 'PARSE_SERVER_LIVEQUERY_', - 'LiveQueryServerOptions': 'PARSE_LIVE_QUERY_SERVER_', - 'PagesCustomUrlsOptions': 'PARSE_SERVER_PAGES_CUSTOM_URL_', - 'PagesOptions': 'PARSE_SERVER_PAGES_', - 'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_', - 'ParseServerOptions': 'PARSE_SERVER_', - 'PasswordPolicyOptions': 'PARSE_SERVER_PASSWORD_POLICY_', - 'SecurityOptions': 'PARSE_SERVER_SECURITY_', - 'SchemaOptions': 'PARSE_SERVER_SCHEMA_', + AccountLockoutOptions: 'PARSE_SERVER_ACCOUNT_LOCKOUT_', + CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_', + DatabaseOptions: 'PARSE_SERVER_DATABASE_', + FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_', + IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', + LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', + LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', + PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', + PagesOptions: 'PARSE_SERVER_PAGES_', + PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', + ParseServerOptions: 'PARSE_SERVER_', + PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', + SecurityOptions: 'PARSE_SERVER_SECURITY_', + SchemaOptions: 'PARSE_SERVER_SCHEMA_', + LogLevels: 'PARSE_SERVER_LOG_LEVELS_', }; function last(array) { return array[array.length - 1]; } -const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; function toENV(key) { let str = ''; let previousIsUpper = false; @@ -68,13 +70,15 @@ function toENV(key) { } function getCommentValue(comment) { - if (!comment) { return } + if (!comment) { + return; + } return comment.value.trim(); } function getENVPrefix(iface) { if (nestedOptionEnvPrefix[iface.id.name]) { - return nestedOptionEnvPrefix[iface.id.name] + return nestedOptionEnvPrefix[iface.id.name]; } } @@ -86,11 +90,11 @@ function processProperty(property, iface) { if (!firstComment) { return; } - const lines = firstComment.split('\n').map((line) => line.trim()); + const lines = firstComment.split('\n').map(line => line.trim()); let help = ''; let envLine; let defaultLine; - lines.forEach((line) => { + lines.forEach(line => { if (line.indexOf(':ENV:') === 0) { envLine = line; } else if (line.indexOf(':DEFAULT:') === 0) { @@ -103,7 +107,7 @@ function processProperty(property, iface) { if (envLine) { env = envLine.split(' ')[1]; } else { - env = (prefix + toENV(name)); + env = prefix + toENV(name); } let defaultValue; if (defaultLine) { @@ -123,21 +127,20 @@ function processProperty(property, iface) { defaultValue, types: property.value.types, typeAnnotation: property.value.typeAnnotation, - required: isRequired + required: isRequired, }; } - function doInterface(iface) { return iface.body.properties .sort((a, b) => a.key.name.localeCompare(b.key.name)) - .map((prop) => processProperty(prop, iface)) - .filter((e) => e !== undefined); + .map(prop => processProperty(prop, iface)) + .filter(e => e !== undefined); } function mapperFor(elt, t) { const p = t.identifier('parsers'); - const wrap = (identifier) => t.memberExpression(p, identifier); + const wrap = identifier => t.memberExpression(p, identifier); if (t.isNumberTypeAnnotation(elt)) { return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]); @@ -171,27 +174,29 @@ function parseDefaultValue(elt, value, t) { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } else if (t.isArrayTypeAnnotation(elt)) { const array = parsers.objectParser(value); - literalValue = t.arrayExpression(array.map((value) => { - if (typeof value == 'string') { - return t.stringLiteral(value); - } else if (typeof value == 'number') { - return t.numericLiteral(value); - } else if (typeof value == 'object') { - const object = parsers.objectParser(value); - const props = Object.entries(object).map(([k, v]) => { - if (typeof v == 'string') { - return t.objectProperty(t.identifier(k), t.stringLiteral(v)); - } else if (typeof v == 'number') { - return t.objectProperty(t.identifier(k), t.numericLiteral(v)); - } else if (typeof v == 'boolean') { - return t.objectProperty(t.identifier(k), t.booleanLiteral(v)); - } - }); - return t.objectExpression(props); - } else { - throw new Error('Unable to parse array'); - } - })); + literalValue = t.arrayExpression( + array.map(value => { + if (typeof value == 'string') { + return t.stringLiteral(value); + } else if (typeof value == 'number') { + return t.numericLiteral(value); + } else if (typeof value == 'object') { + const object = parsers.objectParser(value); + const props = Object.entries(object).map(([k, v]) => { + if (typeof v == 'string') { + return t.objectProperty(t.identifier(k), t.stringLiteral(v)); + } else if (typeof v == 'number') { + return t.objectProperty(t.identifier(k), t.numericLiteral(v)); + } else if (typeof v == 'boolean') { + return t.objectProperty(t.identifier(k), t.booleanLiteral(v)); + } + }); + return t.objectExpression(props); + } else { + throw new Error('Unable to parse array'); + } + }) + ); } else if (t.isAnyTypeAnnotation(elt)) { literalValue = t.arrayExpression([]); } else if (t.isBooleanTypeAnnotation(elt)) { @@ -204,15 +209,16 @@ function parseDefaultValue(elt, value, t) { if (nestedOptionTypes.includes(type)) { const object = parsers.objectParser(value); - const props = Object.keys(object).map((key) => { + const props = Object.keys(object).map(key => { return t.objectProperty(key, object[value]); }); literalValue = t.objectExpression(props); } if (type == 'ProtectedFields') { const prop = t.objectProperty( - t.stringLiteral('_User'), t.objectPattern([ - t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])) + t.stringLiteral('_User'), + t.objectPattern([ + t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])), ]) ); literalValue = t.objectExpression([prop]); @@ -223,62 +229,69 @@ function parseDefaultValue(elt, value, t) { function inject(t, list) { let comments = ''; - const results = list.map((elt) => { - if (!elt.name) { - return; - } - const props = ['env', 'help'].map((key) => { - if (elt[key]) { - return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key])); + const results = list + .map(elt => { + if (!elt.name) { + return; } - }).filter((e) => e !== undefined); - if (elt.required) { - props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true))) - } - const action = mapperFor(elt, t); - if (action) { - props.push(t.objectProperty(t.stringLiteral('action'), action)) - } - if (elt.defaultValue) { - const parsedValue = parseDefaultValue(elt, elt.defaultValue, t); - if (parsedValue) { - props.push(t.objectProperty(t.stringLiteral('default'), parsedValue)); - } else { - throw new Error(`Unable to parse value for ${elt.name} `); + const props = ['env', 'help'] + .map(key => { + if (elt[key]) { + return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key])); + } + }) + .filter(e => e !== undefined); + if (elt.required) { + props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true))); } - } - let type = elt.type.replace('TypeAnnotation', ''); - if (type === 'Generic') { - type = elt.typeAnnotation.id.name; - } - if (type === 'Array') { - type = elt.typeAnnotation.elementType.id - ? `${elt.typeAnnotation.elementType.id.name}[]` - : `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; - } - if (type === 'NumberOrBoolean') { - type = 'Number|Boolean'; - } - if (type === 'NumberOrString') { - type = 'Number|String'; - } - if (type === 'Adapter') { - const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name; - type = `Adapter<${adapterType}>`; - } - comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`; - const obj = t.objectExpression(props); - return t.objectProperty(t.stringLiteral(elt.name), obj); - }).filter((elt) => { - return elt != undefined; - }); + const action = mapperFor(elt, t); + if (action) { + props.push(t.objectProperty(t.stringLiteral('action'), action)); + } + if (elt.defaultValue) { + const parsedValue = parseDefaultValue(elt, elt.defaultValue, t); + if (parsedValue) { + props.push(t.objectProperty(t.stringLiteral('default'), parsedValue)); + } else { + throw new Error(`Unable to parse value for ${elt.name} `); + } + } + let type = elt.type.replace('TypeAnnotation', ''); + if (type === 'Generic') { + type = elt.typeAnnotation.id.name; + } + if (type === 'Array') { + type = elt.typeAnnotation.elementType.id + ? `${elt.typeAnnotation.elementType.id.name}[]` + : `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; + } + if (type === 'NumberOrBoolean') { + type = 'Number|Boolean'; + } + if (type === 'NumberOrString') { + type = 'Number|String'; + } + if (type === 'Adapter') { + const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name; + type = `Adapter<${adapterType}>`; + } + comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`; + const obj = t.objectExpression(props); + return t.objectProperty(t.stringLiteral(elt.name), obj); + }) + .filter(elt => { + return elt != undefined; + }); return { results, comments }; } const makeRequire = function (variableName, module, t) { - const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)])); - return t.variableDeclaration('var', [decl]) -} + const decl = t.variableDeclarator( + t.identifier(variableName), + t.callExpression(t.identifier('require'), [t.stringLiteral(module)]) + ); + return t.variableDeclaration('var', [decl]); +}; let docs = ``; const plugin = function (babel) { const t = babel.types; @@ -294,27 +307,34 @@ const plugin = function (babel) { }, ExportDeclaration: function (path) { // Export declaration on an interface - if (path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration') { + if ( + path.node && + path.node.declaration && + path.node.declaration.type == 'InterfaceDeclaration' + ) { const { results, comments } = inject(t, doInterface(path.node.declaration)); const id = path.node.declaration.id.name; const exports = t.memberExpression(moduleExports, t.identifier(id)); docs += `/**\n * @interface ${id}\n${comments} */\n\n`; - path.replaceWith( - t.assignmentExpression('=', exports, t.objectExpression(results)) - ) + path.replaceWith(t.assignmentExpression('=', exports, t.objectExpression(results))); } - } - } - } + }, + }, + }; }; const auxiliaryCommentBefore = ` **** GENERATED CODE **** This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js -` +`; -const babel = require("@babel/core"); -const res = babel.transformFileSync('./src/Options/index.js', { plugins: [plugin, '@babel/transform-flow-strip-types'], babelrc: false, auxiliaryCommentBefore, sourceMaps: false }); +const babel = require('@babel/core'); +const res = babel.transformFileSync('./src/Options/index.js', { + plugins: [plugin, '@babel/transform-flow-strip-types'], + babelrc: false, + auxiliaryCommentBefore, + sourceMaps: false, +}); require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n'); require('fs').writeFileSync('./src/Options/docs.js', docs); diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 162a3390b7..2cde9de640 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -182,6 +182,41 @@ describe('Cloud Code Logger', () => { }); }); + it('should log cloud function triggers using the custom log level', async () => { + Parse.Cloud.beforeSave('TestClass', () => {}); + Parse.Cloud.afterSave('TestClass', () => {}); + + const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => { + await reconfigureServer({ + silent: true, + logLevel, + logLevels: { + triggerAfter, + triggerBeforeSuccess, + }, + }); + + spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); + const obj = new Parse.Object('TestClass'); + await obj.save(); + + return { + beforeSave: spy.calls + .allArgs() + .find(log => log[1].startsWith('beforeSave triggered for TestClass for user '))?.[0], + afterSave: spy.calls + .allArgs() + .find(log => log[1].startsWith('afterSave triggered for TestClass for user '))?.[0], + }; + }; + + let calls = await execTest('silly', 'silly', 'debug'); + expect(calls).toEqual({ beforeSave: 'silly', afterSave: 'debug' }); + + calls = await execTest('info', 'warn', 'debug'); + expect(calls).toEqual({ beforeSave: 'warn', afterSave: undefined }); + }); + it('should log cloud function failure', done => { Parse.Cloud.define('aFunction', () => { throw 'it failed!'; diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 8ff4d2b8eb..f2b9c48fad 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -254,7 +254,7 @@ export class GridFSBucketAdapter extends FilesAdapter { stream.on('data', chunk => { res.write(chunk); }); - stream.on('error', (e) => { + stream.on('error', e => { res.status(404); res.send(e.message); }); diff --git a/src/Config.js b/src/Config.js index d2cd3b94f8..f7deb7bcdf 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,19 +2,21 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. +import { isBoolean, isString } from 'lodash'; +import net from 'net'; import AppCache from './cache'; import DatabaseController from './Controllers/DatabaseController'; -import net from 'net'; +import { logLevels as validLogLevels } from './Controllers/LoggerController'; import { - IdempotencyOptions, - FileUploadOptions, AccountLockoutOptions, + FileUploadOptions, + IdempotencyOptions, + LogLevels, PagesOptions, - SecurityOptions, - SchemaOptions, ParseServerOptions, + SchemaOptions, + SecurityOptions, } from './Options/Definitions'; -import { isBoolean, isString } from 'lodash'; function removeTrailingSlash(str) { if (!str) { @@ -82,6 +84,7 @@ export class Config { schema, requestKeywordDenylist, allowExpiredAuthDataToken, + logLevels, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -123,6 +126,7 @@ export class Config { this.validateEnforcePrivateUsers(enforcePrivateUsers); this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken); this.validateRequestKeywordDenylist(requestKeywordDenylist); + this.validateLogLevels(logLevels); } static validateRequestKeywordDenylist(requestKeywordDenylist) { @@ -501,6 +505,18 @@ export class Config { } } + static validateLogLevels(logLevels) { + for (const key of Object.keys(LogLevels)) { + if (logLevels[key]) { + if (validLogLevels.indexOf(logLevels[key]) === -1) { + throw `'${key}' must be one of ${JSON.stringify(validLogLevels)}`; + } + } else { + logLevels[key] = LogLevels[key].default; + } + } + } + generateEmailVerifyTokenExpiresAt() { if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { return undefined; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index 8ee492cf4b..8466e5459a 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -16,7 +16,7 @@ export const LogOrder = { ASCENDING: 'asc', }; -const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly']; +export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly']; export class LoggerController extends AdaptableController { constructor(adapter, appId, options = { logLevel: 'info' }) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 548f40e3ed..7d6ed16677 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -290,6 +290,12 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_LOG_LEVEL', help: 'Sets the level for logs', }, + logLevels: { + env: 'PARSE_SERVER_LOG_LEVELS', + help: '(Optional) Overrides the log levels used internally by Parse Server to log events.', + action: parsers.objectParser, + default: {}, + }, logsFolder: { env: 'PARSE_SERVER_LOGS_FOLDER', help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", @@ -898,3 +904,23 @@ module.exports.AuthAdapter = { default: true, }, }; +module.exports.LogLevels = { + triggerAfter: { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', + help: + 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`.', + default: 'info', + }, + triggerBeforeError: { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', + help: + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `.', + default: 'error', + }, + triggerBeforeSuccess: { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', + help: + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`.', + default: 'info', + }, +}; diff --git a/src/Options/docs.js b/src/Options/docs.js index fa67846a64..7dee3ef995 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -56,6 +56,7 @@ * @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server) * @property {Adapter} loggerAdapter Adapter module for the logging sub-system * @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 {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging * @property {String} masterKey Your Parse Master Key * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

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.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. @@ -215,3 +216,10 @@ * @interface AuthAdapter * @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise. */ + +/** + * @interface LogLevels + * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`. + * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `. + * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`. + */ diff --git a/src/Options/index.js b/src/Options/index.js index 7998f0ee09..3458497b63 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -1,11 +1,11 @@ // @flow import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; -import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; -import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; -import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { CacheAdapter } from '../Adapters/Cache/CacheAdapter'; import { MailAdapter } from '../Adapters/Email/MailAdapter'; +import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; +import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter'; +import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter'; import { CheckGroup } from '../Security/CheckGroup'; @@ -81,6 +81,9 @@ export interface ParseServerOptions { verbose: ?boolean; /* Sets the level for logs */ logLevel: ?string; + /* (Optional) Overrides the log levels used internally by Parse Server to log events. + :DEFAULT: {} */ + logLevels: ?LogLevels; /* 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) */ maxLogFiles: ?NumberOrString; /* Disables console output @@ -520,3 +523,18 @@ export interface AuthAdapter { */ enabled: ?boolean; } + +export interface LogLevels { + /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`. + :DEFAULT: info + */ + triggerAfter: ?string; + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`. + :DEFAULT: info + */ + triggerBeforeSuccess: ?string; + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `. + :DEFAULT: error + */ + triggerBeforeError: ?string; +} diff --git a/src/Security/CheckGroups/CheckGroupDatabase.js b/src/Security/CheckGroups/CheckGroupDatabase.js index d0da79a4ba..f9b9340eb1 100644 --- a/src/Security/CheckGroups/CheckGroupDatabase.js +++ b/src/Security/CheckGroups/CheckGroupDatabase.js @@ -8,9 +8,9 @@ import Config from '../../Config'; import Parse from 'parse/node'; /** -* The security checks group for Parse Server configuration. -* Checks common Parse Server parameters such as access keys. -*/ + * The security checks group for Parse Server configuration. + * Checks common Parse Server parameters such as access keys. + */ class CheckGroupDatabase extends CheckGroup { setName() { return 'Database'; @@ -23,7 +23,8 @@ class CheckGroupDatabase extends CheckGroup { new Check({ title: 'Secure database password', warning: 'The database password is insecure and vulnerable to brute force attacks.', - solution: 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.', + solution: + 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.', check: () => { const password = databaseUrl.match(/\/\/\S+:(\S+)@/)[1]; const hasUpperCase = /[A-Z]/.test(password); diff --git a/src/triggers.js b/src/triggers.js index 4ba21b32ea..b5f11435df 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -373,9 +373,9 @@ function userIdForLog(auth) { return auth && auth.user ? auth.user.id : undefined; } -function logTriggerAfterHook(triggerType, className, input, auth) { +function logTriggerAfterHook(triggerType, className, input, auth, logLevel) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); - logger.info( + logger[logLevel]( `${triggerType} triggered for ${className} for user ${userIdForLog( auth )}:\n Input: ${cleanInput}`, @@ -387,10 +387,10 @@ function logTriggerAfterHook(triggerType, className, input, auth) { ); } -function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth) { +function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth, logLevel) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); const cleanResult = logger.truncateLogMessage(JSON.stringify(result)); - logger.info( + logger[logLevel]( `${triggerType} triggered for ${className} for user ${userIdForLog( auth )}:\n Input: ${cleanInput}\n Result: ${cleanResult}`, @@ -402,9 +402,9 @@ function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth ); } -function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { +function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, logLevel) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); - logger.error( + logger[logLevel]( `${triggerType} failed for ${className} for user ${userIdForLog( auth )}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`, @@ -444,7 +444,14 @@ export function maybeRunAfterFindTrigger( reject(error); } ); - logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth); + logTriggerSuccessBeforeHook( + triggerType, + className, + 'AfterFind', + JSON.stringify(objects), + auth, + config.logLevels.triggerBeforeSuccess + ); request.objects = objects.map(object => { //setting the class name to transform into parse object object.className = className; @@ -468,7 +475,13 @@ export function maybeRunAfterFindTrigger( }) .then(success, error); }).then(results => { - logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth); + logTriggerAfterHook( + triggerType, + className, + JSON.stringify(results), + auth, + config.logLevels.triggerAfter + ); return results; }); } @@ -842,7 +855,10 @@ export function maybeRunTrigger( parseObject.className, parseObject.toJSON(), object, - auth + auth, + triggerType.startsWith('after') + ? config.logLevels.triggerAfter + : config.logLevels.triggerBeforeSuccess ); if ( triggerType === Types.beforeSave || @@ -860,7 +876,8 @@ export function maybeRunTrigger( parseObject.className, parseObject.toJSON(), auth, - error + error, + config.logLevels.triggerBeforeError ); reject(error); } @@ -885,7 +902,13 @@ export function maybeRunTrigger( triggerType === Types.afterDelete || triggerType === Types.afterLogin ) { - logTriggerAfterHook(triggerType, parseObject.className, parseObject.toJSON(), auth); + logTriggerAfterHook( + triggerType, + parseObject.className, + parseObject.toJSON(), + auth, + config.logLevels.triggerAfter + ); } // beforeSave is expected to return null (nothing) if (triggerType === Types.beforeSave) { @@ -965,7 +988,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, result, - auth + auth, + config.logLevels.triggerBeforeSuccess ); return result || fileObject; } catch (error) { @@ -974,7 +998,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, auth, - error + error, + config.logLevels.triggerBeforeError ); throw error; }