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 @@
+
+
+
Permissions
+
+
+
+
+
+
{{ permission.id }}
+
{{ permission.description }}
+
{{ i18n.permission_required }}
+
+
+
+
+
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 @@
@@ -34,11 +41,6 @@
{{ i18n.resize_popup_page }}
-
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: {