From 722fc4bf1a4f12a891f1a1ef3592d7755d08cb0d Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Thu, 17 Dec 2020 14:02:53 +0100 Subject: [PATCH] Website access management (#589) * list websites * persist in local storage * with filter and styling * cleanup * styling for long list * fix icon alignment and lint * Apply suggestions from code review Co-authored-by: Jaco Greeff * add return ( ) * lint Co-authored-by: Jaco Greeff --- .../src/background/handlers/Extension.ts | 16 +++- .../src/background/handlers/State.ts | 36 +++++++- .../extension-base/src/background/types.ts | 7 ++ .../src/Popup/AuthManagement/WebsiteEntry.tsx | 58 ++++++++++++ .../src/Popup/AuthManagement/index.tsx | 88 +++++++++++++++++++ packages/extension-ui/src/Popup/index.tsx | 2 + .../src/components/InputFilter.tsx | 40 +++++++++ packages/extension-ui/src/components/index.ts | 1 + packages/extension-ui/src/messaging.ts | 10 ++- .../src/partials/MenuSettings.tsx | 23 ++++- 10 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 packages/extension-ui/src/Popup/AuthManagement/WebsiteEntry.tsx create mode 100644 packages/extension-ui/src/Popup/AuthManagement/index.tsx create mode 100644 packages/extension-ui/src/components/InputFilter.tsx diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 3b651ba0..e8f1de09 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -4,7 +4,7 @@ import type { MetadataDef } from '@polkadot/extension-inject/types'; import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types'; import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types'; +import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types'; import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults'; import chrome from '@polkadot/extension-inject/chrome'; @@ -167,6 +167,10 @@ export default class Extension { return true; } + private getAuthList (): ResponseAuthorizeList { + return { list: this.#state.authUrls }; + } + private authorizeReject ({ id }: RequestAuthorizeReject): boolean { const queued = this.#state.getAuthRequest(id); @@ -447,6 +451,10 @@ export default class Extension { return true; } + private toggleAuthorization (url: string):ResponseAuthorizeList { + return { list: this.#state.toggleAuthorization(url) }; + } + // Weird thought, the eslint override is not needed in Tabs // eslint-disable-next-line @typescript-eslint/require-await public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise> { @@ -454,9 +462,15 @@ export default class Extension { case 'pri(authorize.approve)': return this.authorizeApprove(request as RequestAuthorizeApprove); + case 'pri(authorize.list)': + return this.getAuthList(); + case 'pri(authorize.reject)': return this.authorizeReject(request as RequestAuthorizeReject); + case 'pri(authorize.toggle)': + return this.toggleAuthorization(request as string); + case 'pri(authorize.requests)': return this.authorizeSubscribe(id, port); diff --git a/packages/extension-base/src/background/handlers/State.ts b/packages/extension-base/src/background/handlers/State.ts index f7897fe0..a7d1750f 100644 --- a/packages/extension-base/src/background/handlers/State.ts +++ b/packages/extension-base/src/background/handlers/State.ts @@ -25,13 +25,15 @@ interface AuthRequest extends Resolver { url: string; } -type AuthUrls = Record; + +export interface AuthUrlInfo { count: number; id: string; isAllowed: boolean; origin: string; url: string; -}>; +} interface MetaRequest extends Resolver { id: string; @@ -68,6 +70,8 @@ const WINDOW_OPTS = { width: 560 }; +const AUTH_URLS_KEY = 'authUrls'; + function getId (): string { return `${Date.now()}.${++idCounter}`; } @@ -103,6 +107,12 @@ export default class State { this.#metaStore.all((_key: string, def: MetadataDef): void => { addMetadata(def); }); + + // retrieve previously set authorizations + const authString = localStorage.getItem(AUTH_URLS_KEY) || '{}'; + const previousAuth = JSON.parse(authString) as AuthUrls; + + this.#authUrls = previousAuth; } public get knownMetadata (): MetadataDef[] { @@ -139,6 +149,10 @@ export default class State { .map(({ account, id, request, url }): SigningRequest => ({ account, id, request, url })); } + public get authUrls (): AuthUrls { + return this.#authUrls; + } + private popupClose (): void { this.#windows.forEach((id: number): void => chrome.windows.remove(id) @@ -167,6 +181,7 @@ export default class State { url }; + this.saveCurrentAuthList(); delete this.#authRequests[id]; this.updateIconAuth(true); }; @@ -183,6 +198,10 @@ export default class State { }; } + private saveCurrentAuthList () { + localStorage.setItem(AUTH_URLS_KEY, JSON.stringify(this.#authUrls)); + } + private metaComplete = (id: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver => { const complete = (): void => { delete this.#metaRequests[id]; @@ -246,6 +265,18 @@ export default class State { } } + public toggleAuthorization (url: string): AuthUrls { + const entry = this.#authUrls[url]; + + assert(entry, `The source ${url} is not known`); + + const newAllowState = !entry.isAllowed; + + this.#authUrls[url].isAllowed = newAllowState; + + return this.#authUrls; + } + private updateIconAuth (shouldClose?: boolean): void { this.authSubject.next(this.allAuthRequests); this.updateIcon(shouldClose); @@ -265,6 +296,7 @@ export default class State { const idStr = this.stripUrl(url); if (this.#authUrls[idStr]) { + // this url was seen in the past assert(this.#authUrls[idStr].isAllowed, `The source ${url} is not allowed to interact with this extension`); return false; diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 8f517443..de12c50d 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -12,6 +12,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types'; import { TypeRegistry } from '@polkadot/types'; import { ALLOWED_PATH } from '../defaults'; +import { AuthUrls } from './handlers/State'; type KeysWithDefinedValues = { [K in keyof T]: T[K] extends undefined ? never : K @@ -81,8 +82,10 @@ export interface RequestSignatures { 'pri(accounts.validate)': [RequestAccountValidate, boolean]; 'pri(accounts.changePassword)': [RequestAccountChangePassword, boolean]; 'pri(authorize.approve)': [RequestAuthorizeApprove, boolean]; + 'pri(authorize.list)': [null, ResponseAuthorizeList]; 'pri(authorize.reject)': [RequestAuthorizeReject, boolean]; 'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]]; + 'pri(authorize.toggle)': [string, ResponseAuthorizeList]; 'pri(derivation.create)': [RequestDeriveCreate, boolean]; 'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate]; 'pri(json.restore)': [RequestJsonRestore, void]; @@ -365,3 +368,7 @@ export interface ResponseJsonGetAccountInfo { name: string; genesisHash: string; } + +export interface ResponseAuthorizeList { + list: AuthUrls; +} diff --git a/packages/extension-ui/src/Popup/AuthManagement/WebsiteEntry.tsx b/packages/extension-ui/src/Popup/AuthManagement/WebsiteEntry.tsx new file mode 100644 index 00000000..ed987cd7 --- /dev/null +++ b/packages/extension-ui/src/Popup/AuthManagement/WebsiteEntry.tsx @@ -0,0 +1,58 @@ +// Copyright 2019-2020 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ThemeProps } from '../../types'; + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { AuthUrlInfo } from '@polkadot/extension-base/background/handlers/State'; +import { Switch } from '@polkadot/extension-ui/components'; + +import useTranslation from '../../hooks/useTranslation'; + +interface Props extends ThemeProps { + className?: string; + info: AuthUrlInfo; + toggleAuth: (url: string) => void + url: string; +} + +function WebsiteEntry ({ className = '', info, toggleAuth, url }: Props): React.ReactElement { + const { t } = useTranslation(); + + const switchAccess = useCallback(() => { + toggleAuth(url); + }, [toggleAuth, url]); + + return ( +
+
+ {url} +
+ ('allowed')} + className='info' + onChange={switchAccess} + uncheckedLabel={t('denied')} + /> +
+ ); +} + +export default styled(WebsiteEntry)` + display: flex; + align-items: center; + + .url{ + flex: 1; + } + + &.denied { + .slider::before { + opacity: 0.4; + background-color: rgb(36, 37, 41); + } + } +`; diff --git a/packages/extension-ui/src/Popup/AuthManagement/index.tsx b/packages/extension-ui/src/Popup/AuthManagement/index.tsx new file mode 100644 index 00000000..79524c97 --- /dev/null +++ b/packages/extension-ui/src/Popup/AuthManagement/index.tsx @@ -0,0 +1,88 @@ +// Copyright 2019-2020 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ThemeProps } from '../../types'; + +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { AuthUrlInfo, AuthUrls } from '@polkadot/extension-base/background/handlers/State'; +import { InputFilter } from '@polkadot/extension-ui/components'; + +import useTranslation from '../../hooks/useTranslation'; +import { getAuthList, toggleAuthorization } from '../../messaging'; +import { Header } from '../../partials'; +import WebsiteEntry from './WebsiteEntry'; + +interface Props extends ThemeProps { + className?: string; +} + +function AuthManagement ({ className }: Props): React.ReactElement { + const { t } = useTranslation(); + const [authList, setAuthList] = useState(null); + const [filter, setFilter] = useState(''); + + useEffect(() => { + getAuthList() + .then(({ list }) => setAuthList(list)) + .catch((e) => console.error(e)); + }, []); + + const _onChangeFilter = useCallback((filter: string) => { + setFilter(filter); + }, []); + + const toggleAuth = useCallback((url: string) => { + toggleAuthorization(url) + .then(({ list }) => setAuthList(list)) + .catch(console.error); + }, []); + + return ( + <> +
('Manage Website Access')} + /> + <> + ('example.com')} + value={filter} + /> +
+ { + !authList || !Object.entries(authList)?.length + ?
{t('No website request yet!')}
+ : <> +
+ {Object.entries(authList) + .filter(([url]:[string, AuthUrlInfo]) => url.includes(filter)) + .map( + ([url, info]: [string, AuthUrlInfo]) => + + )} +
+ + } +
+ + + ); +} + +export default styled(AuthManagement)` + height: 100%; + overflow-y: auto; + + .empty-list { + text-align: center; + } +`; diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index b46bc49b..79920458 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -16,6 +16,7 @@ import ToastProvider from '../components/Toast/ToastProvider'; import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging'; import { buildHierarchy } from '../util/buildHierarchy'; import Accounts from './Accounts'; +import AuthList from './AuthManagement'; import Authorize from './Authorize'; import CreateAccount from './CreateAccount'; import Derive from './Derive'; @@ -129,6 +130,7 @@ export default function Popup (): React.ReactElement { + {wrapWithErrorBoundary(, 'auth-list')} {wrapWithErrorBoundary(, 'account-creation')} {wrapWithErrorBoundary(, 'forget-address')} {wrapWithErrorBoundary(, 'export-address')} diff --git a/packages/extension-ui/src/components/InputFilter.tsx b/packages/extension-ui/src/components/InputFilter.tsx new file mode 100644 index 00000000..508e8342 --- /dev/null +++ b/packages/extension-ui/src/components/InputFilter.tsx @@ -0,0 +1,40 @@ +// Copyright 2017-2020 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { Input } from './TextInputs'; + +interface Props { + className?: string; + onChange: (filter: string) => void; + placeholder: string; + value: string; +} + +function InputFilter ({ className, onChange, placeholder, value } : Props) { + const onChangeFilter = useCallback((event: React.ChangeEvent) => { + onChange(event.target.value); + }, [onChange]); + + return ( +
+ +
+ ); +} + +export default styled(InputFilter)` + padding-left: 1rem !important; + padding-right: 1rem !important; +`; diff --git a/packages/extension-ui/src/components/index.ts b/packages/extension-ui/src/components/index.ts index 38367b24..921dc9f2 100644 --- a/packages/extension-ui/src/components/index.ts +++ b/packages/extension-ui/src/components/index.ts @@ -14,6 +14,7 @@ export { default as Dropdown } from './Dropdown'; export { default as ErrorBoundary } from './ErrorBoundary'; export { default as Icon } from './Icon'; export { default as Identicon } from './Identicon'; +export { default as InputFilter } from './InputFilter'; export { default as InputWithLabel } from './InputWithLabel'; export { default as InputFileWithLabel } from './InputFileWithLabel'; export { default as Label } from './Label'; diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index 9ed94d0a..56a5eace 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -1,7 +1,7 @@ // Copyright 2019-2020 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, SeedLengths, SigningRequest, SubscriptionMessageTypes } from '@polkadot/extension-base/background/types'; +import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, MetadataRequest, RequestTypes, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSigningIsLocked, ResponseTypes, SeedLengths, SigningRequest, SubscriptionMessageTypes } from '@polkadot/extension-base/background/types'; import type { Message } from '@polkadot/extension-base/types'; import type { Chain } from '@polkadot/extension-chains/types'; import type { KeyringPair$Json } from '@polkadot/keyring/types'; @@ -174,6 +174,14 @@ export async function subscribeAuthorizeRequests (cb: (accounts: AuthorizeReques return sendMessage('pri(authorize.requests)', null, cb); } +export async function getAuthList (): Promise { + return sendMessage('pri(authorize.list)'); +} + +export async function toggleAuthorization (url: string): Promise { + return sendMessage('pri(authorize.toggle)', url); +} + export async function subscribeMetadataRequests (cb: (accounts: MetadataRequest[]) => void): Promise { return sendMessage('pri(metadata.requests)', null, cb); } diff --git a/packages/extension-ui/src/partials/MenuSettings.tsx b/packages/extension-ui/src/partials/MenuSettings.tsx index c088fcf1..f4fb7042 100644 --- a/packages/extension-ui/src/partials/MenuSettings.tsx +++ b/packages/extension-ui/src/partials/MenuSettings.tsx @@ -3,13 +3,13 @@ import type { Theme, ThemeProps } from '../types'; -import { faExpand } from '@fortawesome/free-solid-svg-icons'; +import { faExpand, faTasks } from '@fortawesome/free-solid-svg-icons'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import styled, { ThemeContext } from 'styled-components'; import settings from '@polkadot/ui-settings'; -import { ActionText, Checkbox, Dropdown, Menu, MenuDivider, MenuItem, Svg, Switch, themes, ThemeSwitchContext } from '../components'; +import { ActionContext, ActionText, Checkbox, Dropdown, Menu, MenuDivider, MenuItem, Svg, Switch, themes, ThemeSwitchContext } from '../components'; import useIsPopup from '../hooks/useIsPopup'; import useTranslation from '../hooks/useTranslation'; import { windowOpen } from '../messaging'; @@ -37,6 +37,7 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement getLanguageOptions(), []); + const onAction = useContext(ActionContext); useEffect(() => { settings.set({ camera: camera ? 'on' : 'off' }); @@ -67,6 +68,12 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement { + onAction('auth-list'); + }, [onAction] + ); + return ( + + ('Manage website access')} + /> + + ('External QR accounts and Access')} @@ -143,12 +159,13 @@ export default React.memo(styled(MenuSettings)(({ theme }: Props) => ` right: 24px; user-select: none; - .openWindow { + .openWindow, .manageWebsiteAccess{ span { color: ${theme.textColor}; font-size: ${theme.fontSize}; line-height: ${theme.lineHeight}; text-decoration: none; + vertical-align: middle; } ${Svg} {