From d63c5e7134070a4cd44f36dfb30f94f161370c4f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Mar 2019 00:34:34 -0600 Subject: [PATCH 1/8] Basic widget OpenID reauth implementation Covers the minimum of https://github.com/vector-im/riot-web/issues/7153 This does not handling automatically accepting/blocking widgets yet, however. This could lead to dialog irritation. --- src/FromWidgetPostMessageApi.js | 42 ++++++++++++++++++++++++++++-- src/WidgetMessaging.js | 45 +++++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index ea7eeba756..577eabf5ec 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 Travis Ralston Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. @@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -const WIDGET_API_VERSION = '0.0.1'; // Current API version +const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ '0.0.1', + '0.0.2', ]; const INBOUND_API_NAME = 'fromWidget'; -// Listen for and handle incomming requests using the 'fromWidget' postMessage +// Listen for and handle incoming requests using the 'fromWidget' postMessage // API and initiate responses export default class FromWidgetPostMessageApi { constructor() { this.widgetMessagingEndpoints = []; + this.widgetListeners = {}; // {action: func[]} this.start = this.start.bind(this); this.stop = this.stop.bind(this); @@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi { window.removeEventListener('message', this.onPostMessage); } + /** + * Adds a listener for a given action + * @param {string} action The action to listen for. + * @param {Function} callbackFn A callback function to be called when the action is + * encountered. Called with two parameters: the interesting request information and + * the raw event received from the postMessage API. The raw event is meant to be used + * for sendResponse and similar functions. + */ + addListener(action, callbackFn) { + if (!this.widgetListeners[action]) this.widgetListeners[action] = []; + this.widgetListeners[action].push(callbackFn); + } + + /** + * Removes a listener for a given action. + * @param {string} action The action that was subscribed to. + * @param {Function} callbackFn The original callback function that was used to subscribe + * to updates. + */ + removeListener(action, callbackFn) { + if (!this.widgetListeners[action]) return; + + const idx = this.widgetListeners.indexOf(callbackFn); + if (idx !== -1) this.widgetListeners.splice(idx, 1); + } + /** * Register a widget endpoint for trusted postMessage communication * @param {string} widgetId Unique widget identifier @@ -117,6 +146,13 @@ export default class FromWidgetPostMessageApi { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } + // Call any listeners we have registered + if (this.widgetListeners[event.data.action]) { + for (const fn of this.widgetListeners[event.data.action]) { + fn(event.data, event); + } + } + // Although the requestId is required, we don't use it. We'll be nice and process the message // if the property is missing, but with a warning for widget developers. if (!event.data.requestId) { @@ -164,6 +200,8 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } + } else if (action === 'get_openid') { + // Handled by caller } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 5b722df65f..17ce9360b7 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +Copyright 2019 Travis Ralston Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +22,10 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; +import Modal from "./Modal"; +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import {_t} from "./languageHandler"; +import MatrixClientPeg from "./MatrixClientPeg"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -40,6 +45,7 @@ export default class WidgetMessaging { this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; + this._openIdHandlerRef = this._onOpenIdRequest.bind(this); this.start(); } @@ -109,9 +115,48 @@ export default class WidgetMessaging { start() { this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.addListener("get_openid", this._openIdHandlerRef); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); + } + + _onOpenIdRequest(ev, rawEv) { + if (ev.widgetId !== this.widgetId) return; // not interesting + + // Confirm that we received the request + this.fromWidget.sendResponse(rawEv, {state: "request"}); + + // TODO: Support blacklisting widgets + // TODO: Support whitelisting widgets + + // Actually ask for permission to send the user's data + Modal.createTrackedDialog("OpenID widget permissions", '', QuestionDialog, { + title: _t("A widget would like to verify your identity"), + description: _t( + "A widget located at %(widgetUrl)s would like to verify your identity. " + + "By allowing this, the widget will be able to verify your user ID, but not " + + "perform actions as you.", { + widgetUrl: this.widgetUrl, + }, + ), + button: _t("Allow"), + onFinished: async (confirm) => { + const responseBody = {success: confirm}; + if (confirm) { + const credentials = await MatrixClientPeg.get().getOpenIdToken(); + Object.assign(responseBody, credentials); + } + this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "openid_credentials", + data: responseBody, + }).catch((error) => { + console.error("Failed to send OpenID credentials: ", error); + }); + }, + }); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8cc85b6036..e13390e226 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -230,6 +230,9 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "A widget would like to verify your identity": "A widget would like to verify your identity", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", + "Allow": "Allow", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -924,7 +927,6 @@ "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", From f045beafc37ea62c176ccbd7a69c98481b6a352a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Mar 2019 21:33:31 -0600 Subject: [PATCH 2/8] Support whitelisting/blacklisting widgets for OpenID --- res/css/_components.scss | 1 + .../_WidgetOpenIDPermissionsDialog.scss | 28 +++++ src/WidgetMessaging.js | 62 +++++----- .../dialogs/WidgetOpenIDPermissionsDialog.js | 106 ++++++++++++++++++ .../views/elements/LabelledToggleSwitch.js | 20 +++- src/i18n/strings/en_EN.json | 8 +- src/settings/Settings.js | 7 ++ 7 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss create mode 100644 src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4fb0eed4af..f29e30dcb4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -70,6 +70,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; +@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; diff --git a/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss new file mode 100644 index 0000000000..a419c105a9 --- /dev/null +++ b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss @@ -0,0 +1,28 @@ +/* +Copyright 2019 Travis Ralston + +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. +*/ + +.mx_WidgetOpenIDPermissionsDialog .mx_SettingsFlag { + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-right: 8px; + } + + .mx_SettingsFlag_label { + display: inline-block; + vertical-align: middle; + } +} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 17ce9360b7..dba703ffb8 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -26,6 +26,8 @@ import Modal from "./Modal"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import {_t} from "./languageHandler"; import MatrixClientPeg from "./MatrixClientPeg"; +import SettingsStore from "./settings/SettingsStore"; +import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -123,40 +125,46 @@ export default class WidgetMessaging { this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); } - _onOpenIdRequest(ev, rawEv) { + async _onOpenIdRequest(ev, rawEv) { if (ev.widgetId !== this.widgetId) return; // not interesting + const settings = SettingsStore.getValue("widgetOpenIDPermissions"); + if (settings.blacklist && settings.blacklist.includes(this.widgetId)) { + this.fromWidget.sendResponse(rawEv, {state: "blocked"}); + return; + } + if (settings.whitelist && settings.whitelist.includes(this.widgetId)) { + const responseBody = {state: "allowed"}; + const credentials = await MatrixClientPeg.get().getOpenIdToken(); + Object.assign(responseBody, credentials); + this.fromWidget.sendResponse(rawEv, responseBody); + return; + } + // Confirm that we received the request this.fromWidget.sendResponse(rawEv, {state: "request"}); - // TODO: Support blacklisting widgets - // TODO: Support whitelisting widgets - // Actually ask for permission to send the user's data - Modal.createTrackedDialog("OpenID widget permissions", '', QuestionDialog, { - title: _t("A widget would like to verify your identity"), - description: _t( - "A widget located at %(widgetUrl)s would like to verify your identity. " + - "By allowing this, the widget will be able to verify your user ID, but not " + - "perform actions as you.", { - widgetUrl: this.widgetUrl, + Modal.createTrackedDialog("OpenID widget permissions", '', + WidgetOpenIDPermissionsDialog, { + widgetUrl: this.widgetUrl, + widgetId: this.widgetId, + + onFinished: async (confirm) => { + const responseBody = {success: confirm}; + if (confirm) { + const credentials = await MatrixClientPeg.get().getOpenIdToken(); + Object.assign(responseBody, credentials); + } + this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "openid_credentials", + data: responseBody, + }).catch((error) => { + console.error("Failed to send OpenID credentials: ", error); + }); }, - ), - button: _t("Allow"), - onFinished: async (confirm) => { - const responseBody = {success: confirm}; - if (confirm) { - const credentials = await MatrixClientPeg.get().getOpenIdToken(); - Object.assign(responseBody, credentials); - } - this.messageToWidget({ - api: OUTBOUND_API_NAME, - action: "openid_credentials", - data: responseBody, - }).catch((error) => { - console.error("Failed to send OpenID credentials: ", error); - }); }, - }); + ); } } diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js new file mode 100644 index 0000000000..bec71e49a3 --- /dev/null +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -0,0 +1,106 @@ +/* +Copyright 2019 Travis Ralston + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Tab, TabbedView} from "../../structures/TabbedView"; +import {_t, _td} from "../../../languageHandler"; +import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; +import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; +import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; +import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; +import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; +import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; +import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; +import sdk from "../../../index"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; + +export default class WidgetOpenIDPermissionsDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + widgetUrl: PropTypes.string.isRequired, + widgetId: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + rememberSelection: false, + }; + } + + _onAllow = () => { + this._onPermissionSelection(true); + }; + + _onDeny = () => { + this._onPermissionSelection(false); + }; + + _onPermissionSelection(allowed) { + if (this.state.rememberSelection) { + console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); + + const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); + if (!currentValues.whitelist) currentValues.whitelist = []; + if (!currentValues.blacklist) currentValues.blacklist = []; + + (allowed ? currentValues.whitelist : currentValues.blacklist).push(this.props.widgetId); + SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); + } + + this.props.onFinished(allowed); + } + + _onRememberSelectionChange = (newVal) => { + this.setState({rememberSelection: newVal}); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
+

+ {_t( + "A widget located at %(widgetUrl)s would like to verify your identity. " + + "By allowing this, the widget will be able to verify your user ID, but not " + + "perform actions as you.", { + widgetUrl: this.props.widgetUrl, + }, + )} +

+ +
+ +
+ ); + } +} diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js index 292c978e88..0cb9b224cf 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.js @@ -31,15 +31,29 @@ export default class LabelledToggleSwitch extends React.Component { // Whether or not to disable the toggle switch disabled: PropTypes.bool, + + // True to put the toggle in front of the label + // Default false. + toggleInFront: PropTypes.bool, }; render() { // This is a minimal version of a SettingsFlag + + let firstPart = {this.props.label}; + let secondPart = ; + + if (this.props.toggleInFront) { + const temp = firstPart; + firstPart = secondPart; + secondPart = temp; + } + return (
- {this.props.label} - + {firstPart} + {secondPart}
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21ad35a44c..4322d6cf7b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -230,9 +230,6 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", - "A widget would like to verify your identity": "A widget would like to verify your identity", - "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", - "Allow": "Allow", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -927,6 +924,7 @@ "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", "Do you want to load widget from URL:": "Do you want to load widget from URL:", + "Allow": "Allow", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", @@ -1176,6 +1174,10 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", + "A widget would like to verify your identity": "A widget would like to verify your identity", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", + "Remember my selection for this widget": "Remember my selection for this widget", + "Deny": "Deny", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 4fe53633ff..fcf70b4df7 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -340,6 +340,13 @@ export const SETTINGS = { displayName: _td('Show developer tools'), default: false, }, + "widgetOpenIDPermissions": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + whitelisted: [], + blacklisted: [], + }, + }, "RoomList.orderByImportance": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Order rooms in the room list by most important first instead of most recent'), From b48842e070e089872795c107955eea3b536cc6b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Mar 2019 21:51:19 -0600 Subject: [PATCH 3/8] Fix imports for linter --- src/WidgetMessaging.js | 2 -- .../views/dialogs/WidgetOpenIDPermissionsDialog.js | 11 +---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index dba703ffb8..501fc34c7a 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -23,8 +23,6 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; import Modal from "./Modal"; -import QuestionDialog from "./components/views/dialogs/QuestionDialog"; -import {_t} from "./languageHandler"; import MatrixClientPeg from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index bec71e49a3..e6e97a3305 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -16,17 +16,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {Tab, TabbedView} from "../../structures/TabbedView"; -import {_t, _td} from "../../../languageHandler"; -import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; +import {_t} from "../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; -import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; -import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; -import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; -import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; -import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; -import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; From 1ed2e6dcc1def5c3dd8d9684d34d4eaeb733e297 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Mar 2019 15:03:02 -0600 Subject: [PATCH 4/8] Remove the correct widget listener --- src/FromWidgetPostMessageApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 577eabf5ec..4dd3ea6e6d 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -70,8 +70,8 @@ export default class FromWidgetPostMessageApi { removeListener(action, callbackFn) { if (!this.widgetListeners[action]) return; - const idx = this.widgetListeners.indexOf(callbackFn); - if (idx !== -1) this.widgetListeners.splice(idx, 1); + const idx = this.widgetListeners[action].indexOf(callbackFn); + if (idx !== -1) this.widgetListeners[action].splice(idx, 1); } /** From 21d52a83113eab3654c36cff431660124961535a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 22:50:26 -0600 Subject: [PATCH 5/8] Use the same function name to bind the OpenID request handler --- src/WidgetMessaging.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 501fc34c7a..bbecdfa086 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -45,7 +45,7 @@ export default class WidgetMessaging { this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; - this._openIdHandlerRef = this._onOpenIdRequest.bind(this); + this._onOpenIdRequest = this._onOpenIdRequest.bind(this); this.start(); } @@ -115,12 +115,12 @@ export default class WidgetMessaging { start() { this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); - this.fromWidget.addListener("get_openid", this._openIdHandlerRef); + this.fromWidget.addListener("get_openid", this._onOpenIdRequest); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); - this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); + this.fromWidget.removeListener("get_openid", this._onOpenIdRequest); } async _onOpenIdRequest(ev, rawEv) { From 2dcb40f1be6a90b628a84581f838a012821547f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:25:31 -0600 Subject: [PATCH 6/8] Track OpenID automatic permissions by (widgetLocation, widgetUrl) --- src/WidgetMessaging.js | 11 ++++++--- .../dialogs/WidgetOpenIDPermissionsDialog.js | 8 ++++++- src/components/views/elements/AppTile.js | 2 +- src/settings/Settings.js | 4 ++-- src/utils/WidgetUtils.js | 23 +++++++++++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index bbecdfa086..8d419ba6eb 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -26,6 +26,7 @@ import Modal from "./Modal"; import MatrixClientPeg from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; +import WidgetUtils from "./utils/WidgetUtils"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -39,9 +40,10 @@ if (!global.mxToWidgetMessaging) { const OUTBOUND_API_NAME = 'toWidget'; export default class WidgetMessaging { - constructor(widgetId, widgetUrl, target) { + constructor(widgetId, widgetUrl, isUserWidget, target) { this.widgetId = widgetId; this.widgetUrl = widgetUrl; + this.isUserWidget = isUserWidget; this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; @@ -126,12 +128,14 @@ export default class WidgetMessaging { async _onOpenIdRequest(ev, rawEv) { if (ev.widgetId !== this.widgetId) return; // not interesting + const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget); + const settings = SettingsStore.getValue("widgetOpenIDPermissions"); - if (settings.blacklist && settings.blacklist.includes(this.widgetId)) { + if (settings.blacklist && settings.blacklist.includes(widgetSecurityKey)) { this.fromWidget.sendResponse(rawEv, {state: "blocked"}); return; } - if (settings.whitelist && settings.whitelist.includes(this.widgetId)) { + if (settings.whitelist && settings.whitelist.includes(widgetSecurityKey)) { const responseBody = {state: "allowed"}; const credentials = await MatrixClientPeg.get().getOpenIdToken(); Object.assign(responseBody, credentials); @@ -147,6 +151,7 @@ export default class WidgetMessaging { WidgetOpenIDPermissionsDialog, { widgetUrl: this.widgetUrl, widgetId: this.widgetId, + isUserWidget: this.isUserWidget, onFinished: async (confirm) => { const responseBody = {success: confirm}; diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index e6e97a3305..5f341261f8 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -20,12 +20,14 @@ import {_t} from "../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import WidgetUtils from "../../../utils/WidgetUtils"; export default class WidgetOpenIDPermissionsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, widgetUrl: PropTypes.string.isRequired, widgetId: PropTypes.string.isRequired, + isUserWidget: PropTypes.bool.isRequired, }; constructor() { @@ -52,7 +54,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { if (!currentValues.whitelist) currentValues.whitelist = []; if (!currentValues.blacklist) currentValues.blacklist = []; - (allowed ? currentValues.whitelist : currentValues.blacklist).push(this.props.widgetId); + const securityKey = WidgetUtils.getWidgetSecurityKey( + this.props.widgetId, + this.props.widgetUrl, + this.props.isUserWidget); + (allowed ? currentValues.whitelist : currentValues.blacklist).push(securityKey); SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 8ed408ffbe..955c2d5480 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -351,7 +351,7 @@ export default class AppTile extends React.Component { _setupWidgetMessaging() { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. - const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow); + const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fcf70b4df7..765b2f85c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -343,8 +343,8 @@ export const SETTINGS = { "widgetOpenIDPermissions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: { - whitelisted: [], - blacklisted: [], + whitelist: [], + blacklist: [], }, }, "RoomList.orderByImportance": { diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index b5a2ae31fb..41a241c905 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2019 Travis Ralston Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,6 +26,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore'; // before waitFor[Room/User]Widget rejects its promise const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -396,4 +398,25 @@ export default class WidgetUtils { return capWhitelist; } + + static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) { + let widgetLocation = ActiveWidgetStore.getRoomId(widgetId); + + if (isUserWidget) { + const userWidget = WidgetUtils.getUserWidgetsArray() + .find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl); + + if (!userWidget) { + throw new Error("No matching user widget to form security key"); + } + + widgetLocation = userWidget.sender; + } + + if (!widgetLocation) { + throw new Error("Failed to locate where the widget resides"); + } + + return encodeURIComponent(`${widgetLocation}::${widgetUrl}`); + } } From 3654c895ebf75dca452bc7f955e09d470a670662 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Mar 2019 23:31:19 -0600 Subject: [PATCH 7/8] Appease the linter --- src/components/views/elements/AppTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 955c2d5480..c410b31799 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -351,7 +351,8 @@ export default class AppTile extends React.Component { _setupWidgetMessaging() { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. - const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); + const widgetMessaging = new WidgetMessaging( + this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow); ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); From 69fcebf045863a9196eea2d9ed18948b3a876212 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Mar 2019 21:14:21 -0600 Subject: [PATCH 8/8] Use allow/deny instead of whitelist/blacklist for terminology --- src/WidgetMessaging.js | 4 ++-- .../views/dialogs/WidgetOpenIDPermissionsDialog.js | 6 +++--- src/settings/Settings.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 8d419ba6eb..1d8e1b9cd3 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -131,11 +131,11 @@ export default class WidgetMessaging { const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget); const settings = SettingsStore.getValue("widgetOpenIDPermissions"); - if (settings.blacklist && settings.blacklist.includes(widgetSecurityKey)) { + if (settings.deny && settings.deny.includes(widgetSecurityKey)) { this.fromWidget.sendResponse(rawEv, {state: "blocked"}); return; } - if (settings.whitelist && settings.whitelist.includes(widgetSecurityKey)) { + if (settings.allow && settings.allow.includes(widgetSecurityKey)) { const responseBody = {state: "allowed"}; const credentials = await MatrixClientPeg.get().getOpenIdToken(); Object.assign(responseBody, credentials); diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index 5f341261f8..62bd1d2521 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -51,14 +51,14 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); const currentValues = SettingsStore.getValue("widgetOpenIDPermissions"); - if (!currentValues.whitelist) currentValues.whitelist = []; - if (!currentValues.blacklist) currentValues.blacklist = []; + if (!currentValues.allow) currentValues.allow = []; + if (!currentValues.deny) currentValues.deny = []; const securityKey = WidgetUtils.getWidgetSecurityKey( this.props.widgetId, this.props.widgetUrl, this.props.isUserWidget); - (allowed ? currentValues.whitelist : currentValues.blacklist).push(securityKey); + (allowed ? currentValues.allow : currentValues.deny).push(securityKey); SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues); } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 765b2f85c6..6e17ffbbd7 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -343,8 +343,8 @@ export const SETTINGS = { "widgetOpenIDPermissions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: { - whitelist: [], - blacklist: [], + allow: [], + deny: [], }, }, "RoomList.orderByImportance": {