diff --git a/config/roles.json b/config/roles.json index 9b1b269d..42d224a0 100644 --- a/config/roles.json +++ b/config/roles.json @@ -237,12 +237,12 @@ }, "userlisting": { - "read:own": ["*", "!tags", "!metaData", "!disabledScopes"] + "read:own": ["*", "!tags", "!disabledScopes", "!internalData"] }, "users": { - "read:own": ["*", "!tags", "!metaData", "!disabledScopes"], - "update:own": ["*", "!tags", "!metaData", "!disabledScopes"] + "read:own": ["*", "!tags", "!disabledScopes", "!internalData"], + "update:own": ["*", "!tags", "!disabledScopes", "!internalData"] }, "asps": { diff --git a/lib/api/users.js b/lib/api/users.js index 001ca841..2a6a90e4 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -31,6 +31,7 @@ module.exports = (db, server, userHandler) => { * @apiParam {String} [tags] Comma separated list of tags. The User must have at least one to be set * @apiParam {String} [requiredTags] Comma separated list of tags. The User must have all listed tags to be set * @apiParam {Boolean} [metaData] If true, then includes metaData in the response + * @apiParam {Boolean} [internalData] If true, then includes internalData in the response. Not shown for user-role tokens. * @apiParam {Number} [limit=20] How many records to return * @apiParam {Number} [page=1] Current page number. Informational only, page numbers start from 1 * @apiParam {Number} [next] Cursor value for next page, retrieved from nextCursor response value @@ -54,6 +55,7 @@ module.exports = (db, server, userHandler) => { * @apiSuccess {Boolean} results.encryptForwarded If true then forwarded messages are encrypted * @apiSuccess {Object} results.quota Quota usage limits * @apiSuccess {Object} [results.metaData] Custom metadata value. Included if metaData query argument was true + * @apiSuccess {Object} [results.internalData] Custom metadata value for internal use. Included if internalData query argument was true and request was not made using user-role token * @apiSuccess {Number} results.quota.allowed Allowed quota of the user in bytes * @apiSuccess {Number} results.quota.used Space used in bytes * @apiSuccess {Boolean} results.hasPasswordSet If true then the User has a password set and can authenticate @@ -114,6 +116,7 @@ module.exports = (db, server, userHandler) => { tags: Joi.string().trim().empty('').max(1024), requiredTags: Joi.string().trim().empty('').max(1024), metaData: booleanSchema, + internalData: booleanSchema, limit: Joi.number().default(20).min(1).max(250), next: nextPageCursorSchema, previous: previousPageCursorSchema, @@ -253,6 +256,10 @@ module.exports = (db, server, userHandler) => { opts.fields.projection.metaData = true; } + if (result.value.internalData) { + opts.fields.projection.internalData = true; + } + if (pageNext) { opts.next = pageNext; } else if ((!page || page > 1) && pagePrevious) { @@ -307,6 +314,10 @@ module.exports = (db, server, userHandler) => { values.metaData = formatMetaData(userData.metaData); } + if (userData.internalData) { + values.internalData = formatMetaData(userData.internalData); + } + return permission.filter(values); }) }; @@ -342,6 +353,7 @@ module.exports = (db, server, userHandler) => { * @apiParam {Boolean} [encryptForwarded] If true then forwarded messages are encrypted * @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key * @apiParam {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object + * @apiParam {Object|String} [internalData] Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens * @apiParam {String} [language] Language code for the User * @apiParam {String[]} [targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to * @apiParam {Number} [spamLevel=50] Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam @@ -493,6 +505,14 @@ module.exports = (db, server, userHandler) => { Joi.object() ), + internalData: Joi.alternatives().try( + Joi.string() + .empty('') + .trim() + .max(1024 * 1024), + Joi.object() + ), + pubKey: Joi.string() .empty('') .trim() @@ -659,29 +679,31 @@ module.exports = (db, server, userHandler) => { return next(); } - if (result.value.metaData) { - if (typeof result.value.metaData === 'object') { - try { - result.value.metaData = JSON.stringify(result.value.metaData); - } catch (err) { - res.json({ - error: 'metaData value must be serializable to JSON', - code: 'InputValidationError' - }); - return next(); - } - } else { - try { - let value = JSON.parse(result.value.metaData); - if (!value || typeof value !== 'object') { - throw new Error('Not an object'); + for (let key of ['metaData', 'internalData']) { + if (result.value[key]) { + if (typeof result.value[key] === 'object') { + try { + result.value[key] = JSON.stringify(result.value[key]); + } catch (err) { + res.json({ + error: `${key} value must be serializable to JSON`, + code: 'InputValidationError' + }); + return next(); + } + } else { + try { + let value = JSON.parse(result.value[key]); + if (!value || typeof value !== 'object') { + throw new Error('Not an object'); + } + } catch (err) { + res.json({ + error: `${key} value must be valid JSON object string`, + code: 'InputValidationError' + }); + return next(); } - } catch (err) { - res.json({ - error: 'metaData value must be valid JSON object string', - code: 'InputValidationError' - }); - return next(); } } } @@ -842,6 +864,7 @@ module.exports = (db, server, userHandler) => { * @apiSuccess {String} keyInfo.address E-mail address listed in public key * @apiSuccess {String} keyInfo.fingerprint Fingerprint of the public key * @apiSuccess {Object} metaData Custom metadata object set for this user + * @apiSuccess {Object} internalData Custom interna metadata object set for this user. Not available for user-role tokens * @apiSuccess {String[]} targets List of forwarding targets * @apiSuccess {Number} spamLevel Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam * @apiSuccess {Object} limits Account limits and usage @@ -1088,6 +1111,7 @@ module.exports = (db, server, userHandler) => { keyInfo, metaData: formatMetaData(userData.metaData), + internalData: formatMetaData(userData.internalData), targets: [].concat(userData.targets || []).map(targetData => targetData.value), @@ -1183,6 +1207,7 @@ module.exports = (db, server, userHandler) => { * @apiParam {Boolean} [encryptForwarded] If true then forwarded messages are encrypted * @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key * @apiParam {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object + * @apiParam {Object|String} [internalData] Optional internal metadata, must be an object or JSON formatted string of an object. Not available for user-role tokens * @apiParam {String} [language] Language code for the User * @apiParam {String[]} [targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to * @apiParam {Number} [spamLevel] Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam @@ -1266,6 +1291,14 @@ module.exports = (db, server, userHandler) => { Joi.object() ), + internalData: Joi.alternatives().try( + Joi.string() + .empty('') + .trim() + .max(1024 * 1024), + Joi.object() + ), + pubKey: Joi.string() .empty('') .trim() @@ -1417,29 +1450,31 @@ module.exports = (db, server, userHandler) => { return next(); } - if (result.value.metaData) { - if (typeof result.value.metaData === 'object') { - try { - result.value.metaData = JSON.stringify(result.value.metaData); - } catch (err) { - res.json({ - error: 'metaData value must be serializable to JSON', - code: 'InputValidationError' - }); - return next(); - } - } else { - try { - let value = JSON.parse(result.value.metaData); - if (!value || typeof value !== 'object') { - throw new Error('Not an object'); + for (let key of ['metaData', 'internalData']) { + if (result.value[key]) { + if (typeof result.value[key] === 'object') { + try { + result.value[key] = JSON.stringify(result.value[key]); + } catch (err) { + res.json({ + error: `${key} value must be serializable to JSON`, + code: 'InputValidationError' + }); + return next(); + } + } else { + try { + let value = JSON.parse(result.value[key]); + if (!value || typeof value !== 'object') { + throw new Error('Not an object'); + } + } catch (err) { + res.json({ + error: `${key} value must be valid JSON object string`, + code: 'InputValidationError' + }); + return next(); } - } catch (err) { - res.json({ - error: 'metaData value must be valid JSON object string', - code: 'InputValidationError' - }); - return next(); } } } diff --git a/package.json b/package.json index 9bf87bdd..87e5a004 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "author": "Andris Reinman", "license": "EUPL-1.1+", "devDependencies": { - "ajv": "6.12.4", + "ajv": "6.12.5", "apidoc": "0.25.0", "chai": "4.2.0", "docsify-cli": "4.4.1", - "eslint": "7.8.1", + "eslint": "7.9.0", "eslint-config-nodemailer": "1.2.0", "eslint-config-prettier": "6.11.0", "grunt": "1.3.0", @@ -57,7 +57,7 @@ "mailsplit": "5.0.0", "mobileconfig": "2.3.1", "mongo-cursor-pagination": "7.3.1", - "mongodb": "3.6.1", + "mongodb": "3.6.2", "mongodb-extended-json": "1.11.0", "node-forge": "0.10.0", "nodemailer": "6.4.11", @@ -76,7 +76,7 @@ "unixcrypt": "1.0.11", "uuid": "8.3.0", "wild-config": "1.5.1", - "yargs": "16.0.0" + "yargs": "16.0.3" }, "repository": { "type": "git",