From d38c465932574eee513c991b36ea96ce655162cd Mon Sep 17 00:00:00 2001 From: miketwc1984 Date: Mon, 13 May 2024 13:51:25 -0400 Subject: [PATCH] sync to 0.9.48 / user 1.0.22 --- lib/user.js | 332 ++++++++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 209 insertions(+), 125 deletions(-) diff --git a/lib/user.js b/lib/user.js index 7fb4872..477a479 100644 --- a/lib/user.js +++ b/lib/user.js @@ -91,6 +91,10 @@ module.exports = Class.create({ return this.doError('user', "Username is blocked: " + user.username, callback); } + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // first, make sure user doesn't already exist this.storage.get(path, function (err, old_user) { if (old_user) { @@ -295,6 +299,10 @@ module.exports = Class.create({ // set session expiration self.storage.expire('sessions/' + session_id, expiration_date); + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + callback(Tools.mergeHashes({ code: 0, username: user.username, @@ -402,6 +410,10 @@ module.exports = Class.create({ self.storage.expire('sessions/' + session.id, expiration_date); } + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + callback(Tools.mergeHashes({ code: 0, username: session.username, @@ -468,6 +480,10 @@ module.exports = Class.create({ user[key] = updates[key]; } + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // update user record user.modified = Tools.timeNow(true); @@ -547,14 +563,14 @@ module.exports = Class.create({ self.logDebug(6, "Successfully deleted user"); self.logTransaction('user_delete', user.username, self.getClientInfo(args)); - callback({ - code: 0 - }); + // remove from manager user list in the background self.storage.listFindCut('global/users', { username: user.username }, function (err) { if (err) self.logError(1, "Failed to remove user from manager list: " + err); + callback({ code: 0 }); + self.fireHook('after_delete', args); }); @@ -595,7 +611,7 @@ module.exports = Class.create({ var date_code = Math.floor(Tools.timeNow() / 3600); if (user.fp_date_code && (date_code == user.fp_date_code) && (user.fp_count > self.config.get('max_forgot_passwords_per_hour'))) { // lockout until next hour - return self.doError('login', "This feature is locked due to too many requests. Please try again later."); + return self.doError('login', "This feature is locked due to too many requests. Please try again later.", callback); } args.user = user; @@ -764,6 +780,10 @@ module.exports = Class.create({ password: /.+/ }, callback)) return; + // sanitize + new_user.email = new_user.email.replace(/<.+>/g, ''); + new_user.full_name = new_user.full_name.replace(/<.+>/g, ''); + this.loadSession(args, function (err, session, admin_user) { if (!session) { return self.doError('session', "Session has expired or is invalid.", callback); @@ -815,13 +835,15 @@ module.exports = Class.create({ self.logTransaction('user_create', new_user.username, self.getClientInfo(args, { user: Tools.copyHashRemoveKeys(new_user, { password: 1, salt: 1 }) })); - callback({ code: 0 }); + // add to manager user list in the background if (self.config.get('sort_global_users')) { self.storage.listInsertSorted('global/users', { username: new_user.username }, ['username', 1], function (err) { if (err) self.logError(1, "Failed to add user to manager list: " + err); + callback({ code: 0 }); + // fire after hook in background self.fireHook('after_create', args); }); @@ -891,16 +913,35 @@ module.exports = Class.create({ if (updates.new_password) { updates.salt = Tools.generateUniqueID(64, user.username); updates.password = self.generatePasswordHash(updates.new_password, updates.salt); + + // reset lockouts if password changed by admin + updates.unlock = true; + } // change password else delete updates.password; delete updates.new_password; + if (updates.unlock) { + // optionally "reset" lockouts on account + // (changing password triggers this as well) + delete user.force_password_reset; + delete user.fp_date_code; + delete user.fp_count; + delete user.fl_date_code; + delete user.fl_count; + delete updates.unlock; + } + // apply updates for (var key in updates) { user[key] = updates[key]; } + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // update user record user.modified = Tools.timeNow(true); @@ -974,14 +1015,13 @@ module.exports = Class.create({ self.logDebug(6, "Successfully deleted user"); self.logTransaction('user_delete', user.username, self.getClientInfo(args)); - callback({ - code: 0 - }); // remove from manager user list in the background self.storage.listFindCut('global/users', { username: user.username }, function (err) { if (err) self.logError(1, "Failed to remove user from manager list: " + err); + callback({ code: 0 }); + self.fireHook('after_delete', args); }); @@ -1022,6 +1062,10 @@ module.exports = Class.create({ return self.doError('user', "Failed to load user: " + err, callback); } + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // success, return user record callback({ code: 0, @@ -1081,6 +1125,10 @@ module.exports = Class.create({ users[idx] = Tools.copyHashRemoveKeys(users[idx], { password: 1, salt: 1 }); } + // sanitize + users[idx].email = users[idx].email.replace(/<.+>/g, ''); + users[idx].full_name = users[idx].full_name.replace(/<.+>/g, ''); + // success, return users and list header callback({ code: 0, @@ -1092,124 +1140,129 @@ module.exports = Class.create({ }); // loaded session }, - api_external_login: function (args, callback) { + api_external_login: function(args, callback) { // query external user management system for login var self = this; var url = this.config.get('external_user_api'); if (!url) return this.doError('user', "No external_user_api config param set.", callback); - + this.logDebug(6, "Externally logging in via: " + url, args.request.headers); - + // must pass along cookie and user-agent - var request = new Request(args.request.headers['user-agent'] || 'PixlUser API'); - request.get(url, { + var request = new Request( args.request.headers['user-agent'] || 'PixlUser API' ); + request.get( url, { headers: { 'Cookie': args.request.headers['cookie'] || args.params.cookie || args.query.cookie || '' } - }, - function (err, resp, data) { - // check for error - if (err) return self.doError('user', err, callback); - if (resp.statusCode != 200) { - return self.doError('user', "Bad HTTP Response: " + resp.statusMessage, callback); - } - - var json = null; - try { json = JSON.parse(data.toString()); } - catch (err) { - return self.doError('user', "Failed to parse JSON response: " + err, callback); - } - var code = json.code || json.Code; - if (code) { - return self.doError('user', "External API Error: " + (json.description || json.Description), callback); + }, + function(err, resp, data) { + // check for error + if (err) return self.doError('user', err, callback); + if (resp.statusCode != 200) { + return self.doError('user', "Bad HTTP Response: " + resp.statusMessage, callback); + } + + var json = null; + try { json = JSON.parse( data.toString() ); } + catch (err) { + return self.doError('user', "Failed to parse JSON response: " + err, callback); + } + var code = json.code || json.Code; + if (code) { + return self.doError('user', "External API Error: " + (json.description || json.Description), callback); + } + + self.logDebug(6, "Got response from external user system:", json); + + var username = json.username || json.Username || ''; + var remote_user = json.user || json.User || null; + + if (username && remote_user) { + // user found in response! update our records and create a local session + var path = 'users/' + self.normalizeUsername(username); + + if (!username.match(self.usernameMatch)) { + return self.doError('user', "Username contains illegal characters: " + username, callback); } - - self.logDebug(6, "Got response from external user system:", json); - - var username = json.username || json.Username || ''; - var remote_user = json.user || json.User || null; - - if (username && remote_user) { - // user found in response! update our records and create a local session - var path = 'users/' + self.normalizeUsername(username); - - if (!username.match(self.usernameMatch)) { - return self.doError('user', "Username contains illegal characters: " + username); - } - - self.logDebug(7, "Testing if user exists: " + path); - - self.storage.get(path, function (err, user) { - var new_user = false; - if (!user) { - // first time, create new user - self.logDebug(6, "Creating new user: " + username); - new_user = true; - user = { - username: username, - active: 1, - created: Tools.timeNow(true), - modified: Tools.timeNow(true), - salt: Tools.generateUniqueID(64, username), - password: Tools.generateUniqueID(64), // unused - privileges: Tools.copyHash(self.config.get('default_privileges') || {}) - }; - } // new user - else { - self.logDebug(7, "User already exists: " + username); - if (user.force_password_reset) { - return self.doError('login', "Account is locked out. Please reset your password to unlock it.", callback); - } - if (!user.active) { - return self.doError('login', "User account is disabled: " + username, callback); - } + + self.logDebug(7, "Testing if user exists: " + path); + + self.storage.get(path, function(err, user) { + var new_user = false; + if (!user) { + // first time, create new user + self.logDebug(6, "Creating new user: " + username); + new_user = true; + user = { + username: username, + active: 1, + created: Tools.timeNow(true), + modified: Tools.timeNow(true), + salt: Tools.generateUniqueID( 64, username ), + password: Tools.generateUniqueID(64), // unused + privileges: Tools.copyHash( self.config.get('default_privileges') || {} ) + }; + } // new user + else { + self.logDebug(7, "User already exists: " + username); + if (user.force_password_reset) { + return self.doError('login', "Account is locked out. Please reset your password to unlock it.", callback); } - + if (!user.active) { + return self.doError('login', "User account is disabled: " + username, callback); + } + } + + // copy to args for logging + args.user = user; + + var finish = function() { // sync user info user.full_name = remote_user.full_name || remote_user.FullName || username; user.email = remote_user.email || remote_user.Email || (username + '@' + self.server.hostname); - + + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // must reset all privileges here, as remote system may delete keys when privs are revoked for (var key in user.privileges) { user.privileges[key] = 0; } - + // copy over privileges var privs = remote_user.privileges || remote_user.Privileges || {}; for (var key in privs) { var ckey = key.replace(/\W+/g, '_').toLowerCase(); user.privileges[ckey] = privs[key] ? 1 : 0; } - + // copy over avatar url user.avatar = json.avatar || json.Avatar || ''; - + // save user locally - self.storage.put(path, user, function (err) { + self.storage.put( path, user, function(err) { if (err) return self.doError('user', "Failed to create user: " + err, callback); - - // copy to args for logging - args.user = user; - + if (new_user) { self.logDebug(6, "Successfully created user: " + username); - self.logTransaction('user_create', username, - self.getClientInfo(args, { user: Tools.copyHashRemoveKeys(user, { password: 1, salt: 1 }) })); + self.logTransaction('user_create', username, + self.getClientInfo(args, { user: Tools.copyHashRemoveKeys( user, { password: 1, salt: 1 } ) })); } - + // now perform a local login - self.fireHook('before_login', args, function (err) { + self.fireHook('before_login', args, function(err) { if (err) { return self.doError('login', "Failed to login: " + err, callback); } - + // now create session var now = Tools.timeNow(true); var expiration_date = Tools.normalizeTime( now + (86400 * self.config.get('session_expire_days')), { hour: 0, min: 0, sec: 0 } ); - + // create session id and object - var session_id = Tools.generateUniqueID(64, username); + var session_id = Tools.generateUniqueID( 64, username ); var session = { id: session_id, username: username, @@ -1220,63 +1273,86 @@ module.exports = Class.create({ expires: expiration_date }; self.logDebug(6, "Logging user in: " + username + ": New Session ID: " + session_id, session); - + // store session object - self.storage.put('sessions/' + session_id, session, function (err, data) { + self.storage.put('sessions/' + session_id, session, function(err, data) { if (err) { return self.doError('user', "Failed to create session: " + err, callback); } - + // copy to args to logging args.session = session; - + self.logDebug(6, "Successfully logged in", username); self.logTransaction('user_login', username, self.getClientInfo(args)); - + // set session expiration - self.storage.expire('sessions/' + session_id, expiration_date); - - callback(Tools.mergeHashes({ - code: 0, + self.storage.expire( 'sessions/' + session_id, expiration_date ); + + callback( Tools.mergeHashes({ + code: 0, username: username, - user: Tools.copyHashRemoveKeys(user, { password: 1, salt: 1 }), - session_id: session_id - }, args.resp || {})); - + user: Tools.copyHashRemoveKeys( user, { password: 1, salt: 1 } ), + session_id: session_id + }, args.resp || {}) ); + self.fireHook('after_login', args); - - // add to manager user list in the background + + // add to master user list in the background if (new_user) { if (self.config.get('sort_global_users')) { - self.storage.listInsertSorted('global/users', { username: username }, ['username', 1], function (err) { - if (err) self.logError(1, "Failed to add user to manager list: " + err); + self.storage.listInsertSorted( 'global/users', { username: username }, ['username', 1], function(err) { + if (err) self.logError( 1, "Failed to add user to master list: " + err ); self.fireHook('after_create', args); - }); + } ); } else { - self.storage.listUnshift('global/users', { username: username }, function (err) { - if (err) self.logError(1, "Failed to add user to manager list: " + err); + self.storage.listUnshift( 'global/users', { username: username }, function(err) { + if (err) self.logError( 1, "Failed to add user to master list: " + err ); self.fireHook('after_create', args); - }); + } ); } } // new user - - }); // save session - }); // before_login - }); // save user - }); // user get - } // user is logged in - else { - // API must require a browser redirect, so pass back to client - // add our encoded self URL onto end of redirect URL - var url = json.location || json.Location; - url += encodeURIComponent(self.web.getSelfURL(args.request, '/')); - - self.logDebug(6, "Browser redirect required: " + url); - - callback({ code: 0, location: url }); - } - }); + else { + self.fireHook('after_update', args); + } + + } ); // save session + } ); // before_login + } ); // save user + }; // finish + + // fire correct hook for action + if (new_user) { + self.fireHook('before_create', args, function(err) { + if (err) { + return self.doError('user', "Failed to create user: " + err, callback); + } + finish(); + }); + } + else { + self.fireHook('before_update', args, function(err) { + if (err) { + return self.doError('user', "Failed to update user: " + err, callback); + } + finish(); + }); + } + + } ); // user get + } // user is logged in + else { + // API must require a browser redirect, so pass back to client + // add our encoded self URL onto end of redirect URL + var url = json.location || json.Location; + url += encodeURIComponent( self.web.getSelfURL(args.request, '/') ); + + self.logDebug(6, "Browser redirect required: " + url); + + callback({ code: 0, location: url }); + } + } ); }, sendEmail: function (name, args, callback) { @@ -1344,6 +1420,10 @@ module.exports = Class.create({ // get session_id out of args.params, so it doesn't interfere with API calls delete args.params.session_id; + // sanitize + user.email = user.email.replace(/<.+>/g, ''); + user.full_name = user.full_name.replace(/<.+>/g, ''); + // pass both session and user to callback callback(null, session, user); }); @@ -1359,6 +1439,10 @@ module.exports = Class.create({ this.doError('api', "Missing parameter: " + key, callback); return false; } + if (params[key] === null) { + this.doError('api', "Null parameter: " + key, callback); + return false; + } if (!params[key].toString().match(regexp)) { this.doError('api', "Malformed parameter: " + key, callback); return false; diff --git a/package.json b/package.json index 91d77a6..1966c91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cronicle-edge", - "version": "1.9.3", + "version": "1.9.4", "description": "A simple, distributed task scheduler and runner with a web based UI.", "author": "Joseph Huckaby ", "homepage": "https://github.com/jhuckaby/Cronicle",