UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

991 lines (989 loc) 38.7 kB
"use strict"; /* * Copyright 2017 Scott Bender <scott@scottbender.net> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = require("./debug"); const debug = (0, debug_1.createDebug)('signalk-server:tokensecurity'); const jwt = require('jsonwebtoken'); const _ = require('lodash'); const bcrypt = require('bcryptjs'); const getSourceId = require('@signalk/signalk-schema').getSourceId; const { InvalidTokenError } = require('./security'); const { createRequest, updateRequest, findRequest, filterRequests } = require('./requestResponse'); const ms = require('ms'); const CONFIG_PLUGINID = 'sk-simple-token-security-config'; const passwordSaltRounds = 10; const permissionDeniedMessage = "You do not have permission to view this resource, <a href='/admin/#/login'>Please Login</a>"; const skPrefix = '/signalk/v1'; const skAuthPrefix = `${skPrefix}/auth`; //cookie to hold login info for webapps to use const BROWSER_LOGININFO_COOKIE_NAME = 'skLoginInfo'; const constants_1 = require("./constants"); const LOGIN_FAILED_MESSAGE = 'Invalid username/password'; module.exports = function (app, config) { const strategy = {}; let { expiration = 'NEVER', users = [], immutableConfig = false, allowDeviceAccessRequests = true, allowNewUserRegistration = true } = config; const { allow_readonly = true, secretKey = process.env.SECRETKEY || require('crypto').randomBytes(256).toString('hex'), devices = [], acls = [] } = config; if (process.env.ADMINUSER) { const adminUserParts = process.env.ADMINUSER.split(':'); if (adminUserParts.length !== 2) { console.error('ADMINUSER env parameters must be in username:password format'); process.exit(-1); } users = [ { username: adminUserParts[0], type: 'admin', password: bcrypt.hashSync(adminUserParts[1], bcrypt.genSaltSync(passwordSaltRounds)) } ]; immutableConfig = true; } if (process.env.ALLOW_DEVICE_ACCESS_REQUESTS) { allowDeviceAccessRequests = process.env.ALLOW_DEVICE_ACCESS_REQUESTS === 'true'; } if (process.env.ALLOW_NEW_USER_REGISTRATION) { allowNewUserRegistration = process.env.ALLOW_NEW_USER_REGISTRATION === 'true'; } let options = { allow_readonly, expiration, secretKey, users, devices, immutableConfig, acls, allowDeviceAccessRequests, allowNewUserRegistration }; // so that enableSecurity gets the defaults to save _.merge(config, options); function getConfiguration() { return options; } strategy.getConfiguration = getConfiguration; function getIsEnabled() { // var options = getOptions(); // return typeof options.enabled !== 'undefined' && options.enabled; return true; } function assertConfigImmutability() { if (options.immutableConfig) { throw new Error('Configuration is immutable'); } } function handlePermissionDenied(req, res) { res.status(401); if (req.accepts('application/json') && !req.accepts('text/html')) { res.set('Content-Type', 'application/json'); res.json({ error: 'Permission Denied' }); } else { res.type('text/plain').send(permissionDeniedMessage); } } function writeAuthenticationMiddleware() { return function (req, res, next) { if (!getIsEnabled()) { return next(); } debug('skIsAuthenticated: ' + req.skIsAuthenticated); if (req.skIsAuthenticated) { if (req.skPrincipal.permissions === 'admin' || req.skPrincipal.permissions === 'readwrite') { return next(); } } handlePermissionDenied(req, res, next); }; } function adminAuthenticationMiddleware(redirect) { return function (req, res, next) { if (!getIsEnabled()) { return next(); } if (req.skIsAuthenticated && req.skPrincipal) { if (req.skPrincipal.permissions === 'admin') { return next(); } else if (req.skPrincipal.identifier === 'AUTO' && redirect) { res.redirect('/@signalk/server-admin-ui/#/login'); } else { handlePermissionDenied(req, res, next); } } else if (redirect) { res.redirect('/@signalk/server-admin-ui/#/login'); } else { handlePermissionDenied(req, res, next); } }; } function setupApp() { app.use(require('body-parser').urlencoded({ extended: true })); app.use(require('cookie-parser')()); app.post(['/login', `${skAuthPrefix}/login`], (req, res) => { const name = req.body.username; const password = req.body.password; const remember = req.body.rememberMe; const configuration = getConfiguration(); login(name, password) .then((reply) => { const requestType = req.get('Content-Type'); if (reply.statusCode === 200) { let cookieOptions = { httpOnly: true }; if (remember) { cookieOptions.maxAge = ms(configuration.expiration === 'NEVER' ? '10y' : configuration.expiration || '1h'); } res.cookie('JAUTHENTICATION', reply.token, cookieOptions); res.cookie(BROWSER_LOGININFO_COOKIE_NAME, JSON.stringify({ status: 'loggedIn', user: reply.user })); if (requestType === 'application/json') { res.json({ token: reply.token }); } else { res.redirect(req.body.destination ? req.body.destination : '/'); } } else { if (requestType === 'application/json') { res.status(reply.statusCode).send(reply); } else { res.status(reply.statusCode).send(reply.message); } } }) .catch((err) => { console.log(err); res.status(502).send('Login Failure'); }); }); app.use('/', http_authorize(false, true)) //semicolon required ; [ '/apps', '/appstore', '/plugins', '/restart', '/runDiscovery', '/security', '/vessel', '/providers', '/settings', '/webapps', '/skServer/inputTest' ].forEach((p) => app.use(`${constants_1.SERVERROUTESPREFIX}${p}`, http_authorize(false))); app.put(['/logout', `${skAuthPrefix}/logout`], function (req, res) { res.clearCookie('JAUTHENTICATION'); res.clearCookie(BROWSER_LOGININFO_COOKIE_NAME); res.json('Logout OK'); }); [ '/restart', '/runDiscovery', '/plugins', '/appstore', '/security', '/settings', '/backup', '/restore', '/providers', '/vessel' ].forEach((p) => app.use(`${constants_1.SERVERROUTESPREFIX}${p}`, adminAuthenticationMiddleware(false))); app.use('/plugins', adminAuthenticationMiddleware(false)); //TODO remove after grace period app.use('/loginStatus', http_authorize(false, true)); app.use(`${constants_1.SERVERROUTESPREFIX}/loginStatus`, http_authorize(false, true)); const no_redir = http_authorize(false); app.use('/signalk/v1/api/*', function (req, res, next) { no_redir(req, res, next); }); app.put('/signalk/v1/*', writeAuthenticationMiddleware(false)); } function login(name, password) { return new Promise((resolve, reject) => { debug('logging in user: ' + name); const configuration = getConfiguration(); const user = configuration.users.find((aUser) => aUser.username === name); if (!user) { resolve({ statusCode: 401, message: LOGIN_FAILED_MESSAGE }); return; } if (!user.password) { resolve({ statusCode: 401, message: LOGIN_FAILED_MESSAGE }); return; } bcrypt.compare(password, user.password, (err, matches) => { if (err) { reject(err); } else if (matches === true) { const payload = { id: user.username }; const theExpiration = configuration.expiration || '1h'; const jwtOptions = {}; if (theExpiration !== 'NEVER') { jwtOptions.expiresIn = theExpiration; } debug(`jwt expiration:${JSON.stringify(jwtOptions)}`); try { const token = jwt.sign(payload, configuration.secretKey, jwtOptions); resolve({ statusCode: 200, token, user: user.username }); } catch (err) { resolve({ statusCode: 500, message: 'Unable to sign token: ' + err.message }); } } else { debug('password did not match'); resolve({ statusCode: 401, message: LOGIN_FAILED_MESSAGE }); } }); }); } strategy.validateConfiguration = (newConfiguration) => { const configuration = getConfiguration(); const theExpiration = newConfiguration.expiration || '1h'; if (theExpiration !== 'NEVER') { jwt.sign({ dummy: 'payload' }, configuration.secretKey, { expiresIn: theExpiration }); } }; strategy.getAuthRequiredString = () => { return strategy.allowReadOnly() ? 'forwrite' : 'always'; }; strategy.supportsLogin = () => true; strategy.login = login; strategy.addAdminMiddleware = function (aPath) { app.use(aPath, http_authorize(false)); app.use(aPath, adminAuthenticationMiddleware(false)); }; strategy.addAdminWriteMiddleware = function (aPath) { app.use(aPath, http_authorize(false)); app.put(aPath, adminAuthenticationMiddleware(false)); app.post(aPath, adminAuthenticationMiddleware(false)); }; strategy.addWriteMiddleware = function (aPath) { app.use(aPath, http_authorize(false)); app.put(aPath, writeAuthenticationMiddleware(false)); app.post(aPath, writeAuthenticationMiddleware(false)); }; strategy.generateToken = function (req, res, next, id, theExpiration) { const configuration = getConfiguration(); const payload = { id: id }; const token = jwt.sign(payload, configuration.secretKey, { expiresIn: theExpiration }); res.type('text/plain').send(token); }; strategy.allowReadOnly = function () { const configuration = getConfiguration(); return configuration.allow_readonly; }; strategy.allowRestart = function (req) { return req.skIsAuthenticated && req.skPrincipal.permissions === 'admin'; }; strategy.allowConfigure = function (req) { return req.skIsAuthenticated && req.skPrincipal.permissions === 'admin'; }; strategy.getLoginStatus = function (req) { const configuration = getConfiguration(); const result = { status: req.skIsAuthenticated ? 'loggedIn' : 'notLoggedIn', readOnlyAccess: configuration.allow_readonly, authenticationRequired: true, allowNewUserRegistration: configuration.allowNewUserRegistration, allowDeviceAccessRequests: configuration.allowDeviceAccessRequests }; if (req.skIsAuthenticated) { result.userLevel = req.skPrincipal.permissions; result.username = req.skPrincipal.identifier; } if (configuration.users.length === 0) { result.noUsers = true; } return result; }; strategy.getConfig = (aConfig) => { delete aConfig.users; delete aConfig.secretKey; return aConfig; }; strategy.setConfig = (aConfig, newConfig) => { assertConfigImmutability(); newConfig.users = aConfig.users; newConfig.devices = aConfig.devices; newConfig.secretKey = aConfig.secretKey; options = newConfig; return newConfig; }; strategy.getUsers = (aConfig) => { if (aConfig && aConfig.users) { return aConfig.users.map((user) => { return { userId: user.username, type: user.type }; }); } else { return []; } }; function addUser(theConfig, user, callback) { assertConfigImmutability(); const newUser = { username: user.userId, type: user.type }; function finish(newUser, err) { if (!theConfig.users) { theConfig.users = []; } theConfig.users.push(newUser); options = theConfig; callback(err, theConfig); } if (user.password) { bcrypt.hash(user.password, passwordSaltRounds, (err, hash) => { if (err) { callback(err); } else { newUser.password = hash; finish(newUser, err); } }); } else { finish(newUser, undefined); } } strategy.updateUser = (theConfig, username, updates, callback) => { assertConfigImmutability(); const user = theConfig.users.find((aUser) => aUser.username === username); if (!user) { callback(new Error('user not found')); return; } if (updates.type) { user.type = updates.type; } if (updates.password) { bcrypt.hash(updates.password, passwordSaltRounds, (err, hash) => { if (err) { callback(err); } else { user.password = hash; callback(err, theConfig); } }); } else { callback(null, theConfig); } options = theConfig; }; strategy.addUser = addUser; strategy.setPassword = (theConfig, username, password, callback) => { assertConfigImmutability(); bcrypt.hash(password, passwordSaltRounds, (err, hash) => { if (err) { callback(err); } else { const user = theConfig.users[username]; user.password = hash; options = theConfig; callback(err, theConfig); } }); }; strategy.deleteUser = (theConfig, username, callback) => { assertConfigImmutability(); for (let i = theConfig.users.length - 1; i >= 0; i--) { if (theConfig.users[i].username === username) { theConfig.users.splice(i, 1); break; } } options = theConfig; callback(null, theConfig); }; strategy.getDevices = (theConfig) => { if (theConfig && theConfig.devices) { return theConfig.devices; } else { return []; } }; strategy.deleteDevice = (theConfig, clientId, callback) => { assertConfigImmutability(); for (let i = theConfig.devices.length - 1; i >= 0; i--) { if (theConfig.devices[i].clientId === clientId) { theConfig.devices.splice(i, 1); break; } } options = theConfig; callback(null, theConfig); }; strategy.updateDevice = (theConfig, clientId, updates, callback) => { assertConfigImmutability(); const device = theConfig.devices.find((d) => d.clientId === clientId); if (!device) { callback(new Error('device not found')); return; } if (updates.permissions) { device.permissions = updates.permissions; } if (updates.description) { device.description = updates.description; } callback(null, theConfig); options = theConfig; }; strategy.shouldAllowWrite = function (req, delta) { if (req.skPrincipal && (req.skPrincipal.permissions === 'admin' || req.skPrincipal.permissions === 'readwrite')) { const context = delta.context === app.selfContext ? 'vessels.self' : delta.context; const notAllowed = delta.updates.find((update) => { let source = update.$source; if (!source) { source = getSourceId(update.source); } return ((update.values && update.values.find((valuePath) => { return (strategy.checkACL(req.skPrincipal.identifier, context, valuePath.path, source, 'write') === false); })) || (update.meta && update.meta.find((valuePath) => { return (strategy.checkACL(req.skPrincipal.identifier, context, valuePath.path, source, 'write') === false); }))); }); // true if we did not find anything disallowing the write return _.isUndefined(notAllowed); } return false; }; strategy.shouldAllowPut = function (req, _context, source, thePath) { if (req.skPrincipal && (req.skPrincipal.permissions === 'admin' || req.skPrincipal.permissions === 'readwrite')) { const context = _context === app.selfContext ? 'vessels.self' : _context; return strategy.checkACL(req.skPrincipal.identifier, context, thePath, source, 'put'); } return false; }; strategy.anyACLs = () => { const configuration = getConfiguration(); return configuration.acls && configuration.acls.length; }; strategy.filterReadDelta = (principal, delta) => { const configuration = getConfiguration(); if (delta.updates && configuration.acls && configuration.acls.length && principal) { const filtered = { ...delta }; const context = delta.context === app.selfContext ? 'vessels.self' : delta.context; filtered.updates = delta.updates .map((update) => { let res = (update.values || update.meta) .map((valuePath) => { return strategy.checkACL(principal.identifier, context, valuePath.path, update.source, 'read') ? valuePath : null; }) .filter((vp) => vp != null); if (update.values) { update.values = res; return update.values.length > 0 ? update : null; } else { update.meta = res; return update.meta.length > 0 ? update : null; } }) .filter((update) => update != null); return filtered.updates.length > 0 ? filtered : null; } else if (!principal) { return null; } else { return delta; } }; strategy.verifyWS = function (spark) { if (!spark.lastTokenVerify) { spark.lastTokenVerify = Date.now(); return; } if (!getIsEnabled()) { return; } const now = Date.now(); if (now - spark.lastTokenVerify > 60 * 1000) { debug('verify token'); spark.lastTokenVerify = now; strategy.authorizeWS(spark); } }; function getAuthorizationFromHeaders(req) { if (req.headers) { let header = req.headers.authorization; if (!header) { header = req.headers['x-authorization']; } if (header && header.startsWith('Bearer ')) { return header.substring('Bearer '.length); } if (header && header.startsWith('JWT ')) { return header.substring('JWT '.length); } } return undefined; } strategy.authorizeWS = function (req) { let token = req.token; let error; let payload; if (!getIsEnabled()) { return; } const configuration = getConfiguration(); if (!token) { if (req.query && req.query.token) { token = req.query.token; } else { token = getAuthorizationFromHeaders(req); } } if (!token) { token = req.cookies && req.cookies.JAUTHENTICATION; } // // `jwt-simple` throws errors if something goes wrong when decoding the JWT. // if (token) { payload = jwt.verify(token, configuration.secretKey); if (!payload) { error = new InvalidTokenError('Invalid access token'); } else if (Date.now() / 1000 > payload.exp) { // // At this point we have decoded and verified the token. Check if it is // expired. // error = new InvalidTokenError('Expired access token'); } } if (!token || error) { if (configuration.allow_readonly) { req.skPrincipal = { identifier: 'AUTO', permissions: 'readonly' }; return; } else { if (!error) { error = new Error('Missing access token'); } debug(error.message); throw error; } } // // Check if the user/device is still present and allowed in our db. You could tweak // this to invalidate a token. // const principal = getPrincipal(payload); if (!principal) { error = new InvalidTokenError(`Invalid identity ${JSON.stringify(payload)}`); debug(error.message); throw error; } req.skPrincipal = principal; req.skIsAuthenticated = true; }; strategy.checkACL = (id, context, thePath, source, operation) => { const configuration = getConfiguration(); if (!configuration.acls || configuration.acls.length === 0) { // no acls, so allow anything return true; } const acl = configuration.acls.find((theAcl) => { const pattern = theAcl.context.replace('.', '\\.').replace('*', '.*'); const matcher = new RegExp('^' + pattern + '$'); return matcher.test(context); }); if (acl) { const pathPerms = acl.resources.find((p) => { let perms; if (p.paths) { perms = p.paths.find((aPath) => { const pattern = aPath.replace('.', '\\.').replace('*', '.*'); const matcher = new RegExp('^' + pattern + '$'); return matcher.test(thePath); }); } else if (p.sources) { perms = p.sources.find((s) => { const pattern = s.replace('.', '\\.').replace('*', '.*'); const matcher = new RegExp('^' + pattern + '$'); return matcher.test(source); }); } return perms; }); if (pathPerms) { let perms = pathPerms.permissions.filter((p) => p.subject === id); perms = perms.concat(pathPerms.permissions.filter((p) => p.subject === 'any')); if (perms.length === 0) { return false; } return (perms.find((perm) => { if (operation === 'read' && (perm.permission === 'write' || perm.permission === 'read')) { return true; } else if (operation === 'write' && perm.permission === 'write') { return true; } else if (operation === 'put' && perm.permission === 'put') { return true; } else { return false; } }) != null); } } return false; }; strategy.isDummy = () => { return false; }; strategy.canAuthorizeWS = () => { return true; }; strategy.shouldFilterDeltas = () => { const configuration = getConfiguration(); return configuration.acls && configuration.acls.length > 0; }; function getPrincipal(payload) { let principal; if (payload.id) { const user = options.users.find((theUser) => theUser.username === payload.id); if (user) { principal = { identifier: user.username, permissions: user.type }; } } else if (payload.device && options.devices) { const device = options.devices.find((aDevice) => aDevice.clientId === payload.device); if (device) { principal = { identifier: device.clientId, permissions: device.permissions }; } } return principal; } function http_authorize(redirect, forLoginStatus) { // debug('http_authorize: ' + redirect) return function (req, res, next) { let token = req.cookies.JAUTHENTICATION; debug(`http_authorize: ${req.path} (forLogin: ${forLoginStatus})`); if (!getIsEnabled()) { return next(); } const configuration = getConfiguration(); if (!token) { token = getAuthorizationFromHeaders(req); } if (token) { jwt.verify(token, configuration.secretKey, function (err, decoded) { debug('verify'); if (!err) { const principal = getPrincipal(decoded); if (principal) { debug('authorized'); req.skPrincipal = principal; req.skIsAuthenticated = true; req.userLoggedIn = true; next(); return; } else { debug('unknown user: ' + (decoded.id || decoded.device)); } } else { debug(`bad token: ${err.message} ${req.path}`); res.clearCookie('JAUTHENTICATION'); } if (configuration.allow_readonly) { req.skIsAuthenticated = false; next(); } else { res.status(401).send('bad auth token'); } }); } else { debug('no token'); if (configuration.allow_readonly && !forLoginStatus) { req.skPrincipal = { identifier: 'AUTO', permissions: 'readonly' }; req.skIsAuthenticated = true; return next(); } else { req.skIsAuthenticated = false; if (forLoginStatus) { next(); } else if (redirect) { debug('redirecting to login'); res.redirect('/@signalk/server-admin-ui/#/login'); } else { res.status(401).send('Unauthorized'); } } } }; } strategy.getAccessRequestsResponse = () => { return filterRequests('accessRequest', 'PENDING'); }; function sendAccessRequestsUpdate() { app.emit('serverevent', { type: 'ACCESS_REQUEST', from: CONFIG_PLUGINID, data: strategy.getAccessRequestsResponse() }); } strategy.setAccessRequestStatus = (theConfig, identifier, status, body, cb) => { const request = findRequest((r) => r.state === 'PENDING' && r.accessIdentifier === identifier); if (!request) { cb(new Error('not found')); return; } const permissoinPart = request.requestedPermissions ? request.permissions : 'any'; app.handleMessage(CONFIG_PLUGINID, { context: 'vessels.' + app.selfId, updates: [ { values: [ { path: `notifications.security.accessRequest.${permissoinPart}.${identifier}`, value: { state: 'normal', method: [], message: `The device "${request.accessDescription}" has been ${status}`, timestamp: new Date().toISOString() } } ] } ] }); let approved; if (status === 'approved') { if (request.clientRequest.accessRequest.clientId) { const payload = { device: identifier }; const jwtOptions = {}; const expiresIn = body.expiration || theConfig.expiration; if (expiresIn !== 'NEVER') { jwtOptions.expiresIn = expiresIn; } const token = jwt.sign(payload, theConfig.secretKey, jwtOptions); if (!theConfig.devices) { theConfig.devices = []; } theConfig.devices = theConfig.devices.filter((d) => d.clientId !== identifier); theConfig.devices.push({ clientId: request.accessIdentifier, permissions: !request.clientRequest.requestedPermissions ? body.permissions : request.permissions, config: body.config, description: request.accessDescription, requestedPermissions: request.clientRequest.requestedPermissions }); request.token = token; } else { theConfig.users.push({ username: identifier, password: request.accessPassword, type: body.permissions }); } approved = true; } else if (status === 'denied') { approved = false; } else { cb(new Error('Unkown status value'), theConfig); return; } options = theConfig; updateRequest(request.requestId, 'COMPLETED', { statusCode: 200, data: { permission: approved ? 'APPROVED' : 'DENIED', token: request.token } }) .then(() => { cb(null, theConfig); sendAccessRequestsUpdate(); }) .catch((err) => { cb(err); }); }; function validateAccessRequest(request) { if (request.userId) { return !_.isUndefined(request.password); } else if (request.clientId) { return !_.isUndefined(request.description); } else { return false; } } strategy.requestAccess = (theConfig, clientRequest, sourceIp, updateCb) => { return new Promise((resolve, reject) => { createRequest(app, 'accessRequest', clientRequest, null, sourceIp, updateCb) .then((request) => { const accessRequest = clientRequest.accessRequest; if (!validateAccessRequest(accessRequest)) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 400 }) .then(resolve) .catch(reject); return; } request.requestedPermissions = !_.isUndefined(accessRequest.permissions); if (!request.requestedPermissions) { request.permissions = 'readonly'; } else { request.permissions = accessRequest.permissions; } let alertMessage; if (accessRequest.clientId) { if (!options.allowDeviceAccessRequests) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } if (findRequest((r) => r.state === 'PENDING' && r.accessIdentifier === accessRequest.clientId)) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 400, message: `A device with clientId '${accessRequest.clientId}' has already requested access` }) .then(resolve) .catch(reject); return; } request.accessIdentifier = accessRequest.clientId; request.accessDescription = accessRequest.description; debug(`A device with IP ${request.ip} and CLIENTID ${accessRequest.clientId} has requested access to the server`); alertMessage = `The device "${accessRequest.description}" has requested access to the server`; } else { if (!options.allowNewUserRegistration) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } const existing = options.users.find((user) => user.username === accessRequest.userId); if (existing) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 400, message: 'User already exists' }) .then(resolve) .catch(reject); return; } request.accessDescription = 'New User Request'; request.accessIdentifier = accessRequest.userId; request.accessPassword = bcrypt.hashSync(request.accessPassword, bcrypt.genSaltSync(passwordSaltRounds)); alertMessage = `${accessRequest.userId} has requested server access`; debug(alertMessage); } const permissoinPart = request.requestedPermissions ? request.permissions : 'any'; sendAccessRequestsUpdate(); app.handleMessage(CONFIG_PLUGINID, { context: 'vessels.' + app.selfId, updates: [ { values: [ { path: `notifications.security.accessRequest.${permissoinPart}.${request.accessIdentifier}`, value: { state: 'alert', method: ['visual', 'sound'], message: alertMessage, timestamp: new Date().toISOString() } } ] } ] }); updateRequest(request.requestId, 'PENDING', { statusCode: 202 }) .then((reply) => { resolve(reply, theConfig); }) .catch(reject); }) .catch(reject); }); }; setupApp(); return strategy; };