Skip to content

Commit

Permalink
feature: provide a way for devices to obtain a security token and new…
Browse files Browse the repository at this point in the history
… user to sign up
  • Loading branch information
sbender9 committed Sep 14, 2018
1 parent f90be89 commit 90c5b29
Show file tree
Hide file tree
Showing 12 changed files with 868 additions and 132 deletions.
78 changes: 78 additions & 0 deletions lib/serverroutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,84 @@ module.exports = function(app, saveSecurityConfig, getSecurityConfig) {
)
})

app.put('/security/access/requests/:identifier/:status', (req, res) => {
if (app.securityStrategy.allowConfigure(req)) {
var config = getSecurityConfig(app)
app.securityStrategy.setAccessRequestStatus(
config,
req.params.identifier,
req.params.status,
req.body,
(err, config) => {
if (err) {
console.log(err)
res.status(500)
res.send(`Unable update request: ${err.message}`)
return
}
if (config) {
saveSecurityConfig(app, config, err => {
if (err) {
console.log(err)
res.status(500)
res.send('Unable to save configuration change')
return
}
res.send('Request updated')
})
} else {
res.send('Request updated')
}
}
)
} else {
res.status(401).json('Security config not allowed')
}
})

app.get('/security/access/requests', (req, res) => {
if (app.securityStrategy.allowConfigure(req)) {
res.json(app.securityStrategy.getAccessRequestsResponse())
} else {
res.status(401).json('Security config not allowed')
}
})

app.post('/access/requests', (req, res) => {
var config = getSecurityConfig(app)
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
app.securityStrategy.requestAccess(
config,
req.body,
ip,
(err, response, config) => {
if (err) {
console.log(err)
res.status(500)
res.send(`Unable to create request: ${err.message}`)
return
}
if (!response) {
res.send('Request created')
} else {
res.json(response)
}
}
)
})

app.get('/access/requests/:id', (req, res) => {
app.securityStrategy.checkRequest(req.params.id, (err, result) => {
if (err) {
console.log(err)
res.status(500)
res.send(`Unable to check request: ${err.message}`)
return
}
res.json(result)
})
})

app.get('/settings', (req, res, next) => {
var settings = {
interfaces: {},
Expand Down
223 changes: 220 additions & 3 deletions lib/tokensecurity.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const fs = require('fs')
const path = require('path')
const bcrypt = require('bcryptjs')
const getSourceId = require('@signalk/signalk-schema').getSourceId
const uuidv4 = require('uuid/v4')

const CONFIG_PLUGINID = 'sk-simple-token-security-config'
const passwordSaltRounds = 10
Expand All @@ -30,6 +31,7 @@ const permissionDeniedMessage =

module.exports = function (app, config) {
const strategy = {}
let accessRequests = []

let {
allow_readonly = true,
Expand All @@ -39,8 +41,11 @@ module.exports = function (app, config) {
.randomBytes(256)
.toString('hex'),
users = [],
devices = [],
immutableConfig = false,
acls = []
acls = [],
allowDeviceAccessRequests = true,
allowNewUserRegistration = true
} = config

if (process.env.ADMINUSER) {
Expand All @@ -64,13 +69,25 @@ module.exports = function (app, config) {
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,
immutableConfig,
acls
acls,
allowDeviceAccessRequests,
allowNewUserRegistration
}

// so that enableSecurity gets the defaults to save
Expand Down Expand Up @@ -273,7 +290,9 @@ module.exports = function (app, config) {
var result = {
status: req.skIsAuthenticated ? 'loggedIn' : 'notLoggedIn',
readOnlyAccess: configuration.allow_readonly,
authenticationRequired: true
authenticationRequired: true,
allowNewUserRegistration: configuration.allowNewUserRegistration,
allowDeviceAccessRequests: configuration.allowDeviceAccessRequests
}
if (req.skIsAuthenticated) {
result.userLevel = req.skUser.type
Expand All @@ -294,6 +313,7 @@ module.exports = function (app, config) {
strategy.setConfig = (config, newConfig) => {
assertConfigImmutability()
newConfig.users = config.users
newConfig.devices = config.devices
newConfig.secretKey = config.secretKey
options = newConfig
return newConfig
Expand Down Expand Up @@ -701,6 +721,203 @@ module.exports = function (app, config) {
}
}

strategy.getAccessRequestsResponse = () => {
return accessRequests.filter(r => r.active).map(r => ({
identifier: r.identifier,
description: r.description,
ip: r.ip,
expiration: r.expiration,
config: r.config,
active: r.active
}))
}

function sendAccessRequestsUpdate () {
app.emit('serverevent', {
type: 'ACCESS_REQUEST',
from: CONFIG_PLUGINID,
data: strategy.getAccessRequestsResponse()
})
}

strategy.checkRequest = (requestId, cb) => {
var idx = accessRequests.findIndex(r => r.requestId == requestId)
if (idx == -1) {
cb(new Error('not found'))
return
}

var request = accessRequests[idx]
var result = {
status: request.active
? 'PENDING'
: request.approved ? 'APPROVED' : 'DENIED'
}
if (request.approved) {
result.token = request.token
delete request.token
}
cb(null, result)
}

strategy.setAccessRequestStatus = (config, identifier, status, body, cb) => {
var idx = accessRequests.findIndex(r => r.identifier == identifier)
if (idx == -1) {
cb(new Error('not found'))
return
}

var request = accessRequests[idx]

if (status === 'approved') {
if (request.uuid) {
var payload = { device: identifier }
var jwtOptions = {}

expiration = body.expiration
if (expiration !== 'NEVER') {
jwtOptions.expiresIn = expiration
}
debug(`expiration ${expiration} ${body.expiration}`)
var token = jwt.sign(payload, config.secretKey, jwtOptions)

if (!config.devices) {
config.devices = []
}

config.devices.push({
uuid: request.uuid,
permissions: request.permissions,
config: body.config
})
request.token = token
} else {
config.users.push({
username: identifier,
password: request.password,
type: 'readonly'
})
}
request.approved = true
} else if (status === 'denied') {
request.approved = false
} else {
cb(new Error('Unkown status value'), config)
return
}
// accessRequests = accessRequests.filter(r => r.identifier != identifier)
request.active = false
cb(null, config)
options = config
sendAccessRequestsUpdate()
}

function validateAccessRequest (request) {
if (request.userId) {
return !_.isUndefined(request.password)
} else if (request.uuid) {
return !_.isUndefined(request.description)
} else {
return false
}
}

strategy.requestAccess = (config, request, sourceIp, cb) => {
if (!validateAccessRequest(request)) {
cb(new Error('Invalid request'))
return
}

request.ip = sourceIp
request.active = true
request.date = new Date()

var alertMessage
var response
if (request.uuid) {
if (!options.allowDeviceAccessRequests) {
cb(new Error('Device access not allowed'))
return
}

if (
config.devices &&
config.devices.find(device => device.uuid == request.uuid)
) {
cb(new Error(`A device with uuid '${request.uuid}' already has access`))
return
}

if (accessRequests.find(r => r.uuid == request.uuid)) {
cb(
new Error(
`A device with uuid '${request.uuid}' has already requested access`
)
)
return
}

request.identifier = request.uuid
request.requestId = uuidv4()

/*
req.on('close', (err) => {
accessRequests = accessRequests.filter(r => r.uuid != request.uuid)
sendAccessRequestsUpdate()
})
*/
response = { requestId: request.requestId }

alertMessage = `A device with IP ${request.ip} and UUID ${
request.uuid
} has requested access to the server`
debug(alertMessage)
} else {
if (!options.allowNewUserRegistration) {
cb(new Error('new user registration not allowed'))
return
}

var existing = options.users.find(user => user.username == request.userId)
if (existing) {
cb(new Error('User already exists'))
return
}
request.description = 'New User Request'
request.identifier = request.userId
request.password = bcrypt.hashSync(
request.password,
bcrypt.genSaltSync(passwordSaltRounds)
)
alertMessage = `${request.userid} has requested server access`
debug(alertMessage)
}

request.permissions = 'readonly'
request.expiration = config.expiration
accessRequests.push(request)
sendAccessRequestsUpdate()
app.handleMessage(CONFIG_PLUGINID, {
context: 'vessels.' + app.selfId,
updates: [
{
values: [
{
path: 'notifications.security.accessRequest',
value: {
state: 'alert',
method: ['visual', 'sound'],
message: alertMessage,
timestamp: new Date().toISOString()
}
}
]
}
]
})
cb(null, response, config)
}

setupApp()

return strategy
Expand Down
4 changes: 3 additions & 1 deletion packages/server-admin-ui/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,16 @@ export const fetchLoginStatus = buildFetchAction('/loginStatus', 'RECEIVE_LOGIN_
export const fetchPlugins = buildFetchAction('/plugins', 'RECEIVE_PLUGIN_LIST')
export const fetchWebapps = buildFetchAction('/webapps', 'RECEIVE_WEBAPPS_LIST')
export const fetchApps = buildFetchAction('/appstore/available', 'RECEIVE_APPSTORE_LIST')
export const fetchAccessRequests = buildFetchAction('/security/access/requests', 'ACCESS_REQUEST')
export const fetchServerSpecification = buildFetchAction('/signalk', 'RECEIVE_SERVER_SPEC')

export function fetchAllData (dispatch) {
fetchPlugins(dispatch)
fetchWebapps(dispatch)
fetchApps(dispatch)
fetchLoginStatus(dispatch)
fetchServerSpecification(dispatch)
fetchServerSpecification(dispatch),
fetchAccessRequests(dispatch)
}

export function openServerEventsConnection (dispatch) {
Expand Down
Loading

0 comments on commit 90c5b29

Please sign in to comment.