diff --git a/lib/api/status.js b/lib/api/status.js index 56196114a07..b1a15952970 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -20,6 +20,10 @@ function configure (app, wares, env, ctx) { var authToken = req.query.token || req.query.secret || ''; + function getRemoteIP (req) { + return req.headers['x-forwarded-for'] || req.connection.remoteAddress; + } + var date = new Date(); var info = { status: 'ok' , name: app.get('name') @@ -31,7 +35,7 @@ function configure (app, wares, env, ctx) { , boluscalcEnabled: app.enabled('api') && env.settings.enable.indexOf('boluscalc') > -1 , settings: settings , extendedSettings: extended - , authorized: ctx.authorization.authorize(authToken) + , authorized: ctx.authorization.authorize(authToken, getRemoteIP(req)) , runtimeState: ctx.runtimeState }; diff --git a/lib/api/verifyauth.js b/lib/api/verifyauth.js index a4eb2edf4ee..849ce9bef9a 100644 --- a/lib/api/verifyauth.js +++ b/lib/api/verifyauth.js @@ -8,15 +8,24 @@ function configure (ctx) { api.get('/verifyauth', function(req, res) { ctx.authorization.resolveWithRequest(req, function resolved (err, result) { - // this is used to see if req has api-secret equivalent authorization - var authorized = !err && - ctx.authorization.checkMultiple('*:*:create,update,delete', result.shiros) && //can write to everything - ctx.authorization.checkMultiple('admin:*:*:*', result.shiros); //full admin permissions too + var canRead = !err && + ctx.authorization.checkMultiple('*:*:read', result.shiros); + var canWrite = !err && + ctx.authorization.checkMultiple('*:*:write', result.shiros); + var isAdmin = !err && + ctx.authorization.checkMultiple('*:*:admin', result.shiros); + var authorized = canRead && !result.defaults; + var response = { + canRead, + canWrite, + isAdmin, message: authorized ? 'OK' : 'UNAUTHORIZED', - rolefound: result.subject ? 'FOUND' : 'NOTFOUND' - } + rolefound: result.subject ? 'FOUND' : 'NOTFOUND', + permissions: result.defaults ? 'DEFAULT' : 'ROLE' + }; + res.sendJSONStatus(res, consts.HTTP_OK, response); }); diff --git a/lib/api3/security.js b/lib/api3/security.js index 33099d88f12..1488a0ee3b8 100644 --- a/lib/api3/security.js +++ b/lib/api3/security.js @@ -9,6 +9,10 @@ const moment = require('moment') ; +function getRemoteIP (req) { + return req.headers['x-forwarded-for'] || req.connection.remoteAddress; +} + /** * Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending) */ @@ -68,7 +72,7 @@ function authenticate (opCtx) { opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN)); } - ctx.authorization.resolve({ token }, function resolveFinish (err, result) { + ctx.authorization.resolve({ token, ip: getRemoteIP(req) }, function resolveFinish (err, result) { if (err) { return reject( opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_TOKEN)); diff --git a/lib/authorization/delaylist.js b/lib/authorization/delaylist.js new file mode 100644 index 00000000000..cfc0509e6ab --- /dev/null +++ b/lib/authorization/delaylist.js @@ -0,0 +1,58 @@ +'use strict'; + +function init () { + + const ipDelayList = {}; + + const DELAY_ON_FAIL = 5000; + const FAIL_AGE = 60000; + + const sleep = require('util').promisify(setTimeout); + + ipDelayList.addFailedRequest = function addFailedRequest (ip) { + const ipString = String(ip); + let entry = ipDelayList[ipString]; + const now = Date.now(); + if (!entry) { + ipDelayList[ipString] = now + DELAY_ON_FAIL; + return; + } + if (now >= entry) { entry = now; } + ipDelayList[ipString] = entry + DELAY_ON_FAIL; + }; + + ipDelayList.shouldDelayRequest = function shouldDelayRequest (ip) { + const ipString = String(ip); + const entry = ipDelayList[ipString]; + let now = Date.now(); + if (entry) { + if (now < entry) { + return entry - now; + } + } + return false; + }; + + ipDelayList.requestSucceeded = function requestSucceeded (ip) { + const ipString = String(ip); + if (ipDelayList[ipString]) { + delete ipDelayList[ipString]; + } + }; + + // Clear items older than a minute + + setTimeout(function clearList () { + for (var key in ipDelayList) { + if (ipDelayList.hasOwnProperty(key)) { + if (Date.now() > ipDelayList[key] + FAIL_AGE) { + delete ipDelayList[key]; + } + } + } + }, 30000); + + return ipDelayList; +} + +module.exports = init; diff --git a/lib/authorization/index.js b/lib/authorization/index.js index b81578e0dca..0f5fe138287 100644 --- a/lib/authorization/index.js +++ b/lib/authorization/index.js @@ -1,96 +1,85 @@ 'use strict'; -var _ = require('lodash'); -var jwt = require('jsonwebtoken'); -var shiroTrie = require('shiro-trie'); - -var consts = require('./../constants'); - -var log_green = '\x1B[32m'; -var log_red = '\x1b[31m'; -var log_reset = '\x1B[0m'; -var LOG_GRANTED = log_green + 'GRANTED: ' + log_reset; -var LOG_DENIED = log_red + 'DENIED: ' + log_reset; - -function mkopts (opts) { - var options = opts && !_.isEmpty(opts) ? opts : { }; - if (!options.redirectDeniedURL) { - options.redirectDeniedURL = null; - } - return options; -} +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const shiroTrie = require('shiro-trie'); + +const ipdelaylist = require('./delaylist')(); +const consts = require('./../constants'); + +const sleep = require('util').promisify(setTimeout); + +const addFailedRequest = ipdelaylist.addFailedRequest; +const shouldDelayRequest = ipdelaylist.shouldDelayRequest; +const requestSucceeded = ipdelaylist.requestSucceeded; function getRemoteIP (req) { return req.headers['x-forwarded-for'] || req.connection.remoteAddress; } function init (env, ctx) { - var authorization = { }; + var authorization = {}; var storage = authorization.storage = require('./storage')(env, ctx); var defaultRoles = (env.settings.authDefaultRoles || '').split(/[, :]/); - function extractToken (req) { - var token; - var authorization = req.header('Authorization'); + /** + * Loads JWT from request + * + * @param {*} req + */ + function extractJWTfromRequest (req) { + + if (req.auth_token) return req.auth_token; - if (authorization) { - var parts = authorization.split(' '); + let token; + + if (req.header('Authorization')) { + const parts = req.header('Authorization').split(' '); if (parts.length === 2 && parts[0] === 'Bearer') { token = parts[1]; } } - if (!token && req.auth_token) { - token = req.auth_token; - } - if (!token) { - token = authorizeAccessToken(req); - } + let accessToken = req.query.token; + if (!accessToken && req.body) { + if (_.isArray(req.body) && req.body.length > 0 && req.body[0].token) { + accessToken = req.body[0].token; + delete req.body[0].token; + } else if (req.body.token) { + accessToken = req.body.token; + delete req.body.token; + } + } - if (token) { - req.auth_token = token; + if (accessToken) { + // validate and parse the token + const authed = authorization.authorize(accessToken); + if (authed && authed.token) { + token = authed.token; + } + } } + if (token) { req.auth_token = token; } + return token; } - authorization.extractToken = extractToken; - - function authorizeAccessToken (req) { + authorization.extractToken = extractJWTfromRequest; - var accessToken = req.query.token; - - if (!accessToken && req.body) { - if (_.isArray(req.body) && req.body.length > 0 && req.body[0].token) { - accessToken = req.body[0].token; - delete req.body[0].token; - } else if (req.body.token) { - accessToken = req.body.token; - delete req.body.token; - } - } + /** + * Fetches the API_SECRET from the request + * + * @param {*} req Express request object + */ + function apiSecretFromRequest (req) { - var authToken = null; + if (req.api_secret) return req.api_secret; - if (accessToken) { - // make an auth token on the fly, based on an access token - var authed = authorization.authorize(accessToken); - if (authed && authed.token) { - authToken = authed.token; - } - } + let secret = req.query && req.query.secret ? req.query.secret : req.header('api-secret'); - return authToken; - } - - function adminSecretFromRequest (req) { - var secret = req.query && req.query.secret ? req.query.secret : req.header('api-secret'); - - if (!secret && req.api_secret) { - //see if we already got the secret from the body, since it gets deleted - secret = req.api_secret; - } else if (!secret && req.body) { + if (!secret && req.body) { // try to get the secret from the body, but don't leave it there if (_.isArray(req.body) && req.body.length > 0 && req.body[0].secret) { secret = req.body[0].secret; @@ -101,71 +90,125 @@ function init (env, ctx) { } } - if (secret) { - // store the secret hash on the request since the req may get processed again - req.api_secret = secret; - } - + // store the secret hash on the request since the req may get processed again + if (secret) { req.api_secret = secret; } return secret; } - function authorizeAdminSecretWithRequest (req) { - return authorizeAdminSecret(adminSecretFromRequest(req)); - } - function authorizeAdminSecret (secret) { return (env.api_secret && env.api_secret.length > 12) ? (secret === env.api_secret) : false; } - authorization.seenPermissions = [ ]; + authorization.seenPermissions = []; - authorization.expandedPermissions = function expandedPermissions ( ) { + authorization.expandedPermissions = function expandedPermissions () { var permissions = shiroTrie.new(); permissions.add(authorization.seenPermissions); return permissions; }; authorization.resolveWithRequest = function resolveWithRequest (req, callback) { - authorization.resolve({ - api_secret: adminSecretFromRequest(req) - , token: extractToken(req) - }, callback); + const resolveData = { + api_secret: apiSecretFromRequest(req) + , token: extractJWTfromRequest(req) + , ip: getRemoteIP(req) + }; + authorization.resolve(resolveData, callback); }; - authorization.checkMultiple = function checkMultiple(permission, shiros) { + /** + * Check if the Apache Shiro-style permission object includes the permission. + * + * Returns a boolean true / false depending on if the permission is found. + * + * @param {*} permission Desired permission + * @param {*} shiros Shiros + */ + + authorization.checkMultiple = function checkMultiple (permission, shiros) { var found = _.find(shiros, function checkEach (shiro) { return shiro && shiro.check(permission); }); return _.isObject(found); }; - authorization.resolve = function resolve (data, callback) { + /** + * Resolve an API secret or token and return the permissions associated with + * the secret / token + * + * @param {*} data + * @param {*} callback + */ + authorization.resolve = async function resolve (data, callback) { + + if (!data.ip) { + console.error('Trying to authorize without IP information'); + return callback(null, { shiros: [] }); + } + + data.api_secret = data.api_secret || null; + + if (data.api_secret == 'null') { // TODO find what's sending this anomaly + data.api_secret = null; + } - var defaultShiros = storage.rolesToShiros(defaultRoles); + const requestDelay = shouldDelayRequest(data.ip); - if (storage.doesAccessTokenExist(data.api_secret)) { - authorization.resolveAccessToken (data.api_secret, callback, defaultShiros); - return; + if (requestDelay) { + await sleep(requestDelay); } - if (authorizeAdminSecret(data.api_secret)) { + const authAttempted = (data.api_secret || data.token) ? true : false; + const defaultShiros = storage.rolesToShiros(defaultRoles); + + // If there is no token or secret, return default permissions + if (!authAttempted) { + const result = { shiros: defaultShiros, defaults: true }; + if (callback) { callback(null, result); } + return result; + } + + // Check for API_SECRET first as that allows bailing out fast + + if (data.api_secret && authorizeAdminSecret(data.api_secret)) { + requestSucceeded(data.ip); var admin = shiroTrie.new(); admin.add(['*']); - return callback(null, { shiros: [ admin ] }); + const result = { shiros: [admin] }; + if (callback) { callback(null, result); } + return result; } - if (data.token) { - jwt.verify(data.token, env.api_secret, function result(err, verified) { - if (err) { - return callback(err, { shiros: [ ] }); - } else { - authorization.resolveAccessToken (verified.accessToken, callback, defaultShiros); - } - }); - } else { - return callback(null, { shiros: defaultShiros }); + // If we reach this point, we must be dealing with a role based token + + let token = null; + + // Tokens have to be well formed JWTs + try { + const verified = await jwt.verify(data.token, env.api_secret); + token = verified.accessToken; + } catch (err) {} + + // Check if there's a token in the secret + + if (!token && data.api_secret) { + if (storage.doesAccessTokenExist(data.api_secret)) { + token = data.api_secret; + } } + if (token) { + requestSucceeded(data.ip); + const results = authorization.resolveAccessToken(token, null, defaultShiros); + if (callback) { callback(null, results); } + return results; + } + + console.error('Resolving secret/token to permissions failed'); + addFailedRequest(data.ip); + callback('All validation failed', {}); + return {}; + }; authorization.resolveAccessToken = function resolveAccessToken (accessToken, callback, defaultShiros) { @@ -176,94 +219,67 @@ function init (env, ctx) { let resolved = storage.resolveSubjectAndPermissions(accessToken); if (!resolved || !resolved.subject) { - return callback('Subject not found', null); + if (callback) { callback('Subject not found', null); } + return null; } let shiros = resolved.shiros.concat(defaultShiros); - return callback(null, { shiros: shiros, subject: resolved.subject }); + const result = { shiros, subject: resolved.subject }; + if (callback) { callback(null, result); } + return result; }; - authorization.isPermitted = function isPermitted (permission, opts) { - + /** + * Check if the client has a permission execute an action, + * based on an API_SECRET or JWT in the request. + * + * Used to authorize API calls + * + * @param {*} permission Permission being checked + */ + authorization.isPermitted = function isPermitted (permission) { - mkopts(opts); authorization.seenPermissions = _.chain(authorization.seenPermissions) .push(permission) .sort() .uniq() .value(); - function check(req, res, next) { + async function check (req, res, next) { var remoteIP = getRemoteIP(req); + var secret = apiSecretFromRequest(req); + var token = extractJWTfromRequest(req); - var secret = adminSecretFromRequest(req); - var defaultShiros = storage.rolesToShiros(defaultRoles); - - if (storage.doesAccessTokenExist(secret)) { - var resolved = storage.resolveSubjectAndPermissions (secret); - - if (authorization.checkMultiple(permission, resolved.shiros)) { - console.log(LOG_GRANTED, remoteIP, resolved.accessToken , permission); - next(); - } else if (authorization.checkMultiple(permission, defaultShiros)) { - console.log(LOG_GRANTED, remoteIP, resolved.accessToken, permission, 'default'); - next( ); - } else { - console.log(LOG_DENIED, remoteIP, resolved.accessToken, permission); - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); - } - return; - } + const data = { api_secret: secret, token, ip: remoteIP }; - if (authorizeAdminSecretWithRequest(req)) { - console.log(LOG_GRANTED, remoteIP, 'api-secret', permission); - next( ); - return; - } + const permissions = await authorization.resolve(data); + const permitted = authorization.checkMultiple(permission, permissions.shiros); - var token = extractToken(req); - - if (token) { - jwt.verify(token, env.api_secret, function result(err, verified) { - if (err) { - console.info('Error verifying Authorized Token', err); - res.status(consts.HTTP_UNAUTHORIZED).send('Unauthorized - Invalid/Missing'); - } else { - var resolved = storage.resolveSubjectAndPermissions(verified.accessToken); - if (authorization.checkMultiple(permission, resolved.shiros)) { - console.log(LOG_GRANTED, remoteIP, verified.accessToken , permission); - next(); - } else if (authorization.checkMultiple(permission, defaultShiros)) { - console.log(LOG_GRANTED, remoteIP, verified.accessToken, permission, 'default'); - next( ); - } else { - console.log(LOG_DENIED, remoteIP, verified.accessToken, permission); - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); - } - } - }); - } else { - if (authorization.checkMultiple(permission, defaultShiros)) { - console.log(LOG_GRANTED, remoteIP, 'no-token', permission, 'default'); - return next( ); - } - console.log(LOG_DENIED, remoteIP, 'no-token', permission); - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + if (permitted) { + next(); + return; } + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); } return check; + }; + /** + * Generates a JWT based on an access token / authorizes an existing token + * + * @param {*} accessToken token to be used for generating a JWT for the client + */ authorization.authorize = function authorize (accessToken) { - var subject = storage.findSubject(accessToken); + var subject = storage.findSubject(accessToken); var authorized = null; if (subject) { - var token = jwt.sign( { accessToken: subject.accessToken }, env.api_secret, { expiresIn: '1h' } ); + var token = jwt.sign({ accessToken: subject.accessToken }, env.api_secret, { expiresIn: '1h' }); //decode so we can tell the client the issued and expired times var decoded = jwt.decode(token); @@ -271,10 +287,10 @@ function init (env, ctx) { var roles = _.uniq(subject.roles.concat(defaultRoles)); authorized = { - token: token + token , sub: subject.name - // not sending roles to client to prevent us from treating them as magic - // instead group permissions by role so the we can create correct shiros on the client + // not sending roles to client to prevent us from treating them as magic + // instead group permissions by role so the we can create correct shiros on the client , permissionGroups: _.map(roles, storage.roleToPermissions) , iat: decoded.iat , exp: decoded.exp diff --git a/lib/hashauth.js b/lib/client/hashauth.js similarity index 59% rename from lib/hashauth.js rename to lib/client/hashauth.js index 8848ca08ef0..6a2def96b31 100644 --- a/lib/hashauth.js +++ b/lib/client/hashauth.js @@ -4,21 +4,22 @@ var crypto = require('crypto'); var Storages = require('js-storage'); var hashauth = { - apisecret: '' - , storeapisecret: false - , apisecrethash: null - , authenticated: false - , initialized: false - , tokenauthenticated: false + initialized: false }; -hashauth.init = function init(client, $) { +hashauth.init = function init (client, $) { - if (hashauth.initialized) { - return hashauth; - } + hashauth.apisecret = ''; + hashauth.storeapisecret = false; + hashauth.apisecrethash = null; + hashauth.authenticated = false; + hashauth.tokenauthenticated = false; + hashauth.hasReadPermission = false; + hashauth.isAdmin = false; + hashauth.hasWritePermission = false; + hashauth.permissionlevel = 'NONE'; - hashauth.verifyAuthentication = function verifyAuthentication(next) { + hashauth.verifyAuthentication = function verifyAuthentication (next) { hashauth.authenticated = false; $.ajax({ method: 'GET' @@ -26,15 +27,22 @@ hashauth.init = function init(client, $) { , headers: client.headers() }).done(function verifysuccess (response) { - if (response.message.rolefound == 'FOUND') { + + var message = response.message; + + if (message.canRead) { hashauth.hasReadPermission = true; } + if (message.canWrite) { hashauth.hasWritePermission = true; } + if (message.isAdmin) { hashauth.isAdmin = true; } + if (message.permissions) { hashauth.permissionlevel = message.permissions; } + + if (message.rolefound == 'FOUND') { hashauth.tokenauthenticated = true; console.log('Token Authentication passed.'); - client.authorizeSocket(); next(true); return; } - if (response.message.message === 'OK') { + if (message.message === 'OK') { hashauth.authenticated = true; console.log('Authentication passed.'); next(true); @@ -42,10 +50,10 @@ hashauth.init = function init(client, $) { } console.log('Authentication failed.', response); - hashauth.removeAuthentication(); - next(false); - return; - + hashauth.removeAuthentication(); + next(false); + return; + }).fail(function verifyfail (err) { console.log('Authentication failed.', err); hashauth.removeAuthentication(); @@ -53,23 +61,23 @@ hashauth.init = function init(client, $) { }); }; - hashauth.injectHtml = function injectHtml ( ) { + hashauth.injectHtml = function injectHtml () { if (!hashauth.injectedHtml) { $('#authentication_placeholder').html(hashauth.inlineCode()); hashauth.injectedHtml = true; } }; - hashauth.initAuthentication = function initAuthentication(next) { + hashauth.initAuthentication = function initAuthentication (next) { hashauth.apisecrethash = hashauth.apisecrethash || Storages.localStorage.get('apisecrethash') || null; - hashauth.verifyAuthentication(function () { + hashauth.verifyAuthentication(function() { hashauth.injectHtml(); - if (next) { next( hashauth.isAuthenticated() ); } + if (next) { next(hashauth.isAuthenticated()); } }); return hashauth; }; - hashauth.removeAuthentication = function removeAuthentication(event) { + hashauth.removeAuthentication = function removeAuthentication (event) { Storages.localStorage.remove('apisecrethash'); @@ -92,14 +100,14 @@ hashauth.init = function init(client, $) { var translate = client.translate; hashauth.injectHtml(); - var clientWidth = window.innerWidth - || document.documentElement.clientWidth - || document.body.clientWidth; + var clientWidth = window.innerWidth || + document.documentElement.clientWidth || + document.body.clientWidth; clientWidth = Math.min(400, clientWidth); - $( '#requestauthenticationdialog' ).dialog({ - width: clientWidth + $('#requestauthenticationdialog').dialog({ + width: clientWidth , height: 270 , closeText: '' , buttons: [ @@ -115,7 +123,7 @@ hashauth.init = function init(client, $) { } else { client.afterAuth(true); } - $( dialog ).dialog( 'close' ); + $(dialog).dialog('close'); } else { $('#apisecret').val('').focus(); } @@ -123,8 +131,8 @@ hashauth.init = function init(client, $) { } } ] - , open: function open ( ) { - $('#apisecret').off('keyup').on('keyup' ,function pressed (e) { + , open: function open () { + $('#apisecret').off('keyup').on('keyup', function pressed (e) { if (e.keyCode === $.ui.keyCode.ENTER) { $('#requestauthenticationdialog-btn').trigger('click'); } @@ -140,7 +148,7 @@ hashauth.init = function init(client, $) { return false; }; - hashauth.processSecret = function processSecret(apisecret, storeapisecret, callback) { + hashauth.processSecret = function processSecret (apisecret, storeapisecret, callback) { var translate = client.translate; hashauth.apisecret = apisecret; @@ -155,10 +163,10 @@ hashauth.init = function init(client, $) { shasum.update(hashauth.apisecret); hashauth.apisecrethash = shasum.digest('hex'); - hashauth.verifyAuthentication( function(isok) { + hashauth.verifyAuthentication(function(isok) { if (isok) { if (hashauth.storeapisecret) { - Storages.localStorage.set('apisecrethash',hashauth.apisecrethash); + Storages.localStorage.set('apisecrethash', hashauth.apisecrethash); // TODO show dialog first, then reload if (hashauth.tokenauthenticated) client.browserUtils.reload(); } @@ -176,43 +184,57 @@ hashauth.init = function init(client, $) { } }; - hashauth.inlineCode = function inlineCode() { + hashauth.inlineCode = function inlineCode () { var translate = client.translate; var status = null; + if (!hashauth.isAdmin) { + $('.needsadminaccess').hide(); + } else { + $('.needsadminaccess').show(); + } + + if (client.updateAdminMenu) client.updateAdminMenu(); + if (client.authorized || hashauth.tokenauthenticated) { status = translate('Authorized by token'); if (client.authorized && client.authorized.sub) { - status += '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + status += '
' + translate('Auth role') + ': ' + client.authorized.sub; + if (hashauth.hasReadPermission) { status += '
' + translate('Data reads enabled'); } + if (hashauth.hasWritePermission) { status += '
' + translate('Data writes enabled'); } + if (!hashauth.hasWritePermission) { status += '
' + translate('Data writes not enabled'); } } - if (hashauth.apisecrethash) - { + if (hashauth.apisecrethash) { status += '
(' + translate('Remove stored token') + ')'; } else { - status += '
(' + translate('view without token') + ')'; + status += '
(' + translate('view without token') + ')'; } } else if (hashauth.isAuthenticated()) { - console.info('status isAuthenticated', hashauth); status = translate('Admin authorized') + ' (' + translate('Remove') + ')'; } else { - status = translate('Unauthorized') + ' (' + translate('Authenticate') + ')'; + status = translate('Unauthorized') + + '
' + + translate('Reads enabled in default permissions') + + '
' + + ' (' + + translate('Authenticate') + ')'; } var html = - ''+ + '' + '
' + status + '
'; return html; }; - hashauth.updateSocketAuth = function updateSocketAuth() { + hashauth.updateSocketAuth = function updateSocketAuth () { client.socket.emit( 'authorize' , { @@ -220,8 +242,7 @@ hashauth.init = function init(client, $) { , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() , token: client.authorized && client.authorized.token } - , function authCallback(data) { - console.log('Client rights: ',data); + , function authCallback (data) { if (!data.read && !client.authorized) { hashauth.requestAuthentication(); } @@ -229,16 +250,17 @@ hashauth.init = function init(client, $) { ); }; - hashauth.hash = function hash() { + hashauth.hash = function hash () { return hashauth.apisecrethash; }; - hashauth.isAuthenticated = function isAuthenticated() { + hashauth.isAuthenticated = function isAuthenticated () { return hashauth.authenticated || hashauth.tokenauthenticated; }; hashauth.initialized = true; + return hashauth; -}; +} module.exports = hashauth; diff --git a/lib/client/index.js b/lib/client/index.js index 75719976261..15b05698b83 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -21,9 +21,10 @@ var browserSettings; var client = {}; -$('#loadingMessageText').html('Connecting to server'); +var hashauth = require('./hashauth'); +client.hashauth = hashauth.init(client, $); -client.hashauth = require('../hashauth').init(client, $); +$('#loadingMessageText').html('Connecting to server'); client.headers = function headers () { if (client.authorized) { @@ -105,7 +106,7 @@ client.init = function init (callback) { // auth failed, hide loader and request for key $('#centerMessagePanel').hide(); client.hashauth.requestAuthentication(function afterRequest () { - client.init(callback); + window.setTimeout(client.init(callback), 5000); }); } }); @@ -1084,9 +1085,6 @@ client.load = function load (serverSettings, callback) { 'authorize' , auth_data , function authCallback (data) { - - console.log('Socket auth response', data); - if (!data) { console.log('Crashed!'); client.crashed(); @@ -1137,7 +1135,6 @@ client.load = function load (serverSettings, callback) { socket.on('notification', function(notify) { console.log('notification from server:', notify); - if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) { previousNotifyTimestamp = notify.timestamp; client.plugins.visualizeAlarm(client.sbx, notify, notify.title + ' ' + notify.message); @@ -1148,7 +1145,6 @@ client.load = function load (serverSettings, callback) { socket.on('announcement', function(notify) { console.info('announcement received from server'); - console.log('notify:', notify); currentAnnouncement = notify; currentAnnouncement.received = Date.now(); updateTitle(); @@ -1156,7 +1152,6 @@ client.load = function load (serverSettings, callback) { socket.on('alarm', function(notify) { console.info('alarm received from server'); - console.log('notify:', notify); var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow); if (enabled) { console.log('Alarm raised!'); @@ -1169,8 +1164,6 @@ client.load = function load (serverSettings, callback) { socket.on('urgent_alarm', function(notify) { console.info('urgent alarm received from server'); - console.log('notify:', notify); - var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow); if (enabled) { console.log('Urgent alarm raised!'); @@ -1182,7 +1175,6 @@ client.load = function load (serverSettings, callback) { }); socket.on('clear_alarm', function(notify) { - console.info('got clear_alarm', notify); if (alarmInProgress) { console.log('clearing alarm'); stopAlarm(false, null, notify); @@ -1217,10 +1209,15 @@ client.load = function load (serverSettings, callback) { } } - // hide food control if not enabled - $('.foodcontrol').toggle(client.settings.enable.indexOf('food') > -1); - // hide cob control if not enabled - $('.cobcontrol').toggle(client.settings.enable.indexOf('cob') > -1); + client.updateAdminMenu = function updateAdminMenu() { + // hide food control if not enabled + $('.foodcontrol').toggle(client.settings.enable.indexOf('food') > -1); + // hide cob control if not enabled + $('.cobcontrol').toggle(client.settings.enable.indexOf('cob') > -1); +} + + client.updateAdminMenu(); + container.toggleClass('has-minor-pills', client.plugins.hasShownType('pill-minor', client.settings)); function prepareEntries () { diff --git a/lib/server/websocket.js b/lib/server/websocket.js index 7a685ccde05..a0142db8fd1 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -82,8 +82,8 @@ function init (env, ctx, server) { }); } - function verifyAuthorization (message, callback) { - ctx.authorization.resolve({ api_secret: message.secret, token: message.token }, function resolved (err, result) { + function verifyAuthorization (message, ip, callback) { + ctx.authorization.resolve({ api_secret: message.secret, token: message.token, ip: ip }, function resolved (err, result) { if (err) { return callback(err, { @@ -498,7 +498,8 @@ function init (env, ctx, server) { // [, status : true ] // } socket.on('authorize', function authorize (message, callback) { - verifyAuthorization(message, function verified (err, authorization) { + const remoteIP = socket.request.connection.remoteAddress; + verifyAuthorization(message, remoteIP, function verified (err, authorization) { socketAuthorization = authorization; clientType = message.client; history = message.history || 48; //default history is 48 hours diff --git a/tests/admintools.test.js b/tests/admintools.test.js index 2fe6169f59a..b151381cf07 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -202,7 +202,7 @@ describe('admintools', function ( ) { it ('should produce some html', function (done) { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js index 5785069f77e..f3603ebb8b4 100644 --- a/tests/api.unauthorized.test.js +++ b/tests/api.unauthorized.test.js @@ -8,6 +8,8 @@ var language = require('../lib/language')(); describe('authed REST api', function ( ) { var entries = require('../lib/api/entries/'); + this.timeout(20000); + before(function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js index 48a05f2d9c4..318ba610a29 100644 --- a/tests/api.verifyauth.test.js +++ b/tests/api.verifyauth.test.js @@ -8,10 +8,13 @@ require('should'); describe('Verifyauth REST api', function ( ) { var self = this; + this.timeout(10000); + var api = require('../lib/api/'); before(function (done) { self.env = require('../env')( ); self.env.api_secret = 'this is my long pass phrase'; + self.env.settings.authDefaultRoles = 'denied'; this.wares = require('../lib/middleware/')(self.env); self.app = require('express')( ); self.app.enable('api'); diff --git a/tests/careportal.test.js b/tests/careportal.test.js index 36f48d3a5a4..d16ec95e472 100644 --- a/tests/careportal.test.js +++ b/tests/careportal.test.js @@ -40,7 +40,7 @@ describe('client', function ( ) { var client = window.Nightscout.client; - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; diff --git a/tests/hashauth.test.js b/tests/hashauth.test.js index 011ca689d10..64412da96df 100644 --- a/tests/hashauth.test.js +++ b/tests/hashauth.test.js @@ -66,7 +66,7 @@ describe('hashauth', function ( ) { it ('should make module unauthorized', function () { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { @@ -84,7 +84,7 @@ describe('hashauth', function ( ) { it ('should make module authorized', function () { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { @@ -100,7 +100,7 @@ describe('hashauth', function ( ) { it ('should store hash and the remove authentication', function () { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); @@ -126,7 +126,7 @@ describe('hashauth', function ( ) { it ('should not store hash', function () { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); @@ -149,7 +149,7 @@ describe('hashauth', function ( ) { it ('should report secret too short', function () { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js index 42656b511af..cdca12764b7 100644 --- a/tests/profileeditor.test.js +++ b/tests/profileeditor.test.js @@ -101,7 +101,7 @@ describe('Profile editor', function ( ) { it ('should produce some html', function (done) { var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; diff --git a/tests/reports.test.js b/tests/reports.test.js index c77f87f2e8f..cfdda992b9d 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -206,7 +206,7 @@ describe('reports', function ( ) { it ('should produce some html', function (done) { var client = window.Nightscout.client; - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; @@ -281,7 +281,7 @@ describe('reports', function ( ) { it ('should produce week to week report', function (done) { var client = window.Nightscout.client; - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; diff --git a/tests/verifyauth.test.js b/tests/verifyauth.test.js index 36624e03023..ce970f26bab 100644 --- a/tests/verifyauth.test.js +++ b/tests/verifyauth.test.js @@ -1,5 +1,6 @@ 'use strict'; +const { geoNaturalEarth1 } = require('d3'); var request = require('supertest'); var language = require('../lib/language')(); require('should'); @@ -7,6 +8,8 @@ require('should'); describe('verifyauth', function ( ) { var api = require('../lib/api/'); + this.timeout(25000); + var scope = this; function setup_app (env, fn) { require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { @@ -20,7 +23,7 @@ describe('verifyauth', function ( ) { done(); }); - it('should fail unauthorized', function (done) { + it('should return defaults when called without secret', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; @@ -29,12 +32,59 @@ describe('verifyauth', function ( ) { setup_app(env, function (ctx) { ctx.app.enabled('api').should.equal(true); ctx.app.api_secret = ''; - ping_authorized_endpoint(ctx.app, 401, done); + ping_authorized_endpoint(ctx.app, 200, done); + }); + }); + + it('should fail when calling with wrong secret', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = 'wrong secret'; + + function check(res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + done(); + } + + ping_authorized_endpoint(ctx.app, 200, check, true); }); + }); + + + it('should fail unauthorized and delay subsequent attempts', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = 'wrong secret'; + const time = Date.now(); + + function checkTimer(res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + const delta = Date.now() - time; + delta.should.be.greaterThan(1000); + done(); + } + function pingAgain (res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + ping_authorized_endpoint(ctx.app, 200, checkTimer, true); + } + + ping_authorized_endpoint(ctx.app, 200, pingAgain, true); + }); }); + it('should work fine authorized', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; @@ -50,17 +100,14 @@ describe('verifyauth', function ( ) { }); - function ping_authorized_endpoint (app, fails, fn) { + function ping_authorized_endpoint (app, httpResponse, fn, passres) { request(app) .get('/verifyauth') .set('api-secret', app.api_secret || '') - .expect(fails) + .expect(httpResponse) .end(function (err, res) { - //console.log(res.body); - if (fails < 400) { - res.body.status.should.equal(200); - } - fn( ); + res.body.status.should.equal(httpResponse); + if (passres) { fn(res); } else { fn(); } // console.log('err', err, 'res', res); }); } diff --git a/translations/en/en.json b/translations/en/en.json index 9f58a686344..9692f9555c1 100644 --- a/translations/en/en.json +++ b/translations/en/en.json @@ -656,4 +656,9 @@ ,"virtAsstDatabaseSize":"%1 MiB. That is %2% of available database space." ,"virtAsstTitleDatabaseSize":"Database file size" ,"Carbs/Food/Time":"Carbs/Food/Time" + ,"Reads enabled in default permissions":"Reads enabled in default permissions" + ,"Data reads enabled": "Data reads enabled" + ,"Data writes enabled": "Data writes enabled" + ,"Data writes not enabled": "Data writes not enabled" + } \ No newline at end of file diff --git a/views/index.html b/views/index.html index 8e60b30f471..3f249e1d57c 100644 --- a/views/index.html +++ b/views/index.html @@ -166,9 +166,9 @@