Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

feat: add permissions management #4019

Merged
merged 62 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
121f603
Add infrastructure for permissions management
yagopv Jul 8, 2022
cac1a5b
return permissions when adding
yagopv Jul 11, 2022
f39e790
Merge branch 'dev' of github.com:safe-global/safe-react into feat/add…
yagopv Jul 12, 2022
1753db7
Add reject text
yagopv Jul 13, 2022
5c1d7e1
Use Storage without chain prefix
yagopv Jul 13, 2022
449da58
Improve permissions popup
yagopv Jul 13, 2022
b389078
Remove yalc from package.json
yagopv Jul 14, 2022
c323346
Remove log
yagopv Jul 14, 2022
3d4b51f
Remove log
yagopv Jul 14, 2022
c191a0b
Move permissions hook to a folder
yagopv Jul 15, 2022
84fc01f
Merge branch 'dev' of github.com:safe-global/safe-react into feat/add…
yagopv Jul 15, 2022
4f8aa40
check for permissions here as well
yagopv Jul 15, 2022
2e9618b
Add hook and Modal behavior
yagopv Jul 18, 2022
1e5fc47
Extract some common logic for get the manifest
yagopv Jul 18, 2022
8402a7b
Add 3 status in Permissions
yagopv Jul 18, 2022
82c8282
Update test file
yagopv Jul 18, 2022
fd182f1
Add permission selection to storage
yagopv Jul 19, 2022
0ec19d1
Update tests
yagopv Jul 19, 2022
a8e186f
Add fn to get the allow attr
yagopv Jul 19, 2022
6043fd3
Add permissions to allow attribute
yagopv Jul 19, 2022
24ad1ae
Improve permissions component
yagopv Jul 19, 2022
41506dc
Add route for manage permissions
yagopv Jul 20, 2022
1cec8b6
Adding accordion UI to permissions
yagopv Jul 20, 2022
fcf77e8
Merge branch 'dev' of github.com:safe-global/safe-react into feat/add…
yagopv Jul 20, 2022
b6aae7b
Manage permissions in settings
yagopv Jul 21, 2022
8f3b507
Extract userRestricted constant
yagopv Jul 21, 2022
0fd5f39
Merge branch 'dev' of github.com:safe-global/safe-react into feat/add…
yagopv Jul 21, 2022
046eda6
Only update the storage when receiving changes
yagopv Jul 21, 2022
bba6114
Improve some methods
yagopv Jul 22, 2022
a38a3c1
Renamed handlers in AppFrame
yagopv Jul 22, 2022
bbae8b9
Renamed some methods and types
yagopv Jul 22, 2022
2a9235e
Remove some logs
yagopv Jul 22, 2022
a015e4d
Add some relative paths
yagopv Jul 22, 2022
a05095d
Improve some logic in hooks
yagopv Jul 22, 2022
e5b23e0
Remove unnecessary state
yagopv Jul 22, 2022
c2e7fb9
Renamed some methods
yagopv Jul 22, 2022
5dd731b
Make some common logic hook for store the permissions
yagopv Jul 22, 2022
fa475f2
Refactor confirmPermissionRequest
yagopv Jul 22, 2022
472facc
Add security feedback modal test
yagopv Jul 26, 2022
457b5ae
Remove permissions when app is removed
yagopv Jul 27, 2022
2198730
Add iframe test
yagopv Jul 27, 2022
2a39ec2
Add cancel status to permissions prompt
yagopv Jul 27, 2022
f094bea
Chaging permissions management
yagopv Jul 28, 2022
481c92c
Improve UI in SafeAppsPermissions
yagopv Jul 28, 2022
bf2ac43
Update package.json
yagopv Jul 28, 2022
1b50ed1
Make update permissions function accept changesets
yagopv Jul 28, 2022
1dc9826
Improve texts, components ...
yagopv Jul 28, 2022
948bbf2
Add app names
yagopv Jul 28, 2022
a83f935
Restore open zelelin contract change
yagopv Jul 28, 2022
8604085
Update test
yagopv Jul 28, 2022
119f59a
Merge branch 'dev' of github.com:safe-global/safe-react into feat/add…
yagopv Aug 16, 2022
f6e9c87
Some minor changes
yagopv Aug 16, 2022
b56a04e
Add param instead using hook
yagopv Aug 16, 2022
2c5f34d
Renaming allowList to allowedFeaturesList
yagopv Aug 16, 2022
389d7a1
Remove FETCH_STATUS usage
yagopv Aug 16, 2022
b60277c
Use var in SafeAppLandingPage instead obtaining isSafeAppInTheDefault…
yagopv Aug 16, 2022
7a3028b
Change hasAnyKey param type for avoid null check
yagopv Aug 17, 2022
7b9daaf
Remove unnecesary anonymous fns
yagopv Aug 17, 2022
81e7ce7
Improve domains calc in SafeAppsPermissions
yagopv Aug 17, 2022
1cfbb78
Remove console.log
yagopv Aug 17, 2022
13ce92e
fix issue deleting last app
yagopv Aug 23, 2022
6eb76cf
Allow to remove completely the app from the stored permissions
yagopv Aug 24, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
},
"dependencies": {
"@ethersproject/hash": "^5.5.0",
"@gnosis.pm/safe-apps-sdk": "7.3.0",
"@gnosis.pm/safe-apps-sdk": "7.6.0",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/[email protected]",
"@gnosis.pm/safe-core-sdk": "^2.0.0",
"@gnosis.pm/safe-deployments": "^1.15.0",
Expand Down
5 changes: 5 additions & 0 deletions src/components/AppLayout/Sidebar/useSidebarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ const useSidebarItems = (): ListItemType[] => {
iconType: 'settingsTool',
href: currentSafeRoutes.SETTINGS_ADVANCED,
}),
makeEntryItem({
label: 'Safe Apps Permissions',
iconType: 'info',
href: currentSafeRoutes.SETTINGS_SAFE_APPS_PERMISSIONS,
}),
].filter(Boolean)

return [
Expand Down
15 changes: 13 additions & 2 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,26 @@ interface GnoModalProps {
open: boolean
paperClassName?: string
title: string
style?: React.CSSProperties
}

const GnoModal = ({ children, description, handleClose, open, paperClassName, title }: GnoModalProps): ReactElement => {
const GnoModal = ({
children,
description,
handleClose,
open,
paperClassName,
title,
style,
}: GnoModalProps): ReactElement => {
return (
<ModalStyled
BackdropProps={{ className: 'overlay' }}
aria-describedby={description}
aria-labelledby={title}
onClose={handleClose}
open={open}
style={style}
>
<div className={cn('paper', paperClassName)}>{children}</div>
</ModalStyled>
Expand Down Expand Up @@ -216,7 +226,7 @@ const Buttons = ({ cancelButtonProps = {}, confirmButtonProps = {} }: ButtonsPro
<Button
size="md"
color="primary"
variant="outlined"
variant={cancelButtonProps.variant || 'outlined'}
type={cancelOnClick ? 'button' : 'submit'}
disabled={cancelDisabled || [ButtonStatus.DISABLED, ButtonStatus.LOADING].includes(cancelStatus)}
data-testid={cancelTestId}
Expand Down Expand Up @@ -278,6 +288,7 @@ interface ModalProps {
handleClose: () => void
open?: boolean
title?: string
style?: React.CSSProperties
}

export const Modal = ({ children, description = '', open = true, title = '', ...props }: ModalProps): ReactElement => {
Expand Down
2 changes: 2 additions & 0 deletions src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('<SafeAppLandingPage>', () => {
type: safeAppsGatewaySDK.SafeAppAccessPolicyTypes.NoRestrictions,
},
tags: [],
safeAppsPermissions: [],
},
]),
)
Expand All @@ -59,6 +60,7 @@ describe('<SafeAppLandingPage>', () => {
type: safeAppsGatewaySDK.SafeAppAccessPolicyTypes.NoRestrictions,
},
tags: [],
safeAppsPermissions: [],
}),
)

Expand Down
15 changes: 11 additions & 4 deletions src/routes/SafeAppLandingPage/components/SafeAppsDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactElement, SyntheticEvent } from 'react'
import { ReactElement, SyntheticEvent, useMemo } from 'react'
import styled from 'styled-components'
import Divider from '@material-ui/core/Divider'
import { Title, Text } from '@gnosis.pm/safe-react-components'
Expand All @@ -7,8 +7,9 @@ import { getChainById } from 'src/config'
import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel'
import { black300 } from 'src/theme/variables'
import fallbackSafeAppLogoSvg from 'src/assets/icons/apps.svg'
import { useSecurityFeedbackModal } from 'src/routes/safe/components/Apps/hooks/useSecurityFeedbackModal'
import UnknownAppWarning from 'src/routes/safe/components/Apps/components/SecurityFeedbackModal/UnknownAppWarning'
import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl'
import { useAppList } from 'src/routes/safe/components/Apps/hooks/appList/useAppList'

type SafeAppDetailsTypes = {
iconUrl: string
Expand All @@ -19,7 +20,13 @@ type SafeAppDetailsTypes = {

const SafeAppDetails = ({ iconUrl, name, description, availableChains }: SafeAppDetailsTypes): ReactElement => {
const showAvailableChains = availableChains?.length > 0
const { isModalVisible: isLoaded, isSafeAppInDefaultList } = useSecurityFeedbackModal()
const { isLoading: isSafeAppListLoading, getSafeApp } = useAppList()
const { getAppUrl } = useSafeAppUrl()
const url = getAppUrl()
mmv08 marked this conversation as resolved.
Show resolved Hide resolved
mmv08 marked this conversation as resolved.
Show resolved Hide resolved

const isSafeAppInDefaultList = useMemo(() => {
mmv08 marked this conversation as resolved.
Show resolved Hide resolved
return !!getSafeApp(url)
}, [getSafeApp, url])

return (
<>
Expand Down Expand Up @@ -51,7 +58,7 @@ const SafeAppDetails = ({ iconUrl, name, description, availableChains }: SafeApp
<Separator />
</>
)}
{isLoaded && !isSafeAppInDefaultList && (
{!isSafeAppListLoading && !isSafeAppInDefaultList && (
<>
<UnknownAppWarning />
<Separator />
Expand Down
1 change: 1 addition & 0 deletions src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const SAFE_ROUTES = {
SETTINGS_POLICIES: `${ADDRESSED_ROUTE}/settings/policies`,
SETTINGS_SPENDING_LIMIT: `${ADDRESSED_ROUTE}/settings/spending-limit`,
SETTINGS_ADVANCED: `${ADDRESSED_ROUTE}/settings/advanced`,
SETTINGS_SAFE_APPS_PERMISSIONS: `${ADDRESSED_ROUTE}/settings/safe-apps-permissions`,
}

export const getNetworkRootRoutes = (): Array<{ chainId: ChainId; route: string; shortName: string }> =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('rankTrackedSafeApps', () => {
},
fetchStatus: FETCH_STATUS.SUCCESS,
tags: [],
safeAppsPermissions: [],
},
]

Expand Down
1 change: 1 addition & 0 deletions src/routes/safe/components/Apps/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('SafeApp manifest', () => {
},
],
providedBy: 'test',
safeAppsPermissions: [],
}

const result = isAppManifestValid(manifest)
Expand Down
33 changes: 33 additions & 0 deletions src/routes/safe/components/Apps/components/AppFrame.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { render, screen, fireEvent, within, act, waitFor } from 'src/utils/test-utils'
import AppFrame from './AppFrame'
import axios from 'axios'
import * as safeAppsUtils from 'src/routes/safe/components/Apps/utils'

jest.mock('axios')

describe('Safe Apps -> AppFrame', () => {
beforeEach(() => {
// @ts-ignore
safeAppsUtils.canLoadAppImage = jest.fn().mockResolvedValue(true)
// @ts-ignore
axios.get.mockImplementation((url: string) => {
console.log(url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log sneaked in

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!. Yep, i will wait to Dani's to review it

return Promise.resolve({
data: {
name: 'Safe Test App',
description: 'Safe Test App description',
iconPath: '/image-path.svg',
},
})
})
})

it('should render the correct allow attribute', async () => {
render(<AppFrame appUrl="http://test.eth" allowList="camera; microphone" />)

const iframeElement = await screen.findByTitle('Safe Test App')

expect(iframeElement).toBeInTheDocument()
expect(iframeElement).toHaveAttribute('allow', 'camera; microphone')
})
})
81 changes: 63 additions & 18 deletions src/routes/safe/components/Apps/components/AppFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@gnosis.pm/safe-apps-sdk'
import { useSelector } from 'react-redux'
import { INTERFACE_MESSAGES, Transaction, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
import { PermissionRequest } from '@gnosis.pm/safe-apps-sdk/dist/src/types/permissions'
import Web3 from 'web3'

import { currentSafe } from 'src/logic/safe/store/selectors'
Expand All @@ -22,23 +23,26 @@ import { SAFE_POLLING_INTERVAL } from 'src/utils/constants'
import { ConfirmTxModal } from './ConfirmTxModal'
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
import { LegacyMethods, useAppCommunicator } from '../communicator'
import { SafeApp } from '../types'
import { EMPTY_SAFE_APP, getAppInfoFromUrl, getEmptySafeApp, getLegacyChainName } from '../utils'
import { PermissionStatus, SafeApp } from '../types'
import { EMPTY_SAFE_APP, getLegacyChainName } from '../utils'
import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { fetchSafeTransaction } from 'src/logic/safe/transactions/api/fetchSafeTransaction'
import { logError, Errors } from 'src/logic/exceptions/CodedException'
import { addressBookEntryName } from 'src/logic/addressBook/store/selectors'
import { useSignMessageModal } from '../hooks/useSignMessageModal'
import { SignMessageModal } from './SignMessageModal'
import { web3HttpProviderOptions } from 'src/logic/wallets/getWeb3'
import { useThirdPartyCookies } from '../hooks/useThirdPartyCookies'
import { ThirdPartyCookiesWarning } from './ThirdPartyCookiesWarning'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { currentNetworkAddressBook } from 'src/logic/addressBook/store/selectors'
import { SAFE_APPS_EVENTS } from 'src/utils/events/safeApps'
import { trackEvent } from 'src/utils/googleTagManager'
import { checksumAddress } from 'src/utils/checksumAddress'
import { useRemoteSafeApps } from 'src/routes/safe/components/Apps/hooks/appList/useRemoteSafeApps'
import { trackSafeAppOpenCount } from 'src/routes/safe/components/Apps/trackAppUsageCount'
import PermissionsPrompt from 'src/routes/safe/components/Apps/components/PermissionsPrompt'
import { useSafeAppFromManifest } from 'src/routes/safe/components/Apps/hooks/useSafeAppFromManifest'
import { useSafePermissions } from 'src/routes/safe/components/Apps/hooks/permissions'

const AppWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -74,6 +78,7 @@ type ConfirmTransactionModalState = {

type Props = {
appUrl: string
allowList: string
}

const APP_LOAD_ERROR_TIMEOUT = 30000
Expand All @@ -88,16 +93,16 @@ const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = {
const URL_NOT_PROVIDED_ERROR = 'App url No provided or it is invalid.'
const APP_LOAD_ERROR = 'There was an error loading the Safe App. There might be a problem with the App provider.'

const AppFrame = ({ appUrl }: Props): ReactElement => {
const AppFrame = ({ appUrl, allowList }: Props): ReactElement => {
const { address: safeAddress, ethBalance, owners, threshold } = useSelector(currentSafe)
const { nativeCurrency, chainId, chainName, shortName, blockExplorerUriTemplate } = getChainInfo()
const safeName = useSelector((state) => addressBookEntryName(state, { address: safeAddress }))
const granted = useSelector(grantedSelector)
const addressBook = useSelector(currentNetworkAddressBook)
const iframeRef = useRef<HTMLIFrameElement>(null)
const [confirmTransactionModal, setConfirmTransactionModal] =
useState<ConfirmTransactionModalState>(INITIAL_CONFIRM_TX_MODAL_STATE)
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
const [safeApp, setSafeApp] = useState<SafeApp>(() => getEmptySafeApp(appUrl))
const [signMessageModalState, openSignMessageModal, closeSignMessageModal] = useSignMessageModal()
const timer = useRef<number>()
const [isLoadingSlow, setIsLoadingSlow] = useState<boolean>(false)
Expand All @@ -106,12 +111,14 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
const { thirdPartyCookiesDisabled, setThirdPartyCookiesDisabled } = useThirdPartyCookies()
const { remoteSafeApps } = useRemoteSafeApps()
const currentApp = remoteSafeApps.filter((app) => app.url === appUrl)[0]

const { confirmPermissionRequest, permissionsRequest, setPermissionsRequest, getPermissions, hasPermission } =
useSafePermissions()
const safeAppsRpc = getSafeAppsRpcServiceUrl()
const safeAppWeb3Provider = useMemo(
() => new Web3.providers.HttpProvider(safeAppsRpc, web3HttpProviderOptions),
[safeAppsRpc],
)
const { safeApp } = useSafeAppFromManifest(appUrl)

useEffect(() => {
const clearTimeouts = () => {
Expand Down Expand Up @@ -225,6 +232,26 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
return balances
})

communicator?.on(Methods.wallet_getPermissions, (msg) => {
return getPermissions(msg.origin)
})

communicator?.on(Methods.wallet_requestPermissions, async (msg) => {
setPermissionsRequest({
origin: msg.origin,
request: msg.data.params as PermissionRequest[],
requestId: msg.data.id,
})
})

communicator?.on(Methods.requestAddressBook, async (msg) => {
if (hasPermission(msg.origin, Methods.requestAddressBook)) {
return addressBook
}

return []
})

communicator?.on(Methods.rpcCall, async (msg) => {
const params = msg.data.params as RPCPayload

Expand Down Expand Up @@ -292,6 +319,10 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
safeAppWeb3Provider,
granted,
blockExplorerUriTemplate,
addressBook,
getPermissions,
setPermissionsRequest,
hasPermission,
])

const onUserTxConfirm = (safeTxHash: string, requestId: RequestId) => {
Expand Down Expand Up @@ -320,21 +351,24 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
trackEvent({ ...SAFE_APPS_EVENTS.TRANSACTION_REJECTED, label: safeApp.name })
}

const onAcceptPermissionRequest = (origin: string, requestId: RequestId) => {
const permissions = confirmPermissionRequest(PermissionStatus.GRANTED)
communicator?.send(permissions, requestId as string)
}

const onRejectPermissionRequest = (requestId: RequestId) => {
if (requestId) {
confirmPermissionRequest(PermissionStatus.DENIED)
communicator?.send('Permissions were rejected', requestId as string, true)
} else {
setPermissionsRequest(undefined)
}
}

useEffect(() => {
if (!appUrl) {
throw Error(URL_NOT_PROVIDED_ERROR)
}

const loadApp = async () => {
try {
const app = await getAppInfoFromUrl(appUrl, false)
setSafeApp(app)
} catch (err) {
logError(Errors._900, `${appUrl}, ${err.message}`)
}
}

loadApp()
}, [appUrl])

useEffect(() => {
Expand Down Expand Up @@ -362,7 +396,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
src={appUrl}
title={safeApp.name}
onLoad={onIframeLoad}
allow="camera"
allow={allowList}
/>
</StyledCard>

Expand Down Expand Up @@ -393,6 +427,17 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
onUserConfirm={onUserTxConfirm}
onTxReject={onTxReject}
/>

{permissionsRequest && (
<PermissionsPrompt
isOpen
origin={permissionsRequest.origin}
requestId={permissionsRequest.requestId}
onAccept={onAcceptPermissionRequest}
onReject={onRejectPermissionRequest}
permissions={permissionsRequest.request}
/>
)}
</AppWrapper>
)
}
Expand Down
Loading