Merge pull request #1056 from denis99999/group-feature

Addition of a powerful system for device booking & partitioning
This commit is contained in:
Karol Wrótniak
2020-02-08 01:30:37 +01:00
committed by GitHub
124 changed files with 12442 additions and 395 deletions

View File

@@ -1,79 +1,527 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var _ = require('lodash')
var Promise = require('bluebird')
var dbapi = require('../../../db/api')
var logger = require('../../../util/logger')
var datautil = require('../../../util/datautil')
var log = logger.createLogger('api:controllers:devices')
module.exports = {
getDevices: getDevices
, getDeviceBySerial: getDeviceBySerial
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const util = require('util')
const uuid = require('uuid')
const wire = require('../../../wire')
const wireutil = require('../../../wire/util')
const wirerouter = require('../../../wire/router')
/* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */
function filterGenericDevices(req, res, devices) {
apiutil.respond(res, 200, 'Devices Information', {
devices: devices.map(function(device) {
return apiutil.filterDevice(req, device)
})
})
}
function getDevices(req, res) {
var fields = req.swagger.params.fields.value
function getGenericDevices(req, res, loadDevices) {
loadDevices(req.user.groups.subscribed).then(function(devices) {
filterGenericDevices(req, res, devices)
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to load device list: ', err.stack)
})
}
dbapi.loadDevices()
.then(function(cursor) {
return Promise.promisify(cursor.toArray, cursor)()
.then(function(list) {
var deviceList = []
list.forEach(function(device) {
datautil.normalize(device, req.user)
var responseDevice = device
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
deviceList.push(responseDevice)
})
res.json({
success: true
, devices: deviceList
})
})
function getDeviceFilteredGroups(serial, fields, bookingOnly) {
return dbapi.getDeviceGroups(serial).then(function(groups) {
return Promise.map(groups, function(group) {
return !bookingOnly || !apiutil.isOriginGroup(group.class) ?
group :
'filtered'
})
.catch(function(err) {
log.error('Failed to load device list: ', err.stack)
res.status(500).json({
success: false
.then(function(groups) {
return _.without(groups, 'filtered').map(function(group) {
if (fields) {
return _.pick(apiutil.publishGroup(group), fields.split(','))
}
return apiutil.publishGroup(group)
})
})
})
}
function extractStandardizableDevices(devices) {
return dbapi.getTransientGroups().then(function(groups) {
return Promise.map(devices, function(device) {
return Promise.map(groups, function(group) {
if (group.devices.indexOf(device.serial) > -1) {
return Promise.reject('booked')
}
return true
})
.then(function() {
return device
})
.catch(function(err) {
if (err !== 'booked') {
throw err
}
return err
})
})
.then(function(devices) {
return _.without(devices, 'booked')
})
})
}
function getStandardizableDevices(req, res) {
dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
extractStandardizableDevices(devices).then(function(devices) {
filterGenericDevices(req, res, devices)
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to load device list: ', err.stack)
})
}
function removeDevice(serial, req, res) {
const presentState = req.swagger.params.present.value
const bookingState = req.swagger.params.booked.value
const notesState = req.swagger.params.annotated.value
const controllingState = req.swagger.params.controlled.value
const anyPresentState = typeof presentState === 'undefined'
const anyBookingState = typeof bookingState === 'undefined'
const anyNotesState = typeof notesState === 'undefined'
const anyControllingState = typeof controllingState === 'undefined'
const lock = {}
function deleteGroupDevice(email, id) {
const lock = {}
return dbapi.lockGroupByOwner(email, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const group = lock.group = stats.changes[0].new_val
if (group.devices.indexOf(serial) > -1) {
return apiutil.isOriginGroup(group.class) ?
dbapi.removeOriginGroupDevice(group, serial) :
dbapi.removeGroupDevices(group, [serial])
}
return group
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function deleteDeviceInDatabase() {
function wrappedDeleteDeviceInDatabase() {
const result = {
status: false
, data: 'not deleted'
}
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (device && device.group.id === device.group.origin) {
return deleteGroupDevice(device.group.owner.email, device.group.id)
.then(function(group) {
if (group !== 'not found') {
return dbapi.deleteDevice(serial).then(function() {
result.status = true
result.data = 'deleted'
})
}
return false
})
}
return false
})
.then(function() {
return result
})
}
return apiutil.setIntervalWrapper(
wrappedDeleteDeviceInDatabase
, 10
, Math.random() * 500 + 50)
}
return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const device = lock.device = stats.changes[0].new_val
if (!anyPresentState && device.present !== presentState ||
!anyControllingState && (device.owner === null) === controllingState ||
!anyNotesState &&
(typeof device.notes !== 'undefined' && device.notes !== '') !== notesState ||
!anyBookingState && (device.group.id !== device.group.origin && !bookingState ||
device.group.class === apiutil.STANDARD && bookingState)) {
return 'unchanged'
}
if (device.group.class === apiutil.STANDARD) {
return deleteDeviceInDatabase()
}
return dbapi.getDeviceTransientGroups(serial).then(function(groups) {
if (groups.length && !anyBookingState && !bookingState) {
return 'unchanged'
}
return Promise.each(groups, function(group) {
return deleteGroupDevice(group.owner.email, group.id)
})
.then(function() {
if (!groups.length && !anyBookingState && bookingState) {
return 'unchanged'
}
return deleteDeviceInDatabase()
})
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
/* ------------------------------------ PUBLIC FUNCTIONS ------------------------------- */
function getDevices(req, res) {
const target = req.swagger.params.target.value
switch(target) {
case apiutil.BOOKABLE:
getGenericDevices(req, res, dbapi.loadBookableDevices)
break
case apiutil.ORIGIN:
getGenericDevices(req, res, dbapi.loadDevicesByOrigin)
break
case apiutil.STANDARD:
getGenericDevices(req, res, dbapi.loadStandardDevices)
break
case apiutil.STANDARDIZABLE:
getStandardizableDevices(req, res)
break
default:
getGenericDevices(req, res, dbapi.loadDevices)
}
}
function getDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
var fields = req.swagger.params.fields.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
let responseDevice = apiutil.publishDevice(device, req.user)
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
res.json({
success: true
, device: responseDevice
})
}
datautil.normalize(device, req.user)
var responseDevice = device
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
res.json({
success: true
, device: responseDevice
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
log.error('Failed to load device "%s": ', serial, err.stack)
res.status(500).json({
success: false
})
})
}
function getDeviceGroups(req, res) {
const serial = req.swagger.params.serial.value
const fields = req.swagger.params.fields.value
dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
return cursor.toArray()
})
.then(function(devices) {
if (!devices.length) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDeviceFilteredGroups(serial, fields, false)
.then(function(groups) {
return apiutil.respond(res, 200, 'Groups Information', {groups: groups})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get device groups: ', err.stack)
})
}
function getDeviceBookings(req, res) {
const serial = req.swagger.params.serial.value
const fields = req.swagger.params.fields.value
dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
return cursor.toArray()
})
.then(function(devices) {
if (!devices.length) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDeviceFilteredGroups(serial, fields, true)
.then(function(bookings) {
apiutil.respond(res, 200, 'Bookings Information', {bookings: bookings})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get device bookings: ', err.stack)
})
}
function addOriginGroupDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const fields = apiutil.getQueryParameter(req.swagger.params.fields)
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
function askUpdateDeviceOriginGroup(group, serial) {
return new Promise(function(resolve, reject) {
const signature = util.format('%s', uuid.v4()).replace(/-/g, '')
let messageListener
const responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
apiutil.respond(res, 504, 'Gateway Time-out')
reject('timeout')
}, 5000)
messageListener = wirerouter()
.on(wire.DeviceOriginGroupMessage, function(channel, message) {
if (message.signature === signature) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
dbapi.loadDeviceBySerial(serial).then(function(device) {
if (fields) {
resolve(_.pick(apiutil.publishDevice(device, req.user), fields.split(',')))
}
else {
resolve(apiutil.publishDevice(device, req.user))
}
})
}
})
.handler()
req.options.channelRouter.on(wireutil.global, messageListener)
return dbapi.askUpdateDeviceOriginGroup(serial, group, signature)
})
}
function updateDeviceOriginGroup(group, serial) {
const lock = {}
return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.device = stats.changes[0].new_val
return dbapi.isUpdateDeviceOriginGroupAllowed(serial, group)
.then(function(updatingAllowed) {
if (!updatingAllowed) {
apiutil.respond(res, 403, 'Forbidden (device is currently booked)')
return Promise.reject('booked')
}
return askUpdateDeviceOriginGroup(group, serial)
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
function updateDevicesOriginGroup(group, serials) {
let results = []
return Promise.each(serials, function(serial) {
return updateDeviceOriginGroup(group, serial).then(function(result) {
results.push(result)
})
})
.then(function() {
const result = target === 'device' ? {device: {}} : {devices: []}
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`, result)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
if (target === 'device') {
result.device = results[0]
}
else {
result.devices = results
}
return apiutil.respond(res, 200, `Updated (${target})`, result)
})
.catch(function(err) {
if (err !== 'booked' && err !== 'timeout' && err !== 'busy') {
throw err
}
})
}
return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (!apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (this group cannot act as an origin one)')
}
if (typeof serials !== 'undefined') {
return updateDevicesOriginGroup(
group
, _.without(serials.split(','), '').filter(function(serial) {
return group.devices.indexOf(serial) < 0
})
)
}
return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
if (group.class === apiutil.BOOKABLE) {
return devices
}
return extractStandardizableDevices(devices)
})
.then(function(devices) {
const serials = []
devices.forEach(function(device) {
if (group.devices.indexOf(device.serial) < 0) {
serials.push(device.serial)
}
})
return updateDevicesOriginGroup(group, serials)
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to update ${target} origin group: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function addOriginGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', addOriginGroupDevices, req, res)
}
function removeOriginGroupDevices(req, res) {
const lock = {}
return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (!apiutil.checkBodyParameter(req.body, 'serials')) {
req.body = {serials: group.devices.join()}
}
return dbapi.getRootGroup().then(function(group) {
req.swagger.params.id = {value: group.id}
return addOriginGroupDevices(req, res)
})
}
return false
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeOriginGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', removeOriginGroupDevices, req, res)
}
function deleteDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
function removeDevices(serials) {
let results = []
return Promise.each(serials, function(serial) {
return removeDevice(serial, req, res).then(function(result) {
if (result === 'not deleted') {
apiutil.respond(res, 503, 'Server too busy [code: 2], please try again later')
return Promise.reject('busy')
}
return results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
if (!_.without(results, 'not found').length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof serials === 'undefined') {
return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
return removeDevices(devices.map(function(device) {
return device.serial
}))
})
}
else {
return removeDevices(_.without(serials.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, `Failed to delete ${target}: `, err.stack)
})
}
function deleteDevice(req, res) {
apiutil.redirectApiWrapper('serial', deleteDevices, req, res)
}
module.exports = {
getDevices: getDevices
, getDeviceBySerial: getDeviceBySerial
, getDeviceGroups: getDeviceGroups
, getDeviceBookings: getDeviceBookings
, addOriginGroupDevice: addOriginGroupDevice
, addOriginGroupDevices: addOriginGroupDevices
, removeOriginGroupDevice: removeOriginGroupDevice
, removeOriginGroupDevices: removeOriginGroupDevices
, deleteDevice: deleteDevice
, deleteDevices: deleteDevices
}

View File

@@ -0,0 +1,931 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const _ = require('lodash')
const dbapi = require('../../../db/api')
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const util = require('util')
const uuid = require('uuid')
const Promise = require('bluebird')
const usersapi = require('./users')
/* ---------------------------------- PRIVATE FUNCTIONS --------------------------------- */
function groupApiWrapper(email, fn, req, res) {
dbapi.loadUser(email).then(function(user) {
if (!user) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
req.user = user
fn(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack)
})
}
function getDevice(req, serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (!device) {
throw new Error(`Device not found: ${serial}`)
}
return apiutil.filterDevice(req, device)
})
}
function checkConflicts(id, devices, dates) {
function computeConflicts(conflicts, liteGroup, otherGroup) {
if (otherGroup.id !== liteGroup.id) {
const devices = _.intersection(liteGroup.devices, otherGroup.devices)
if (devices.length) {
for (let liteGroupDate of liteGroup.dates) {
for (let otherGroupDate of otherGroup.dates) {
if (liteGroupDate.start < otherGroupDate.stop &&
liteGroupDate.stop > otherGroupDate.start) {
conflicts.push({
devices: devices
, date: {
start: new Date(
Math.max(liteGroupDate.start.getTime()
, otherGroupDate.start.getTime()))
, stop: new Date(
Math.min(liteGroupDate.stop.getTime()
, otherGroupDate.stop.getTime()))
}
, group: otherGroup.name
, owner: otherGroup.owner
})
}
}
}
}
}
}
return dbapi.getTransientGroups().then(function(groups) {
const conflicts = []
groups.forEach(function(otherGroup) {
computeConflicts(
conflicts
, {id: id, devices: devices, dates: dates}
, otherGroup)
})
return conflicts
})
}
function checkSchedule(res, oldGroup, _class, email, repetitions, privilege, start, stop) {
if (oldGroup && oldGroup.devices.length &&
(apiutil.isOriginGroup(_class) && !apiutil.isOriginGroup(oldGroup.class) ||
apiutil.isOriginGroup(oldGroup.class) && !apiutil.isOriginGroup(_class))) {
return Promise.resolve(apiutil.respond(res, 403,
'Forbidden (unauthorized class while device list is not empty)'))
}
if (apiutil.isAdminGroup(_class) && privilege === apiutil.USER) {
return Promise.resolve(apiutil.respond(res, 403, 'Forbidden (unauthorized class)'))
}
if (isNaN(start.getTime())) {
return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid startTime format)'))
}
if (isNaN(stop.getTime())) {
return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid stopTime format)'))
}
if (start >= stop) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid life time: startTime >= stopTime)'))
}
if ((stop - start) > apiutil.CLASS_DURATION[_class]) {
return Promise.resolve(apiutil.respond(res, 400,
'Bad Request (Invalid Life time & class combination: life time > class duration)'
))
}
switch(_class) {
case apiutil.BOOKABLE:
case apiutil.STANDARD:
case apiutil.ONCE:
if (repetitions !== 0) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)'))
}
break
default:
if (repetitions === 0) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)'))
}
break
}
return dbapi.loadUser(email).then(function(owner) {
if (repetitions > owner.groups.quotas.repetitions) {
return apiutil.respond(res, 400, 'Bad Request (Invalid repetitions value)')
}
return true
})
}
/* ---------------------------------- PUBLIC FUNCTIONS ------------------------------------- */
function addGroupDevices(req, res) {
const id = req.swagger.params.id.value
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
let email = null
function addGroupDevice(group, serial) {
const lock = {}
return dbapi.lockBookableDevice(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.device = stats.changes[0].new_val
return dbapi.lockGroup(lock.device.group.origin).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.group = {id: lock.device.group.origin}
return checkConflicts(id, [serial], group.dates).then(function(conflicts) {
return conflicts.length ?
Promise.reject(conflicts) :
dbapi.addGroupDevices(group, [serial])
})
})
.finally(function() {
lockutil.unlockGroup(lock)
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
function _addGroupDevices(lockedGroup, serials) {
let results = []
let group = lockedGroup
return Promise.each(serials, function(serial) {
return addGroupDevice(group, serial).then(function(result) {
results.push(result)
if (result.hasOwnProperty('id')) {
group = result
}
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
else {
results = _.without(results, 'not found')
if (!results.length) {
apiutil.respond(res, 404, `Not Found (group ${target})`)
}
else {
apiutil.respond(res, 200, `Added (group ${target})`
, {group: apiutil.publishGroup(results[results.length - 1])})
}
}
})
.catch(function(err) {
if (err === 'quota is reached') {
apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)')
}
else if (Array.isArray(err)) {
apiutil.respond(res, 409, 'Conflicts Information', {conflicts: err})
}
else if (err !== 'busy') {
throw err
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
let group = lock.group
if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) {
email = group.owner.email
return false
}
if (apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)')
}
return (function() {
if (typeof serials === 'undefined') {
return dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) {
const serials = []
devices.forEach(function(device) {
if (group.devices.indexOf(device.serial) < 0) {
serials.push(device.serial)
}
})
return _addGroupDevices(group, serials)
})
}
else {
return _addGroupDevices(
group
, _.difference(
_.without(serials.split(','), '')
, group.devices)
)
}
})()
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to add group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
if (email) {
groupApiWrapper(email, addGroupDevices, req, res)
}
})
}
function addGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', addGroupDevices, req, res)
}
function removeGroupDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)')
}
let serialsToRemove = group.devices
if (typeof serials !== 'undefined') {
serialsToRemove = _.without(serials.split(','), '')
}
if (!serialsToRemove.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
serialsToRemove = _.intersection(serialsToRemove, group.devices)
if (!serialsToRemove.length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
return dbapi.removeGroupDevices(group, serialsToRemove).then(function(group) {
apiutil.respond(res, 200, `Removed (group ${target})`, {group: apiutil.publishGroup(group)})
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', removeGroupDevices, req, res)
}
function getGroupDevice(req, res) {
const id = req.swagger.params.id.value
const serial = req.swagger.params.serial.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else if (group.devices.indexOf(serial) < 0) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDevice(req, serial).then(function(device) {
apiutil.respond(res, 200, 'Device Information', {device: device})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group device: ', err.stack)
})
}
function getGroupUser(req, res) {
const id = req.swagger.params.id.value
const email = req.swagger.params.email.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else if (group.users.indexOf(email) < 0) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
usersapi.getUserByEmail(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group user: ', err.stack)
})
}
function getGroupUsers(req, res) {
const id = req.swagger.params.id.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else {
Promise.map(group.users, function(email) {
return usersapi.getUserInfo(req, email).then(function(user) {
return user || Promise.reject(`Group user not found: ${email}`)
})
})
.then(function(users) {
apiutil.respond(res, 200, 'Users Information', {users: users})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group users: ', err.stack)
})
}
function removeGroupUsers(req, res) {
const id = req.swagger.params.id.value
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
const lock = {}
function removeGroupUser(email, group, rootGroup) {
if (group.users.indexOf(email) < 0) {
return Promise.resolve('not found')
}
if (email === rootGroup.owner.email || email === group.owner.email) {
return Promise.resolve('forbidden')
}
const lock = {}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.user = stats.changes[0].new_val
return dbapi.isRemoveGroupUserAllowed(email, group)
.then(function(isAllowed) {
return isAllowed ? dbapi.removeGroupUser(id, email) : 'forbidden'
})
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
return dbapi.getRootGroup().then(function(rootGroup) {
let emailsToRemove = group.users
let results = []
if (typeof emails !== 'undefined') {
emailsToRemove = _.without(emails.split(','), '')
}
return Promise.each(emailsToRemove, function(email) {
return removeGroupUser(email, group, rootGroup).then(function(result) {
results.push(result)
})
})
.then(function() {
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
if (!_.without(results, 'forbidden').length) {
return apiutil.respond(res, 403, `Forbidden (group ${target})`)
}
return dbapi.getGroup(id).then(function(group) {
apiutil.respond(res, 200, `Removed (group ${target})`, {
group: apiutil.publishGroup(group)})
})
})
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroupUser(req, res) {
apiutil.redirectApiWrapper('email', removeGroupUsers, req, res)
}
function addGroupUsers(req, res) {
const id = req.swagger.params.id.value
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
const lock = {}
function addGroupUser(email) {
const lock = {}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.user = stats.changes[0].new_val
return dbapi.addGroupUser(id, email)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function _addGroupUsers(emails) {
let results = []
return Promise.each(emails, function(email) {
return addGroupUser(email).then(function(result) {
results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
if (!_.without(results, 'not found').length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
return dbapi.getGroup(id).then(function(group) {
apiutil.respond(res, 200, `Added (group ${target})`, {group: apiutil.publishGroup(group)})
})
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (!lockingSuccessed) {
return false
}
const group = lock.group
return (function() {
if (typeof emails === 'undefined') {
return dbapi.getUsers().then(function(users) {
const emails = []
users.forEach(function(user) {
if (group.users.indexOf(user.email) < 0) {
emails.push(user.email)
}
})
return _addGroupUsers(emails)
})
}
else {
return _addGroupUsers(
_.difference(
_.without(emails.split(','), '')
, group.users)
)
}
})()
})
.catch(function(err) {
apiutil.internalError(res, `Failed to add group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function addGroupUser(req, res) {
apiutil.redirectApiWrapper('email', addGroupUsers, req, res)
}
function getGroup(req, res) {
const id = req.swagger.params.id.value
const fields = req.swagger.params.fields.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
return
}
let publishedGroup = apiutil.publishGroup(group)
if (fields) {
publishedGroup = _.pick(publishedGroup, fields.split(','))
}
apiutil.respond(res, 200, 'Group Information', {group: publishedGroup})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group: ', err.stack)
})
}
function getGroups(req, res) {
const fields = req.swagger.params.fields.value
const owner = req.swagger.params.owner.value
let getGenericGroups
switch(owner) {
case true:
getGenericGroups = dbapi.getOwnerGroups
break
case false:
getGenericGroups = dbapi.getOnlyUserGroups
break
default:
getGenericGroups = dbapi.getUserGroups
}
getGenericGroups(req.user.email).then(function(groups) {
return apiutil.respond(res, 200, 'Groups Information', {
groups: groups.map(function(group) {
if (fields) {
return _.pick(apiutil.publishGroup(group), fields.split(','))
}
return apiutil.publishGroup(group)
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get groups: ', err.stack)
})
}
function createGroup(req, res) {
const _class = typeof req.body.class === 'undefined' ? apiutil.ONCE : req.body.class
const repetitions =
apiutil.isOriginGroup(_class) || typeof req.body.repetitions === 'undefined' ?
0 :
req.body.repetitions
const now = Date.now()
const start =
apiutil.isOriginGroup(_class) ?
new Date(now) :
new Date(req.body.startTime || now)
const stop =
apiutil.isOriginGroup(_class) ?
new Date(now + apiutil.ONE_YEAR) :
new Date(req.body.stopTime || now + apiutil.ONE_HOUR)
checkSchedule(res, null, _class, req.user.email, repetitions, req.user.privilege,
start, stop).then(function(checkingSuccessed) {
if (!checkingSuccessed) {
return
}
const name =
typeof req.body.name === 'undefined' ?
'New_' + util.format('%s', uuid.v4()).replace(/-/g, '') :
req.body.name
const state =
apiutil.isOriginGroup(_class) || typeof req.body.state === 'undefined' ?
apiutil.READY :
req.body.state
const isActive = state === apiutil.READY && apiutil.isOriginGroup(_class)
const duration = 0
const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions)
dbapi.createUserGroup({
name: name
, owner: {
email: req.user.email
, name: req.user.name
}
, privilege: req.user.privilege
, class: _class
, repetitions: repetitions
, isActive: isActive
, dates: dates
, duration: duration
, state: state
})
.then(function(group) {
if (group) {
apiutil.respond(res, 201, 'Created', {group: apiutil.publishGroup(group)})
}
else {
apiutil.respond(res, 403, 'Forbidden (groups number quota is reached)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create group: ', err.stack)
})
})
}
function deleteGroups(req, res) {
const ids = apiutil.getBodyParameter(req.body, 'ids')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'group' : 'groups'
function removeGroup(id) {
const lock = {}
return dbapi.lockGroupByOwner(req.user.email, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const group = lock.group = stats.changes[0].new_val
if (group.privilege === apiutil.ROOT) {
return 'forbidden'
}
if (group.class === apiutil.BOOKABLE) {
return Promise.each(group.devices, function(serial) {
return dbapi.isDeviceBooked(serial)
.then(function(isBooked) {
return isBooked ? Promise.reject('booked') : true
})
})
.then(function() {
return dbapi.deleteUserGroup(id)
})
.catch(function(err) {
if (err !== 'booked') {
throw err
}
return 'forbidden'
})
}
else {
return dbapi.deleteUserGroup(id)
}
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroups(ids) {
let results = []
return Promise.each(ids, function(id) {
return removeGroup(id).then(function(result) {
results.push(result)
})
})
.then(function() {
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
results = _.without(results, 'forbidden')
if (!results.length) {
return apiutil.respond(res, 403, `Forbidden (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof ids === 'undefined') {
return dbapi.getOwnerGroups(req.user.email).then(function(groups) {
const ids = []
groups.forEach(function(group) {
if (group.privilege !== apiutil.ROOT) {
ids.push(group.id)
}
})
return removeGroups(ids)
})
}
else {
return removeGroups(_.without(ids.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, `Failed to delete ${target}: `, err.stack)
})
}
function deleteGroup(req, res) {
apiutil.redirectApiWrapper('id', deleteGroups, req, res)
}
function updateGroup(req, res) {
const id = req.swagger.params.id.value
const lock = {}
function updateUserGroup(group, data) {
return dbapi.updateUserGroup(group, data)
.then(function(group) {
if (group) {
apiutil.respond(res, 200, 'Updated (group)', {group: apiutil.publishGroup(group)})
}
else {
apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)')
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (!lockingSuccessed) {
return false
}
const group = lock.group
const _class = typeof req.body.class === 'undefined' ? group.class : req.body.class
const name = typeof req.body.name === 'undefined' ? group.name : req.body.name
const repetitions =
typeof req.body.repetitions === 'undefined' ?
group.repetitions :
req.body.repetitions
const start = new Date(req.body.startTime || group.dates[0].start)
const stop = new Date(req.body.stopTime || group.dates[0].stop)
let state, isActive
if (apiutil.isOriginGroup(_class)) {
state = apiutil.READY
isActive = true
}
else {
state = typeof req.body.state === 'undefined' ? apiutil.PENDING : req.body.state
isActive = false
}
if (group.state === apiutil.READY && state === apiutil.PENDING) {
return apiutil.respond(res, 403, 'Forbidden (group is ready)')
}
return checkSchedule(res, group, _class, group.owner.email, repetitions, group.privilege,
start, stop).then(function(checkingSuccessed) {
if (!checkingSuccessed) {
return false
}
if (name === group.name &&
start.toISOString() === group.dates[0].start.toISOString() &&
stop.toISOString() === group.dates[0].stop.toISOString() &&
state === group.state &&
_class === group.class &&
repetitions === group.repetitions) {
return apiutil.respond(res, 200, 'Unchanged (group)', {group: {}})
}
const duration = group.devices.length * (stop - start) * (repetitions + 1)
const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions)
if (start < group.dates[0].start ||
stop > group.dates[0].stop ||
repetitions > group.repetitions ||
_class !== group.class) {
return checkConflicts(id, group.devices, dates)
.then(function(conflicts) {
if (!conflicts.length) {
return updateUserGroup(group, {
name: name
, state: state
, class: _class
, isActive: isActive
, repetitions: repetitions
, dates: dates
, duration: duration
})
}
return apiutil.respond(res, 409, 'Conflicts Information', {conflicts: conflicts})
})
}
return updateUserGroup(group, {
name: name
, state: state
, class: _class
, isActive: isActive
, repetitions: repetitions
, dates: dates
, duration: duration
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update group: ', err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function getGroupDevices(req, res) {
const id = req.swagger.params.id.value
const bookable = req.swagger.params.bookable.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
return
}
if (bookable) {
if (apiutil.isOriginGroup(group.class)) {
apiutil.respond(res, 400, 'Bad Request (group is not transient)')
return
}
if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) {
groupApiWrapper(group.owner.email, getGroupDevices, req, res)
return
}
dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) {
Promise.map(devices, function(device) {
return device.serial
})
.then(function(serials) {
return checkConflicts(group.id, serials, group.dates)
.then(function(conflicts) {
let bookableSerials = serials
conflicts.forEach(function(conflict) {
bookableSerials = _.difference(bookableSerials, conflict.devices)
})
return bookableSerials
})
})
.then(function(bookableSerials) {
const deviceList = []
devices.forEach(function(device) {
if (bookableSerials.indexOf(device.serial) > -1) {
deviceList.push(apiutil.filterDevice(req, device))
}
})
apiutil.respond(res, 200, 'Devices Information', {devices: deviceList})
})
})
}
else {
Promise.map(group.devices, function(serial) {
return getDevice(req, serial)
})
.then(function(devices) {
apiutil.respond(res, 200, 'Devices Information', {devices: devices})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group devices: ', err.stack)
})
}
module.exports = {
createGroup: createGroup
, updateGroup: updateGroup
, deleteGroup: deleteGroup
, deleteGroups: deleteGroups
, getGroup: getGroup
, getGroups: getGroups
, getGroupUser: getGroupUser
, getGroupUsers: getGroupUsers
, addGroupUser: addGroupUser
, addGroupUsers: addGroupUsers
, removeGroupUser: removeGroupUser
, removeGroupUsers: removeGroupUsers
, getGroupDevice: getGroupDevice
, getGroupDevices: getGroupDevices
, addGroupDevice: addGroupDevice
, addGroupDevices: addGroupDevices
, removeGroupDevice: removeGroupDevice
, removeGroupDevices: removeGroupDevices
}

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var util = require('util')
var _ = require('lodash')
@@ -12,6 +16,9 @@ var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var wirerouter = require('../../../wire/router')
const apiutil = require('../../../util/apiutil')
const jwtutil = require('../../../util/jwtutil')
var log = logger.createLogger('api:controllers:user')
module.exports = {
@@ -24,9 +31,16 @@ module.exports = {
, remoteDisconnectUserDeviceBySerial: remoteDisconnectUserDeviceBySerial
, getUserAccessTokens: getUserAccessTokens
, addAdbPublicKey: addAdbPublicKey
, addUserDeviceV2: addUserDevice
, getAccessTokens: getAccessTokens
, getAccessToken: getAccessToken
, createAccessToken: createAccessToken
, deleteAccessToken: deleteAccessToken
, deleteAccessTokens: deleteAccessTokens
}
function getUser(req, res) {
// delete req.user.groups.lock
res.json({
success: true
, user: req.user
@@ -53,6 +67,7 @@ function getUserDevices(req, res) {
res.json({
success: true
, description: 'Controlled devices information'
, devices: deviceList
})
})
@@ -61,6 +76,7 @@ function getUserDevices(req, res) {
log.error('Failed to load device list: ', err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@@ -69,113 +85,121 @@ function getUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
var fields = req.swagger.params.fields.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you'
})
}
var responseDevice = device
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
res.json({
success: true
, description: 'Controlled device information'
, device: responseDevice
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you'
})
}
var responseDevice = device
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
res.json({
success: true
, device: responseDevice
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
function addUserDevice(req, res) {
var serial = req.body.serial
var timeout = req.body.timeout || null
var serial = req.hasOwnProperty('body') ? req.body.serial : req.swagger.params.serial.value
var timeout = req.hasOwnProperty('body') ? req.body.timeout ||
null : req.swagger.params.timeout.value || null
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isAddable(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is being used or not available'
})
}
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
return res.status(504).json({
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
, description: 'Device not found'
})
}
var messageListener = wirerouter()
.on(wire.JoinGroupMessage, function(channel, message) {
if (message.serial === serial && message.owner.email === req.user.email) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
datautil.normalize(device, req.user)
if (!deviceutil.isAddable(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is being used or not available'
})
}
return res.json({
success: true
, description: 'Device successfully added'
})
}
})
.handler()
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
return res.status(504).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
req.options.channelRouter.on(wireutil.global, messageListener)
var usage = 'automation'
var messageListener = wirerouter()
.on(wire.JoinGroupMessage, function(channel, message) {
if (message.serial === serial && message.owner.email === req.user.email) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
req.options.push.send([
device.channel
, wireutil.envelope(
new wire.GroupMessage(
new wire.OwnerMessage(
req.user.email
, req.user.name
, req.user.group
)
, timeout
, wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
return res.json({
success: true
, description: 'Device successfully added'
})
}
})
, usage
.handler()
req.options.channelRouter.on(wireutil.global, messageListener)
var usage = 'automation'
req.options.push.send([
device.channel
, wireutil.envelope(
new wire.GroupMessage(
new wire.OwnerMessage(
req.user.email
, req.user.name
, req.user.group
)
, timeout
, wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
, usage
)
)
)
])
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@@ -183,66 +207,70 @@ function addUserDevice(req, res) {
function deleteUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'You cannot release this device. Not owned by you'
})
}
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
return res.status(504).json({
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
, description: 'Device not found'
})
}
var messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === serial && message.owner.email === req.user.email) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'You cannot release this device. Not owned by you'
})
}
return res.json({
success: true
, description: 'Device successfully removed'
})
}
})
.handler()
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
return res.status(504).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
req.options.channelRouter.on(wireutil.global, messageListener)
var messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === serial &&
(message.owner.email === req.user.email || req.user.privilege === 'admin')) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
req.options.push.send([
device.channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
return res.json({
success: true
, description: 'Device successfully removed'
})
}
})
.handler()
req.options.channelRouter.on(wireutil.global, messageListener)
req.options.push.send([
device.channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
)
)
)
])
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@@ -250,65 +278,68 @@ function deleteUserDeviceBySerial(req, res) {
function remoteConnectUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you or is not available'
})
}
var responseChannel = 'txn_' + uuid.v4()
req.options.sub.subscribe(responseChannel)
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var timer = setTimeout(function() {
req.options.channelRouter.removeListener(responseChannel, messageListener)
req.options.sub.unsubscribe(responseChannel)
return res.status(504).json({
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
, description: 'Device not found'
})
}
var messageListener = wirerouter()
.on(wire.ConnectStartedMessage, function(channel, message) {
if (message.serial === serial) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you or is not available'
})
}
return res.json({
success: true
, remoteConnectUrl: message.url
})
}
})
.handler()
var responseChannel = 'txn_' + uuid.v4()
req.options.sub.subscribe(responseChannel)
req.options.channelRouter.on(responseChannel, messageListener)
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var timer = setTimeout(function() {
req.options.channelRouter.removeListener(responseChannel, messageListener)
req.options.sub.unsubscribe(responseChannel)
return res.status(504).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
req.options.push.send([
device.channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStartMessage()
)
])
var messageListener = wirerouter()
.on(wire.ConnectStartedMessage, function(channel, message) {
if (message.serial === serial) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
return res.json({
success: true
, description: 'Remote connection is enabled'
, remoteConnectUrl: message.url
})
}
})
.handler()
req.options.channelRouter.on(responseChannel, messageListener)
req.options.push.send([
device.channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStartMessage()
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@@ -316,65 +347,67 @@ function remoteConnectUserDeviceBySerial(req, res) {
function remoteDisconnectUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you or is not available'
})
}
datautil.normalize(device, req.user)
if (!deviceutil.isOwnedByUser(device, req.user)) {
return res.status(403).json({
success: false
, description: 'Device is not owned by you or is not available'
})
}
var responseChannel = 'txn_' + uuid.v4()
req.options.sub.subscribe(responseChannel)
var responseChannel = 'txn_' + uuid.v4()
req.options.sub.subscribe(responseChannel)
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var timer = setTimeout(function() {
req.options.channelRouter.removeListener(responseChannel, messageListener)
req.options.sub.unsubscribe(responseChannel)
return res.status(504).json({
// Timer will be called if no JoinGroupMessage is received till 5 seconds
var timer = setTimeout(function() {
req.options.channelRouter.removeListener(responseChannel, messageListener)
req.options.sub.unsubscribe(responseChannel)
return res.status(504).json({
success: false
, description: 'Device is not responding'
})
}, 5000)
})
}, 5000)
var messageListener = wirerouter()
.on(wire.ConnectStoppedMessage, function(channel, message) {
if (message.serial === serial) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
var messageListener = wirerouter()
.on(wire.ConnectStoppedMessage, function(channel, message) {
if (message.serial === serial) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
return res.json({
success: true
, description: 'Device remote disconnected successfully'
})
}
})
.handler()
return res.json({
success: true
, description: 'Device remote disconnected successfully'
})
}
})
.handler()
req.options.channelRouter.on(responseChannel, messageListener)
req.options.channelRouter.on(responseChannel, messageListener)
req.options.push.send([
device.channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStopMessage()
)
])
req.options.push.send([
device.channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStopMessage()
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@@ -454,3 +487,111 @@ function addAdbPublicKey(req, res) {
})
})
}
function getAccessToken(req, res) {
const id = req.swagger.params.id.value
dbapi.loadAccessToken(id).then(function(token) {
if (!token || token.email !== req.user.email) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
apiutil.respond(res, 200, 'Access Token Information', {
token: apiutil.publishAccessToken(token)
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack)
})
}
function getAccessTokens(req, res) {
dbapi.loadAccessTokens(req.user.email).then(function(cursor) {
Promise.promisify(cursor.toArray, cursor)().then(function(tokens) {
const tokenList = []
tokens.forEach(function(token) {
tokenList.push(apiutil.publishAccessToken(token))
})
apiutil.respond(res, 200, 'Access Tokens Information', {tokens: tokenList})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get access tokens: ', err.stack)
})
}
function createAccessToken(req, res) {
const title = req.swagger.params.title.value
const jwt = jwtutil.encode({
payload: {
email: req.user.email
, name: req.user.name
}
, secret: req.options.secret
})
const id = util.format('%s-%s', uuid.v4(), uuid.v4()).replace(/-/g, '')
dbapi.saveUserAccessToken(req.user.email, {
title: title
, id: id
, jwt: jwt
})
.then(function(stats) {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 201, 'Created (access token)',
{token: apiutil.publishAccessToken(stats.changes[0].new_val)})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create access token "%s": ', title, err.stack)
})
}
function deleteAccessTokens(req, res) {
dbapi.removeUserAccessTokens(req.user.email).then(function(stats) {
if (!stats.deleted) {
apiutil.respond(res, 200, 'Unchanged (access tokens)')
}
else {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 200, 'Deleted (access tokens)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access tokens: ', err.stack)
})
}
function deleteAccessToken(req, res) {
const id = req.swagger.params.id.value
dbapi.loadAccessToken(id).then(function(token) {
if (!token || token.email !== req.user.email) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
dbapi.removeAccessToken(id).then(function(stats) {
if (!stats.deleted) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 200, 'Deleted (access token)')
}
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack)
})
}

View File

@@ -0,0 +1,398 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const dbapi = require('../../../db/api')
const _ = require('lodash')
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const Promise = require('bluebird')
const wire = require('../../../wire')
const wireutil = require('../../../wire/util')
const userapi = require('./user')
/* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */
function userApiWrapper(fn, req, res) {
const email = req.swagger.params.email.value
dbapi.loadUser(email).then(function(user) {
if (!user) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
req.user = user
fn(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack)
})
}
function getPublishedUser(user, userEmail, adminEmail, fields) {
let publishedUser = apiutil.publishUser(user)
if (userEmail !== adminEmail) {
publishedUser = _.pick(user, 'email', 'name', 'privilege')
}
if (fields) {
publishedUser = _.pick(publishedUser, fields.split(','))
}
return publishedUser
}
function removeUser(email, req, res) {
const groupOwnerState = req.swagger.params.groupOwner.value
const anyGroupOwnerState = typeof groupOwnerState === 'undefined'
const lock = {}
function removeGroupUser(owner, id) {
const lock = {}
return dbapi.lockGroupByOwner(owner, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.group = stats.changes[0].new_val
return owner === email ?
dbapi.deleteUserGroup(id) :
dbapi.removeGroupUser(id, email)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function deleteUserInDatabase(channel) {
return dbapi.removeUserAccessTokens(email).then(function() {
return dbapi.deleteUser(email).then(function() {
req.options.pushdev.send([
channel
, wireutil.envelope(new wire.DeleteUserMessage(
email
))
])
return 'deleted'
})
})
}
function computeUserGroupOwnership(groups) {
if (anyGroupOwnerState) {
return Promise.resolve(true)
}
return Promise.map(groups, function(group) {
if (!groupOwnerState && group.owner.email === email) {
return Promise.reject('filtered')
}
return !groupOwnerState || group.owner.email === email
})
.then(function(results) {
return _.without(results, false).length > 0
})
.catch(function(err) {
if (err === 'filtered') {
return false
}
throw err
})
}
if (req.user.email === email) {
return Promise.resolve('forbidden')
}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const user = lock.user = stats.changes[0].new_val
return dbapi.getGroupsByUser(user.email).then(function(groups) {
return computeUserGroupOwnership(groups).then(function(doContinue) {
if (!doContinue) {
return 'unchanged'
}
return Promise.each(groups, function(group) {
return removeGroupUser(group.owner.email, group.id)
})
.then(function() {
return deleteUserInDatabase(user.group)
})
})
})
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
/* --------------------------------- PUBLIC FUNCTIONS --------------------------------------- */
function getUserInfo(req, email) {
const fields = req.swagger.params.fields.value
return dbapi.loadUser(email).then(function(user) {
if (user) {
return dbapi.getRootGroup().then(function(group) {
return getPublishedUser(user, req.user.email, group.owner.email, fields)
})
}
return false
})
}
function updateUserGroupsQuotas(req, res) {
const email = req.swagger.params.email.value
const duration =
typeof req.swagger.params.duration.value !== 'undefined' ?
req.swagger.params.duration.value :
null
const number =
typeof req.swagger.params.number.value !== 'undefined' ?
req.swagger.params.number.value :
null
const repetitions =
typeof req.swagger.params.repetitions.value !== 'undefined' ?
req.swagger.params.repetitions.value :
null
const lock = {}
lockutil.lockUser(email, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.updateUserGroupsQuotas(email, duration, number, repetitions)
.then(function(stats) {
if (stats.replaced) {
return apiutil.respond(res, 200, 'Updated (user quotas)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
if ((duration === null || duration === lock.user.groups.quotas.allocated.duration) &&
(number === null || number === lock.user.groups.quotas.allocated.number) &&
(repetitions === null || repetitions === lock.user.groups.quotas.repetitions)
) {
return apiutil.respond(res, 200, 'Unchanged (user quotas)', {user: {}})
}
return apiutil.respond(
res
, 400
, 'Bad Request (quotas must be >= actual consumed resources)')
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update user groups quotas: ', err.stack)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function updateDefaultUserGroupsQuotas(req, res) {
const duration =
typeof req.swagger.params.duration.value !== 'undefined' ?
req.swagger.params.duration.value :
null
const number =
typeof req.swagger.params.number.value !== 'undefined' ?
req.swagger.params.number.value :
null
const repetitions =
typeof req.swagger.params.repetitions.value !== 'undefined' ?
req.swagger.params.repetitions.value :
null
const lock = {}
lockutil.lockUser(req.user.email, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions)
.then(function(stats) {
if (stats.replaced) {
return apiutil.respond(res, 200, 'Updated (user default quotas)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
return apiutil.respond(res, 200, 'Unchanged (user default quotas)', {user: {}})
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update default user groups quotas: ', err.stack)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function getUserByEmail(req, res) {
const email = req.swagger.params.email.value
getUserInfo(req, email).then(function(user) {
if (user) {
apiutil.respond(res, 200, 'User Information', {user: user})
}
else {
apiutil.respond(res, 404, 'Not Found (user)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get user: ', err.stack)
})
}
function getUsers(req, res) {
const fields = req.swagger.params.fields.value
dbapi.getUsers().then(function(users) {
return dbapi.getRootGroup().then(function(group) {
apiutil.respond(res, 200, 'Users Information', {
users: users.map(function(user) {
return getPublishedUser(user, req.user.email, group.owner.email, fields)
})
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get users: ', err.stack)
})
}
function createUser(req, res) {
const email = req.swagger.params.email.value
const name = req.swagger.params.name.value
dbapi.createUser(email, name, req.user.ip).then(function(stats) {
if (!stats.inserted) {
apiutil.respond(res, 403, 'Forbidden (user already exists)')
}
else {
apiutil.respond(res, 201, 'Created (user)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create user: ', err.stack)
})
}
function deleteUsers(req, res) {
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
function removeUsers(emails) {
let results = []
return Promise.each(emails, function(email) {
return removeUser(email, req, res).then(function(result) {
results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
results = _.without(results, 'forbidden')
if (!results.length) {
apiutil.respond(res, 403, `Forbidden (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof emails === 'undefined') {
return dbapi.getEmails().then(function(emails) {
return removeUsers(emails)
})
}
else {
return removeUsers(_.without(emails.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete ${target}: ', err.stack)
})
}
function deleteUser(req, res) {
apiutil.redirectApiWrapper('email', deleteUsers, req, res)
}
function createUserAccessToken(req, res) {
userApiWrapper(userapi.createAccessToken, req, res)
}
function deleteUserAccessToken(req, res) {
userApiWrapper(userapi.deleteAccessToken, req, res)
}
function deleteUserAccessTokens(req, res) {
userApiWrapper(userapi.deleteAccessTokens, req, res)
}
function getUserAccessToken(req, res) {
userApiWrapper(userapi.getAccessToken, req, res)
}
function getUserAccessTokens(req, res) {
userApiWrapper(userapi.getAccessTokens, req, res)
}
function getUserDevices(req, res) {
userApiWrapper(userapi.getUserDevices, req, res)
}
function getUserDevice(req, res) {
userApiWrapper(userapi.getUserDeviceBySerial, req, res)
}
function addUserDevice(req, res) {
userApiWrapper(userapi.addUserDevice, req, res)
}
function deleteUserDevice(req, res) {
userApiWrapper(userapi.deleteUserDeviceBySerial, req, res)
}
function remoteConnectUserDevice(req, res) {
userApiWrapper(userapi.remoteConnectUserDeviceBySerial, req, res)
}
function remoteDisconnectUserDevice(req, res) {
userApiWrapper(userapi.remoteDisconnectUserDeviceBySerial, req, res)
}
module.exports = {
updateUserGroupsQuotas: updateUserGroupsQuotas
, updateDefaultUserGroupsQuotas: updateDefaultUserGroupsQuotas
, getUsers: getUsers
, getUserByEmail: getUserByEmail
, getUserInfo: getUserInfo
, createUser: createUser
, deleteUser: deleteUser
, deleteUsers: deleteUsers
, createUserAccessToken: createUserAccessToken
, deleteUserAccessToken: deleteUserAccessToken
, deleteUserAccessTokens: deleteUserAccessTokens
, getUserAccessTokensV2: getUserAccessTokens
, getUserAccessToken: getUserAccessToken
, getUserDevicesV2: getUserDevices
, getUserDevice: getUserDevice
, addUserDeviceV3: addUserDevice
, deleteUserDevice: deleteUserDevice
, remoteConnectUserDevice: remoteConnectUserDevice
, remoteDisconnectUserDevice: remoteDisconnectUserDevice
}

View File

@@ -1,7 +1,12 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var dbapi = require('../../../db/api')
var jwtutil = require('../../../util/jwtutil')
var urlutil = require('../../../util/urlutil')
var logger = require('../../../util/logger')
const apiutil = require('../../../util/apiutil')
var log = logger.createLogger('api:helpers:securityHandlers')
@@ -47,17 +52,27 @@ function accessTokenAuth(req, res, next) {
if (!data) {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
dbapi.loadUser(data.email)
.then(function(user) {
if (user) {
if (user.privilege === apiutil.USER &&
req.swagger.operation.definition.tags.indexOf('admin') > -1) {
return res.status(403).json({
success: false
, description: 'Forbidden: privileged operation (admin)'
})
}
req.user = user
next()
}
else {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
})
@@ -86,6 +101,7 @@ function accessTokenAuth(req, res, next) {
else {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
})

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var path = require('path')
var events = require('events')
@@ -52,16 +56,51 @@ module.exports = function(options) {
lifecycle.fatal()
})
var pushdev = zmqutil.socket('push')
Promise.map(options.endpoints.pushdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
pushdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to pushdev endpoint', err)
lifecycle.fatal()
})
var subdev = zmqutil.socket('sub')
Promise.map(options.endpoints.subdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
subdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to subdev endpoint', err)
lifecycle.fatal()
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
subdev.subscribe(channel)
})
sub.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
subdev.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
// Swagger Express Config
var config = {
appRoot: __dirname
@@ -83,6 +122,8 @@ module.exports = function(options) {
push: push
, sub: sub
, channelRouter: channelRouter
, pushdev: pushdev
, subdev: subdev
})
req.options = reqOptions
@@ -96,7 +137,7 @@ module.exports = function(options) {
}))
lifecycle.observe(function() {
[push, sub].forEach(function(sock) {
[push, sub, pushdev, subdev].forEach(function(sock) {
try {
sock.close()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var jwtutil = require('../../../util/jwtutil')
var urlutil = require('../../../util/urlutil')
@@ -18,6 +22,7 @@ module.exports = function(options) {
})
.then(function() {
req.session.jwt = data
req.sessionOptions.httpOnly = false
res.redirect(redir)
})
.catch(next)

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var express = require('express')
@@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
const dbapi = require('../../db/api')
module.exports = function(options) {
var log = logger.createLogger('auth-ldap')
var app = express()
@@ -54,6 +60,24 @@ module.exports = function(options) {
res.redirect('/auth/ldap/')
})
app.get('/auth/contact', function(req, res) {
dbapi.getRootGroup().then(function(group) {
res.status(200)
.json({
success: true
, contact: group.owner
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/auth/ldap/', function(req, res) {
res.render('index')
})

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var express = require('express')
@@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
const dbapi = require('../../db/api')
module.exports = function(options) {
var log = logger.createLogger('auth-mock')
var app = express()
@@ -80,6 +86,24 @@ module.exports = function(options) {
res.redirect('/auth/mock/')
})
app.get('/auth/contact', function(req, res) {
dbapi.getRootGroup().then(function(group) {
res.status(200)
.json({
success: true
, contact: group.owner
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/auth/mock/', function(req, res) {
res.render('index')
})

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var util = require('util')
var syrup = require('stf-syrup')
@@ -117,6 +121,7 @@ module.exports = syrup.serial()
if (plugin.isRunning()) {
activeServer.close()
activeServer.end()
activeServer = null
}
})
@@ -131,7 +136,7 @@ module.exports = syrup.serial()
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.end)
group.on('leave', plugin.stop)
router
.on(wire.ConnectStartMessage, function(channel) {
@@ -163,7 +168,7 @@ module.exports = syrup.serial()
})
.on(wire.ConnectStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.end()
plugin.stop()
.then(function() {
push.send([
channel
@@ -187,6 +192,5 @@ module.exports = syrup.serial()
})
})
return plugin.start()
.return(plugin)
return(plugin)
})

View File

@@ -0,0 +1,115 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const events = require('events')
const Promise = require('bluebird')
const logger = require('../../util/logger')
const zmqutil = require('../../util/zmqutil')
const srv = require('../../util/srv')
const lifecycle = require('../../util/lifecycle')
const wireutil = require('../../wire/util')
const groupsScheduler = require('./scheduler')
const groupsWatcher = require('./watchers/groups')
const devicesWatcher = require('./watchers/devices')
const usersWatcher = require('./watchers/users')
module.exports = function(options) {
const log = logger.createLogger('groups-engine')
const channelRouter = new events.EventEmitter()
const push = zmqutil.socket('push')
Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
push.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to push endpoint', err)
lifecycle.fatal()
})
// Input
const sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
sub.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to sub endpoint', err)
lifecycle.fatal()
})
const pushdev = zmqutil.socket('push')
Promise.map(options.endpoints.pushdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
pushdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to pushdev endpoint', err)
lifecycle.fatal()
})
const subdev = zmqutil.socket('sub')
Promise.map(options.endpoints.subdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
subdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to subdev endpoint', err)
lifecycle.fatal()
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
subdev.subscribe(channel)
})
sub.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
subdev.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
groupsScheduler()
groupsWatcher(push, pushdev, channelRouter)
devicesWatcher(push, pushdev, channelRouter)
usersWatcher(pushdev)
lifecycle.observe(function() {
[push, sub, pushdev, subdev].forEach(function(sock) {
try {
sock.close()
}
catch (err) {
// No-op
}
})
})
log.info('Groups engine started')
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const Promise = require('bluebird')
const logger = require('../../../util/logger')
const apiutil = require('../../../util/apiutil')
const db = require('../../../db')
const dbapi = require('../../../db/api')
const r = require('rethinkdb')
module.exports = function() {
const log = logger.createLogger('groups-scheduler')
function updateOriginGroupLifetime(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const now = Date.now()
return db.run(r.table('groups').get(group.id).update({
dates: [{
start: new Date(now)
, stop: new Date(now + (group.dates[0].stop - group.dates[0].start))
}]
}))
}
return false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function deleteUserGroup(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.deleteUserGroup(group.id)
}
else {
return db.run(r.table('groups').get(group.id).update({
isActive: false
, state: apiutil.WAITING
}))
}
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function updateGroupDates(group, incr, isActive) {
const repetitions = group.repetitions - incr
const dates = group.dates.slice(incr)
const duration = group.devices.length * (dates[0].stop - dates[0].start) * (repetitions + 1)
return db.run(r.table('groups').get(group.id).update({
dates: dates
, repetitions: repetitions
, duration: duration
, isActive: isActive
, state: apiutil.READY
}))
.then(function() {
return dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration)
})
}
function doBecomeUnactiveGroup(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return updateGroupDates(group, 1, false)
}
else {
return db.run(r.table('groups').get(group.id).update({
isActive: false
, state: apiutil.WAITING
}))
}
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function doCleanElapsedGroupDates(group, incr) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
return lockingSuccessed ? updateGroupDates(group, incr, false) : false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function doBecomeActiveGroup(group, incr) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
return lockingSuccessed ? updateGroupDates(group, incr, true) : false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
dbapi.unlockBookingObjects().then(function() {
setInterval(function() {
const now = Date.now()
dbapi.getReadyGroupsOrderByIndex('startTime').then(function(groups) {
Promise.each(groups, (function(group) {
if (apiutil.isOriginGroup(group.class)) {
if (now >= group.dates[0].stop.getTime()) {
return updateOriginGroupLifetime(group)
}
}
else if ((group.isActive || group.state === apiutil.WAITING) &&
now >= group.dates[0].stop.getTime()) {
if (group.dates.length === 1) {
return deleteUserGroup(group)
}
else {
return doBecomeUnactiveGroup(group)
}
}
else if (!group.isActive) {
for(const i in group.dates) {
if (now >= group.dates[i].stop.getTime()) {
if (group.dates[i].stop === group.dates[group.dates.length - 1].stop) {
return deleteUserGroup(group)
}
}
else if (now < group.dates[i].start.getTime()) {
return i > 0 ? doCleanElapsedGroupDates(group, i) : false
}
else {
return doBecomeActiveGroup(group, i)
}
}
}
return false
}))
})
.catch(function(err) {
log.error('An error occured during groups scheduling', err.stack)
})
}, 1000)
})
}

View File

@@ -0,0 +1,255 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const wirerouter = require('../../../wire/router')
const _ = require('lodash')
const r = require('rethinkdb')
const util = require('util')
const uuid = require('uuid')
const logger = require('../../../util/logger')
const timeutil = require('../../../util/timeutil')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const dbapi = require('../../../db/api')
const db = require('../../../db')
module.exports = function(push, pushdev, channelRouter) {
const log = logger.createLogger('watcher-devices')
function sendReleaseDeviceControl(serial, channel) {
push.send([
channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
)
)
])
}
function sendDeviceGroupChange(id, group, serial, originName) {
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceGroupChangeMessage(
id
, new wire.DeviceGroupMessage(
group.id
, group.name
, new wire.DeviceGroupOwnerMessage(
group.owner.email
, group.owner.name
)
, new wire.DeviceGroupLifetimeMessage(
group.dates[0].start.getTime()
, group.dates[0].stop.getTime()
)
, group.class
, group.repetitions
, originName
)
, serial
)
)
])
}
function sendDeviceChange(device1, device2, action) {
function publishDevice() {
const device = _.cloneDeep(device1)
delete device.channel
delete device.owner
delete device.group.id
delete device.group.lifeTime
return device
}
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceChangeMessage(
publishDevice()
, action
, device2.group.origin
, timeutil.now('nano')
)
)
])
}
function sendReleaseDeviceControlAndDeviceGroupChange(
device
, sendDeviceGroupChangeWrapper) {
let messageListener
const responseTimer = setTimeout(function() {
channelRouter.removeListener(wireutil.global, messageListener)
sendDeviceGroupChangeWrapper()
}, 5000)
messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === device.serial &&
message.owner.email === device.owner.email) {
clearTimeout(responseTimer)
channelRouter.removeListener(wireutil.global, messageListener)
sendDeviceGroupChangeWrapper()
}
})
.handler()
channelRouter.on(wireutil.global, messageListener)
sendReleaseDeviceControl(device.serial, device.channel)
}
db.run(r
.table('devices')
.pluck(
'serial'
, 'channel'
, 'owner'
, 'model'
, 'operator'
, 'manufacturer'
, {group: ['id', 'origin', 'originName', 'lifeTime']}
, {provider: ['name']}
, {network: ['type', 'subtype']}
, {display: ['height', 'width']}
, 'version'
, 'sdk'
, 'abi'
, 'cpuPlatform'
, 'openGLESVersion'
, {phone: ['imei']}
, 'marketName'
)
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
if (err) {
throw err
}
if (data.old_val === null) {
return sendDeviceChange(data.new_val, data.new_val, 'created')
}
else if (data.new_val === null) {
sendDeviceChange(data.old_val, data.old_val, 'deleted')
}
else if (data.new_val.model !== data.old_val.model ||
data.new_val.group.origin !== data.old_val.group.origin ||
data.new_val.operator !== data.old_val.operator ||
data.new_val.hasOwnProperty('network') &&
(!data.old_val.hasOwnProperty('network') ||
data.new_val.network.type !== data.old_val.network.type ||
data.new_val.network.subtype !== data.old_val.network.subtype
) ||
data.new_val.provider.name !== data.old_val.provider.name) {
sendDeviceChange(data.new_val, data.old_val, 'updated')
}
const isDeleted = data.new_val === null
const id = isDeleted ? data.old_val.group.id : data.new_val.group.id
return dbapi.getGroup(id).then(function(group) {
function sendDeviceGroupChangeOnDeviceDeletion() {
const fakeGroup = Object.assign({}, group)
fakeGroup.id = util.format('%s', uuid.v4()).replace(/-/g, '')
fakeGroup.name = 'none'
sendDeviceGroupChange(
group.id
, fakeGroup
, data.old_val.serial
, data.old_val.group.originName
)
}
function sendDeviceGroupChangeOnDeviceCurrentGroupUpdating() {
sendDeviceGroupChange(
data.old_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
}
if (group) {
if (isDeleted) {
if (data.old_val.owner) {
sendReleaseDeviceControlAndDeviceGroupChange(
data.old_val
, sendDeviceGroupChangeOnDeviceDeletion
)
return
}
sendDeviceGroupChangeOnDeviceDeletion()
return
}
const isChangeCurrentGroup = data.new_val.group.id !== data.old_val.group.id
const isChangeOriginGroup = data.new_val.group.origin !== data.old_val.group.origin
const isChangeLifeTime =
data.new_val.group.lifeTime.start.getTime() !==
data.old_val.group.lifeTime.start.getTime()
if (isChangeLifeTime && !isChangeCurrentGroup && !isChangeOriginGroup) {
sendDeviceGroupChange(
data.old_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
return
}
if (isChangeCurrentGroup) {
if (data.new_val.owner && group.users.indexOf(data.new_val.owner.email) < 0) {
sendReleaseDeviceControlAndDeviceGroupChange(
data.new_val
, sendDeviceGroupChangeOnDeviceCurrentGroupUpdating
)
}
else {
sendDeviceGroupChangeOnDeviceCurrentGroupUpdating()
}
}
if (isChangeOriginGroup) {
dbapi.getGroup(data.old_val.group.origin).then(function(originGroup) {
if (originGroup) {
dbapi.removeOriginGroupDevice(originGroup, data.new_val.serial)
}
})
dbapi.getGroup(data.new_val.group.origin).then(function(originGroup) {
if (originGroup) {
dbapi.addOriginGroupDevice(originGroup, data.new_val.serial)
}
})
if (!isChangeCurrentGroup) {
sendDeviceGroupChange(
data.new_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
}
}
}
})
})
})
.catch(function(err) {
log.error('An error occured during DEVICES table watching', err.stack)
})
}

View File

@@ -0,0 +1,346 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const wirerouter = require('../../../wire/router')
const Promise = require('bluebird')
const _ = require('lodash')
const r = require('rethinkdb')
const logger = require('../../../util/logger')
const timeutil = require('../../../util/timeutil')
const apiutil = require('../../../util/apiutil')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const dbapi = require('../../../db/api')
const db = require('../../../db')
module.exports = function(push, pushdev, channelRouter) {
const log = logger.createLogger('watcher-groups')
function sendReleaseDeviceControl(serial, channel) {
push.send([
channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
)
)
])
}
function sendGroupChange(
group
, subscribers
, isChangedDates
, isChangedClass
, isAddedUser
, users
, isAddedDevice
, devices
, action) {
function dates2String(dates) {
return dates.map(function(date) {
return {
start: date.start.toJSON()
, stop: date.stop.toJSON()
}
})
}
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.GroupChangeMessage(
new wire.GroupField(
group.id
, group.name
, group.class
, group.privilege
, group.owner
, dates2String(group.dates)
, group.duration
, group.repetitions
, group.devices
, group.users
, group.state
, group.isActive
)
, action
, subscribers
, isChangedDates
, isChangedClass
, isAddedUser
, users
, isAddedDevice
, devices
, timeutil.now('nano')
)
)
])
}
function sendGroupUsersChange(group, users, devices, isAdded, action) {
const isDeletedLater = action === 'GroupDeletedLater'
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.GroupUserChangeMessage(users, isAdded, group.id, isDeletedLater, devices))
])
}
function doUpdateDeviceOriginGroup(group) {
return dbapi.updateDeviceOriginGroup(group.ticket.serial, group).then(function() {
push.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceOriginGroupMessage(group.ticket.signature)
)
])
})
}
function doUpdateDevicesCurrentGroup(group, devices) {
return Promise.map(devices, function(serial) {
return dbapi.updateDeviceCurrentGroup(serial, group)
})
}
function doUpdateDevicesCurrentGroupFromOrigin(devices) {
return Promise.map(devices, function(serial) {
return dbapi.updateDeviceCurrentGroupFromOrigin(serial)
})
}
function doUpdateDevicesCurrentGroupDates(group) {
if (apiutil.isOriginGroup(group.class)) {
return Promise.map(group.devices, function(serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
return device.group.id === group.id ?
doUpdateDevicesCurrentGroup(group, [serial]) :
false
})
})
}
else {
return Promise.map(group.devices, function(serial) {
return doUpdateDevicesCurrentGroup(group, [serial])
})
}
}
function treatGroupUsersChange(group, users, isActive, isAddedUser) {
if (isActive) {
return Promise.map(users, function(email) {
return Promise.map(group.devices, function(serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (device && device.group.id === group.id) {
if (!isAddedUser && device.owner && device.owner.email === email) {
return new Promise(function(resolve) {
let messageListener
const responseTimer = setTimeout(function() {
channelRouter.removeListener(wireutil.global, messageListener)
resolve(serial)
}, 5000)
messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === serial &&
message.owner.email === email) {
clearTimeout(responseTimer)
channelRouter.removeListener(wireutil.global, messageListener)
resolve(serial)
}
})
.handler()
channelRouter.on(wireutil.global, messageListener)
sendReleaseDeviceControl(serial, device.channel)
})
}
return serial
}
return false
})
})
.then(function(devices) {
sendGroupUsersChange(
group, [email], _.without(devices, false), isAddedUser, 'GroupUser(s)Updated')
})
})
}
else {
return sendGroupUsersChange(group, users, [], isAddedUser, 'GroupUser(s)Updated')
}
}
function treatGroupDevicesChange(oldGroup, group, devices, isAddedDevice) {
if (isAddedDevice) {
return doUpdateDevicesCurrentGroup(group, devices)
}
else {
return doUpdateDevicesCurrentGroupFromOrigin(devices)
.then(function() {
if (group === null) {
sendGroupUsersChange(oldGroup, oldGroup.users, [], false, 'GroupDeletedLater')
}
})
}
}
function treatGroupDeletion(group) {
if (apiutil.isOriginGroup(group.class)) {
return dbapi.getRootGroup().then(function(rootGroup) {
return Promise.map(group.devices, function(serial) {
return dbapi.updateDeviceOriginGroup(serial, rootGroup)
})
.then(function() {
sendGroupUsersChange(group, group.users, [], false, 'GroupDeletedLater')
})
})
}
else {
return sendGroupUsersChange(group, group.users, [], false, 'GroupDeleted')
}
}
db.run(r
.table('groups')
.pluck(
'id'
, 'name'
, 'class'
, 'privilege'
, 'owner'
, 'dates'
, 'duration'
, 'repetitions'
, 'devices'
, 'users'
, 'state'
, 'isActive'
, 'ticket'
)
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
let users, devices, isBecomeActive, isBecomeUnactive, isActive
, isAddedUser, isAddedDevice, isUpdatedDeviceOriginGroup, isChangedDates
if (err) {
throw err
}
if (data.old_val === null) {
sendGroupChange(
data.new_val
, data.new_val.users
, false
, false
, false
, []
, false
, []
, 'created'
)
return sendGroupUsersChange(
data.new_val
, data.new_val.users
, data.new_val.devices
, true
, 'GroupCreated'
)
}
if (data.new_val === null) {
sendGroupChange(
data.old_val
, data.old_val.users
, false
, false
, false
, []
, false
, []
, 'deleted'
)
users = data.old_val.users
devices = data.old_val.devices
isChangedDates = false
isActive = data.old_val.isActive
isBecomeActive = isBecomeUnactive = false
isAddedUser = isAddedDevice = false
isUpdatedDeviceOriginGroup = false
}
else {
users = _.xor(data.new_val.users, data.old_val.users)
devices = _.xor(data.new_val.devices, data.old_val.devices)
isChangedDates =
data.old_val.dates.length !== data.new_val.dates.length ||
data.old_val.dates[0].start.getTime() !==
data.new_val.dates[0].start.getTime() ||
data.old_val.dates[0].stop.getTime() !==
data.new_val.dates[0].stop.getTime()
isActive = data.new_val.isActive
isBecomeActive = !data.old_val.isActive && data.new_val.isActive
isBecomeUnactive = data.old_val.isActive && !data.new_val.isActive
isAddedUser = data.new_val.users.length > data.old_val.users.length
isAddedDevice = data.new_val.devices.length > data.old_val.devices.length
isUpdatedDeviceOriginGroup =
data.new_val.ticket !== null &&
(data.old_val.ticket === null ||
data.new_val.ticket.signature !== data.old_val.ticket.signature)
if (!isUpdatedDeviceOriginGroup) {
sendGroupChange(
data.new_val
, _.union(data.old_val.users, data.new_val.users)
, isChangedDates
, data.old_val.class !== data.new_val.class
, isAddedUser
, users
, isAddedDevice
, devices
, 'updated'
)
}
}
if (isUpdatedDeviceOriginGroup) {
return doUpdateDeviceOriginGroup(data.new_val)
}
else if (isBecomeActive && data.new_val.devices.length) {
return doUpdateDevicesCurrentGroup(data.new_val, data.new_val.devices)
}
else if (isBecomeUnactive && data.new_val.devices.length) {
return doUpdateDevicesCurrentGroupFromOrigin(data.new_val.devices)
}
else if (devices.length && isActive && !apiutil.isOriginGroup(data.old_val.class)) {
return treatGroupDevicesChange(data.old_val, data.new_val, devices, isAddedDevice)
}
else if (data.new_val === null) {
return treatGroupDeletion(data.old_val)
}
else if (isChangedDates && isActive) {
return doUpdateDevicesCurrentGroupDates(data.new_val)
}
else if (users.length) {
return treatGroupUsersChange(data.old_val, users, isActive, isAddedUser)
}
return true
})
})
.catch(function(err) {
log.error('An error occured during GROUPS table watching', err.stack)
})
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const timeutil = require('../../../util/timeutil')
const r = require('rethinkdb')
const _ = require('lodash')
const logger = require('../../../util/logger')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const db = require('../../../db')
module.exports = function(pushdev) {
const log = logger.createLogger('watcher-users')
function sendUserChange(user, isAddedGroup, groups, action, targets) {
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.UserChangeMessage(
user
, isAddedGroup
, groups
, action
, targets
, timeutil.now('nano')))
])
}
db.run(r
.table('users')
.pluck(
'email'
, 'name'
, 'privilege'
, {groups: ['quotas', 'subscribed']
})
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
if (err) {
throw err
}
if (data.old_val === null) {
sendUserChange(data.new_val, false, [], 'created', ['settings'])
}
else if (data.new_val === null) {
sendUserChange(data.old_val, false, [], 'deleted', ['settings'])
}
else {
const targets = []
if (!_.isEqual(
data.new_val.groups.quotas.allocated
, data.old_val.groups.quotas.allocated)) {
targets.push('settings')
targets.push('view')
}
else if (!_.isEqual(
data.new_val.groups.quotas.consumed
, data.old_val.groups.quotas.consumed)) {
targets.push('view')
}
else if (data.new_val.groups.quotas.defaultGroupsNumber !==
data.old_val.groups.quotas.defaultGroupsNumber ||
data.new_val.groups.quotas.defaultGroupsDuration !==
data.old_val.groups.quotas.defaultGroupsDuration ||
data.new_val.groups.quotas.defaultGroupsRepetitions !==
data.old_val.groups.quotas.defaultGroupsRepetitions ||
data.new_val.groups.quotas.repetitions !==
data.old_val.groups.quotas.repetitions ||
!_.isEqual(data.new_val.groups.subscribed, data.old_val.groups.subscribed)) {
targets.push('settings')
}
if (targets.length) {
sendUserChange(
data.new_val
, data.new_val.groups.subscribed.length > data.old_val.groups.subscribed.length
, _.xor(data.new_val.groups.subscribed, data.old_val.groups.subscribed)
, 'updated'
, targets)
}
}
})
})
.catch(function(err) {
log.error('An error occured during USERS table watching', err.stack)
})
}

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var Promise = require('bluebird')
var logger = require('../../util/logger')
@@ -55,17 +59,70 @@ module.exports = db.ensureConnectivity(function(options) {
})
devDealer.on('message', wirerouter()
.on(wire.UpdateAccessTokenMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeleteUserMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeviceChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.UserChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.GroupChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeviceGroupChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.GroupUserChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
// Initial device message
.on(wire.DeviceIntroductionMessage, function(channel, message, data) {
.on(wire.DeviceIntroductionMessage, function(channel, message) {
dbapi.saveDeviceInitialState(message.serial, message)
.then(function() {
.then(function(device) {
devDealer.send([
message.provider.channel
, wireutil.envelope(new wire.DeviceRegisteredMessage(
message.serial
))
])
appDealer.send([channel, data])
appDealer.send([
channel
, wireutil.envelope(new wire.DeviceIntroductionMessage(
message.serial
, message.status
, new wire.ProviderMessage(
message.provider.channel
, message.provider.name
)
, new wire.DeviceGroupMessage(
device.group.id
, device.group.name
, new wire.DeviceGroupOwnerMessage(
device.group.owner.email
, device.group.owner.name
)
, new wire.DeviceGroupLifetimeMessage(
device.group.lifeTime.start.getTime()
, device.group.lifeTime.stop.getTime()
)
, device.group.class
, device.group.repetitions
, device.group.originName
)
))
])
})
.catch(function(err) {
log.error(
'Unable to save the initial state of Device "%s"'
, message.serial
, err.stack
)
})
})
// Workerless messages

View File

@@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var events = require('events')
var util = require('util')
@@ -23,6 +27,8 @@ var ip = require('./middleware/remote-ip')
var auth = require('./middleware/auth')
var jwtutil = require('../../util/jwtutil')
const apiutil = require('../../util/apiutil')
module.exports = function(options) {
var log = logger.createLogger('websocket')
var server = http.createServer()
@@ -118,23 +124,99 @@ module.exports = function(options) {
}
}
let disconnectSocket
var messageListener = wirerouter()
.on(wire.UpdateAccessTokenMessage, function() {
socket.emit('user.keys.accessToken.updated')
})
.on(wire.DeleteUserMessage, function() {
disconnectSocket(true)
})
.on(wire.DeviceChangeMessage, function(channel, message) {
if (user.groups.subscribed.indexOf(message.device.group.origin) > -1 ||
user.groups.subscribed.indexOf(message.oldOriginGroupId) > -1) {
socket.emit('user.settings.devices.' + message.action, message)
}
})
.on(wire.UserChangeMessage, function(channel, message) {
Promise.map(message.targets, function(target) {
socket.emit('user.' + target + '.users.' + message.action, message)
})
})
.on(wire.GroupChangeMessage, function(channel, message) {
if (user.privilege === 'admin' ||
user.email === message.group.owner.email ||
!apiutil.isOriginGroup(message.group.class) &&
(message.action === 'deleted' ||
message.action === 'updated' &&
(message.isChangedDates || message.isChangedClass || message.devices.length))) {
socket.emit('user.settings.groups.' + message.action, message)
}
if (message.subscribers.indexOf(user.email) > -1) {
socket.emit('user.view.groups.' + message.action, message)
}
})
.on(wire.DeviceGroupChangeMessage, function(channel, message) {
if (user.groups.subscribed.indexOf(message.id) > -1) {
if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.updateGroupDevice', {
important: true
, data: {
serial: message.serial
, group: message.group
}
})
}
else {
socket.emit('device.removeGroupDevices', {important: true, devices: [message.serial]})
}
}
else if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.addGroupDevices', {important: true, devices: [message.serial]})
}
})
.on(wire.GroupUserChangeMessage, function(channel, message) {
if (message.users.indexOf(user.email) > -1) {
if (message.isAdded) {
user.groups.subscribed = _.union(user.groups.subscribed, [message.id])
if (message.devices.length) {
socket.emit('device.addGroupDevices', {important: true, devices: message.devices})
}
}
else {
if (message.devices.length) {
socket.emit('device.removeGroupDevices', {important: true, devices: message.devices})
}
if (message.isDeletedLater) {
setTimeout(function() {
user.groups.subscribed = _.without(user.groups.subscribed, message.id)
}, 5000)
}
else {
user.groups.subscribed = _.without(user.groups.subscribed, message.id)
}
}
}
})
.on(wire.DeviceLogMessage, function(channel, message) {
socket.emit('device.log', message)
})
.on(wire.DeviceIntroductionMessage, function(channel, message) {
socket.emit('device.add', {
important: true
, data: {
serial: message.serial
, present: false
, provider: message.provider
, owner: null
, status: message.status
, ready: false
, reverseForwards: []
}
})
if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.add', {
important: true
, data: {
serial: message.serial
, present: true
, provider: message.provider
, owner: null
, status: message.status
, ready: false
, reverseForwards: []
, group: message.group
}
})
}
})
.on(wire.DeviceReadyMessage, function(channel, message) {
socket.emit('device.change', {
@@ -307,6 +389,7 @@ module.exports = function(options) {
joinChannel(user.group)
new Promise(function(resolve) {
disconnectSocket = resolve
socket.on('disconnect', resolve)
// Global messages for all clients using socket.io
//
@@ -314,15 +397,19 @@ module.exports = function(options) {
.on('device.note', function(data) {
return dbapi.setDeviceNote(data.serial, data.note)
.then(function() {
return dbapi.loadDevice(data.serial)
return dbapi.loadDevice(user.groups.subscribed, data.serial)
})
.then(function(device) {
if (device) {
io.emit('device.change', {
important: true
, data: {
serial: device.serial
, notes: device.notes
.then(function(cursor) {
if (cursor) {
cursor.next(function(err, device) {
if (!err) {
io.emit('device.change', {
important: true
, data: {
serial: device.serial
, notes: device.notes
}
})
}
})
}
@@ -364,7 +451,7 @@ module.exports = function(options) {
.on('user.keys.accessToken.remove', function(data) {
return dbapi.removeUserAccessToken(user.email, data.title)
.then(function() {
socket.emit('user.keys.accessToken.removed', data.title)
socket.emit('user.keys.accessToken.updated')
})
})
.on('user.keys.adb.add', function(data) {
@@ -916,6 +1003,7 @@ module.exports = function(options) {
channelRouter.removeListener(channel, messageListener)
sub.unsubscribe(channel)
})
socket.disconnect(true)
})
.catch(function(err) {
// Cannot guarantee integrity of client
@@ -923,8 +1011,7 @@ module.exports = function(options) {
'Client had an error, disconnecting due to probable loss of integrity'
, err.stack
)
socket.disconnect(true)
// move 'socket.disconnect(true)' statement to finally block instead!
})
})