From 0fbacfd3fcb0e26a13555269fc00f6c9966a144b Mon Sep 17 00:00:00 2001 From: Ben West Date: Sun, 22 Oct 2023 09:10:35 -0700 Subject: [PATCH 1/5] alarmSocket: customizing authorization requirements This patch is intended to allow customizing the behavior for whether or not to prompt for authorization before subscribing or acknolweding alarms. There was a bug in previous attempts where the profileeditor would be double initialized, causing the profileeditor to remove some buttons from the GUI. This patch adds checking for a permission specifically related to acknolwedging alarms, as well as avoids double-initializing the editor, which causes the issue with the GUI. --- lib/api3/alarmSocket.js | 50 ++++++++++++++++++++++++------------ lib/client/index.js | 29 +++++++++++++++------ lib/profile/profileeditor.js | 2 ++ 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js index 5fe30a620c1..0a6968c9ac4 100644 --- a/lib/api3/alarmSocket.js +++ b/lib/api3/alarmSocket.js @@ -46,7 +46,7 @@ function AlarmSocket (app, env, ctx) { socket.on('subscribe', function onSubscribe (message, returnCallback) { self.subscribe(socket, message, returnCallback); }); - + }); ctx.bus.on('notification', self.emitNotification); @@ -65,7 +65,7 @@ function AlarmSocket (app, env, ctx) { self.subscribe = function subscribe (socket, message, returnCallback) { const shouldCallBack = typeof(returnCallback) === 'function'; - // Native client + // Native client if (message && message.accessToken) { return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinishForToken (err, auth) { if (err) { @@ -76,13 +76,13 @@ function AlarmSocket (app, env, ctx) { } return err; } else { - // Subscribe for acking alarms - socket.on('ack', function onAck (level, group, silenceTime) { - ctx.notifications.ack(level, group, silenceTime, true); - console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); - }); + // Subscribe for acking alarms + socket.on('ack', function onAck (level, group, silenceTime) { + ctx.notifications.ack(level, group, silenceTime, true); + console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); + }); - var okResponse = { success: true, message: 'Subscribed for alarms' } + var okResponse = { success: true, message: 'Subscribed for alarms' } if (shouldCallBack) { returnCallback(okResponse); } @@ -90,10 +90,24 @@ function AlarmSocket (app, env, ctx) { } }); } - - // Web client (jwt access token or api_hash) - if (message && (message.jwtToken || message.secret)) { + + if (!message) { message = {}; } + console.log("AUTH TEST", message, env.settings.authenticationPromptOnLoad); + // Web client (jwt access token or api_hash) + var shouldTry = true; + if (env.settings.authenticationPromptOnLoad) { + if (!message.jwtToken || !message.secret) { + shouldTry = false; + } + } + if (message && shouldTry) { return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) { + console.log("AUTH FOR ALARMS", err, auth); + var perms = { + read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) + , ack: ctx.authorization.checkMultiple('api:*:write', auth.shiros) + }; + console.log("AUTH FOR ALARMS", err, auth, perms); if (err) { console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken); @@ -102,13 +116,15 @@ function AlarmSocket (app, env, ctx) { } return err; } else { - // Subscribe for acking alarms - socket.on('ack', function onAck (level, group, silenceTime) { - ctx.notifications.ack(level, group, silenceTime, true); - console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); - }); + // Subscribe for acking alarms + if (perms.ack) { + socket.on('ack', function onAck (level, group, silenceTime) { + ctx.notifications.ack(level, group, silenceTime, true); + console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); + }); + } - var okResponse = { success: true, message: 'Subscribed for alarms' } + var okResponse = { success: true, message: 'Subscribed for alarms', ...perms }; if (shouldCallBack) { returnCallback(okResponse); } diff --git a/lib/client/index.js b/lib/client/index.js index 0f0d17b7d64..c570f3f4af8 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1146,15 +1146,28 @@ client.load = function load (serverSettings, callback) { } console.log('Subscribed for alarms', data); - if (client.settings.authenticationPromptOnLoad && !data.success) { - client.hashauth.requestAuthentication(function afterRequest () { - client.hashauth.updateSocketAuth(); - if (callback) { - callback(); - } - }); + var shouldAuthenticationPromptOnLoad = client.settings.authenticationPromptOnLoad ; + console.log("shouldAuthenticationPromptOnLoad", shouldAuthenticationPromptOnLoad, !shouldAuthenticationPromptOnLoad && !data.success, data, data.read, hasRequiredPermission( )); + if (!data.success) { + console.log("SHOULD REQUEST AUTHENTICATION", callback); + if (!data.read || !hasRequiredPermission() || shouldAuthenticationPromptOnLoad) { + return client.hashauth.requestAuthentication(function afterRequest () { + console.log("SHOULD REQUEST AUTHENTICATION"); + return client.hashauth.updateSocketAuth(); + if (callback) { + callback(); + } + }); + } + if (callback) { + console.log("ISSUING CALLBACK NEW BRANCH"); + callback(); + } } else if (callback) { - callback(); + console.log("HAS OTHER BRANCH", callback); + if (shouldAuthenticationPromptOnLoad) { + callback(); + } } } ); diff --git a/lib/profile/profileeditor.js b/lib/profile/profileeditor.js index 9a9062603c1..ca049573e51 100644 --- a/lib/profile/profileeditor.js +++ b/lib/profile/profileeditor.js @@ -18,6 +18,7 @@ var init = function init () { client.init(function loaded () { + console.log("LOADING CLIENT INIT"); if (c_profile !== null) { return; // already loaded so don't load again } @@ -151,6 +152,7 @@ var init = function init () { } function initeditor() { + console.log("INITEDITOR", client.settings.extendedSettings.profile, client.settings.extendedSettings.profile?.history, client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); $('#pe_history').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); $('#pe_multiple').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.multiple); From d8fe025ad2a430eee21187ab70dd17970d4cd0b8 Mon Sep 17 00:00:00 2001 From: Ben West Date: Sun, 22 Oct 2023 10:59:54 -0700 Subject: [PATCH 2/5] alarmSocket: document and prep concept This patch eliminates debugging logging in favor of commentary to capture how, where, and why alarmSocket feature is causing different pages to demand the authentication prompt in a variety of circumstances. --- lib/api3/alarmSocket.js | 20 ++++++++++++++++---- lib/client/index.js | 6 +----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js index 0a6968c9ac4..6bba6208297 100644 --- a/lib/api3/alarmSocket.js +++ b/lib/api3/alarmSocket.js @@ -92,22 +92,34 @@ function AlarmSocket (app, env, ctx) { } if (!message) { message = {}; } - console.log("AUTH TEST", message, env.settings.authenticationPromptOnLoad); // Web client (jwt access token or api_hash) + /* + * On the web: a client may have saved a secret or using a jwtToken, or may have none. + * Some pages will automatically prompt for authorization, when needed. + * To make the main homepage require authorization as well, set + * AUTHENTICATION_PROMPT_ON_LOAD=true. + * + * If there is missing authorization when authorization is required, + * rejecting the attempt in order to trigger a prompt on the client. + * If there is no authorization required, or there are available + * credentials, attempt to resolve the available permissions. + * When processing ACK messages that dismiss alarms, Authorization should be + * required. + */ var shouldTry = true; if (env.settings.authenticationPromptOnLoad) { - if (!message.jwtToken || !message.secret) { + if (!message.jwtToken && !message.secret) { shouldTry = false; } } + if (message && shouldTry) { return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) { - console.log("AUTH FOR ALARMS", err, auth); + var perms = { read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) , ack: ctx.authorization.checkMultiple('api:*:write', auth.shiros) }; - console.log("AUTH FOR ALARMS", err, auth, perms); if (err) { console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken); diff --git a/lib/client/index.js b/lib/client/index.js index c570f3f4af8..cbfd51bc20e 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1147,12 +1147,9 @@ client.load = function load (serverSettings, callback) { console.log('Subscribed for alarms', data); var shouldAuthenticationPromptOnLoad = client.settings.authenticationPromptOnLoad ; - console.log("shouldAuthenticationPromptOnLoad", shouldAuthenticationPromptOnLoad, !shouldAuthenticationPromptOnLoad && !data.success, data, data.read, hasRequiredPermission( )); if (!data.success) { - console.log("SHOULD REQUEST AUTHENTICATION", callback); if (!data.read || !hasRequiredPermission() || shouldAuthenticationPromptOnLoad) { return client.hashauth.requestAuthentication(function afterRequest () { - console.log("SHOULD REQUEST AUTHENTICATION"); return client.hashauth.updateSocketAuth(); if (callback) { callback(); @@ -1160,11 +1157,10 @@ client.load = function load (serverSettings, callback) { }); } if (callback) { - console.log("ISSUING CALLBACK NEW BRANCH"); callback(); } } else if (callback) { - console.log("HAS OTHER BRANCH", callback); + // Callback is client.init, causing the prompt to appear. if (shouldAuthenticationPromptOnLoad) { callback(); } From 1fa6af412a9269f533b05f96c155df5c8e8e5d61 Mon Sep 17 00:00:00 2001 From: Ben West Date: Sun, 22 Oct 2023 12:14:28 -0700 Subject: [PATCH 3/5] alarmSocket: don't double initialize, stub out non-global-ack Create an opportunity to respond with something other than a global ack when someone that is not authorized sends an acknolwedgement to an alarm. Eliminate double-initializing when subscribing to alarms. The `callback` variable is not local to the function, anyway. --- lib/api3/alarmSocket.js | 20 ++++++++++++-------- lib/client/index.js | 11 ----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js index 6bba6208297..e242aa9480d 100644 --- a/lib/api3/alarmSocket.js +++ b/lib/api3/alarmSocket.js @@ -116,10 +116,6 @@ function AlarmSocket (app, env, ctx) { if (message && shouldTry) { return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) { - var perms = { - read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) - , ack: ctx.authorization.checkMultiple('api:*:write', auth.shiros) - }; if (err) { console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken); @@ -128,13 +124,21 @@ function AlarmSocket (app, env, ctx) { } return err; } else { + var perms = { + read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) + , ack: ctx.authorization.checkMultiple('notifications:*:ack', auth.shiros) + + }; // Subscribe for acking alarms - if (perms.ack) { - socket.on('ack', function onAck (level, group, silenceTime) { + socket.on('ack', function onAck (level, group, silenceTime) { + if (perms.ack) { ctx.notifications.ack(level, group, silenceTime, true); console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); - }); - } + } else { + // TODO: send a message to client to silence locally, but not + // globally, and request authorization. + } + }); var okResponse = { success: true, message: 'Subscribed for alarms', ...perms }; if (shouldCallBack) { diff --git a/lib/client/index.js b/lib/client/index.js index cbfd51bc20e..9a0bdcc68e9 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1151,19 +1151,8 @@ client.load = function load (serverSettings, callback) { if (!data.read || !hasRequiredPermission() || shouldAuthenticationPromptOnLoad) { return client.hashauth.requestAuthentication(function afterRequest () { return client.hashauth.updateSocketAuth(); - if (callback) { - callback(); - } }); } - if (callback) { - callback(); - } - } else if (callback) { - // Callback is client.init, causing the prompt to appear. - if (shouldAuthenticationPromptOnLoad) { - callback(); - } } } ); From c0892863a2f631efd76315dae8c5fb8b7036c256 Mon Sep 17 00:00:00 2001 From: Ben West Date: Mon, 23 Oct 2023 08:46:04 -0700 Subject: [PATCH 4/5] remove spurious logging in profileeditor --- lib/profile/profileeditor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/profile/profileeditor.js b/lib/profile/profileeditor.js index ca049573e51..71355dd194a 100644 --- a/lib/profile/profileeditor.js +++ b/lib/profile/profileeditor.js @@ -152,7 +152,6 @@ var init = function init () { } function initeditor() { - console.log("INITEDITOR", client.settings.extendedSettings.profile, client.settings.extendedSettings.profile?.history, client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); $('#pe_history').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); $('#pe_multiple').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.multiple); From a7ebb301b6d52a58e18914aa7c845dfc3d186fff Mon Sep 17 00:00:00 2001 From: Ben West Date: Mon, 23 Oct 2023 09:50:00 -0700 Subject: [PATCH 5/5] add notes regarding handling unauthorized ACK request When someone is looking at Nightscout and needs the alarm silenced, it is very desirable to always silence the local UI. This patch documents some of the working code around handling the alarm notification process, as well as provides commentary on handling unauthorized scenarios. There are some open questions such as how to update the permission set after authorization. --- lib/api3/alarmSocket.js | 16 +++++++++++++++- lib/client/index.js | 22 ++++++++++++++++++++++ lib/notifications.js | 6 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js index e242aa9480d..88bb699a553 100644 --- a/lib/api3/alarmSocket.js +++ b/lib/api3/alarmSocket.js @@ -49,6 +49,8 @@ function AlarmSocket (app, env, ctx) { }); + // Turns all notifications on the event bus back into events to be + // broadcast to clients. ctx.bus.on('notification', self.emitNotification); }; @@ -77,6 +79,7 @@ function AlarmSocket (app, env, ctx) { return err; } else { // Subscribe for acking alarms + // Client sends ack, which sends a notificaiton through our internal bus socket.on('ack', function onAck (level, group, silenceTime) { ctx.notifications.ack(level, group, silenceTime, true); console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); @@ -127,18 +130,29 @@ function AlarmSocket (app, env, ctx) { var perms = { read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) , ack: ctx.authorization.checkMultiple('notifications:*:ack', auth.shiros) - }; // Subscribe for acking alarms + // TODO: does this produce double ACK after the authorizing? Only if reconnecting? + // TODO: how will perms get updated after authorizing? socket.on('ack', function onAck (level, group, silenceTime) { if (perms.ack) { + // This goes through the server-wide event bus. ctx.notifications.ack(level, group, silenceTime, true); console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); } else { // TODO: send a message to client to silence locally, but not // globally, and request authorization. + // This won't go through th event bus. + // var acked = { silenceTime, group, level }; + // socket.emit('authorization_needed', acked); } }); + /* TODO: need to know when to update the permissions. + // Can we use + socket.on('resubscribe', function update_permissions ( ) { + // perms = { ... }; + }); + */ var okResponse = { success: true, message: 'Subscribed for alarms', ...perms }; if (shouldCallBack) { diff --git a/lib/client/index.js b/lib/client/index.js index 9a0bdcc68e9..c4c83b32a01 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1233,6 +1233,28 @@ client.load = function load (serverSettings, callback) { stopAlarm(false, null, notify); } }); + /* + * + // TODO: When an unauthorized client attempts to silence an alarm, we should + // allow silencing locally, request for authorization, and if the + // authorization succeeds even republish the ACK notification. something like... + alarmSocket.on('authorization_needed', function(details) { + if (alarmInProgress) { + console.log('clearing alarm'); + stopAlarm(true, details.silenceTime, currentNotify); + } + client.hashauth.requestAuthentication(function afterRequest () { + console.log("SUCCESSFULLY AUTHORIZED, REPUBLISHED ACK?"); + // easiest way to update permission set on server side is to send another message. + alarmSocket.emit('resubscribe', currentNotify, details); + + if (isClient && currentNotify) { + alarmSocket.emit('ack', currentNotify.level, currentNotify.group, details.silenceTime); + } + }); + }); + + */ $('#testAlarms').click(function(event) { diff --git a/lib/notifications.js b/lib/notifications.js index 9b6adab3ac4..4bd5481ffb3 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -185,6 +185,10 @@ function init (env, ctx) { notifications.ack(1, group, time); } + /* + * TODO: modify with a local clear, this will clear all connected clients, + * globally + */ if (sendClear) { var notify = { clear: true @@ -192,6 +196,8 @@ function init (env, ctx) { , message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d' , group: group }; + // When web client sends ack, this translates the websocket message into + // an event on our internal bus. ctx.bus.emit('notification', notify); logEmitEvent(notify); }