From f045beafc37ea62c176ccbd7a69c98481b6a352a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Mar 2019 21:33:31 -0600 Subject: [PATCH] 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'),