From c3909f44920bc57792d7bd5b9b58309a39dc5a3a Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 16 Jan 2022 22:24:56 +0800 Subject: [PATCH] add permissions management (#827) * add permissions management * hide required permission by default * update permission description * update strings Co-authored-by: Brendan Early --- _locales/en/messages.json | 54 ++++++ sass/permissions.scss | 43 +++++ src/components/Permissions.vue | 53 ++++++ src/components/Popup/MenuPage.vue | 25 ++- src/definitions/module-interface.d.ts | 4 + src/definitions/permission.d.ts | 11 ++ src/models/permission.ts | 13 ++ src/permissions.ts | 45 +++++ src/store/Permissions.ts | 246 ++++++++++++++++++++++++++ svg/clipboard-check.svg | 1 + view/permissions.html | 15 ++ webpack.config.js | 1 + 12 files changed, 503 insertions(+), 8 deletions(-) create mode 100644 sass/permissions.scss create mode 100644 src/components/Permissions.vue create mode 100644 src/definitions/permission.d.ts create mode 100644 src/models/permission.ts create mode 100644 src/permissions.ts create mode 100644 src/store/Permissions.ts create mode 100644 svg/clipboard-check.svg create mode 100644 view/permissions.html diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c81ea2b52..ada569d91 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -445,5 +445,59 @@ }, "no_entires": { "message": "No accounts to display. Add your first account now." + }, + "permissions": { + "message": "Permissions" + }, + "permission_revoke": { + "message": "Revoke" + }, + "permission_show_required_permissions": { + "message": "Show non-revocable permissions" + }, + "permission_required": { + "message": "This is a required permission and cannot be revoked." + }, + "permission_active_tab": { + "message": "Access to the current tab to scan QR codes." + }, + "permission_storage": { + "message": "Access to browser storage to store account data." + }, + "permission_identity": { + "message": "Allows sign in to 3rd party storage services." + }, + "permission_clipboard_write": { + "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." + }, + "permission_context_menus": { + "message": "Adds Authenticator to context menu." + }, + "permission_all_urls": { + "message": "Access to all websites to scan QR codes." + }, + "permission_sync_clock": { + "message": "Allows clock sync with Google." + }, + "permission_dropbox": { + "message": "Allows backup to Dropbox." + }, + "permission_dropbox_cannot_revoke": { + "message": "You must disable Dropbox backup first." + }, + "permission_drive": { + "message": "Allows backup to Google Drive." + }, + "permission_drive_cannot_revoke": { + "message": "You must disable Google Drive backup first." + }, + "permission_onedrive": { + "message": "Allows backup to OneDrive." + }, + "permission_onedrive_cannot_revoke": { + "message": "You must disable OneDrive backup first." + }, + "permission_unknown_permission": { + "message": "Unknown permission. If see this message, please send a bug report." } } diff --git a/sass/permissions.scss b/sass/permissions.scss new file mode 100644 index 000000000..0bef0c1bf --- /dev/null +++ b/sass/permissions.scss @@ -0,0 +1,43 @@ +@import "ui"; + +[v-cloak] { + display: none; +} + +* { + font-family: arial, "Microsoft YaHei"; +} + +p { + font-size: 16px; +} + +#permissions { + width: 900px; + position: relative; + margin: 0 auto; +} + +h2 { + margin-top: 3em; +} + +button { + display: inline-grid; + padding: 10px 20px; + border: #ccc 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + align-items: center; + font-size: 16px; + color: gray; + cursor: pointer; + outline: none; + margin-left: 0px !important; + + &:not(:disabled):hover { + color: black; + } +} diff --git a/src/components/Permissions.vue b/src/components/Permissions.vue new file mode 100644 index 000000000..c53a10691 --- /dev/null +++ b/src/components/Permissions.vue @@ -0,0 +1,53 @@ + + diff --git a/src/components/Popup/MenuPage.vue b/src/components/Popup/MenuPage.vue index 369d1621d..57beba193 100644 --- a/src/components/Popup/MenuPage.vue +++ b/src/components/Popup/MenuPage.vue @@ -8,9 +8,16 @@ -
Version {{ version }}
@@ -75,6 +82,7 @@ import IconAdvisor from "../../../svg/lightbulb.svg"; import IconComments from "../../../svg/comments.svg"; import IconGlobe from "../../../svg/globe.svg"; import IconCode from "../../../svg/code.svg"; +import IconClipboardCheck from "../../../svg/clipboard-check.svg"; export default Vue.extend({ components: { @@ -89,6 +97,7 @@ export default Vue.extend({ IconComments, IconGlobe, IconCode, + IconClipboardCheck, }, computed: { version: function () { diff --git a/src/definitions/module-interface.d.ts b/src/definitions/module-interface.d.ts index 21d84639d..3dc038637 100644 --- a/src/definitions/module-interface.d.ts +++ b/src/definitions/module-interface.d.ts @@ -96,3 +96,7 @@ interface AdvisorState { insights: AdvisorInsightInterface[]; ignoreList: string[]; } + +interface PermissionsState { + permissions: PermissionInterface[]; +} diff --git a/src/definitions/permission.d.ts b/src/definitions/permission.d.ts new file mode 100644 index 000000000..6ed1875b1 --- /dev/null +++ b/src/definitions/permission.d.ts @@ -0,0 +1,11 @@ +interface ValidationResult { + valid: boolean; + message?: string; +} + +interface PermissionInterface { + id: string; + description: string; + revocable: boolean; + validation?: Array<() => ValidationResult>; +} diff --git a/src/models/permission.ts b/src/models/permission.ts new file mode 100644 index 000000000..e2fdfc444 --- /dev/null +++ b/src/models/permission.ts @@ -0,0 +1,13 @@ +export class Permission implements PermissionInterface { + id: string; + description: string; + revocable: boolean; + validation?: Array<() => ValidationResult>; + + constructor(permission: PermissionInterface) { + this.id = permission.id; + this.description = permission.description; + this.revocable = permission.revocable; + this.validation = permission.validation; + } +} diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 000000000..d0636485c --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,45 @@ +// Vue +import Vue from "vue"; +import Vuex from "vuex"; + +// Components +import PermissionsView from "./components/Permissions.vue"; +import CommonComponents from "./components/common/index"; + +// Other +import { loadI18nMessages } from "./store/i18n"; +import { Permissions } from "./store/Permissions"; + +async function init() { + // i18n + Vue.prototype.i18n = await loadI18nMessages(); + + // Load modules + Vue.use(Vuex); + + // Load common components globally + for (const component of CommonComponents) { + Vue.component(component.name, component.component); + } + + // State + const store = new Vuex.Store({ + modules: { + permissions: await new Permissions().getModule(), + }, + }); + + const instance = new Vue({ + render: (h) => h(PermissionsView), + store, + }).$mount("#permissions"); + + // Set title + try { + document.title = instance.i18n.extName; + } catch (e) { + console.error(e); + } +} + +init(); diff --git a/src/store/Permissions.ts b/src/store/Permissions.ts new file mode 100644 index 000000000..142fdf864 --- /dev/null +++ b/src/store/Permissions.ts @@ -0,0 +1,246 @@ +import { Permission } from "../models/permission"; + +const permissions: Permission[] = [ + { + id: "activeTab", + description: chrome.i18n.getMessage("permission_active_tab"), + revocable: false, + }, + { + id: "storage", + description: chrome.i18n.getMessage("permission_storage"), + revocable: false, + }, + { + id: "identity", + description: chrome.i18n.getMessage("permission_identity"), + revocable: false, + }, + { + id: "clipboardWrite", + description: chrome.i18n.getMessage("permission_clipboard_write"), + revocable: true, + }, + { + id: "contextMenus", + description: chrome.i18n.getMessage("permission_context_menus"), + revocable: true, + }, + { + id: "", + description: chrome.i18n.getMessage("permission_all_urls"), + revocable: true, + }, + { + id: "https://www.google.com/*", + description: chrome.i18n.getMessage("permission_sync_clock"), + revocable: true, + }, + { + id: "https://*.dropboxapi.com/*", + description: chrome.i18n.getMessage("permission_dropbox"), + revocable: true, + validation: [ + () => { + if (localStorage.dropboxToken !== undefined) { + return { + valid: false, + message: chrome.i18n.getMessage("permission_dropbox_cannot_revoke"), + }; + } + return { + valid: true, + }; + }, + ], + }, + { + id: "https://www.googleapis.com/*", + description: chrome.i18n.getMessage("permission_drive"), + revocable: true, + validation: [ + () => { + if (localStorage.driveToken !== undefined) { + return { + valid: false, + message: chrome.i18n.getMessage("permission_drive_cannot_revoke"), + }; + } + return { + valid: true, + }; + }, + ], + }, + { + id: "https://accounts.google.com/*", + description: chrome.i18n.getMessage("permission_drive"), + revocable: true, + validation: [ + () => { + if (localStorage.driveToken !== undefined) { + return { + valid: false, + message: chrome.i18n.getMessage("permission_drive_cannot_revoke"), + }; + } + return { + valid: true, + }; + }, + ], + }, + { + id: "https://graph.microsoft.com/*", + description: chrome.i18n.getMessage("permission_onedrive"), + revocable: true, + validation: [ + () => { + if (localStorage.oneDriveToken !== undefined) { + return { + valid: false, + message: chrome.i18n.getMessage( + "permission_onedrive_cannot_revoke" + ), + }; + } + return { + valid: true, + }; + }, + ], + }, + { + id: "https://login.microsoftonline.com/*", + description: chrome.i18n.getMessage("permission_onedrive"), + revocable: true, + validation: [ + () => { + if (localStorage.oneDriveToken !== undefined) { + return { + valid: false, + message: chrome.i18n.getMessage( + "permission_onedrive_cannot_revoke" + ), + }; + } + return { + valid: true, + }; + }, + ], + }, +]; + +export class Permissions implements Module { + async getModule() { + return { + state: { + permissions: await this.getPermissions(), + }, + mutations: { + revokePermission: async ( + state: PermissionsState, + permissionId: string + ) => { + const permissionObject = this.getPermissionById(permissionId); + const validators = permissionObject.validation ?? []; + const validationResults = validators + .map((validator) => validator()) + .filter((result) => !result.valid); + + if (validationResults.length > 0) { + const messages = validationResults.map( + (result) => "• " + result.message + ); + alert(messages.join("\n")); + return; + } + + await this.revokePermission(permissionId); + state.permissions = await this.getPermissions(); + }, + }, + namespaced: true, + }; + } + + private async getPermissions(): Promise { + return new Promise((resolve: (permissions: Permission[]) => void) => { + chrome.permissions.getAll( + (permissions: chrome.permissions.Permissions) => { + const permissionList: Permission[] = []; + + for (const permissionId of permissions.permissions ?? []) { + const permissionObject = this.getPermissionById(permissionId); + + permissionList.push(permissionObject); + } + + for (const permissionId of permissions.origins ?? []) { + const permissionObject = this.getPermissionById(permissionId); + + permissionList.push(permissionObject); + } + + permissionList.sort((a, b) => { + return a.revocable !== b.revocable ? (a.revocable ? 1 : -1) : 0; + }); + + return resolve(permissionList); + } + ); + }); + } + + private getPermissionById(permissionId: string): Permission { + const permissionObject = permissions.find((p) => p.id === permissionId); + + if (permissionObject === undefined) { + return new Permission({ + id: permissionId, + description: chrome.i18n.getMessage("permission_unknown_permission"), + revocable: true, + }); + } + + return permissionObject; + } + + private async revokePermission(permissionId: string): Promise { + return new Promise((resolve: () => void) => { + chrome.permissions.getAll( + (permissions: chrome.permissions.Permissions) => { + for (const _permissionId of permissions.permissions ?? []) { + if (_permissionId === permissionId) { + return chrome.permissions.remove( + { + permissions: [permissionId], + }, + () => { + resolve(); + } + ); + } + } + + for (const _permissionId of permissions.origins ?? []) { + if (_permissionId === permissionId) { + return chrome.permissions.remove( + { + origins: [permissionId], + }, + () => { + resolve(); + } + ); + } + } + } + ); + + // Timeout for remove permissions failed + setTimeout(resolve, 100); + }); + } +} diff --git a/svg/clipboard-check.svg b/svg/clipboard-check.svg new file mode 100644 index 000000000..76d3411eb --- /dev/null +++ b/svg/clipboard-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view/permissions.html b/view/permissions.html new file mode 100644 index 000000000..3c16b4778 --- /dev/null +++ b/view/permissions.html @@ -0,0 +1,15 @@ + + + + + + + + + + +
+ + + + diff --git a/webpack.config.js b/webpack.config.js index 3eade1af3..7a901adf2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ module.exports = { import: "./src/import.ts", options: "./src/options.ts", qrdebug: "./src/qrdebug.ts", + permissions: "./src/permissions.ts", }, // For argon2-browser & mocha node: {