From 81ae0e6ab2563e6ed5835f55c4b549becd28b235 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:47:15 -0600 Subject: [PATCH 01/14] Minor refactors --- .../cloud-variables/cloud-variables.js | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 6c252c93..588663a2 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -25,13 +25,26 @@ const getCollections = function() { return _collections; }; +/** + * Throws an error if the given variable does not exist + * @param {Object?} variable Variable to test + */ const ensureVariableExists = function(variable) { if (!variable) { throw new Error('Variable not found'); } }; +/** + * Maximum duration of a locked public variable + */ let MAX_LOCK_AGE = 5 * 1000; + +/** + * Get the owner of a variable's lock, if it exists and has a currently valid one + * @param {Object} variable Variable to get lock owner of + * @returns Owner of variable's lock if there is one + */ const getLockOwnerId = function(variable) { if (variable && variable.lock) { if (!isLockStale(variable)) { @@ -40,17 +53,32 @@ const getLockOwnerId = function(variable) { } }; +/** + * Determine if a variable has a stale lock + * @param {Object} variable + * @returns {Boolean} True if variable has been locked longer than MAX_LOCK_AGE, false otherwise + */ const isLockStale = function(variable) { if (variable && variable.lock) { - return new Date() - variable.lock.creationTime > MAX_LOCK_AGE; + return (new Date() - variable.lock.creationTime) > MAX_LOCK_AGE; } return false; }; +/** + * Determine if a variable is locked + * @param {Object} variable + * @returns {Boolean} If variable is locked + */ const isLocked = function(variable) { return !!getLockOwnerId(variable); }; +/** + * Throw an error if the variable is locked by another client + * @param {Object} variable Variable to test + * @param {Object} clientId Client attempting to access variable + */ const ensureOwnsMutex = function(variable, clientId) { const ownerId = getLockOwnerId(variable); if (ownerId && ownerId !== clientId) { @@ -58,6 +86,11 @@ const ensureOwnsMutex = function(variable, clientId) { } }; +/** + * Throw an error if the given password does not match + * @param {Object} variable Variable to test + * @param {String} password Password to test + */ const ensureAuthorized = function(variable, password) { if (variable) { const authorized = !variable.password || @@ -69,22 +102,38 @@ const ensureAuthorized = function(variable, password) { } }; +/** + * Throw an error if the user is not logged in + * @param {Object} caller User to test + */ const ensureLoggedIn = function(caller) { if (!caller.username) { throw new Error('Login required.'); } }; +/** + * Throw an error if given variable name is not valid + * @param {String} name Variable name to test + */ const validateVariableName = function(name) { if (!/^[\w _()-]+$/.test(name)) { throw new Error('Invalid variable name.'); } }; +/** + * Size, in bytes, of maximum variable content + */ +const MAX_CONTENT_SIZE = 4 * 1024 * 1024; + +/** + * Throws an error if content is too large to store in a cloud variable + * @param {Object} content Content to test + */ const validateContentSize = function(content) { const sizeInBytes = content.length*2; // assuming utf8. Figure ~2 bytes per char - const mb = 1024*1024; - if (sizeInBytes > (4*mb)) { + if (sizeInBytes > MAX_CONTENT_SIZE) { throw new Error('Variable value is too large.'); } }; From 54fc50faede253941ebd8fe9782606c8abbf5bb1 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:28:54 -0600 Subject: [PATCH 02/14] Initial version --- .../cloud-variables/cloud-variables.js | 197 +++++++++++++++++- 1 file changed, 186 insertions(+), 11 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 588663a2..5c7233d1 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -86,6 +86,17 @@ const ensureOwnsMutex = function(variable, clientId) { } }; +/** + * Determine if a given password is valid for a variable + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @returns {Boolean} If the password is correct or the variable has no password + */ +function isAuthorized(variable, password) { + return !variable.password || + variable.password === password; +} + /** * Throw an error if the given password does not match * @param {Object} variable Variable to test @@ -93,15 +104,29 @@ const ensureOwnsMutex = function(variable, clientId) { */ const ensureAuthorized = function(variable, password) { if (variable) { - const authorized = !variable.password || - variable.password === password; - - if (!authorized) { + if (!isAuthorized(variable, password)) { throw new Error('Unauthorized: incorrect password'); } } }; +/** + * Throw an error if the given username is not the owner of the variable + * @param {Object} variable Variable to test + * @param {String} username Username to test + */ +const ensureOwnsVariable = function(variable, username){ + if (variable && variable.creator) { + if(variable.creator !== username){ + throw new Error('You do not own this variable'); + } + } + + if (variable && !variable.creator) { + throw new Error('You do not own this variable'); + } +} + /** * Throw an error if the user is not logged in * @param {Object} caller User to test @@ -122,6 +147,64 @@ const validateVariableName = function(name) { } }; +// Mapping of access levels and their full names +const accessLevelNames = { + 'r': 'read', + 'w': 'write', + 'a': 'append', + 'd': 'delete', + 'l': 'lock' +}; + +// Default access level (when correct password is provided), giving full access +const DEFAULT_WITH_PASSWORD_ACCESS = Object.keys(accessLevelNames).join(''); + +// Default access level (when correct password is provided), giving no access +const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; + +/** + * Get the available actions for a variable with the provided authentication. + * If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @param {String} username Username to test + * @returns {String} Access level string + */ +const getAccessLevel = function(variable, password, username) { + if(variable){ + // Creator has full access always + if(variable.creator && variable.creator === username){ + return DEFAULT_WITH_PASSWORD_ACCESS; + } + + if(isAuthorized(variable, password)){ + return variable.withPasswordAccess || DEFAULT_WITH_PASSWORD_ACCESS; + } else { + return variable.withoutPasswordAccess || DEFAULT_WITHOUT_PASSWORD_ACCESS; + } + } + + return DEFAULT_WITH_PASSWORD_ACCESS; +}; + +/** + * Throws an error if the requested access type is not allowed + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @param {String} username Username to test + * @param {String} type Access level to test + */ +const ensureHasAccessLevel = function(variable, password, username, type) { + console.log(getAccessLevel(variable, password, username)); + if(!getAccessLevel(variable, password, username).includes(type)){ + if(type in accessLevelNames){ + throw new Error(`You are not authorized to ${accessLevelNames[type]} this variable, please check your password`); + } else { + throw new Error(`You are not authorized to perform that action on this variable, please check your password`); + } + } +}; + /** * Size, in bytes, of maximum variable content */ @@ -156,7 +239,7 @@ CloudVariables.getVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'r'); const query = { $set: { @@ -188,7 +271,7 @@ CloudVariables._sendUpdate = function(name, value, targets) { * @param {Any} value Value to store in variable * @param {String=} password Password (if password-protected) */ -CloudVariables.setVariable = async function(name, value, password) { + CloudVariables.setVariable = async function(name, value, password) { validateVariableName(name); validateContentSize(value); @@ -196,15 +279,65 @@ CloudVariables.setVariable = async function(name, value, password) { const username = this.caller.username; const variable = await sharedVars.findOne({name: name}); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'w'); ensureOwnsMutex(variable, this.caller.clientId); + let query; + // Set both the password and value in case it gets deleted // during this async fn... + if(variable || !this.caller.username){ + query = { + $set: { + value, + password, + lastWriter: username, + lastWriteTime: new Date(), + } + }; + } else { + // The variable did not exist, set creator data + query = { + $set: { + value, + password, + creator: username, + createdOn: new Date(), + lastWriter: username, + lastWriteTime: new Date(), + } + }; + } + + await sharedVars.updateOne({name: name}, query, {upsert: true}); + this._sendUpdate(name, value, globalListeners[name] || {}); +}; + +/** + * Append to a list cloud variable. + * @param {String} name Variable name + * @param {Any} value Value to append to variable + * @param {String=} password Password (if password-protected) + */ + CloudVariables.appendToVariable = async function(name, value, password) { + validateVariableName(name); + validateContentSize(value); + + const {sharedVars} = getCollections(); + const username = this.caller.username; + const variable = await sharedVars.findOne({name: name}); + + ensureVariableExists(variable); + ensureHasAccessLevel(variable, password, this.caller.username, 'a'); + ensureOwnsMutex(variable, this.caller.clientId); + + if(typeof(variable.value) !== 'object'){ + throw new Error('Can only append to lists.'); + } + const query = { $set: { - value, - password, + value: [...variable.value, value], lastWriter: username, lastWriteTime: new Date(), } @@ -225,7 +358,7 @@ CloudVariables.deleteVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'd'); // Clear the queued locks const id = variable._id; @@ -252,7 +385,8 @@ CloudVariables.lockVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'l'); + // What if the block is killed before a lock can be acquired? // Then should we close the connection on the client? // @@ -517,4 +651,45 @@ CloudVariables.listenToUserVariable = async function(name, msgType, duration = 6 bucket[this.socket.clientId] = [this.socket, msgType, +new Date() + duration]; }; +/** + * Set the access levels for a public variable. + * + * Create a string combining the following letters (in any order) for each category: + * 'r' - Read through the getVariable method + * 'w' - Write through the setVariable method + * 'a' - Append through the appendVariable method + * 'd' - Delete through the deleteVariable method + * 'l' - Lock through the lockVariable method + * + * The default settings give users with the password read, write, append, delete, and lock access ("rwadl"), and users without the password no access. + * The variable's creator will always have full access. + * + * @param {String} name Variable name + * @param {String} withPassword Access level for other users with password + * @param {String} withoutPassword Access level for other users without password + */ +CloudVariables.setVariableAccess = async function(name, withPassword = DEFAULT_WITH_PASSWORD_ACCESS, withoutPassword = DEFAULT_WITHOUT_PASSWORD_ACCESS){ + const filterAccessString = (string) => [...string.toLowerCase()].filter(c => c in accessLevelNames).join(''); + + const withPasswordAccess = filterAccessString(withPassword); + const withoutPasswordAccess = filterAccessString(withoutPassword); + + const {sharedVars} = getCollections(); + const variable = await sharedVars.findOne({name: name}); + ensureVariableExists(variable); + ensureLoggedIn(this.caller); + ensureOwnsVariable(variable, this.caller.username); + + const query = { + $set: { + withPasswordAccess, + withoutPasswordAccess, + lastWriter: this.caller.username, + lastWriteTime: new Date(), + } + }; + + await sharedVars.updateOne({name: name}, query, {upsert: true}); +}; + module.exports = CloudVariables; From d9fb25fbd22dfcb67bcebc4e056dae225119119e Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:36:54 -0600 Subject: [PATCH 03/14] Simplify comments to avoid warnings --- .../cloud-variables/cloud-variables.js | 116 +++++------------- 1 file changed, 30 insertions(+), 86 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 5c7233d1..3bfa1776 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -16,7 +16,8 @@ const globalListeners = {}; // map>> let _collections = null; -const getCollections = function() { + +function getCollections() { if (!_collections) { _collections = {}; _collections.sharedVars = Storage.create('cloud-variables:shared').collection; @@ -25,27 +26,18 @@ const getCollections = function() { return _collections; }; -/** - * Throws an error if the given variable does not exist - * @param {Object?} variable Variable to test - */ -const ensureVariableExists = function(variable) { +// Throws an error if the given variable does not exist +function ensureVariableExists(variable) { if (!variable) { throw new Error('Variable not found'); } }; -/** - * Maximum duration of a locked public variable - */ +// Maximum duration of a locked public variable let MAX_LOCK_AGE = 5 * 1000; -/** - * Get the owner of a variable's lock, if it exists and has a currently valid one - * @param {Object} variable Variable to get lock owner of - * @returns Owner of variable's lock if there is one - */ -const getLockOwnerId = function(variable) { +// Get the owner of a variable's lock, if it exists and has a currently valid one +function getLockOwnerId(variable) { if (variable && variable.lock) { if (!isLockStale(variable)) { return variable.lock.clientId; @@ -53,56 +45,35 @@ const getLockOwnerId = function(variable) { } }; -/** - * Determine if a variable has a stale lock - * @param {Object} variable - * @returns {Boolean} True if variable has been locked longer than MAX_LOCK_AGE, false otherwise - */ -const isLockStale = function(variable) { +// Determine if a variable has a stale lock +function isLockStale(variable) { if (variable && variable.lock) { return (new Date() - variable.lock.creationTime) > MAX_LOCK_AGE; } return false; }; -/** - * Determine if a variable is locked - * @param {Object} variable - * @returns {Boolean} If variable is locked - */ -const isLocked = function(variable) { +// Determine if a variable is locked +function isLocked(variable) { return !!getLockOwnerId(variable); }; -/** - * Throw an error if the variable is locked by another client - * @param {Object} variable Variable to test - * @param {Object} clientId Client attempting to access variable - */ -const ensureOwnsMutex = function(variable, clientId) { +// Throw an error if the variable is locked by another client +function ensureOwnsMutex(variable, clientId) { const ownerId = getLockOwnerId(variable); if (ownerId && ownerId !== clientId) { throw new Error('Variable is locked (by someone else)'); } }; -/** - * Determine if a given password is valid for a variable - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @returns {Boolean} If the password is correct or the variable has no password - */ +// Determine if a given password is valid for a variable function isAuthorized(variable, password) { return !variable.password || variable.password === password; } -/** - * Throw an error if the given password does not match - * @param {Object} variable Variable to test - * @param {String} password Password to test - */ -const ensureAuthorized = function(variable, password) { +// Throw an error if the given password does not match +function ensureAuthorized(variable, password) { if (variable) { if (!isAuthorized(variable, password)) { throw new Error('Unauthorized: incorrect password'); @@ -110,12 +81,8 @@ const ensureAuthorized = function(variable, password) { } }; -/** - * Throw an error if the given username is not the owner of the variable - * @param {Object} variable Variable to test - * @param {String} username Username to test - */ -const ensureOwnsVariable = function(variable, username){ +// Throw an error if the given username is not the owner of the variable +function ensureOwnsVariable(variable, username) { if (variable && variable.creator) { if(variable.creator !== username){ throw new Error('You do not own this variable'); @@ -127,21 +94,15 @@ const ensureOwnsVariable = function(variable, username){ } } -/** - * Throw an error if the user is not logged in - * @param {Object} caller User to test - */ -const ensureLoggedIn = function(caller) { +// Throw an error if the user is not logged in +function ensureLoggedIn(caller) { if (!caller.username) { throw new Error('Login required.'); } }; -/** - * Throw an error if given variable name is not valid - * @param {String} name Variable name to test - */ -const validateVariableName = function(name) { +// Throw an error if given variable name is not valid +function validateVariableName(name) { if (!/^[\w _()-]+$/.test(name)) { throw new Error('Invalid variable name.'); } @@ -162,15 +123,9 @@ const DEFAULT_WITH_PASSWORD_ACCESS = Object.keys(accessLevelNames).join(''); // Default access level (when correct password is provided), giving no access const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; -/** - * Get the available actions for a variable with the provided authentication. - * If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @param {String} username Username to test - * @returns {String} Access level string - */ -const getAccessLevel = function(variable, password, username) { +// Get the available actions for a variable with the provided authentication. +// If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. +function getAccessLevel(variable, password, username) { if(variable){ // Creator has full access always if(variable.creator && variable.creator === username){ @@ -187,14 +142,8 @@ const getAccessLevel = function(variable, password, username) { return DEFAULT_WITH_PASSWORD_ACCESS; }; -/** - * Throws an error if the requested access type is not allowed - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @param {String} username Username to test - * @param {String} type Access level to test - */ -const ensureHasAccessLevel = function(variable, password, username, type) { +// Throws an error if the requested access type is not allowed +function ensureHasAccessLevel(variable, password, username, type) { console.log(getAccessLevel(variable, password, username)); if(!getAccessLevel(variable, password, username).includes(type)){ if(type in accessLevelNames){ @@ -205,16 +154,11 @@ const ensureHasAccessLevel = function(variable, password, username, type) { } }; -/** - * Size, in bytes, of maximum variable content - */ +// Size, in bytes, of maximum variable content const MAX_CONTENT_SIZE = 4 * 1024 * 1024; -/** - * Throws an error if content is too large to store in a cloud variable - * @param {Object} content Content to test - */ -const validateContentSize = function(content) { +// Throws an error if content is too large to store in a cloud variable +function validateContentSize(content) { const sizeInBytes = content.length*2; // assuming utf8. Figure ~2 bytes per char if (sizeInBytes > MAX_CONTENT_SIZE) { throw new Error('Variable value is too large.'); From b8233885f5f28926fa4a7b681e3de0be8ed82bf5 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Fri, 2 Dec 2022 00:08:18 +0000 Subject: [PATCH 04/14] Restore typos config file --- _typos.toml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 _typos.toml diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..8f470abb --- /dev/null +++ b/_typos.toml @@ -0,0 +1,44 @@ +[files] +extend-exclude = [ + # dear lord why is this not ignored by default + "_typos.toml", + + # general ignored file types + "*.jsonl", + "*.list", + "*.csv", + "*.png", + "*.pdf", + "*.tex", + "*.xml", + "*.svg", + "*.min.js", + + # auto-generated files + "package-lock.json", + + # select submodule subdirectories + "src/browser/test/*", + "src/browser/dist/*", + "src/browser/locale/*", + "src/browser/src/morphic.js", # has german text + "src/server/services/procedures/roboscape/speckjs/*", + + # data files + "src/server/services/procedures/common-words/words/*.txt", + "src/server/services/procedures/hurricane-data/*.txt", + "src/server/services/procedures/ice-core-data/data/domec-deuterium/*.txt", + "src/server/services/procedures/nexrad-radar/RadarLocations.js", + "src/server/services/procedures/word-guess/dict/*.*", + + # misc + "src/browser/src/sha512.js", + "src/common/sha512.js", + "test/utils/sha512.js", +] + +[type.js] +extend-glob = ["*.js"] +[type.js.extend-words] +parms = "parms" # too pervasive and part of the custom block api +parm = "parm" # too pervasive and part of the custom block api From 1ca979a9e97bbf3d9771500ac6b76df0537e4ff3 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Fri, 2 Dec 2022 00:10:23 +0000 Subject: [PATCH 05/14] Update for new structure --- _typos.toml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/_typos.toml b/_typos.toml index 8f470abb..2b40a6a8 100644 --- a/_typos.toml +++ b/_typos.toml @@ -18,23 +18,14 @@ extend-exclude = [ "package-lock.json", # select submodule subdirectories - "src/browser/test/*", - "src/browser/dist/*", - "src/browser/locale/*", - "src/browser/src/morphic.js", # has german text - "src/server/services/procedures/roboscape/speckjs/*", + "src/procedures/roboscape/speckjs/*", # data files - "src/server/services/procedures/common-words/words/*.txt", - "src/server/services/procedures/hurricane-data/*.txt", - "src/server/services/procedures/ice-core-data/data/domec-deuterium/*.txt", - "src/server/services/procedures/nexrad-radar/RadarLocations.js", - "src/server/services/procedures/word-guess/dict/*.*", - - # misc - "src/browser/src/sha512.js", - "src/common/sha512.js", - "test/utils/sha512.js", + "src/procedures/common-words/words/*.txt", + "src/procedures/hurricane-data/*.txt", + "src/procedures/ice-core-data/data/domec-deuterium/*.txt", + "src/procedures/nexrad-radar/RadarLocations.js", + "src/procedures/word-guess/dict/*.*", ] [type.js] From 59a153b360f81ad49990b1110c9d869af5fa87a9 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Fri, 2 Dec 2022 00:12:19 +0000 Subject: [PATCH 06/14] Update _typos.toml --- _typos.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/_typos.toml b/_typos.toml index 2b40a6a8..bdd7ae65 100644 --- a/_typos.toml +++ b/_typos.toml @@ -26,6 +26,7 @@ extend-exclude = [ "src/procedures/ice-core-data/data/domec-deuterium/*.txt", "src/procedures/nexrad-radar/RadarLocations.js", "src/procedures/word-guess/dict/*.*", + "/src/procedures/financial-data/currency-types.js", ] [type.js] From 5bc0d255f47076ed40259a75a664a0bd8ce9d078 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Fri, 2 Dec 2022 00:13:11 +0000 Subject: [PATCH 07/14] Update time-sync.js --- src/procedures/time-sync/time-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/procedures/time-sync/time-sync.js b/src/procedures/time-sync/time-sync.js index f9a4de02..0acfbba3 100644 --- a/src/procedures/time-sync/time-sync.js +++ b/src/procedures/time-sync/time-sync.js @@ -5,7 +5,7 @@ * To use this service, you first call :func:`TimeSync.prepare`, followed by performing several (e.g., 100) calls * to :func:`TimeSync.step`, and then finishing with :func:`TimeSync.complete` to get the computed timing metrics. * - * Note that the calls to :func:`TimeSync.step` are indended to be back-to-back. + * Note that the calls to :func:`TimeSync.step` are intended to be back-to-back. * You should perform this in a loop that does nothing else. * In particular, you should not sleep/wait inside the loop; if you need this, * you may provide a ``sleepTime`` to :func:`TimeSync.prepare` and it will do the sleeping/waiting for you (do not also sleep yourself). From 2dbab48234668cbc5580713dfaaf81e3730822a9 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:47:15 -0600 Subject: [PATCH 08/14] Minor refactors --- .../cloud-variables/cloud-variables.js | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 6c252c93..588663a2 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -25,13 +25,26 @@ const getCollections = function() { return _collections; }; +/** + * Throws an error if the given variable does not exist + * @param {Object?} variable Variable to test + */ const ensureVariableExists = function(variable) { if (!variable) { throw new Error('Variable not found'); } }; +/** + * Maximum duration of a locked public variable + */ let MAX_LOCK_AGE = 5 * 1000; + +/** + * Get the owner of a variable's lock, if it exists and has a currently valid one + * @param {Object} variable Variable to get lock owner of + * @returns Owner of variable's lock if there is one + */ const getLockOwnerId = function(variable) { if (variable && variable.lock) { if (!isLockStale(variable)) { @@ -40,17 +53,32 @@ const getLockOwnerId = function(variable) { } }; +/** + * Determine if a variable has a stale lock + * @param {Object} variable + * @returns {Boolean} True if variable has been locked longer than MAX_LOCK_AGE, false otherwise + */ const isLockStale = function(variable) { if (variable && variable.lock) { - return new Date() - variable.lock.creationTime > MAX_LOCK_AGE; + return (new Date() - variable.lock.creationTime) > MAX_LOCK_AGE; } return false; }; +/** + * Determine if a variable is locked + * @param {Object} variable + * @returns {Boolean} If variable is locked + */ const isLocked = function(variable) { return !!getLockOwnerId(variable); }; +/** + * Throw an error if the variable is locked by another client + * @param {Object} variable Variable to test + * @param {Object} clientId Client attempting to access variable + */ const ensureOwnsMutex = function(variable, clientId) { const ownerId = getLockOwnerId(variable); if (ownerId && ownerId !== clientId) { @@ -58,6 +86,11 @@ const ensureOwnsMutex = function(variable, clientId) { } }; +/** + * Throw an error if the given password does not match + * @param {Object} variable Variable to test + * @param {String} password Password to test + */ const ensureAuthorized = function(variable, password) { if (variable) { const authorized = !variable.password || @@ -69,22 +102,38 @@ const ensureAuthorized = function(variable, password) { } }; +/** + * Throw an error if the user is not logged in + * @param {Object} caller User to test + */ const ensureLoggedIn = function(caller) { if (!caller.username) { throw new Error('Login required.'); } }; +/** + * Throw an error if given variable name is not valid + * @param {String} name Variable name to test + */ const validateVariableName = function(name) { if (!/^[\w _()-]+$/.test(name)) { throw new Error('Invalid variable name.'); } }; +/** + * Size, in bytes, of maximum variable content + */ +const MAX_CONTENT_SIZE = 4 * 1024 * 1024; + +/** + * Throws an error if content is too large to store in a cloud variable + * @param {Object} content Content to test + */ const validateContentSize = function(content) { const sizeInBytes = content.length*2; // assuming utf8. Figure ~2 bytes per char - const mb = 1024*1024; - if (sizeInBytes > (4*mb)) { + if (sizeInBytes > MAX_CONTENT_SIZE) { throw new Error('Variable value is too large.'); } }; From f8f71617212f80a3473da8c8f5717529fdf90a00 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:28:54 -0600 Subject: [PATCH 09/14] Initial version --- .../cloud-variables/cloud-variables.js | 197 +++++++++++++++++- 1 file changed, 186 insertions(+), 11 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 588663a2..5c7233d1 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -86,6 +86,17 @@ const ensureOwnsMutex = function(variable, clientId) { } }; +/** + * Determine if a given password is valid for a variable + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @returns {Boolean} If the password is correct or the variable has no password + */ +function isAuthorized(variable, password) { + return !variable.password || + variable.password === password; +} + /** * Throw an error if the given password does not match * @param {Object} variable Variable to test @@ -93,15 +104,29 @@ const ensureOwnsMutex = function(variable, clientId) { */ const ensureAuthorized = function(variable, password) { if (variable) { - const authorized = !variable.password || - variable.password === password; - - if (!authorized) { + if (!isAuthorized(variable, password)) { throw new Error('Unauthorized: incorrect password'); } } }; +/** + * Throw an error if the given username is not the owner of the variable + * @param {Object} variable Variable to test + * @param {String} username Username to test + */ +const ensureOwnsVariable = function(variable, username){ + if (variable && variable.creator) { + if(variable.creator !== username){ + throw new Error('You do not own this variable'); + } + } + + if (variable && !variable.creator) { + throw new Error('You do not own this variable'); + } +} + /** * Throw an error if the user is not logged in * @param {Object} caller User to test @@ -122,6 +147,64 @@ const validateVariableName = function(name) { } }; +// Mapping of access levels and their full names +const accessLevelNames = { + 'r': 'read', + 'w': 'write', + 'a': 'append', + 'd': 'delete', + 'l': 'lock' +}; + +// Default access level (when correct password is provided), giving full access +const DEFAULT_WITH_PASSWORD_ACCESS = Object.keys(accessLevelNames).join(''); + +// Default access level (when correct password is provided), giving no access +const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; + +/** + * Get the available actions for a variable with the provided authentication. + * If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @param {String} username Username to test + * @returns {String} Access level string + */ +const getAccessLevel = function(variable, password, username) { + if(variable){ + // Creator has full access always + if(variable.creator && variable.creator === username){ + return DEFAULT_WITH_PASSWORD_ACCESS; + } + + if(isAuthorized(variable, password)){ + return variable.withPasswordAccess || DEFAULT_WITH_PASSWORD_ACCESS; + } else { + return variable.withoutPasswordAccess || DEFAULT_WITHOUT_PASSWORD_ACCESS; + } + } + + return DEFAULT_WITH_PASSWORD_ACCESS; +}; + +/** + * Throws an error if the requested access type is not allowed + * @param {Object} variable Variable to test + * @param {String} password Password to test + * @param {String} username Username to test + * @param {String} type Access level to test + */ +const ensureHasAccessLevel = function(variable, password, username, type) { + console.log(getAccessLevel(variable, password, username)); + if(!getAccessLevel(variable, password, username).includes(type)){ + if(type in accessLevelNames){ + throw new Error(`You are not authorized to ${accessLevelNames[type]} this variable, please check your password`); + } else { + throw new Error(`You are not authorized to perform that action on this variable, please check your password`); + } + } +}; + /** * Size, in bytes, of maximum variable content */ @@ -156,7 +239,7 @@ CloudVariables.getVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'r'); const query = { $set: { @@ -188,7 +271,7 @@ CloudVariables._sendUpdate = function(name, value, targets) { * @param {Any} value Value to store in variable * @param {String=} password Password (if password-protected) */ -CloudVariables.setVariable = async function(name, value, password) { + CloudVariables.setVariable = async function(name, value, password) { validateVariableName(name); validateContentSize(value); @@ -196,15 +279,65 @@ CloudVariables.setVariable = async function(name, value, password) { const username = this.caller.username; const variable = await sharedVars.findOne({name: name}); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'w'); ensureOwnsMutex(variable, this.caller.clientId); + let query; + // Set both the password and value in case it gets deleted // during this async fn... + if(variable || !this.caller.username){ + query = { + $set: { + value, + password, + lastWriter: username, + lastWriteTime: new Date(), + } + }; + } else { + // The variable did not exist, set creator data + query = { + $set: { + value, + password, + creator: username, + createdOn: new Date(), + lastWriter: username, + lastWriteTime: new Date(), + } + }; + } + + await sharedVars.updateOne({name: name}, query, {upsert: true}); + this._sendUpdate(name, value, globalListeners[name] || {}); +}; + +/** + * Append to a list cloud variable. + * @param {String} name Variable name + * @param {Any} value Value to append to variable + * @param {String=} password Password (if password-protected) + */ + CloudVariables.appendToVariable = async function(name, value, password) { + validateVariableName(name); + validateContentSize(value); + + const {sharedVars} = getCollections(); + const username = this.caller.username; + const variable = await sharedVars.findOne({name: name}); + + ensureVariableExists(variable); + ensureHasAccessLevel(variable, password, this.caller.username, 'a'); + ensureOwnsMutex(variable, this.caller.clientId); + + if(typeof(variable.value) !== 'object'){ + throw new Error('Can only append to lists.'); + } + const query = { $set: { - value, - password, + value: [...variable.value, value], lastWriter: username, lastWriteTime: new Date(), } @@ -225,7 +358,7 @@ CloudVariables.deleteVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'd'); // Clear the queued locks const id = variable._id; @@ -252,7 +385,8 @@ CloudVariables.lockVariable = async function(name, password) { const variable = await sharedVars.findOne({name: name}); ensureVariableExists(variable); - ensureAuthorized(variable, password); + ensureHasAccessLevel(variable, password, this.caller.username, 'l'); + // What if the block is killed before a lock can be acquired? // Then should we close the connection on the client? // @@ -517,4 +651,45 @@ CloudVariables.listenToUserVariable = async function(name, msgType, duration = 6 bucket[this.socket.clientId] = [this.socket, msgType, +new Date() + duration]; }; +/** + * Set the access levels for a public variable. + * + * Create a string combining the following letters (in any order) for each category: + * 'r' - Read through the getVariable method + * 'w' - Write through the setVariable method + * 'a' - Append through the appendVariable method + * 'd' - Delete through the deleteVariable method + * 'l' - Lock through the lockVariable method + * + * The default settings give users with the password read, write, append, delete, and lock access ("rwadl"), and users without the password no access. + * The variable's creator will always have full access. + * + * @param {String} name Variable name + * @param {String} withPassword Access level for other users with password + * @param {String} withoutPassword Access level for other users without password + */ +CloudVariables.setVariableAccess = async function(name, withPassword = DEFAULT_WITH_PASSWORD_ACCESS, withoutPassword = DEFAULT_WITHOUT_PASSWORD_ACCESS){ + const filterAccessString = (string) => [...string.toLowerCase()].filter(c => c in accessLevelNames).join(''); + + const withPasswordAccess = filterAccessString(withPassword); + const withoutPasswordAccess = filterAccessString(withoutPassword); + + const {sharedVars} = getCollections(); + const variable = await sharedVars.findOne({name: name}); + ensureVariableExists(variable); + ensureLoggedIn(this.caller); + ensureOwnsVariable(variable, this.caller.username); + + const query = { + $set: { + withPasswordAccess, + withoutPasswordAccess, + lastWriter: this.caller.username, + lastWriteTime: new Date(), + } + }; + + await sharedVars.updateOne({name: name}, query, {upsert: true}); +}; + module.exports = CloudVariables; From bee721d635fb994eaa10114d67f2c516eb3c8cc2 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:36:54 -0600 Subject: [PATCH 10/14] Simplify comments to avoid warnings --- .../cloud-variables/cloud-variables.js | 116 +++++------------- 1 file changed, 30 insertions(+), 86 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 5c7233d1..3bfa1776 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -16,7 +16,8 @@ const globalListeners = {}; // map>> let _collections = null; -const getCollections = function() { + +function getCollections() { if (!_collections) { _collections = {}; _collections.sharedVars = Storage.create('cloud-variables:shared').collection; @@ -25,27 +26,18 @@ const getCollections = function() { return _collections; }; -/** - * Throws an error if the given variable does not exist - * @param {Object?} variable Variable to test - */ -const ensureVariableExists = function(variable) { +// Throws an error if the given variable does not exist +function ensureVariableExists(variable) { if (!variable) { throw new Error('Variable not found'); } }; -/** - * Maximum duration of a locked public variable - */ +// Maximum duration of a locked public variable let MAX_LOCK_AGE = 5 * 1000; -/** - * Get the owner of a variable's lock, if it exists and has a currently valid one - * @param {Object} variable Variable to get lock owner of - * @returns Owner of variable's lock if there is one - */ -const getLockOwnerId = function(variable) { +// Get the owner of a variable's lock, if it exists and has a currently valid one +function getLockOwnerId(variable) { if (variable && variable.lock) { if (!isLockStale(variable)) { return variable.lock.clientId; @@ -53,56 +45,35 @@ const getLockOwnerId = function(variable) { } }; -/** - * Determine if a variable has a stale lock - * @param {Object} variable - * @returns {Boolean} True if variable has been locked longer than MAX_LOCK_AGE, false otherwise - */ -const isLockStale = function(variable) { +// Determine if a variable has a stale lock +function isLockStale(variable) { if (variable && variable.lock) { return (new Date() - variable.lock.creationTime) > MAX_LOCK_AGE; } return false; }; -/** - * Determine if a variable is locked - * @param {Object} variable - * @returns {Boolean} If variable is locked - */ -const isLocked = function(variable) { +// Determine if a variable is locked +function isLocked(variable) { return !!getLockOwnerId(variable); }; -/** - * Throw an error if the variable is locked by another client - * @param {Object} variable Variable to test - * @param {Object} clientId Client attempting to access variable - */ -const ensureOwnsMutex = function(variable, clientId) { +// Throw an error if the variable is locked by another client +function ensureOwnsMutex(variable, clientId) { const ownerId = getLockOwnerId(variable); if (ownerId && ownerId !== clientId) { throw new Error('Variable is locked (by someone else)'); } }; -/** - * Determine if a given password is valid for a variable - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @returns {Boolean} If the password is correct or the variable has no password - */ +// Determine if a given password is valid for a variable function isAuthorized(variable, password) { return !variable.password || variable.password === password; } -/** - * Throw an error if the given password does not match - * @param {Object} variable Variable to test - * @param {String} password Password to test - */ -const ensureAuthorized = function(variable, password) { +// Throw an error if the given password does not match +function ensureAuthorized(variable, password) { if (variable) { if (!isAuthorized(variable, password)) { throw new Error('Unauthorized: incorrect password'); @@ -110,12 +81,8 @@ const ensureAuthorized = function(variable, password) { } }; -/** - * Throw an error if the given username is not the owner of the variable - * @param {Object} variable Variable to test - * @param {String} username Username to test - */ -const ensureOwnsVariable = function(variable, username){ +// Throw an error if the given username is not the owner of the variable +function ensureOwnsVariable(variable, username) { if (variable && variable.creator) { if(variable.creator !== username){ throw new Error('You do not own this variable'); @@ -127,21 +94,15 @@ const ensureOwnsVariable = function(variable, username){ } } -/** - * Throw an error if the user is not logged in - * @param {Object} caller User to test - */ -const ensureLoggedIn = function(caller) { +// Throw an error if the user is not logged in +function ensureLoggedIn(caller) { if (!caller.username) { throw new Error('Login required.'); } }; -/** - * Throw an error if given variable name is not valid - * @param {String} name Variable name to test - */ -const validateVariableName = function(name) { +// Throw an error if given variable name is not valid +function validateVariableName(name) { if (!/^[\w _()-]+$/.test(name)) { throw new Error('Invalid variable name.'); } @@ -162,15 +123,9 @@ const DEFAULT_WITH_PASSWORD_ACCESS = Object.keys(accessLevelNames).join(''); // Default access level (when correct password is provided), giving no access const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; -/** - * Get the available actions for a variable with the provided authentication. - * If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @param {String} username Username to test - * @returns {String} Access level string - */ -const getAccessLevel = function(variable, password, username) { +// Get the available actions for a variable with the provided authentication. +// If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. +function getAccessLevel(variable, password, username) { if(variable){ // Creator has full access always if(variable.creator && variable.creator === username){ @@ -187,14 +142,8 @@ const getAccessLevel = function(variable, password, username) { return DEFAULT_WITH_PASSWORD_ACCESS; }; -/** - * Throws an error if the requested access type is not allowed - * @param {Object} variable Variable to test - * @param {String} password Password to test - * @param {String} username Username to test - * @param {String} type Access level to test - */ -const ensureHasAccessLevel = function(variable, password, username, type) { +// Throws an error if the requested access type is not allowed +function ensureHasAccessLevel(variable, password, username, type) { console.log(getAccessLevel(variable, password, username)); if(!getAccessLevel(variable, password, username).includes(type)){ if(type in accessLevelNames){ @@ -205,16 +154,11 @@ const ensureHasAccessLevel = function(variable, password, username, type) { } }; -/** - * Size, in bytes, of maximum variable content - */ +// Size, in bytes, of maximum variable content const MAX_CONTENT_SIZE = 4 * 1024 * 1024; -/** - * Throws an error if content is too large to store in a cloud variable - * @param {Object} content Content to test - */ -const validateContentSize = function(content) { +// Throws an error if content is too large to store in a cloud variable +function validateContentSize(content) { const sizeInBytes = content.length*2; // assuming utf8. Figure ~2 bytes per char if (sizeInBytes > MAX_CONTENT_SIZE) { throw new Error('Variable value is too large.'); From 320441b1017a33326046ecdf7df03a6f808c9fa1 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Mon, 5 Dec 2022 15:08:14 -0600 Subject: [PATCH 11/14] Change how list variables are detected --- .../cloud-variables/cloud-variables.js | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 3bfa1776..032fdd83 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -274,21 +274,23 @@ CloudVariables._sendUpdate = function(name, value, targets) { ensureVariableExists(variable); ensureHasAccessLevel(variable, password, this.caller.username, 'a'); ensureOwnsMutex(variable, this.caller.clientId); - - if(typeof(variable.value) !== 'object'){ - throw new Error('Can only append to lists.'); - } const query = { + $push: { + value, + }, $set: { - value: [...variable.value, value], lastWriter: username, lastWriteTime: new Date(), } }; - await sharedVars.updateOne({name: name}, query, {upsert: true}); - this._sendUpdate(name, value, globalListeners[name] || {}); + try { + await sharedVars.updateOne({name: name}, query, {upsert: true}); + this._sendUpdate(name, value, globalListeners[name] || {}); + } catch (error) { + throw new Error('Variable must be of list type to use appendToVariable'); + } }; /** @@ -508,6 +510,36 @@ CloudVariables.getUserVariable = async function(name) { return variable.value; }; +/** + * Set the value of the user cloud variable for the current user. + * @param {String} name Variable name + * @param {Any} value Value to store in variable + */ + CloudVariables.appendToUserVariable = async function(name, value) { + ensureLoggedIn(this.caller); + validateVariableName(name); + validateContentSize(value); + + const {userVars} = getCollections(); + const username = this.caller.username; + const query = { + $push: { + value, + }, + $set: { + lastWriteTime: new Date(), + } + }; + + try { + await userVars.updateOne({name, owner: username}, query, {upsert: true}); + this._sendUpdate(name, value, (userListeners[username] || {})[name] || {}); + } catch (error) { + throw new Error('Variable must be of list type to use appendToUserVariable'); + } +}; + + /** * Set the value of the user cloud variable for the current user. * @param {String} name Variable name @@ -601,7 +633,7 @@ CloudVariables.listenToUserVariable = async function(name, msgType, duration = 6 * Create a string combining the following letters (in any order) for each category: * 'r' - Read through the getVariable method * 'w' - Write through the setVariable method - * 'a' - Append through the appendVariable method + * 'a' - Append through the appendToVariable method * 'd' - Delete through the deleteVariable method * 'l' - Lock through the lockVariable method * From 6988ca14a84b7cf3a6644aa79b67b10f0465b261 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Mon, 5 Dec 2022 15:45:58 -0600 Subject: [PATCH 12/14] Send updated variable to listeners --- src/procedures/cloud-variables/cloud-variables.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 032fdd83..d56e83f6 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -144,7 +144,6 @@ function getAccessLevel(variable, password, username) { // Throws an error if the requested access type is not allowed function ensureHasAccessLevel(variable, password, username, type) { - console.log(getAccessLevel(variable, password, username)); if(!getAccessLevel(variable, password, username).includes(type)){ if(type in accessLevelNames){ throw new Error(`You are not authorized to ${accessLevelNames[type]} this variable, please check your password`); @@ -286,8 +285,8 @@ CloudVariables._sendUpdate = function(name, value, targets) { }; try { - await sharedVars.updateOne({name: name}, query, {upsert: true}); - this._sendUpdate(name, value, globalListeners[name] || {}); + const updatedVar = await sharedVars.findOneAndUpdate({name: name}, query, {upsert: true, returnDocument: "after"}); + this._sendUpdate(name, updatedVar.value.value, globalListeners[name] || {}); } catch (error) { throw new Error('Variable must be of list type to use appendToVariable'); } @@ -532,8 +531,8 @@ CloudVariables.getUserVariable = async function(name) { }; try { - await userVars.updateOne({name, owner: username}, query, {upsert: true}); - this._sendUpdate(name, value, (userListeners[username] || {})[name] || {}); + const updatedVar = await userVars.findOneAndUpdate({name, owner: username}, query, {upsert: true, returnDocument: "after"}); + this._sendUpdate(name, updatedVar.value.value, (userListeners[username] || {})[name] || {}); } catch (error) { throw new Error('Variable must be of list type to use appendToUserVariable'); } From db57b491c36f0fe4a5bfa8c3d1b5ba200f880de3 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:56:58 -0600 Subject: [PATCH 13/14] Restrict owner to password and accept list input --- .../cloud-variables/cloud-variables.js | 173 ++++++++++-------- 1 file changed, 96 insertions(+), 77 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index d56e83f6..51321308 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -84,7 +84,7 @@ function ensureAuthorized(variable, password) { // Throw an error if the given username is not the owner of the variable function ensureOwnsVariable(variable, username) { if (variable && variable.creator) { - if(variable.creator !== username){ + if (variable.creator !== username) { throw new Error('You do not own this variable'); } } @@ -126,13 +126,8 @@ const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; // Get the available actions for a variable with the provided authentication. // If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. function getAccessLevel(variable, password, username) { - if(variable){ - // Creator has full access always - if(variable.creator && variable.creator === username){ - return DEFAULT_WITH_PASSWORD_ACCESS; - } - - if(isAuthorized(variable, password)){ + if (variable) { + if (isAuthorized(variable, password)) { return variable.withPasswordAccess || DEFAULT_WITH_PASSWORD_ACCESS; } else { return variable.withoutPasswordAccess || DEFAULT_WITHOUT_PASSWORD_ACCESS; @@ -144,8 +139,8 @@ function getAccessLevel(variable, password, username) { // Throws an error if the requested access type is not allowed function ensureHasAccessLevel(variable, password, username, type) { - if(!getAccessLevel(variable, password, username).includes(type)){ - if(type in accessLevelNames){ + if (!getAccessLevel(variable, password, username).includes(type)) { + if (type in accessLevelNames) { throw new Error(`You are not authorized to ${accessLevelNames[type]} this variable, please check your password`); } else { throw new Error(`You are not authorized to perform that action on this variable, please check your password`); @@ -158,7 +153,7 @@ const MAX_CONTENT_SIZE = 4 * 1024 * 1024; // Throws an error if content is too large to store in a cloud variable function validateContentSize(content) { - const sizeInBytes = content.length*2; // assuming utf8. Figure ~2 bytes per char + const sizeInBytes = content.length * 2; // assuming utf8. Figure ~2 bytes per char if (sizeInBytes > MAX_CONTENT_SIZE) { throw new Error('Variable value is too large.'); } @@ -166,7 +161,7 @@ function validateContentSize(content) { const CloudVariables = {}; CloudVariables._queuedLocks = {}; -CloudVariables._setMaxLockAge = function(age) { // for testing +CloudVariables._setMaxLockAge = function (age) { // for testing MAX_LOCK_AGE = age; }; @@ -176,10 +171,10 @@ CloudVariables._setMaxLockAge = function(age) { // for testing * @param {String=} password Password (if password-protected) * @returns {Any} the stored value */ -CloudVariables.getVariable = async function(name, password) { - const {sharedVars} = getCollections(); +CloudVariables.getVariable = async function (name, password) { + const { sharedVars } = getCollections(); const username = this.caller.username; - const variable = await sharedVars.findOne({name: name}); + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureHasAccessLevel(variable, password, this.caller.username, 'r'); @@ -190,11 +185,11 @@ CloudVariables.getVariable = async function(name, password) { lastReadTime: new Date(), } }; - await sharedVars.updateOne({_id: variable._id}, query); + await sharedVars.updateOne({ _id: variable._id }, query); return variable.value; }; -CloudVariables._sendUpdate = function(name, value, targets) { +CloudVariables._sendUpdate = function (name, value, targets) { const expired = []; const now = +new Date(); for (const clientId in targets) { @@ -214,22 +209,22 @@ CloudVariables._sendUpdate = function(name, value, targets) { * @param {Any} value Value to store in variable * @param {String=} password Password (if password-protected) */ - CloudVariables.setVariable = async function(name, value, password) { +CloudVariables.setVariable = async function (name, value, password) { validateVariableName(name); validateContentSize(value); - const {sharedVars} = getCollections(); + const { sharedVars } = getCollections(); const username = this.caller.username; - const variable = await sharedVars.findOne({name: name}); + const variable = await sharedVars.findOne({ name: name }); ensureHasAccessLevel(variable, password, this.caller.username, 'w'); ensureOwnsMutex(variable, this.caller.clientId); let query; - + // Set both the password and value in case it gets deleted // during this async fn... - if(variable || !this.caller.username){ + if (variable || !this.caller.username) { query = { $set: { value, @@ -252,7 +247,7 @@ CloudVariables._sendUpdate = function(name, value, targets) { }; } - await sharedVars.updateOne({name: name}, query, {upsert: true}); + await sharedVars.updateOne({ name: name }, query, { upsert: true }); this._sendUpdate(name, value, globalListeners[name] || {}); }; @@ -262,13 +257,13 @@ CloudVariables._sendUpdate = function(name, value, targets) { * @param {Any} value Value to append to variable * @param {String=} password Password (if password-protected) */ - CloudVariables.appendToVariable = async function(name, value, password) { +CloudVariables.appendToVariable = async function (name, value, password) { validateVariableName(name); validateContentSize(value); - const {sharedVars} = getCollections(); + const { sharedVars } = getCollections(); const username = this.caller.username; - const variable = await sharedVars.findOne({name: name}); + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureHasAccessLevel(variable, password, this.caller.username, 'a'); @@ -285,7 +280,7 @@ CloudVariables._sendUpdate = function(name, value, targets) { }; try { - const updatedVar = await sharedVars.findOneAndUpdate({name: name}, query, {upsert: true, returnDocument: "after"}); + const updatedVar = await sharedVars.findOneAndUpdate({ name: name }, query, { upsert: true, returnDocument: "after" }); this._sendUpdate(name, updatedVar.value.value, globalListeners[name] || {}); } catch (error) { throw new Error('Variable must be of list type to use appendToVariable'); @@ -298,9 +293,9 @@ CloudVariables._sendUpdate = function(name, value, targets) { * @param {String} name Variable to delete * @param {String=} password Password (if password-protected) */ -CloudVariables.deleteVariable = async function(name, password) { - const {sharedVars} = getCollections(); - const variable = await sharedVars.findOne({name: name}); +CloudVariables.deleteVariable = async function (name, password) { + const { sharedVars } = getCollections(); + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureHasAccessLevel(variable, password, this.caller.username, 'd'); @@ -308,7 +303,7 @@ CloudVariables.deleteVariable = async function(name, password) { // Clear the queued locks const id = variable._id; this._clearPendingLocks(id); - await sharedVars.deleteOne({_id: id}); + await sharedVars.deleteOne({ _id: id }); delete globalListeners[name]; }; @@ -321,13 +316,13 @@ CloudVariables.deleteVariable = async function(name, password) { * @param {String} name Variable to lock * @param {String=} password Password (if password-protected) */ -CloudVariables.lockVariable = async function(name, password) { +CloudVariables.lockVariable = async function (name, password) { validateVariableName(name); - const {sharedVars} = getCollections(); + const { sharedVars } = getCollections(); const username = this.caller.username; const clientId = this.caller.clientId; - const variable = await sharedVars.findOne({name: name}); + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureHasAccessLevel(variable, password, this.caller.username, 'l'); @@ -346,11 +341,11 @@ CloudVariables.lockVariable = async function(name, password) { } }; -CloudVariables._queueLockFor = async function(variable) { +CloudVariables._queueLockFor = async function (variable) { // Return a promise which will resolve when the lock is applied const deferred = utils.defer(); const id = variable._id; - const {password} = variable; + const { password } = variable; if (!this._queuedLocks[id]) { this._queuedLocks[id] = []; @@ -386,8 +381,8 @@ CloudVariables._queueLockFor = async function(variable) { return deferred.promise; }; -CloudVariables._applyLock = async function(id, clientId, username) { - const {sharedVars} = getCollections(); +CloudVariables._applyLock = async function (id, clientId, username) { + const { sharedVars } = getCollections(); const lock = { clientId, @@ -400,8 +395,8 @@ CloudVariables._applyLock = async function(id, clientId, username) { } }; - setTimeout(() => this._checkVariableLock(id), MAX_LOCK_AGE+1); - const res = await sharedVars.updateOne({_id: id}, query); + setTimeout(() => this._checkVariableLock(id), MAX_LOCK_AGE + 1); + const res = await sharedVars.updateOne({ _id: id }, query); // Ensure that the variable wasn't deleted during this application logger.trace(`${clientId} locked variable ${id}`); @@ -410,15 +405,15 @@ CloudVariables._applyLock = async function(id, clientId, username) { } }; -CloudVariables._clearPendingLocks = function(id) { +CloudVariables._clearPendingLocks = function (id) { const pendingLocks = this._queuedLocks[id] || []; pendingLocks.forEach(lock => lock.promise.reject(new Error('Variable deleted'))); delete this._queuedLocks[id]; }; -CloudVariables._checkVariableLock = async function(id) { - const {sharedVars} = getCollections(); - const variable = await sharedVars.findOne({_id: id}); +CloudVariables._checkVariableLock = async function (id) { + const { sharedVars } = getCollections(); + const variable = await sharedVars.findOne({ _id: id }); if (!variable) { logger.trace(`${id} has been deleted. Clearing locks.`); @@ -438,12 +433,12 @@ CloudVariables._checkVariableLock = async function(id) { * @param {String} name Variable to delete * @param {String=} password Password (if password-protected) */ -CloudVariables.unlockVariable = async function(name, password) { +CloudVariables.unlockVariable = async function (name, password) { validateVariableName(name); - const {sharedVars} = getCollections(); - const {clientId} = this.caller; - const variable = await sharedVars.findOne({name: name}); + const { sharedVars } = getCollections(); + const { clientId } = this.caller; + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureAuthorized(variable, password); @@ -459,9 +454,9 @@ CloudVariables.unlockVariable = async function(name, password) { } }; - const result = await sharedVars.updateOne({_id: variable._id}, query); + const result = await sharedVars.updateOne({ _id: variable._id }, query); - if(result.modifiedCount === 1) { + if (result.modifiedCount === 1) { logger.trace(`${clientId} unlocked ${name} (${variable._id})`); } else { logger.trace(`${clientId} tried to unlock ${name} but variable was deleted`); @@ -469,11 +464,11 @@ CloudVariables.unlockVariable = async function(name, password) { await this._onUnlockVariable(variable._id); }; -CloudVariables._onUnlockVariable = async function(id) { +CloudVariables._onUnlockVariable = async function (id) { // if there is a queued lock, apply it if (this._queuedLocks.hasOwnProperty(id)) { const nextLock = this._queuedLocks[id].shift(); - const {clientId, username} = nextLock; + const { clientId, username } = nextLock; // apply the lock await this._applyLock(id, clientId, username); @@ -489,12 +484,12 @@ CloudVariables._onUnlockVariable = async function(id) { * @param {String} name Variable name * @returns {Any} the stored value */ -CloudVariables.getUserVariable = async function(name) { - const {userVars} = getCollections(); +CloudVariables.getUserVariable = async function (name) { + const { userVars } = getCollections(); const username = this.caller.username; ensureLoggedIn(this.caller); - const variable = await userVars.findOne({name: name, owner: username}); + const variable = await userVars.findOne({ name: name, owner: username }); if (!variable) { throw new Error('Variable not found'); @@ -505,7 +500,7 @@ CloudVariables.getUserVariable = async function(name) { lastReadTime: new Date(), } }; - await userVars.updateOne({name, owner: username}, query); + await userVars.updateOne({ name, owner: username }, query); return variable.value; }; @@ -514,12 +509,12 @@ CloudVariables.getUserVariable = async function(name) { * @param {String} name Variable name * @param {Any} value Value to store in variable */ - CloudVariables.appendToUserVariable = async function(name, value) { +CloudVariables.appendToUserVariable = async function (name, value) { ensureLoggedIn(this.caller); validateVariableName(name); validateContentSize(value); - const {userVars} = getCollections(); + const { userVars } = getCollections(); const username = this.caller.username; const query = { $push: { @@ -531,7 +526,7 @@ CloudVariables.getUserVariable = async function(name) { }; try { - const updatedVar = await userVars.findOneAndUpdate({name, owner: username}, query, {upsert: true, returnDocument: "after"}); + const updatedVar = await userVars.findOneAndUpdate({ name, owner: username }, query, { upsert: true, returnDocument: "after" }); this._sendUpdate(name, updatedVar.value.value, (userListeners[username] || {})[name] || {}); } catch (error) { throw new Error('Variable must be of list type to use appendToUserVariable'); @@ -544,12 +539,12 @@ CloudVariables.getUserVariable = async function(name) { * @param {String} name Variable name * @param {Any} value Value to store in variable */ -CloudVariables.setUserVariable = async function(name, value) { +CloudVariables.setUserVariable = async function (name, value) { ensureLoggedIn(this.caller); validateVariableName(name); validateContentSize(value); - const {userVars} = getCollections(); + const { userVars } = getCollections(); const username = this.caller.username; const query = { $set: { @@ -557,7 +552,7 @@ CloudVariables.setUserVariable = async function(name, value) { lastWriteTime: new Date(), } }; - await userVars.updateOne({name, owner: username}, query, {upsert: true}); + await userVars.updateOne({ name, owner: username }, query, { upsert: true }); this._sendUpdate(name, value, (userListeners[username] || {})[name] || {}); }; @@ -565,12 +560,12 @@ CloudVariables.setUserVariable = async function(name, value) { * Delete the user variable for the current user. * @param {String} name Variable name */ -CloudVariables.deleteUserVariable = async function(name) { - const {userVars} = getCollections(); +CloudVariables.deleteUserVariable = async function (name) { + const { userVars } = getCollections(); const username = this.caller.username; ensureLoggedIn(this.caller); - await userVars.deleteOne({name: name, owner: username}); + await userVars.deleteOne({ name: name, owner: username }); delete (userListeners[username] || {})[name]; }; @@ -607,7 +602,7 @@ CloudVariables._getUserListenBucket = function (name) { * @param {String=} password Password (if password-protected) * @param {Duration=} duration The maximum duration to listen for updates on the variable (default 1hr). */ -CloudVariables.listenToVariable = async function(name, msgType, password, duration = 60*60*1000) { +CloudVariables.listenToVariable = async function (name, msgType, password, duration = 60 * 60 * 1000) { await this.getVariable(name, password); // ensure we can get the value const bucket = this._getListenBucket(name); bucket[this.socket.clientId] = [this.socket, msgType, +new Date() + duration]; @@ -620,7 +615,7 @@ CloudVariables.listenToVariable = async function(name, msgType, password, durati * @param {Any} msgType Message type to send each time the variable is updated * @param {Duration=} duration The maximum duration to listen for updates on the variable (default 1hr). */ -CloudVariables.listenToUserVariable = async function(name, msgType, duration = 60*60*1000) { +CloudVariables.listenToUserVariable = async function (name, msgType, duration = 60 * 60 * 1000) { await this.getUserVariable(name); // ensure we can get the value const bucket = this._getUserListenBucket(name); bucket[this.socket.clientId] = [this.socket, msgType, +new Date() + duration]; @@ -636,21 +631,45 @@ CloudVariables.listenToUserVariable = async function(name, msgType, duration = 6 * 'd' - Delete through the deleteVariable method * 'l' - Lock through the lockVariable method * + * This method will also accept a list of either the letters or names of access levels. + * * The default settings give users with the password read, write, append, delete, and lock access ("rwadl"), and users without the password no access. - * The variable's creator will always have full access. * * @param {String} name Variable name - * @param {String} withPassword Access level for other users with password - * @param {String} withoutPassword Access level for other users without password + * @param {Any=} withPassword Access level for other users with password + * @param {Any=} withoutPassword Access level for other users without password */ -CloudVariables.setVariableAccess = async function(name, withPassword = DEFAULT_WITH_PASSWORD_ACCESS, withoutPassword = DEFAULT_WITHOUT_PASSWORD_ACCESS){ +CloudVariables.setVariableAccess = async function (name, withPassword = '', withoutPassword = '') { const filterAccessString = (string) => [...string.toLowerCase()].filter(c => c in accessLevelNames).join(''); - - const withPasswordAccess = filterAccessString(withPassword); - const withoutPasswordAccess = filterAccessString(withoutPassword); - const {sharedVars} = getCollections(); - const variable = await sharedVars.findOne({name: name}); + let withPasswordAccess = ''; + let withoutPasswordAccess = ''; + + if (withPassword) { + // Handle string input + if (typeof (withPassword) == 'string') { + withPasswordAccess = filterAccessString(withPassword); + } else { + // Assume list input + withPassword = withPassword.filter(c => Object.keys(accessLevelNames).includes(c) || Object.values(accessLevelNames).includes(c)); + withPassword = withPassword.map(c => Object.values(accessLevelNames).includes(c) ? Object.keys(accessLevelNames).find(key => accessLevelNames[key] == c) : c); + withPasswordAccess = filterAccessString(withPassword.join('')); + } + } + + if (withoutPassword) { + if (typeof (withOutPassword) == 'string') { + withoutPasswordAccess = filterAccessString(withoutPassword); + } else { + // Assume list input + withoutPassword = withoutPassword.filter(c => Object.keys(accessLevelNames).includes(c) || Object.values(accessLevelNames).includes(c)); + withoutPassword = withoutPassword.map(c => Object.values(accessLevelNames).includes(c) ? Object.keys(accessLevelNames).find(key => accessLevelNames[key] == c) : c); + withoutPasswordAccess = filterAccessString(withoutPassword.join('')); + } + } + + const { sharedVars } = getCollections(); + const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); ensureLoggedIn(this.caller); ensureOwnsVariable(variable, this.caller.username); @@ -664,7 +683,7 @@ CloudVariables.setVariableAccess = async function(name, withPassword = DEFAULT_W } }; - await sharedVars.updateOne({name: name}, query, {upsert: true}); + await sharedVars.updateOne({ name: name }, query, { upsert: true }); }; module.exports = CloudVariables; From 1ab7e3391ddbb87fb32c6ea4c243c06fd1486d90 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:59:57 -0600 Subject: [PATCH 14/14] Make permissions additive --- .../cloud-variables/cloud-variables.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/procedures/cloud-variables/cloud-variables.js b/src/procedures/cloud-variables/cloud-variables.js index 51321308..024e784e 100644 --- a/src/procedures/cloud-variables/cloud-variables.js +++ b/src/procedures/cloud-variables/cloud-variables.js @@ -125,10 +125,10 @@ const DEFAULT_WITHOUT_PASSWORD_ACCESS = ''; // Get the available actions for a variable with the provided authentication. // If the variable does not exist, all actions are allowed and proper restriction is expected to be implemented by the caller method. -function getAccessLevel(variable, password, username) { +function getAccessLevel(variable, password) { if (variable) { if (isAuthorized(variable, password)) { - return variable.withPasswordAccess || DEFAULT_WITH_PASSWORD_ACCESS; + return (variable.withPasswordAccess + variable.withoutPasswordAccess) || DEFAULT_WITH_PASSWORD_ACCESS; } else { return variable.withoutPasswordAccess || DEFAULT_WITHOUT_PASSWORD_ACCESS; } @@ -138,8 +138,8 @@ function getAccessLevel(variable, password, username) { }; // Throws an error if the requested access type is not allowed -function ensureHasAccessLevel(variable, password, username, type) { - if (!getAccessLevel(variable, password, username).includes(type)) { +function ensureHasAccessLevel(variable, password, type) { + if (!getAccessLevel(variable, password).includes(type)) { if (type in accessLevelNames) { throw new Error(`You are not authorized to ${accessLevelNames[type]} this variable, please check your password`); } else { @@ -177,7 +177,7 @@ CloudVariables.getVariable = async function (name, password) { const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); - ensureHasAccessLevel(variable, password, this.caller.username, 'r'); + ensureHasAccessLevel(variable, password, 'r'); const query = { $set: { @@ -217,7 +217,7 @@ CloudVariables.setVariable = async function (name, value, password) { const username = this.caller.username; const variable = await sharedVars.findOne({ name: name }); - ensureHasAccessLevel(variable, password, this.caller.username, 'w'); + ensureHasAccessLevel(variable, password, 'w'); ensureOwnsMutex(variable, this.caller.clientId); let query; @@ -266,7 +266,7 @@ CloudVariables.appendToVariable = async function (name, value, password) { const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); - ensureHasAccessLevel(variable, password, this.caller.username, 'a'); + ensureHasAccessLevel(variable, password, 'a'); ensureOwnsMutex(variable, this.caller.clientId); const query = { @@ -298,7 +298,7 @@ CloudVariables.deleteVariable = async function (name, password) { const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); - ensureHasAccessLevel(variable, password, this.caller.username, 'd'); + ensureHasAccessLevel(variable, password, 'd'); // Clear the queued locks const id = variable._id; @@ -325,7 +325,7 @@ CloudVariables.lockVariable = async function (name, password) { const variable = await sharedVars.findOne({ name: name }); ensureVariableExists(variable); - ensureHasAccessLevel(variable, password, this.caller.username, 'l'); + ensureHasAccessLevel(variable, password, 'l'); // What if the block is killed before a lock can be acquired? // Then should we close the connection on the client?