API Docs for: 2.0.0

src/node-userlist.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2017

/**
 * @module server
 * @submodule userlist
 */

// require variables to be declared
"use strict";

const pathModule  = require('path');
const fs          = require('fs');
const JsonDB      = require('node-json-db');
const sageutils   = require('../src/node-utils');

// folder to store the user DB
const pathname = 'logs';
// Name of the file storing the user DB
const filename = 'users.json';

let nAnonClients = 0;
const strNames = "Aardvark Albatross Alligator Alpaca Ant Anteater Antelope Armadillo " +
	"Badger Barracuda Bat Beaver Bee Bison Boar Buffalo Butterfly Camel Caribou Cassowary Cat "    +
	"Caterpillar Cheetah Chicken Chinchilla Cobra Cormorant Coyote Crab Crane Crocodile "         +
	"Crow Deer Dinosaur Dog Dolphin Dove Dragonfly Duck Eagle Echidna Eel Elephant "       +
	"Emu Falcon Ferret Finch Flamingo Fox Frog Gazelle Giraffe Goat Goldfish Goose Gorilla "        +
	"Grasshopper Grizzly Hamster Hawk Hedgehog Heron Hippo Horse Hummingbird Hyena Ibex Jackal "   +
	"Jaguar Jellyfish Kangaroo Koala Lark Lemur Leopard Lion Llama Lobster Manatee "      +
	"Mink Mole Mongoose Monkey Mouse Narwhal Newt Nightingale Octopus Okapi Opossum Ostrich "      +
	"Otter Owl Oyster Panther Parrot Panda Partridge Pelican Penguin Pheasant Pigeon Porcupine "   +
	"Porpoise Quail Rabbit Raccoon Raven Rhinoceros Salamander Seahorse Seal Shark Sheep "   +
	"Sloth Snail Squid Squirrel Starling Swan Tapir Tiger T-rex Turtle Walrus Weasel Whale "       +
	"Wolf Wombat Yak Zebra";
const tempNames = strNames.split(' ');

/**
 * Creates an uid.
 *
 * @method     createUid
 * @param      {String}  name    The name
 * @param      {<type>}  email   The email
 * @return     {String}  the uid string
 */
function createUid(name, email) {
	return name.replace(/[;,=]/g, '-') + '-' + Date.now();
}

/**
 * shuffle randomly and array
 *
 * @method     shuffle
 * @param      {<type>}  array   The array
 */
function shuffle(array) {
	let l = array.length, t, i;

	while (l) {
		i = Math.floor(Math.random() * l--);

		// swap random element to end of unshuffled segment
		t = array[l];
		array[l] = array[i];
		array[i] = t;
	}
}

/**
 * Handles users and storage to database
 * as well as user roles and permissions
 *
 * @class UserList
 */
class UserList {
	constructor() {
		this.currentSession = null;

		// make sure that the path and file exist
		if (!sageutils.folderExists(pathname)) {
			fs.mkdirSync(pathname);
		}
		if (!sageutils.fileExists(this.filePath)) {
			fs.writeFileSync(this.filePath, "{}");
		}

		// create the database
		this.db = new JsonDB(
			this.filePath,
			true, 	// save after each push
			true	// save in human-readable format
		);

		// per session
		shuffle(tempNames);
		this.clients = {};
		this.rbac = null;
		this.rbacList = [];

		// get roles/permissions
		let getRbac = this.getData('/rbac');
		if (getRbac.success) {
			this.rbacList = getRbac.data || [];
			this.rbac = this.rbacList[0];
		}
		if (!this.rbac) {
			this.initRolesAndPermissions({
				roles: ['admin', 'user', 'guest'],
				actions: [
					'upload files',
					'use apps',
					'share screen',
					'share pointer',
					'move/resize windows'
				],
				permissions: {
					admin: 0b11111,
					user:  0b11111,
					guest: 0b11111
				}
			});
		}
	}

	/**
	* Track user locally by ip
	*
	* @method track
	* @param ip {String} ip address
	* @param user {Object} user
	* @return {String} name of the user
	*/
	track(ip, user) {
		this.clients[ip] = {
			user: user,
			role: []
		};

		// assign guest role to non-logged in users
		if (!user.name && !user.email) {
			this.assignRole(ip, 'guest');
			if (!user.SAGE2_ptrName) {
				user.SAGE2_ptrName = 'Anon ' + tempNames[nAnonClients];
				nAnonClients = (nAnonClients + 1) % tempNames.length; // FIXME
			}
		} else {
			// assign user role to logged in users by default
			this.assignRole(ip, 'user');

			// find if user already has an active client
			for (let i in this.clients) {
				if (i !== ip && this.clients[i].user.name === user.name && this.clients[i].user.email === user.email) {
					this.clients[ip].role = this.clients[i].role;
				}
				break;
			}
		}

		return user.SAGE2_ptrName;
	}

	/**
	* Stop tracking this ip
	*
	* @method disconnect
	* @param ip {String} ip address
	*/
	disconnect(ip) {
		delete this.clients[ip];
	}

	// ***********  Role Management Functions *************

	/**
	* Initialize role access system
	*
	* @method initRolesAndPermissions
	* @param rbac {Object} object containing the three parameters:
	*  - roles:       an array of strings of role names
	*  - actions:     an array of strings of action names
	*  - permissions: an object of role-bitfield pairs
	*/
	initRolesAndPermissions(rbac) {
		rbac.mask = {};

		let l = rbac.actions.length - 1;
		rbac.actions.forEach((action, i) => {
			rbac.mask[action] = (1 << (l - i));
		});
		rbac.maskAll = (1 << rbac.actions.length) - 1;

		this.rbacList.push(rbac);
		this.rbac = rbac;
	}

	/**
	* Set permissions for this role
	*
	* @method defineRolePermissions
	* @param role {String}
	* @param permissions {Object} list of permission names and values
	* as String-Boolean pairs
	*/
	defineRolePermissions(role, permissions) {
		if (this.rbac.roles.indexOf(role) < 0) {
			this.rbac.roles.push(role);
		}

		// generate permission bit string
		let pBits = 0;
		for (let action in permissions) {
			if (permissions[action] && this.rbac.mask[action]) {
				pBits |= this.rbac.mask[action];
			}
		}

		this.rbac.permissions[role] = pBits;
	}

	/**
	* Add permission for this action to the role
	*
	* @method grantPermission
	* @param role {String}
	* @param action {String}
	*/
	grantPermission(role, action) {
		if (this.rbac.roles.indexOf(role) > -1) {
			this.rbac.permissions[role] |= this.rbac.mask[action];
		}
	}

	/**
	* Remove permission for this action from the role
	*
	* @method revokePermission
	* @param role {String}
	* @param action {String}
	*/
	revokePermission(role, action) {
		if (this.rbac.roles.indexOf(role) > -1) {
			this.rbac.permissions[role] &= (this.rbac.maskAll ^ this.rbac.mask[action]);
		}
	}


	/**
	* Set the user to have only this role
	*
	* @method assignRole
	* @param ip {String}
	* @param role {String}
	*/
	assignRole(ip, role) {
		if (this.clients[ip]) {
			this.clients[ip].role = [role];
		}
	}

	/**
	* Add this role to the list of user's roles
	*
	* @method addRole
	* @param ip {String}
	* @param role {String}
	*/
	addRole(ip, role) {
		if (this.clients[ip] && this.clients[ip].role.indexOf(role) < 0) {
			this.clients[ip].role.push(role);
		}
	}

	/**
	* Remove this role from the list of user's roles
	*
	* @method removeRole
	* @param ip {String}
	* @param role {String}
	*/
	removeRole(ip, role) {
		if (this.clients[ip]) {
			let i = this.clients[ip].role.indexOf(role);
			if (i > -1) {
				this.clients[ip].role.splice(i, 1);
			}
		}
	}

	/**
	* Check if user has permission to do an action
	*
	* @method isAllowed
	* @param ip {String} client ip requesting permission
	* @param action {String} name of the action
	* @return {Boolean} true if user is permitted to perform this action
	*/
	isAllowed(ip, action) {
		// server's special case
		if (ip === "127.0.0.1:42") {
			return true;
		}
		if (!this.clients[ip]) {
			return false;
		}
		let roles = this.clients[ip].role;
		for (let i in roles) {
			if (this.rbac.mask[action] & this.rbac.permissions[roles[i]]) {
				return true;
			}
		}
		return false;
	}

	/**
	* save permissions models to database
	*
	* @method save
	*/
	save() {
		this.push('/rbac', this.rbacList);
	}

	// **************  Database Functions *****************

	/**
	* Reload database if json file was changed externally
	*
	* @method reload
	*/
	reload() {
		this.db.reload();
	}

	/**
	 * Wrapper for JsonDB.getData()
	 * Retrieve data from json database or log an error if it fails
	 *
	 * @method getData
	 * @param path {String}
	 * @return {Object} object with the success flag and the retrieved data
	 */
	getData(path) {
		try {
			let data = this.db.getData(path);
			return {
				success: true,
				data: data
			};
		} catch (error) {
			// sageutils.log("Userlist", "Error", error.message);
			return {
				success: false
			};
		}
	}

	/**
	 * Wrapper for JsonDB.push()
	 * Push data to json database or log an error if it fails
	 *
	 * @method push
	 * @param path {String}
	 * @param data {Object} new data to be pushed
	 * @param checkIfPathExists {Boolean} check if path exists before pushing * the data; default is false
	 * @return {Boolean} true if push succeeds
	 */
	push(path, data, overwrite = true, checkIfPathExists = false) {
		try {
			if (checkIfPathExists) {
				this.db.getData(path);
			}
			this.db.push(path, data, overwrite);
			return true;
		} catch (error) {
			// sageutils.log("Userlist", "Error", error.message);
			return false;
		}
	}

	/**
	 * Wrapper for JsonDB.delete()
	 * Remove data at a path or log an error if it fails
	 *
	 * @method delete
	 * @param path {String}
	 * @return {Boolean} true if delete succeeds
	 */
	delete(path) {
		try {
			this.db.delete(path);
			return true;
		} catch (error) {
			sageutils.log("Userlist", "Error", error.message);
		}
		return false;
	}

	/**
	 * Store a new user in the database
	 *
	 * @method addNewUser
	 * @param name {String}
	 * @param email {String}
	 * @param properties {Object}
	 * @return {Object} object with the user token, user object, and an error
	 * message if the user could not be added
	 */
	addNewUser(name, email, properties = {}) {
		name = name && name.trim();
		email = email && email.trim();
		if (name && email) {
			let req = this.getUser(name, email);
			if (req.error === null) {
				return {
					error: 'User already exists. Sign in instead.',
					user: req.user,
					uid: req.uid
				};
			} else {
				// create a new uid
				let uid = createUid(name, email);

				// add new user to database
				let newUser = Object.assign({name, email}, properties);
				this.push(this.userPath(uid), newUser);
				return {
					error: null,
					uid: uid,
					user: newUser
				};
			}
		}
		return {
			error: 'User must have a name and an email.'
		};
	}

	/**
	 * Retrieve a user in the database by name and email
	 *
	 * @method getUser
	 * @param name {String}
	 * @param email {String}
	 * @return {Object} object with the user token, user object, and an error
	 * message if the user could not be added
	 */
	getUser(name, email) {
		let req = this.getData('/user');
		if (req.success) {
			for (let uid in req.data) {
				if (req.data[uid].name === name && req.data[uid].email === email) {
					return {
						uid: uid,
						user: req.data[uid],
						error: null
					};
					// return uid;
				}
			}
		}
		return {
			user: null,
			uid: null,
			error: "Could not find user."
		};
	}

	/**
	 * Retrieve a user in the database by user id
	 *
	 * @method getUserById
	 * @param uid {String}
	 * @return {Object} object with the user token, user object, and an error
	 * message if the user could not be added
	 */
	getUserById(uid) {
		let req = this.getData(this.userPath(uid));
		if (req.success) {
			return {
				uid: uid,
				user: req.data,
				error: null
			};
		}

		return {
			user: null,
			uid: null,
			error: "Could not find user."
		};
	}

	/**
	 * Remove user from the database
	 *
	 * @method removeUser
	 * @param uid {String}
	 * @return {Boolean} true if delete succeeds
	 */
	removeUser(uid) {
		return this.delete(this.userPath(uid));
	}


	/**
	 * Edit user properties
	 *
	 * @method editUser
	 * @param uid {String}
	 * @param properties {Object}
	 * @return {Boolean} true if edit succeeds
	 */
	editUser(uid, properties) {
		// name and email keys cannot be empty
		if (!properties.name || !properties.name.trim()) {
			delete properties.name;
		}
		if (!properties.email || !properties.email.trim()) {
			delete properties.email;
		}
		return this.push(this.userPath(uid), properties, false, true);
	}

	/**
	 * Get properties of a user
	 *
	 * @method getProperty
	 * @param uid {String}
	 * @param arguments {String} property key(s)
	 * @return the property or an array of properties
	 */
	getProperty(uid) {
		var keys = [].slice.call(arguments, 1);
		let req = this.getData(this.userPath(uid));
		if (req.success) {
			if (keys.length === 0) {
				return null;
			} else if (keys.length === 1) {
				return req.data[keys[0]];
			} else {
				return keys.map(key => req.data[key]);
			}
		}
		return null;
	}

	userPath(uid) {
		return '/user/' + uid;
	}

	get filePath() {
		return pathModule.join(pathname, filename);
	}
}

module.exports = new UserList();