mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Add permissions controller unit tests (#7969)
* add permissions controller, log, middleware, and restricted method unit tests * fix permissions-related bugs * convert permissions log to controller-like class * add permissions unit test coverage requirements * update rpc-cap Co-Authored-By: Whymarrh Whitby <whymarrh.whitby@gmail.com> Co-Authored-By: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
45efbbecb7
commit
b1d090ac4d
@ -19,6 +19,13 @@ export const LOG_IGNORE_METHODS = [
|
|||||||
'wallet_sendDomainMetadata',
|
'wallet_sendDomainMetadata',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const LOG_METHOD_TYPES = {
|
||||||
|
restricted: 'restricted',
|
||||||
|
internal: 'internal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LOG_LIMIT = 100
|
||||||
|
|
||||||
export const SAFE_METHODS = [
|
export const SAFE_METHODS = [
|
||||||
'web3_sha3',
|
'web3_sha3',
|
||||||
'net_listening',
|
'net_listening',
|
||||||
|
@ -4,8 +4,8 @@ import ObservableStore from 'obs-store'
|
|||||||
import log from 'loglevel'
|
import log from 'loglevel'
|
||||||
import { CapabilitiesController as RpcCap } from 'rpc-cap'
|
import { CapabilitiesController as RpcCap } from 'rpc-cap'
|
||||||
import { ethErrors } from 'eth-json-rpc-errors'
|
import { ethErrors } from 'eth-json-rpc-errors'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
import getRestrictedMethods from './restrictedMethods'
|
|
||||||
import createMethodMiddleware from './methodMiddleware'
|
import createMethodMiddleware from './methodMiddleware'
|
||||||
import PermissionsLogController from './permissionsLog'
|
import PermissionsLogController from './permissionsLog'
|
||||||
|
|
||||||
@ -24,7 +24,8 @@ export class PermissionsController {
|
|||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
{
|
{
|
||||||
platform, notifyDomain, notifyAllDomains, getKeyringAccounts,
|
platform, notifyDomain, notifyAllDomains,
|
||||||
|
getKeyringAccounts, getRestrictedMethods,
|
||||||
} = {},
|
} = {},
|
||||||
restoredPermissions = {},
|
restoredPermissions = {},
|
||||||
restoredState = {}) {
|
restoredState = {}) {
|
||||||
@ -40,7 +41,7 @@ export class PermissionsController {
|
|||||||
this.getKeyringAccounts = getKeyringAccounts
|
this.getKeyringAccounts = getKeyringAccounts
|
||||||
this._platform = platform
|
this._platform = platform
|
||||||
this._restrictedMethods = getRestrictedMethods(this)
|
this._restrictedMethods = getRestrictedMethods(this)
|
||||||
this.permissionsLogController = new PermissionsLogController({
|
this.permissionsLog = new PermissionsLogController({
|
||||||
restrictedMethods: Object.keys(this._restrictedMethods),
|
restrictedMethods: Object.keys(this._restrictedMethods),
|
||||||
store: this.store,
|
store: this.store,
|
||||||
})
|
})
|
||||||
@ -51,6 +52,10 @@ export class PermissionsController {
|
|||||||
|
|
||||||
createMiddleware ({ origin, extensionId }) {
|
createMiddleware ({ origin, extensionId }) {
|
||||||
|
|
||||||
|
if (typeof origin !== 'string' || !origin.length) {
|
||||||
|
throw new Error('Must provide non-empty string origin.')
|
||||||
|
}
|
||||||
|
|
||||||
if (extensionId) {
|
if (extensionId) {
|
||||||
this.store.updateState({
|
this.store.updateState({
|
||||||
[METADATA_STORE_KEY]: {
|
[METADATA_STORE_KEY]: {
|
||||||
@ -62,7 +67,7 @@ export class PermissionsController {
|
|||||||
|
|
||||||
const engine = new JsonRpcEngine()
|
const engine = new JsonRpcEngine()
|
||||||
|
|
||||||
engine.push(this.permissionsLogController.createMiddleware())
|
engine.push(this.permissionsLog.createMiddleware())
|
||||||
|
|
||||||
engine.push(createMethodMiddleware({
|
engine.push(createMethodMiddleware({
|
||||||
store: this.store,
|
store: this.store,
|
||||||
@ -76,6 +81,7 @@ export class PermissionsController {
|
|||||||
engine.push(this.permissions.providerMiddlewareFunction.bind(
|
engine.push(this.permissions.providerMiddlewareFunction.bind(
|
||||||
this.permissions, { origin }
|
this.permissions, { origin }
|
||||||
))
|
))
|
||||||
|
|
||||||
return asMiddleware(engine)
|
return asMiddleware(engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,15 +120,18 @@ export class PermissionsController {
|
|||||||
_requestPermissions (origin, permissions) {
|
_requestPermissions (origin, permissions) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
// rpc-cap assigns an id to the request if there is none, as expected by
|
||||||
|
// requestUserApproval below
|
||||||
const req = { method: 'wallet_requestPermissions', params: [permissions] }
|
const req = { method: 'wallet_requestPermissions', params: [permissions] }
|
||||||
const res = {}
|
const res = {}
|
||||||
this.permissions.providerMiddlewareFunction(
|
this.permissions.providerMiddlewareFunction(
|
||||||
{ origin }, req, res, () => {}, _end
|
{ origin }, req, res, () => {}, _end
|
||||||
)
|
)
|
||||||
|
|
||||||
function _end (err) {
|
function _end (_err) {
|
||||||
if (err || res.error) {
|
const err = _err || res.error
|
||||||
reject(err || res.error)
|
if (err) {
|
||||||
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve(res.result)
|
resolve(res.result)
|
||||||
}
|
}
|
||||||
@ -131,9 +140,11 @@ export class PermissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User approval callback. The request can fail if the request is invalid.
|
* User approval callback. Resolves the Promise for the permissions request
|
||||||
|
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
||||||
|
* The request will be rejected if finalizePermissionsRequest fails.
|
||||||
*
|
*
|
||||||
* @param {Object} approved - the approved request object
|
* @param {Object} approved - The request object approved by the user
|
||||||
* @param {Array} accounts - The accounts to expose, if any
|
* @param {Array} accounts - The accounts to expose, if any
|
||||||
*/
|
*/
|
||||||
async approvePermissionsRequest (approved, accounts) {
|
async approvePermissionsRequest (approved, accounts) {
|
||||||
@ -142,16 +153,27 @@ export class PermissionsController {
|
|||||||
const approval = this.pendingApprovals.get(id)
|
const approval = this.pendingApprovals.get(id)
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
log.warn(`Permissions request with id '${id}' not found`)
|
log.error(`Permissions request with id '${id}' not found`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// attempt to finalize the request and resolve it
|
if (Object.keys(approved.permissions).length === 0) {
|
||||||
await this.finalizePermissionsRequest(approved.permissions, accounts)
|
|
||||||
approval.resolve(approved.permissions)
|
|
||||||
|
|
||||||
|
approval.reject(ethErrors.rpc.invalidRequest({
|
||||||
|
message: 'Must request at least one permission.',
|
||||||
|
}))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// attempt to finalize the request and resolve it,
|
||||||
|
// settings caveats as necessary
|
||||||
|
approved.permissions = await this.finalizePermissionsRequest(
|
||||||
|
approved.permissions, accounts
|
||||||
|
)
|
||||||
|
approval.resolve(approved.permissions)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
||||||
// if finalization fails, reject the request
|
// if finalization fails, reject the request
|
||||||
@ -164,15 +186,16 @@ export class PermissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User rejection callback.
|
* User rejection callback. Rejects the Promise for the permissions request
|
||||||
|
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
||||||
*
|
*
|
||||||
* @param {string} id - the id of the rejected request
|
* @param {string} id - The id of the request rejected by the user
|
||||||
*/
|
*/
|
||||||
async rejectPermissionsRequest (id) {
|
async rejectPermissionsRequest (id) {
|
||||||
const approval = this.pendingApprovals.get(id)
|
const approval = this.pendingApprovals.get(id)
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
log.warn(`Permissions request with id '${id}' not found`)
|
log.error(`Permissions request with id '${id}' not found`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +204,7 @@ export class PermissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated
|
||||||
* Grants the given origin the eth_accounts permission for the given account(s).
|
* Grants the given origin the eth_accounts permission for the given account(s).
|
||||||
* This method should ONLY be called as a result of direct user action in the UI,
|
* This method should ONLY be called as a result of direct user action in the UI,
|
||||||
* with the intention of supporting legacy dapps that don't support EIP 1102.
|
* with the intention of supporting legacy dapps that don't support EIP 1102.
|
||||||
@ -190,33 +214,54 @@ export class PermissionsController {
|
|||||||
*/
|
*/
|
||||||
async legacyExposeAccounts (origin, accounts) {
|
async legacyExposeAccounts (origin, accounts) {
|
||||||
|
|
||||||
const permissions = {
|
// accounts are validated by finalizePermissionsRequest
|
||||||
eth_accounts: {},
|
if (typeof origin !== 'string' || !origin.length) {
|
||||||
|
throw new Error('Must provide non-empty string origin.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.finalizePermissionsRequest(permissions, accounts)
|
const existingAccounts = await this.getAccounts(origin)
|
||||||
|
|
||||||
|
if (existingAccounts.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
'May not call legacyExposeAccounts on origin with exposed accounts.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await this.finalizePermissionsRequest(
|
||||||
|
{ eth_accounts: {} }, accounts
|
||||||
|
)
|
||||||
|
|
||||||
let error
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
this.permissions.grantNewPermissions(origin, permissions, {}, (err) => (err ? resolve() : reject(err)))
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
error = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
await new Promise((resolve, reject) => {
|
||||||
if (error.code === 4001) {
|
this.permissions.grantNewPermissions(
|
||||||
throw error
|
origin, permissions, {}, _end
|
||||||
} else {
|
)
|
||||||
throw ethErrors.rpc.internal({
|
|
||||||
message: `Failed to add 'eth_accounts' to '${origin}'.`,
|
function _end (err) {
|
||||||
data: {
|
if (err) {
|
||||||
originalError: error,
|
reject(err)
|
||||||
accounts,
|
} else {
|
||||||
},
|
resolve()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.notifyDomain(origin, {
|
||||||
|
method: NOTIFICATION_NAMES.accountsChanged,
|
||||||
|
result: accounts,
|
||||||
|
})
|
||||||
|
this.permissionsLog.logAccountExposure(origin, accounts)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
throw ethErrors.rpc.internal({
|
||||||
|
message: `Failed to add 'eth_accounts' to '${origin}'.`,
|
||||||
|
data: {
|
||||||
|
originalError: error,
|
||||||
|
accounts,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,19 +291,25 @@ export class PermissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalizes a permissions request.
|
* Finalizes a permissions request. Throws if request validation fails.
|
||||||
* Throws if request validation fails.
|
* Clones the passed-in parameters to prevent inadvertent modification.
|
||||||
|
* Sets (adds or replaces) caveats for the following permissions:
|
||||||
|
* - eth_accounts: the permitted accounts caveat
|
||||||
*
|
*
|
||||||
* @param {Object} requestedPermissions - The requested permissions.
|
* @param {Object} requestedPermissions - The requested permissions.
|
||||||
* @param {string[]} accounts - The accounts to expose, if any.
|
* @param {string[]} requestedAccounts - The accounts to expose, if any.
|
||||||
|
* @returns {Object} The finalized permissions request object.
|
||||||
*/
|
*/
|
||||||
async finalizePermissionsRequest (requestedPermissions, accounts) {
|
async finalizePermissionsRequest (requestedPermissions, requestedAccounts) {
|
||||||
|
|
||||||
const { eth_accounts: ethAccounts } = requestedPermissions
|
const finalizedPermissions = cloneDeep(requestedPermissions)
|
||||||
|
const finalizedAccounts = cloneDeep(requestedAccounts)
|
||||||
|
|
||||||
|
const { eth_accounts: ethAccounts } = finalizedPermissions
|
||||||
|
|
||||||
if (ethAccounts) {
|
if (ethAccounts) {
|
||||||
|
|
||||||
await this.validatePermittedAccounts(accounts)
|
await this.validatePermittedAccounts(finalizedAccounts)
|
||||||
|
|
||||||
if (!ethAccounts.caveats) {
|
if (!ethAccounts.caveats) {
|
||||||
ethAccounts.caveats = []
|
ethAccounts.caveats = []
|
||||||
@ -272,11 +323,13 @@ export class PermissionsController {
|
|||||||
ethAccounts.caveats.push(
|
ethAccounts.caveats.push(
|
||||||
{
|
{
|
||||||
type: 'filterResponse',
|
type: 'filterResponse',
|
||||||
value: accounts,
|
value: finalizedAccounts,
|
||||||
name: CAVEAT_NAMES.exposedAccounts,
|
name: CAVEAT_NAMES.exposedAccounts,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return finalizedPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -309,7 +362,7 @@ export class PermissionsController {
|
|||||||
payload.method === NOTIFICATION_NAMES.accountsChanged &&
|
payload.method === NOTIFICATION_NAMES.accountsChanged &&
|
||||||
Array.isArray(payload.result)
|
Array.isArray(payload.result)
|
||||||
) {
|
) {
|
||||||
this.permissionsLogController.updateAccountsHistory(
|
this.permissionsLog.updateAccountsHistory(
|
||||||
origin, payload.result
|
origin, payload.result
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -325,6 +378,9 @@ export class PermissionsController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the given permissions for the given domain.
|
* Removes the given permissions for the given domain.
|
||||||
|
* Should only be called after confirming that the permissions exist, to
|
||||||
|
* avoid sending unnecessary notifications.
|
||||||
|
*
|
||||||
* @param {Object} domains { origin: [permissions] }
|
* @param {Object} domains { origin: [permissions] }
|
||||||
*/
|
*/
|
||||||
removePermissionsFor (domains) {
|
removePermissionsFor (domains) {
|
||||||
@ -351,6 +407,10 @@ export class PermissionsController {
|
|||||||
/**
|
/**
|
||||||
* When a new account is selected in the UI for 'origin', emit accountsChanged
|
* When a new account is selected in the UI for 'origin', emit accountsChanged
|
||||||
* to 'origin' if the selected account is permitted.
|
* to 'origin' if the selected account is permitted.
|
||||||
|
*
|
||||||
|
* Note: This will emit "false positive" accountsChanged events, but they are
|
||||||
|
* handled by the inpage provider.
|
||||||
|
*
|
||||||
* @param {string} origin - The origin.
|
* @param {string} origin - The origin.
|
||||||
* @param {string} account - The newly selected account's address.
|
* @param {string} account - The newly selected account's address.
|
||||||
*/
|
*/
|
||||||
@ -358,10 +418,17 @@ export class PermissionsController {
|
|||||||
|
|
||||||
const permittedAccounts = await this.getAccounts(origin)
|
const permittedAccounts = await this.getAccounts(origin)
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof origin !== 'string' || !origin.length ||
|
||||||
|
typeof account !== 'string' || !account.length
|
||||||
|
) {
|
||||||
|
throw new Error('Should provide non-empty origin and account strings.')
|
||||||
|
}
|
||||||
|
|
||||||
// do nothing if the account is not permitted for the origin, or
|
// do nothing if the account is not permitted for the origin, or
|
||||||
// if it's already first in the array of permitted accounts
|
// if it's already first in the array of permitted accounts
|
||||||
if (
|
if (
|
||||||
!account || !permittedAccounts.includes(account) ||
|
!permittedAccounts.includes(account) ||
|
||||||
permittedAccounts[0] === account
|
permittedAccounts[0] === account
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
@ -373,7 +440,7 @@ export class PermissionsController {
|
|||||||
|
|
||||||
// update permitted accounts to ensure that accounts are returned
|
// update permitted accounts to ensure that accounts are returned
|
||||||
// in the same order every time
|
// in the same order every time
|
||||||
this.updatePermittedAccounts(origin, newPermittedAccounts)
|
await this.updatePermittedAccounts(origin, newPermittedAccounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -454,7 +521,7 @@ export class PermissionsController {
|
|||||||
|
|
||||||
if (this.pendingApprovalOrigins.has(origin)) {
|
if (this.pendingApprovalOrigins.has(origin)) {
|
||||||
throw ethErrors.rpc.resourceUnavailable(
|
throw ethErrors.rpc.resourceUnavailable(
|
||||||
'Permission request already pending; please wait.'
|
'Permissions request already pending; please wait.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,15 +9,11 @@ export default function createMethodMiddleware ({
|
|||||||
}) {
|
}) {
|
||||||
return createAsyncMiddleware(async (req, res, next) => {
|
return createAsyncMiddleware(async (req, res, next) => {
|
||||||
|
|
||||||
if (typeof req.method !== 'string') {
|
|
||||||
res.error = ethErrors.rpc.invalidRequest({ data: req })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
|
|
||||||
// intercepting eth_accounts requests for backwards compatibility,
|
// Intercepting eth_accounts requests for backwards compatibility:
|
||||||
// i.e. return an empty array instead of an error
|
// The getAccounts call below wraps the rpc-cap middleware, and returns
|
||||||
|
// an empty array in case of errors (such as 4100:unauthorized)
|
||||||
case 'eth_accounts':
|
case 'eth_accounts':
|
||||||
|
|
||||||
res.result = await getAccounts()
|
res.result = await getAccounts()
|
||||||
@ -42,10 +38,12 @@ export default function createMethodMiddleware ({
|
|||||||
|
|
||||||
// get the accounts again
|
// get the accounts again
|
||||||
accounts = await getAccounts()
|
accounts = await getAccounts()
|
||||||
|
/* istanbul ignore else: too hard to induce, see below comment */
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
res.result = accounts
|
res.result = accounts
|
||||||
} else {
|
} else {
|
||||||
// this should never happen
|
// this should never happen, because it should be caught in the
|
||||||
|
// above catch clause
|
||||||
res.error = ethErrors.rpc.internal(
|
res.error = ethErrors.rpc.internal(
|
||||||
'Accounts unexpectedly unavailable. Please report this bug.'
|
'Accounts unexpectedly unavailable. Please report this bug.'
|
||||||
)
|
)
|
||||||
@ -53,7 +51,8 @@ export default function createMethodMiddleware ({
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
// custom method for getting metadata from the requesting domain
|
// custom method for getting metadata from the requesting domain,
|
||||||
|
// sent automatically by the inpage provider
|
||||||
case 'wallet_sendDomainMetadata':
|
case 'wallet_sendDomainMetadata':
|
||||||
|
|
||||||
const storeState = store.getState()[storeKey]
|
const storeState = store.getState()[storeKey]
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
|
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { isValidAddress } from 'ethereumjs-util'
|
|
||||||
import {
|
import {
|
||||||
CAVEAT_NAMES,
|
CAVEAT_NAMES,
|
||||||
HISTORY_STORE_KEY,
|
HISTORY_STORE_KEY,
|
||||||
LOG_STORE_KEY,
|
|
||||||
LOG_IGNORE_METHODS,
|
LOG_IGNORE_METHODS,
|
||||||
|
LOG_LIMIT,
|
||||||
|
LOG_METHOD_TYPES,
|
||||||
|
LOG_STORE_KEY,
|
||||||
WALLET_PREFIX,
|
WALLET_PREFIX,
|
||||||
} from './enums'
|
} from './enums'
|
||||||
|
|
||||||
const LOG_LIMIT = 100
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller with middleware for logging requests and responses to restricted
|
* Controller with middleware for logging requests and responses to restricted
|
||||||
* and permissions-related methods.
|
* and permissions-related methods.
|
||||||
@ -25,7 +23,7 @@ export default class PermissionsLogController {
|
|||||||
/**
|
/**
|
||||||
* Get the activity log.
|
* Get the activity log.
|
||||||
*
|
*
|
||||||
* @returns {Array<Object>} - The activity log.
|
* @returns {Array<Object>} The activity log.
|
||||||
*/
|
*/
|
||||||
getActivityLog () {
|
getActivityLog () {
|
||||||
return this.store.getState()[LOG_STORE_KEY] || []
|
return this.store.getState()[LOG_STORE_KEY] || []
|
||||||
@ -43,7 +41,7 @@ export default class PermissionsLogController {
|
|||||||
/**
|
/**
|
||||||
* Get the permissions history log.
|
* Get the permissions history log.
|
||||||
*
|
*
|
||||||
* @returns {Object} - The permissions history log.
|
* @returns {Object} The permissions history log.
|
||||||
*/
|
*/
|
||||||
getHistory () {
|
getHistory () {
|
||||||
return this.store.getState()[HISTORY_STORE_KEY] || {}
|
return this.store.getState()[HISTORY_STORE_KEY] || {}
|
||||||
@ -81,15 +79,20 @@ export default class PermissionsLogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a permissions log middleware.
|
* Create a permissions log middleware. Records permissions activity and history:
|
||||||
*
|
*
|
||||||
* @returns {JsonRpcEngineMiddleware} - The permissions log middleware.
|
* Activity: requests and responses for restricted and most wallet_ methods.
|
||||||
|
*
|
||||||
|
* History: for each origin, the last time a permission was granted, including
|
||||||
|
* which accounts were exposed, if any.
|
||||||
|
*
|
||||||
|
* @returns {JsonRpcEngineMiddleware} The permissions log middleware.
|
||||||
*/
|
*/
|
||||||
createMiddleware () {
|
createMiddleware () {
|
||||||
return (req, res, next, _end) => {
|
return (req, res, next, _end) => {
|
||||||
|
|
||||||
let requestedMethods
|
let activityEntry, requestedMethods
|
||||||
const { origin, method, id: requestId } = req
|
const { origin, method } = req
|
||||||
const isInternal = method.startsWith(WALLET_PREFIX)
|
const isInternal = method.startsWith(WALLET_PREFIX)
|
||||||
|
|
||||||
// we only log certain methods
|
// we only log certain methods
|
||||||
@ -98,17 +101,18 @@ export default class PermissionsLogController {
|
|||||||
(isInternal || this.restrictedMethods.includes(method))
|
(isInternal || this.restrictedMethods.includes(method))
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.logActivityRequest(req, isInternal)
|
activityEntry = this.logRequest(req, isInternal)
|
||||||
|
|
||||||
if (method === `${WALLET_PREFIX}requestPermissions`) {
|
if (method === `${WALLET_PREFIX}requestPermissions`) {
|
||||||
// get the corresponding methods from the requested permissions
|
// get the corresponding methods from the requested permissions so
|
||||||
|
// that we can record permissions history
|
||||||
requestedMethods = this.getRequestedMethods(req)
|
requestedMethods = this.getRequestedMethods(req)
|
||||||
}
|
}
|
||||||
} else if (method === 'eth_requestAccounts') {
|
} else if (method === 'eth_requestAccounts') {
|
||||||
|
|
||||||
// eth_requestAccounts is a special case; we need to extract the accounts
|
// eth_requestAccounts is a special case; we need to extract the accounts
|
||||||
// from it
|
// from it
|
||||||
this.logActivityRequest(req, isInternal)
|
activityEntry = this.logRequest(req, isInternal)
|
||||||
requestedMethods = [ 'eth_accounts' ]
|
requestedMethods = [ 'eth_accounts' ]
|
||||||
} else {
|
} else {
|
||||||
// no-op
|
// no-op
|
||||||
@ -119,9 +123,9 @@ export default class PermissionsLogController {
|
|||||||
next((cb) => {
|
next((cb) => {
|
||||||
|
|
||||||
const time = Date.now()
|
const time = Date.now()
|
||||||
this.logActivityResponse(requestId, res, time)
|
this.logResponse(activityEntry, res, time)
|
||||||
|
|
||||||
if (!res.error && requestedMethods) {
|
if (requestedMethods && !res.error && res.result) {
|
||||||
// any permissions or accounts changes will be recorded on the response,
|
// any permissions or accounts changes will be recorded on the response,
|
||||||
// so we only log permissions history here
|
// so we only log permissions history here
|
||||||
this.logPermissionsHistory(
|
this.logPermissionsHistory(
|
||||||
@ -140,46 +144,41 @@ export default class PermissionsLogController {
|
|||||||
* @param {Object} request - The request object.
|
* @param {Object} request - The request object.
|
||||||
* @param {boolean} isInternal - Whether the request is internal.
|
* @param {boolean} isInternal - Whether the request is internal.
|
||||||
*/
|
*/
|
||||||
logActivityRequest (request, isInternal) {
|
logRequest (request, isInternal) {
|
||||||
const activityEntry = {
|
const activityEntry = {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
methodType: isInternal ? 'internal' : 'restricted',
|
methodType: (
|
||||||
|
isInternal ? LOG_METHOD_TYPES.internal : LOG_METHOD_TYPES.restricted
|
||||||
|
),
|
||||||
origin: request.origin,
|
origin: request.origin,
|
||||||
request: cloneObj(request),
|
request: cloneDeep(request),
|
||||||
requestTime: Date.now(),
|
requestTime: Date.now(),
|
||||||
response: null,
|
response: null,
|
||||||
responseTime: null,
|
responseTime: null,
|
||||||
success: null,
|
success: null,
|
||||||
}
|
}
|
||||||
this.commitNewActivity(activityEntry)
|
this.commitNewActivity(activityEntry)
|
||||||
|
return activityEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds response data to an existing activity log entry and re-commits it.
|
* Adds response data to an existing activity log entry.
|
||||||
|
* Entry assumed already committed (i.e., in the log).
|
||||||
*
|
*
|
||||||
* @param {string} id - The original request id.
|
* @param {Object} entry - The entry to add a response to.
|
||||||
* @param {Object} response - The response object.
|
* @param {Object} response - The response object.
|
||||||
* @param {number} time - Output from Date.now()
|
* @param {number} time - Output from Date.now()
|
||||||
*/
|
*/
|
||||||
logActivityResponse (id, response, time) {
|
logResponse (entry, response, time) {
|
||||||
|
|
||||||
if (!id || !response) {
|
if (!entry || !response) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = this.getActivityLog()
|
entry.response = cloneDeep(response)
|
||||||
const index = getLastIndexOfObjectArray(logs, 'id', id)
|
|
||||||
if (index === -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = logs[index]
|
|
||||||
entry.response = cloneObj(response)
|
|
||||||
entry.responseTime = time
|
entry.responseTime = time
|
||||||
entry.success = !response.error
|
entry.success = !response.error
|
||||||
|
|
||||||
this.updateActivityLog(logs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -203,6 +202,33 @@ export default class PermissionsLogController {
|
|||||||
this.updateActivityLog(logs)
|
this.updateActivityLog(logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record account exposure and eth_accounts permissions history for the given
|
||||||
|
* origin.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin accounts were exposed to.
|
||||||
|
* @param {Array<string>} accounts - The accounts that were exposed.
|
||||||
|
*/
|
||||||
|
logAccountExposure (origin, accounts) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof origin !== 'string' || !origin.length ||
|
||||||
|
!Array.isArray(accounts) || accounts.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Must provide non-empty string origin and array of accounts.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logPermissionsHistory(
|
||||||
|
['eth_accounts'],
|
||||||
|
origin,
|
||||||
|
accounts,
|
||||||
|
Date.now(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new permissions history log entries, if any, and commit them.
|
* Create new permissions history log entries, if any, and commit them.
|
||||||
*
|
*
|
||||||
@ -212,7 +238,10 @@ export default class PermissionsLogController {
|
|||||||
* @param {string} time - The time of the request, i.e. Date.now().
|
* @param {string} time - The time of the request, i.e. Date.now().
|
||||||
* @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'.
|
* @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'.
|
||||||
*/
|
*/
|
||||||
logPermissionsHistory (requestedMethods, origin, result, time, isEthRequestAccounts) {
|
logPermissionsHistory (
|
||||||
|
requestedMethods, origin, result,
|
||||||
|
time, isEthRequestAccounts
|
||||||
|
) {
|
||||||
|
|
||||||
let accounts, newEntries
|
let accounts, newEntries
|
||||||
|
|
||||||
@ -233,35 +262,35 @@ export default class PermissionsLogController {
|
|||||||
// Special handling for eth_accounts, in order to record the time the
|
// Special handling for eth_accounts, in order to record the time the
|
||||||
// accounts were last seen or approved by the origin.
|
// accounts were last seen or approved by the origin.
|
||||||
newEntries = result
|
newEntries = result
|
||||||
? result
|
.map((perm) => {
|
||||||
.map((perm) => {
|
|
||||||
|
|
||||||
if (perm.parentCapability === 'eth_accounts') {
|
if (perm.parentCapability === 'eth_accounts') {
|
||||||
accounts = this.getAccountsFromPermission(perm)
|
accounts = this.getAccountsFromPermission(perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
return perm.parentCapability
|
return perm.parentCapability
|
||||||
})
|
})
|
||||||
.reduce((acc, method) => {
|
.reduce((acc, method) => {
|
||||||
|
|
||||||
if (requestedMethods.includes(method)) {
|
// all approved permissions will be included in the response,
|
||||||
|
// not just the newly requested ones
|
||||||
|
if (requestedMethods.includes(method)) {
|
||||||
|
|
||||||
if (method === 'eth_accounts') {
|
if (method === 'eth_accounts') {
|
||||||
|
|
||||||
const accountToTimeMap = getAccountToTimeMap(accounts, time)
|
const accountToTimeMap = getAccountToTimeMap(accounts, time)
|
||||||
|
|
||||||
acc[method] = {
|
acc[method] = {
|
||||||
lastApproved: time,
|
lastApproved: time,
|
||||||
accounts: accountToTimeMap,
|
accounts: accountToTimeMap,
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc[method] = { lastApproved: time }
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
acc[method] = { lastApproved: time }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
: {} // no result (e.g. in case of error), no log
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(newEntries).length > 0) {
|
if (Object.keys(newEntries).length > 0) {
|
||||||
@ -292,6 +321,7 @@ export default class PermissionsLogController {
|
|||||||
history[origin] && history[origin]['eth_accounts']
|
history[origin] && history[origin]['eth_accounts']
|
||||||
)
|
)
|
||||||
const newEthAccountsEntry = newEntries['eth_accounts']
|
const newEthAccountsEntry = newEntries['eth_accounts']
|
||||||
|
|
||||||
if (existingEthAccountsEntry && newEthAccountsEntry) {
|
if (existingEthAccountsEntry && newEthAccountsEntry) {
|
||||||
|
|
||||||
// we may intend to update just the accounts, not the permission
|
// we may intend to update just the accounts, not the permission
|
||||||
@ -320,7 +350,7 @@ export default class PermissionsLogController {
|
|||||||
* Get all requested methods from a permissions request.
|
* Get all requested methods from a permissions request.
|
||||||
*
|
*
|
||||||
* @param {Object} request - The request object.
|
* @param {Object} request - The request object.
|
||||||
* @returns {Array<string>} - The names of the requested permissions.
|
* @returns {Array<string>} The names of the requested permissions.
|
||||||
*/
|
*/
|
||||||
getRequestedMethods (request) {
|
getRequestedMethods (request) {
|
||||||
if (
|
if (
|
||||||
@ -339,7 +369,7 @@ export default class PermissionsLogController {
|
|||||||
* Returns an empty array if the permission is not eth_accounts.
|
* Returns an empty array if the permission is not eth_accounts.
|
||||||
*
|
*
|
||||||
* @param {Object} perm - The permissions object.
|
* @param {Object} perm - The permissions object.
|
||||||
* @returns {Array<string>} - The permitted accounts.
|
* @returns {Array<string>} The permitted accounts.
|
||||||
*/
|
*/
|
||||||
getAccountsFromPermission (perm) {
|
getAccountsFromPermission (perm) {
|
||||||
|
|
||||||
@ -347,7 +377,7 @@ export default class PermissionsLogController {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = {}
|
const accounts = new Set()
|
||||||
for (const caveat of perm.caveats) {
|
for (const caveat of perm.caveats) {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -356,51 +386,25 @@ export default class PermissionsLogController {
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
for (const value of caveat.value) {
|
for (const value of caveat.value) {
|
||||||
if (isValidAddress(value)) {
|
accounts.add(value)
|
||||||
accounts[value] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(accounts)
|
return [ ...accounts ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
// the call to clone is set to disallow circular references
|
/**
|
||||||
// we attempt cloning at a depth of 3 and 2, then return a
|
* Get a map from account addresses to the given time.
|
||||||
// shallow copy of the object
|
*
|
||||||
function cloneObj (obj) {
|
* @param {Array<string>} accounts - An array of addresses.
|
||||||
|
* @param {number} time - A time, e.g. Date.now().
|
||||||
for (let i = 3; i > 1; i--) {
|
* @returns {Object} A string:number map of addresses to time.
|
||||||
try {
|
*/
|
||||||
return cloneDeep(obj, false, i)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
return { ...obj }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAccountToTimeMap (accounts, time) {
|
function getAccountToTimeMap (accounts, time) {
|
||||||
return accounts.reduce(
|
return accounts.reduce(
|
||||||
(acc, account) => ({ ...acc, [account]: time }), {}
|
(acc, account) => ({ ...acc, [account]: time }), {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastIndexOfObjectArray (array, key, value) {
|
|
||||||
|
|
||||||
if (Array.isArray(array) && array.length > 0) {
|
|
||||||
|
|
||||||
for (let i = array.length - 1; i >= 0; i--) {
|
|
||||||
|
|
||||||
if (!array[i] || typeof array[i] !== 'object') {
|
|
||||||
throw new Error(`Encountered non-Object element at index ${i}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array[i][key] === value) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
@ -2,17 +2,19 @@ export default function getRestrictedMethods (permissionsController) {
|
|||||||
return {
|
return {
|
||||||
|
|
||||||
'eth_accounts': {
|
'eth_accounts': {
|
||||||
description: 'View the address of the selected account',
|
description: `View the addresses of the user's chosen accounts.`,
|
||||||
method: (_, res, __, end) => {
|
method: (_, res, __, end) => {
|
||||||
permissionsController.getKeyringAccounts()
|
permissionsController.getKeyringAccounts()
|
||||||
.then((accounts) => {
|
.then((accounts) => {
|
||||||
res.result = accounts
|
res.result = accounts
|
||||||
end()
|
end()
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(
|
||||||
res.error = err
|
(err) => {
|
||||||
end(err)
|
res.error = err
|
||||||
})
|
end(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ import TokenRatesController from './controllers/token-rates'
|
|||||||
import DetectTokensController from './controllers/detect-tokens'
|
import DetectTokensController from './controllers/detect-tokens'
|
||||||
import ABTestController from './controllers/ab-test'
|
import ABTestController from './controllers/ab-test'
|
||||||
import { PermissionsController } from './controllers/permissions'
|
import { PermissionsController } from './controllers/permissions'
|
||||||
|
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
|
||||||
import nodeify from './lib/nodeify'
|
import nodeify from './lib/nodeify'
|
||||||
import accountImporter from './account-import-strategies'
|
import accountImporter from './account-import-strategies'
|
||||||
import getBuyEthUrl from './lib/buy-eth-url'
|
import getBuyEthUrl from './lib/buy-eth-url'
|
||||||
@ -211,6 +212,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
platform: opts.platform,
|
platform: opts.platform,
|
||||||
notifyDomain: this.notifyConnections.bind(this),
|
notifyDomain: this.notifyConnections.bind(this),
|
||||||
notifyAllDomains: this.notifyAllConnections.bind(this),
|
notifyAllDomains: this.notifyAllConnections.bind(this),
|
||||||
|
getRestrictedMethods,
|
||||||
}, initState.PermissionsController, initState.PermissionsMetadata)
|
}, initState.PermissionsController, initState.PermissionsMetadata)
|
||||||
|
|
||||||
this.detectTokensController = new DetectTokensController({
|
this.detectTokensController = new DetectTokensController({
|
||||||
|
6
nyc.config.js
Normal file
6
nyc.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
branches: 95,
|
||||||
|
lines: 95,
|
||||||
|
functions: 95,
|
||||||
|
statements: 95,
|
||||||
|
}
|
@ -19,13 +19,16 @@
|
|||||||
"sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080",
|
"sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080",
|
||||||
"test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"",
|
"test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"",
|
||||||
"test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive mocha test/unit-global/*",
|
"test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive mocha test/unit-global/*",
|
||||||
|
"test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/{,**/!(permissions)}/*.js\" \"ui/app/**/*.test.js\"",
|
||||||
|
"test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/permissions/*.js\"",
|
||||||
"test:integration": "yarn test:integration:build && yarn test:flat",
|
"test:integration": "yarn test:integration:build && yarn test:flat",
|
||||||
"test:integration:build": "yarn build styles",
|
"test:integration:build": "yarn build styles",
|
||||||
"test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh",
|
"test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh",
|
||||||
"test:web3:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-web3.sh",
|
"test:web3:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-web3.sh",
|
||||||
"test:web3:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-web3.sh",
|
"test:web3:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-web3.sh",
|
||||||
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
||||||
"test:coverage": "nyc --reporter=text --reporter=html yarn test:unit",
|
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
||||||
|
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
||||||
"test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
"test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
||||||
"test:flat": "yarn test:flat:build && karma start test/flat.conf.js",
|
"test:flat": "yarn test:flat:build && karma start test/flat.conf.js",
|
||||||
"test:flat:build": "yarn test:flat:build:ui && yarn test:flat:build:tests && yarn test:flat:build:locales",
|
"test:flat:build": "yarn test:flat:build:ui && yarn test:flat:build:tests && yarn test:flat:build:locales",
|
||||||
@ -156,7 +159,7 @@
|
|||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^3.0.1",
|
||||||
"rpc-cap": "^1.0.5",
|
"rpc-cap": "^2.0.0",
|
||||||
"safe-event-emitter": "^1.0.1",
|
"safe-event-emitter": "^1.0.1",
|
||||||
"safe-json-stringify": "^1.2.0",
|
"safe-json-stringify": "^1.2.0",
|
||||||
"single-call-balance-checker-abi": "^1.0.0",
|
"single-call-balance-checker-abi": "^1.0.0",
|
||||||
|
@ -146,7 +146,7 @@ describe('MetaMask', function () {
|
|||||||
await domains[0].click()
|
await domains[0].click()
|
||||||
|
|
||||||
const permissionDescription = await driver.findElement(By.css('.connected-sites-list__permission-description'))
|
const permissionDescription = await driver.findElement(By.css('.connected-sites-list__permission-description'))
|
||||||
assert.equal(await permissionDescription.getText(), 'View the address of the selected account')
|
assert.equal(await permissionDescription.getText(), `View the addresses of the user's chosen accounts.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can get accounts within the dapp', async function () {
|
it('can get accounts within the dapp', async function () {
|
||||||
|
117
test/unit/app/controllers/permissions/helpers.js
Normal file
117
test/unit/app/controllers/permissions/helpers.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { strict as assert } from 'assert'
|
||||||
|
|
||||||
|
import { noop } from './mocks'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grants the given permissions to the given origin, using the given permissions
|
||||||
|
* controller.
|
||||||
|
*
|
||||||
|
* Just a wrapper for an rpc-cap middleware function.
|
||||||
|
*
|
||||||
|
* @param {PermissionsController} permController - The permissions controller.
|
||||||
|
* @param {string} origin - The origin to grant permissions to.
|
||||||
|
* @param {Object} permissions - The permissions to grant.
|
||||||
|
*/
|
||||||
|
export function grantPermissions (permController, origin, permissions) {
|
||||||
|
permController.permissions.grantNewPermissions(
|
||||||
|
origin, permissions, {}, noop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the underlying rpc-cap requestUserApproval function, and returns
|
||||||
|
* a promise that's resolved once it has been set.
|
||||||
|
*
|
||||||
|
* This function must be called on the given permissions controller every
|
||||||
|
* time you want such a Promise. As of writing, it's only called once per test.
|
||||||
|
*
|
||||||
|
* @param {PermissionsController} - A permissions controller.
|
||||||
|
* @returns {Promise<void>} A Promise that resolves once a pending approval
|
||||||
|
* has been set.
|
||||||
|
*/
|
||||||
|
export function getUserApprovalPromise (permController) {
|
||||||
|
return new Promise((resolveForCaller) => {
|
||||||
|
permController.permissions.requestUserApproval = async (req) => {
|
||||||
|
const { origin, metadata: { id } } = req
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
permController.pendingApprovals.set(id, { origin, resolve, reject })
|
||||||
|
resolveForCaller()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an activity log entry with respect to a request, response, and
|
||||||
|
* relevant metadata.
|
||||||
|
*
|
||||||
|
* @param {Object} entry - The activity log entry to validate.
|
||||||
|
* @param {Object} req - The request that generated the entry.
|
||||||
|
* @param {Object} [res] - The response for the request, if any.
|
||||||
|
* @param {'restricted'|'internal'} methodType - The method log controller method type of the request.
|
||||||
|
* @param {boolean} success - Whether the request succeeded or not.
|
||||||
|
*/
|
||||||
|
export function validateActivityEntry (
|
||||||
|
entry, req, res, methodType, success
|
||||||
|
) {
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => {
|
||||||
|
_validateActivityEntry(
|
||||||
|
entry, req, res, methodType, success
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'should have expected activity entry'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _validateActivityEntry (
|
||||||
|
entry, req, res, methodType, success
|
||||||
|
) {
|
||||||
|
|
||||||
|
assert.ok(entry, 'entry should exist')
|
||||||
|
|
||||||
|
assert.equal(entry.id, req.id)
|
||||||
|
assert.equal(entry.method, req.method)
|
||||||
|
assert.equal(entry.origin, req.origin)
|
||||||
|
assert.equal(entry.methodType, methodType)
|
||||||
|
assert.deepEqual(
|
||||||
|
entry.request, req,
|
||||||
|
'entry.request should equal the request'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
Number.isInteger(entry.requestTime) &&
|
||||||
|
Number.isInteger(entry.responseTime)
|
||||||
|
),
|
||||||
|
'request and response times should be numbers'
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
(entry.requestTime <= entry.responseTime),
|
||||||
|
'request time should be less than response time'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(entry.success, success)
|
||||||
|
assert.deepEqual(
|
||||||
|
entry.response, res,
|
||||||
|
'entry.response should equal the response'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
Number.isInteger(entry.requestTime) && entry.requestTime > 0,
|
||||||
|
'entry should have non-zero request time'
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
entry.success === null &&
|
||||||
|
entry.responseTime === null &&
|
||||||
|
entry.response === null
|
||||||
|
),
|
||||||
|
'entry response values should be null'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
691
test/unit/app/controllers/permissions/mocks.js
Normal file
691
test/unit/app/controllers/permissions/mocks.js
Normal file
@ -0,0 +1,691 @@
|
|||||||
|
import { ethErrors, ERROR_CODES } from 'eth-json-rpc-errors'
|
||||||
|
import deepFreeze from 'deep-freeze-strict'
|
||||||
|
|
||||||
|
import _getRestrictedMethods
|
||||||
|
from '../../../../../app/scripts/controllers/permissions/restrictedMethods'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CAVEAT_NAMES,
|
||||||
|
NOTIFICATION_NAMES,
|
||||||
|
} from '../../../../../app/scripts/controllers/permissions/enums'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* README
|
||||||
|
* This file contains three primary kinds of mocks:
|
||||||
|
* - Mocks for initializing a permissions controller and getting a permissions
|
||||||
|
* middleware
|
||||||
|
* - Functions for getting various mock objects consumed or produced by
|
||||||
|
* permissions controller methods
|
||||||
|
* - Immutable mock values like Ethereum accounts and expected states
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const noop = () => {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Permissions Controller and Middleware
|
||||||
|
*/
|
||||||
|
|
||||||
|
const platform = {
|
||||||
|
openExtensionInBrowser: noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyringAccounts = deepFreeze([
|
||||||
|
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
|
||||||
|
'0xc42edfcc21ed14dda456aa0756c153f7985d8813',
|
||||||
|
])
|
||||||
|
|
||||||
|
const getKeyringAccounts = async () => [ ...keyringAccounts ]
|
||||||
|
|
||||||
|
// perm controller initialization helper
|
||||||
|
const getRestrictedMethods = (permController) => {
|
||||||
|
return {
|
||||||
|
|
||||||
|
// the actual, production restricted methods
|
||||||
|
..._getRestrictedMethods(permController),
|
||||||
|
|
||||||
|
// our own dummy method for testing
|
||||||
|
'test_method': {
|
||||||
|
description: `This method is only for testing.`,
|
||||||
|
method: (req, res, __, end) => {
|
||||||
|
if (req.params[0]) {
|
||||||
|
res.result = 1
|
||||||
|
} else {
|
||||||
|
res.result = 0
|
||||||
|
}
|
||||||
|
end()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets default mock constructor options for a permissions controller.
|
||||||
|
*
|
||||||
|
* @returns {Object} A PermissionsController constructor options object.
|
||||||
|
*/
|
||||||
|
export function getPermControllerOpts () {
|
||||||
|
return {
|
||||||
|
platform,
|
||||||
|
getKeyringAccounts,
|
||||||
|
notifyDomain: noop,
|
||||||
|
notifyAllDomains: noop,
|
||||||
|
getRestrictedMethods,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a Promise-wrapped permissions controller middleware function.
|
||||||
|
*
|
||||||
|
* @param {PermissionsController} permController - The permissions controller to get a
|
||||||
|
* middleware for.
|
||||||
|
* @param {string} origin - The origin for the middleware.
|
||||||
|
* @param {string} extensionId - The extension id for the middleware.
|
||||||
|
* @returns {Function} A Promise-wrapped middleware function with convenient default args.
|
||||||
|
*/
|
||||||
|
export function getPermissionsMiddleware (permController, origin, extensionId) {
|
||||||
|
const middleware = permController.createMiddleware({ origin, extensionId })
|
||||||
|
return (req, res = {}, next = noop, end) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
end = end || _end
|
||||||
|
|
||||||
|
middleware(req, res, next, end)
|
||||||
|
|
||||||
|
// emulates json-rpc-engine error handling
|
||||||
|
function _end (err) {
|
||||||
|
if (err || res.error) {
|
||||||
|
reject(err || res.error)
|
||||||
|
} else {
|
||||||
|
resolve(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} notifications - An object that will store notifications produced
|
||||||
|
* by the permissions controller.
|
||||||
|
* @returns {Function} A function passed to the permissions controller at initialization,
|
||||||
|
* for recording notifications.
|
||||||
|
*/
|
||||||
|
export const getNotifyDomain = (notifications = {}) => (origin, notification) => {
|
||||||
|
notifications[origin].push(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} notifications - An object that will store notifications produced
|
||||||
|
* by the permissions controller.
|
||||||
|
* @returns {Function} A function passed to the permissions controller at initialization,
|
||||||
|
* for recording notifications.
|
||||||
|
*/
|
||||||
|
export const getNotifyAllDomains = (notifications = {}) => (notification) => {
|
||||||
|
Object.keys(notifications).forEach((origin) => {
|
||||||
|
notifications[origin].push(notification)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants and Mock Objects
|
||||||
|
* - e.g. permissions, caveats, and permission requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ORIGINS = {
|
||||||
|
a: 'foo.xyz',
|
||||||
|
b: 'bar.abc',
|
||||||
|
c: 'baz.def',
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERM_NAMES = {
|
||||||
|
eth_accounts: 'eth_accounts',
|
||||||
|
test_method: 'test_method',
|
||||||
|
does_not_exist: 'does_not_exist',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCOUNT_ARRAYS = {
|
||||||
|
a: [ ...keyringAccounts ],
|
||||||
|
b: [keyringAccounts[0]],
|
||||||
|
c: [keyringAccounts[1]],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers for getting mock caveats.
|
||||||
|
*/
|
||||||
|
const CAVEATS = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a correctly formatted eth_accounts exposedAccounts caveat.
|
||||||
|
*
|
||||||
|
* @param {Array<string>} accounts - The accounts for the caveat
|
||||||
|
* @returns {Object} An eth_accounts exposedAccounts caveats
|
||||||
|
*/
|
||||||
|
eth_accounts: (accounts) => {
|
||||||
|
return {
|
||||||
|
type: 'filterResponse',
|
||||||
|
value: accounts,
|
||||||
|
name: CAVEAT_NAMES.exposedAccounts,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each function here corresponds to what would be a type or interface consumed
|
||||||
|
* by permissions controller functions if we used TypeScript.
|
||||||
|
*/
|
||||||
|
const PERMS = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The argument to approvePermissionsRequest
|
||||||
|
* @param {string} id - The rpc-cap permissions request id.
|
||||||
|
* @param {Object} permissions - The approved permissions, request-formatted.
|
||||||
|
*/
|
||||||
|
approvedRequest: (id, permissions = {}) => {
|
||||||
|
return {
|
||||||
|
permissions: { ...permissions },
|
||||||
|
metadata: { id },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requested permissions objects, as passed to wallet_requestPermissions.
|
||||||
|
*/
|
||||||
|
requests: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} A permissions request object with eth_accounts
|
||||||
|
*/
|
||||||
|
eth_accounts: () => {
|
||||||
|
return { eth_accounts: {} }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} A permissions request object with test_method
|
||||||
|
*/
|
||||||
|
test_method: () => {
|
||||||
|
return { test_method: {} }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} A permissions request object with does_not_exist
|
||||||
|
*/
|
||||||
|
does_not_exist: () => {
|
||||||
|
return { does_not_exist: {} }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalized permission requests, as returned by finalizePermissionsRequest
|
||||||
|
*/
|
||||||
|
finalizedRequests: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<string>} accounts - The accounts for the eth_accounts permission caveat
|
||||||
|
* @returns {Object} A finalized permissions request object with eth_accounts and its caveat
|
||||||
|
*/
|
||||||
|
eth_accounts: (accounts) => {
|
||||||
|
return {
|
||||||
|
eth_accounts: {
|
||||||
|
caveats: [CAVEATS.eth_accounts(accounts)],
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} A finalized permissions request object with test_method
|
||||||
|
*/
|
||||||
|
test_method: () => {
|
||||||
|
return {
|
||||||
|
test_method: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partial members of res.result for successful:
|
||||||
|
* - wallet_requestPermissions
|
||||||
|
* - wallet_getPermissions
|
||||||
|
*/
|
||||||
|
granted: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<string>} accounts - The accounts for the eth_accounts permission caveat
|
||||||
|
* @returns {Object} A granted permissions object with eth_accounts and its caveat
|
||||||
|
*/
|
||||||
|
eth_accounts: (accounts) => {
|
||||||
|
return {
|
||||||
|
parentCapability: PERM_NAMES.eth_accounts,
|
||||||
|
caveats: [CAVEATS.eth_accounts(accounts)],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} A granted permissions object with test_method
|
||||||
|
*/
|
||||||
|
test_method: () => {
|
||||||
|
return {
|
||||||
|
parentCapability: PERM_NAMES.test_method,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objects with function values for getting correctly formatted permissions,
|
||||||
|
* caveats, errors, permissions requests etc.
|
||||||
|
*/
|
||||||
|
export const getters = deepFreeze({
|
||||||
|
|
||||||
|
CAVEATS,
|
||||||
|
|
||||||
|
PERMS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getters for errors by the method or workflow that throws them.
|
||||||
|
*/
|
||||||
|
ERRORS: {
|
||||||
|
|
||||||
|
validatePermittedAccounts: {
|
||||||
|
|
||||||
|
invalidParam: () => {
|
||||||
|
return {
|
||||||
|
name: 'Error',
|
||||||
|
message: 'Must provide non-empty array of account(s).',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nonKeyringAccount: (account) => {
|
||||||
|
return {
|
||||||
|
name: 'Error',
|
||||||
|
message: `Unknown account: ${account}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
finalizePermissionsRequest: {
|
||||||
|
grantEthAcountsFailure: (origin) => {
|
||||||
|
return {
|
||||||
|
// name: 'EthereumRpcError',
|
||||||
|
message: `Failed to add 'eth_accounts' to '${origin}'.`,
|
||||||
|
code: ERROR_CODES.rpc.internal,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePermittedAccounts: {
|
||||||
|
invalidOrigin: () => {
|
||||||
|
return {
|
||||||
|
message: 'No such permission exists for the given domain.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
legacyExposeAccounts: {
|
||||||
|
badOrigin: () => {
|
||||||
|
return {
|
||||||
|
message: 'Must provide non-empty string origin.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forbiddenUsage: () => {
|
||||||
|
return {
|
||||||
|
name: 'Error',
|
||||||
|
message: 'May not call legacyExposeAccounts on origin with exposed accounts.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
handleNewAccountSelected: {
|
||||||
|
invalidParams: () => {
|
||||||
|
return {
|
||||||
|
name: 'Error',
|
||||||
|
message: 'Should provide non-empty origin and account strings.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
approvePermissionsRequest: {
|
||||||
|
noPermsRequested: () => {
|
||||||
|
return {
|
||||||
|
message: 'Must request at least one permission.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rejectPermissionsRequest: {
|
||||||
|
rejection: () => {
|
||||||
|
return {
|
||||||
|
message: ethErrors.provider.userRejectedRequest().message,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methodNotFound: (methodName) => {
|
||||||
|
return {
|
||||||
|
message: `The method '${methodName}' does not exist / is not available.`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
createMiddleware: {
|
||||||
|
badOrigin: () => {
|
||||||
|
return {
|
||||||
|
message: 'Must provide non-empty string origin.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rpcCap: {
|
||||||
|
unauthorized: () => {
|
||||||
|
return {
|
||||||
|
code: 4100,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
logAccountExposure: {
|
||||||
|
invalidParams: () => {
|
||||||
|
return {
|
||||||
|
message: 'Must provide non-empty string origin and array of accounts.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
pendingApprovals: {
|
||||||
|
duplicateOriginOrId: (id, origin) => {
|
||||||
|
return {
|
||||||
|
message: `Pending approval with id ${id} or origin ${origin} already exists.`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestAlreadyPending: () => {
|
||||||
|
return {
|
||||||
|
message: 'Permissions request already pending; please wait.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getters for notifications produced by the permissions controller.
|
||||||
|
*/
|
||||||
|
NOTIFICATIONS: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a removed accounts notification.
|
||||||
|
*
|
||||||
|
* @returns {Object} An accountsChanged notification with an empty array as its result
|
||||||
|
*/
|
||||||
|
removedAccounts: () => {
|
||||||
|
return {
|
||||||
|
method: NOTIFICATION_NAMES.accountsChanged,
|
||||||
|
result: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new accounts notification.
|
||||||
|
*
|
||||||
|
* @param {Array<string>} accounts - The accounts added to the notification.
|
||||||
|
* @returns {Object} An accountsChanged notification with the given accounts as its result
|
||||||
|
*/
|
||||||
|
newAccounts: (accounts) => {
|
||||||
|
return {
|
||||||
|
method: NOTIFICATION_NAMES.accountsChanged,
|
||||||
|
result: accounts,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a test notification that doesn't occur in practice.
|
||||||
|
*
|
||||||
|
* @returns {Object} A notification with the 'test_notification' method name
|
||||||
|
*/
|
||||||
|
test: () => {
|
||||||
|
return {
|
||||||
|
method: 'test_notification',
|
||||||
|
result: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getters for mock RPC request objects.
|
||||||
|
*/
|
||||||
|
RPC_REQUESTS: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an arbitrary RPC request object.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @param {string} method - The request method
|
||||||
|
* @param {Array<any>} params - The request parameters
|
||||||
|
* @param {string} [id] - The request id
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
custom: (origin, method, params = [], id) => {
|
||||||
|
const req = {
|
||||||
|
origin,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
if (id !== undefined) {
|
||||||
|
req.id = id
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an eth_accounts RPC request object.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
eth_accounts: (origin) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'eth_accounts',
|
||||||
|
params: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a test_method RPC request object.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @param {boolean} param - The request param
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
test_method: (origin, param = false) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'test_method',
|
||||||
|
params: [param],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an eth_requestAccounts RPC request object.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
eth_requestAccounts: (origin) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'eth_requestAccounts',
|
||||||
|
params: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a wallet_requestPermissions RPC request object,
|
||||||
|
* for a single permission.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @param {string} permissionName - The name of the permission to request
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
requestPermission: (origin, permissionName) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'wallet_requestPermissions',
|
||||||
|
params: [ PERMS.requests[permissionName]() ],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a wallet_requestPermissions RPC request object,
|
||||||
|
* for multiple permissions.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @param {Object} permissions - A permission request object
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
requestPermissions: (origin, permissions = {}) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'wallet_requestPermissions',
|
||||||
|
params: [ permissions ],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a wallet_sendDomainMetadata RPC request object.
|
||||||
|
*
|
||||||
|
* @param {string} origin - The origin of the request
|
||||||
|
* @param {Object} name - The domainMetadata name
|
||||||
|
* @param {Array<any>} [args] - Any other data for the request's domainMetadata
|
||||||
|
* @returns {Object} An RPC request object
|
||||||
|
*/
|
||||||
|
wallet_sendDomainMetadata: (origin, name, ...args) => {
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
method: 'wallet_sendDomainMetadata',
|
||||||
|
domainMetadata: {
|
||||||
|
...args,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objects with immutable mock values.
|
||||||
|
*/
|
||||||
|
export const constants = deepFreeze({
|
||||||
|
|
||||||
|
DUMMY_ACCOUNT: '0xabc',
|
||||||
|
|
||||||
|
REQUEST_IDS: {
|
||||||
|
a: '1',
|
||||||
|
b: '2',
|
||||||
|
c: '3',
|
||||||
|
},
|
||||||
|
|
||||||
|
ORIGINS: { ...ORIGINS },
|
||||||
|
|
||||||
|
ACCOUNT_ARRAYS: { ...ACCOUNT_ARRAYS },
|
||||||
|
|
||||||
|
PERM_NAMES: { ...PERM_NAMES },
|
||||||
|
|
||||||
|
RESTRICTED_METHODS: [
|
||||||
|
'eth_accounts',
|
||||||
|
'test_method',
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock permissions history objects.
|
||||||
|
*/
|
||||||
|
EXPECTED_HISTORIES: {
|
||||||
|
|
||||||
|
case1: [
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.a[0]]: 1,
|
||||||
|
[ACCOUNT_ARRAYS.a[1]]: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 2,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.a[0]]: 2,
|
||||||
|
[ACCOUNT_ARRAYS.a[1]]: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
case2: [
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
accounts: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
case3: [
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||||
|
},
|
||||||
|
[ORIGINS.b]: {
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.b[0]]: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[ORIGINS.c]: {
|
||||||
|
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.c[0]]: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.test_method]: { lastApproved: 2 },
|
||||||
|
},
|
||||||
|
[ORIGINS.b]: {
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.b[0]]: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[ORIGINS.c]: {
|
||||||
|
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||||
|
[PERM_NAMES.eth_accounts]: {
|
||||||
|
lastApproved: 2,
|
||||||
|
accounts: {
|
||||||
|
[ACCOUNT_ARRAYS.c[0]]: 1,
|
||||||
|
[ACCOUNT_ARRAYS.b[0]]: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
case4: [
|
||||||
|
{
|
||||||
|
[ORIGINS.a]: {
|
||||||
|
[PERM_NAMES.test_method]: {
|
||||||
|
lastApproved: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
1191
test/unit/app/controllers/permissions/permissions-controller-test.js
Normal file
1191
test/unit/app/controllers/permissions/permissions-controller-test.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,690 @@
|
|||||||
|
import { strict as assert } from 'assert'
|
||||||
|
import ObservableStore from 'obs-store'
|
||||||
|
import nanoid from 'nanoid'
|
||||||
|
import { useFakeTimers } from 'sinon'
|
||||||
|
|
||||||
|
import PermissionsLogController
|
||||||
|
from '../../../../../app/scripts/controllers/permissions/permissionsLog'
|
||||||
|
|
||||||
|
import {
|
||||||
|
LOG_LIMIT,
|
||||||
|
LOG_METHOD_TYPES,
|
||||||
|
} from '../../../../../app/scripts/controllers/permissions/enums'
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateActivityEntry,
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
import {
|
||||||
|
constants,
|
||||||
|
getters,
|
||||||
|
noop,
|
||||||
|
} from './mocks'
|
||||||
|
|
||||||
|
const {
|
||||||
|
ERRORS,
|
||||||
|
PERMS,
|
||||||
|
RPC_REQUESTS,
|
||||||
|
} = getters
|
||||||
|
|
||||||
|
const {
|
||||||
|
ACCOUNT_ARRAYS,
|
||||||
|
EXPECTED_HISTORIES,
|
||||||
|
ORIGINS,
|
||||||
|
PERM_NAMES,
|
||||||
|
REQUEST_IDS,
|
||||||
|
RESTRICTED_METHODS,
|
||||||
|
} = constants
|
||||||
|
|
||||||
|
let clock
|
||||||
|
|
||||||
|
const initPermLog = () => {
|
||||||
|
return new PermissionsLogController({
|
||||||
|
store: new ObservableStore(),
|
||||||
|
restrictedMethods: RESTRICTED_METHODS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockNext = (handler) => {
|
||||||
|
if (handler) {
|
||||||
|
handler(noop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initMiddleware = (permLog) => {
|
||||||
|
const middleware = permLog.createMiddleware()
|
||||||
|
return (req, res, next = mockNext) => {
|
||||||
|
middleware(req, res, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initClock = () => {
|
||||||
|
clock = useFakeTimers(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tearDownClock = () => {
|
||||||
|
clock.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSavedMockNext = (arr) => (handler) => {
|
||||||
|
arr.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('permissions log', function () {
|
||||||
|
|
||||||
|
describe('activity log', function () {
|
||||||
|
|
||||||
|
let permLog, logMiddleware
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permLog = initPermLog()
|
||||||
|
logMiddleware = initMiddleware(permLog)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('records activity for restricted methods', function () {
|
||||||
|
|
||||||
|
let log, req, res
|
||||||
|
|
||||||
|
// test_method, success
|
||||||
|
|
||||||
|
req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
req.id = REQUEST_IDS.a
|
||||||
|
res = { foo: 'bar' }
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
const entry1 = log[0]
|
||||||
|
|
||||||
|
assert.equal(log.length, 1, 'log should have single entry')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry1, { ...req }, { ...res },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// eth_accounts, failure
|
||||||
|
|
||||||
|
req = RPC_REQUESTS.eth_accounts(ORIGINS.b)
|
||||||
|
req.id = REQUEST_IDS.b
|
||||||
|
res = { error: new Error('Unauthorized.') }
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
const entry2 = log[1]
|
||||||
|
|
||||||
|
assert.equal(log.length, 2, 'log should have 2 entries')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry2, { ...req }, { ...res },
|
||||||
|
LOG_METHOD_TYPES.restricted, false
|
||||||
|
)
|
||||||
|
|
||||||
|
// eth_requestAccounts, success
|
||||||
|
|
||||||
|
req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.c)
|
||||||
|
req.id = REQUEST_IDS.c
|
||||||
|
res = { result: ACCOUNT_ARRAYS.c }
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
const entry3 = log[2]
|
||||||
|
|
||||||
|
assert.equal(log.length, 3, 'log should have 3 entries')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry3, { ...req }, { ...res },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// test_method, no response
|
||||||
|
|
||||||
|
req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
req.id = REQUEST_IDS.a
|
||||||
|
res = null
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
const entry4 = log[3]
|
||||||
|
|
||||||
|
assert.equal(log.length, 4, 'log should have 4 entries')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry4, { ...req }, null,
|
||||||
|
LOG_METHOD_TYPES.restricted, false
|
||||||
|
)
|
||||||
|
|
||||||
|
// validate final state
|
||||||
|
|
||||||
|
assert.equal(entry1, log[0], 'first log entry should remain')
|
||||||
|
assert.equal(entry2, log[1], 'second log entry should remain')
|
||||||
|
assert.equal(entry3, log[2], 'third log entry should remain')
|
||||||
|
assert.equal(entry4, log[3], 'fourth log entry should remain')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles responses added out of order', function () {
|
||||||
|
|
||||||
|
let log
|
||||||
|
|
||||||
|
const handlerArray = []
|
||||||
|
|
||||||
|
const id1 = nanoid()
|
||||||
|
const id2 = nanoid()
|
||||||
|
const id3 = nanoid()
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
|
||||||
|
// get make requests
|
||||||
|
req.id = id1
|
||||||
|
const res1 = { foo: id1 }
|
||||||
|
logMiddleware({ ...req }, { ...res1 }, getSavedMockNext(handlerArray))
|
||||||
|
|
||||||
|
req.id = id2
|
||||||
|
const res2 = { foo: id2 }
|
||||||
|
logMiddleware({ ...req }, { ...res2 }, getSavedMockNext(handlerArray))
|
||||||
|
|
||||||
|
req.id = id3
|
||||||
|
const res3 = { foo: id3 }
|
||||||
|
logMiddleware({ ...req }, { ...res3 }, getSavedMockNext(handlerArray))
|
||||||
|
|
||||||
|
// verify log state
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
assert.equal(log.length, 3, 'log should have 3 entries')
|
||||||
|
const entry1 = log[0]
|
||||||
|
const entry2 = log[1]
|
||||||
|
const entry3 = log[2]
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
entry1.id === id1 && entry1.response === null &&
|
||||||
|
entry2.id === id2 && entry2.response === null &&
|
||||||
|
entry3.id === id3 && entry3.response === null
|
||||||
|
),
|
||||||
|
'all entries should be in correct order and without responses'
|
||||||
|
)
|
||||||
|
|
||||||
|
// call response handlers
|
||||||
|
for (const i of [1, 2, 0]) {
|
||||||
|
handlerArray[i](noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify log state again
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
assert.equal(log.length, 3, 'log should have 3 entries')
|
||||||
|
|
||||||
|
// verify all entries
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
|
||||||
|
validateActivityEntry(
|
||||||
|
log[0], { ...req, id: id1 }, { ...res1 },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
validateActivityEntry(
|
||||||
|
log[1], { ...req, id: id2 }, { ...res2 },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
validateActivityEntry(
|
||||||
|
log[2], { ...req, id: id3 }, { ...res3 },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles a lack of response', function () {
|
||||||
|
|
||||||
|
let req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
req.id = REQUEST_IDS.a
|
||||||
|
let res = { foo: 'bar' }
|
||||||
|
|
||||||
|
// noop for next handler prevents recording of response
|
||||||
|
logMiddleware({ ...req }, res, noop)
|
||||||
|
|
||||||
|
let log = permLog.getActivityLog()
|
||||||
|
const entry1 = log[0]
|
||||||
|
|
||||||
|
assert.equal(log.length, 1, 'log should have single entry')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry1, { ...req }, null,
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// next request should be handled as normal
|
||||||
|
req = RPC_REQUESTS.eth_accounts(ORIGINS.b)
|
||||||
|
req.id = REQUEST_IDS.b
|
||||||
|
res = { result: ACCOUNT_ARRAYS.b }
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
const entry2 = log[1]
|
||||||
|
assert.equal(log.length, 2, 'log should have 2 entries')
|
||||||
|
validateActivityEntry(
|
||||||
|
entry2, { ...req }, { ...res },
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// validate final state
|
||||||
|
assert.equal(entry1, log[0], 'first log entry remains')
|
||||||
|
assert.equal(entry2, log[1], 'second log entry remains')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores expected methods', function () {
|
||||||
|
|
||||||
|
let log = permLog.getActivityLog()
|
||||||
|
assert.equal(log.length, 0, 'log should be empty')
|
||||||
|
|
||||||
|
const res = { foo: 'bar' }
|
||||||
|
const req1 = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, 'foobar')
|
||||||
|
const req2 = RPC_REQUESTS.custom(ORIGINS.b, 'eth_getBlockNumber')
|
||||||
|
const req3 = RPC_REQUESTS.custom(ORIGINS.b, 'net_version')
|
||||||
|
|
||||||
|
logMiddleware(req1, res)
|
||||||
|
logMiddleware(req2, res)
|
||||||
|
logMiddleware(req3, res)
|
||||||
|
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
assert.equal(log.length, 0, 'log should still be empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enforces log limit', function () {
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
const res = { foo: 'bar' }
|
||||||
|
|
||||||
|
// max out log
|
||||||
|
let lastId
|
||||||
|
for (let i = 0; i < LOG_LIMIT; i++) {
|
||||||
|
lastId = nanoid()
|
||||||
|
logMiddleware({ ...req, id: lastId }, { ...res })
|
||||||
|
}
|
||||||
|
|
||||||
|
// check last entry valid
|
||||||
|
let log = permLog.getActivityLog()
|
||||||
|
assert.equal(
|
||||||
|
log.length, LOG_LIMIT, 'log should have LOG_LIMIT num entries'
|
||||||
|
)
|
||||||
|
|
||||||
|
validateActivityEntry(
|
||||||
|
log[LOG_LIMIT - 1], { ...req, id: lastId }, res,
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// store the id of the current second entry
|
||||||
|
const nextFirstId = log[1]['id']
|
||||||
|
|
||||||
|
// add one more entry to log, putting it over the limit
|
||||||
|
lastId = nanoid()
|
||||||
|
logMiddleware({ ...req, id: lastId }, { ...res })
|
||||||
|
|
||||||
|
// check log length
|
||||||
|
log = permLog.getActivityLog()
|
||||||
|
assert.equal(
|
||||||
|
log.length, LOG_LIMIT, 'log should have LOG_LIMIT num entries'
|
||||||
|
)
|
||||||
|
|
||||||
|
// check first and last entries
|
||||||
|
validateActivityEntry(
|
||||||
|
log[0], { ...req, id: nextFirstId }, res,
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
|
||||||
|
validateActivityEntry(
|
||||||
|
log[LOG_LIMIT - 1], { ...req, id: lastId }, res,
|
||||||
|
LOG_METHOD_TYPES.restricted, true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('permissions history', function () {
|
||||||
|
|
||||||
|
let permLog, logMiddleware
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permLog = initPermLog()
|
||||||
|
logMiddleware = initMiddleware(permLog)
|
||||||
|
initClock()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
tearDownClock()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only updates history on responses', function () {
|
||||||
|
|
||||||
|
let permHistory
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
const res = { result: [ PERMS.granted.test_method() ] }
|
||||||
|
|
||||||
|
// noop => no response
|
||||||
|
logMiddleware({ ...req }, { ...res }, noop)
|
||||||
|
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
assert.deepEqual(permHistory, {}, 'history should not have been updated')
|
||||||
|
|
||||||
|
// response => records granted permissions
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
assert.equal(
|
||||||
|
Object.keys(permHistory).length, 1,
|
||||||
|
'history should have single origin'
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
Boolean(permHistory[ORIGINS.a]),
|
||||||
|
'history should have expected origin'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores malformed permissions requests', function () {
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
delete req.params
|
||||||
|
const res = { result: [ PERMS.granted.test_method() ] }
|
||||||
|
|
||||||
|
// no params => no response
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
assert.deepEqual(permLog.getHistory(), {}, 'history should not have been updated')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('records and updates account history as expected', async function () {
|
||||||
|
|
||||||
|
let permHistory
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {
|
||||||
|
result: [ PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.a) ],
|
||||||
|
}
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permHistory,
|
||||||
|
EXPECTED_HISTORIES.case1[0],
|
||||||
|
'should have correct history'
|
||||||
|
)
|
||||||
|
|
||||||
|
// mock permission requested again, with another approved account
|
||||||
|
|
||||||
|
clock.tick(1)
|
||||||
|
|
||||||
|
res.result = [ PERMS.granted.eth_accounts([ ACCOUNT_ARRAYS.a[0] ]) ]
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permHistory,
|
||||||
|
EXPECTED_HISTORIES.case1[1],
|
||||||
|
'should have correct history'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles eth_accounts response without caveats', async function () {
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {
|
||||||
|
result: [ PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.a) ],
|
||||||
|
}
|
||||||
|
delete res.result[0].caveats
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permLog.getHistory(), EXPECTED_HISTORIES.case2[0],
|
||||||
|
'should have expected history'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles extra caveats for eth_accounts', async function () {
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {
|
||||||
|
result: [ PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.a) ],
|
||||||
|
}
|
||||||
|
res.result[0].caveats.push({ foo: 'bar' })
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permLog.getHistory(),
|
||||||
|
EXPECTED_HISTORIES.case1[0],
|
||||||
|
'should have correct history'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// wallet_requestPermissions returns all permissions approved for the
|
||||||
|
// requesting origin, including old ones
|
||||||
|
it('handles unrequested permissions on the response', async function () {
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {
|
||||||
|
result: [
|
||||||
|
PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.a),
|
||||||
|
PERMS.granted.test_method(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permLog.getHistory(),
|
||||||
|
EXPECTED_HISTORIES.case1[0],
|
||||||
|
'should have correct history'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not update history if no new permissions are approved', async function () {
|
||||||
|
|
||||||
|
let req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
let res = {
|
||||||
|
result: [
|
||||||
|
PERMS.granted.test_method(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permLog.getHistory(),
|
||||||
|
EXPECTED_HISTORIES.case4[0],
|
||||||
|
'should have correct history'
|
||||||
|
)
|
||||||
|
|
||||||
|
// new permission requested, but not approved
|
||||||
|
|
||||||
|
clock.tick(1)
|
||||||
|
|
||||||
|
req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
res = {
|
||||||
|
result: [
|
||||||
|
PERMS.granted.test_method(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
logMiddleware({ ...req }, { ...res })
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permLog.getHistory(),
|
||||||
|
EXPECTED_HISTORIES.case4[0],
|
||||||
|
'should have same history as before'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('records and updates history for multiple origins, regardless of response order', async function () {
|
||||||
|
|
||||||
|
let permHistory
|
||||||
|
|
||||||
|
// make first round of requests
|
||||||
|
|
||||||
|
const round1 = []
|
||||||
|
const handlers1 = []
|
||||||
|
|
||||||
|
// first origin
|
||||||
|
round1.push({
|
||||||
|
req: RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
),
|
||||||
|
res: {
|
||||||
|
result: [ PERMS.granted.test_method() ],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// second origin
|
||||||
|
round1.push({
|
||||||
|
req: RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.b, PERM_NAMES.eth_accounts
|
||||||
|
),
|
||||||
|
res: {
|
||||||
|
result: [ PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.b) ],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// third origin
|
||||||
|
round1.push({
|
||||||
|
req: RPC_REQUESTS.requestPermissions(ORIGINS.c, {
|
||||||
|
[PERM_NAMES.test_method]: {},
|
||||||
|
[PERM_NAMES.eth_accounts]: {},
|
||||||
|
}),
|
||||||
|
res: {
|
||||||
|
result: [
|
||||||
|
PERMS.granted.test_method(),
|
||||||
|
PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.c),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// make requests and process responses out of order
|
||||||
|
round1.forEach((x) => {
|
||||||
|
logMiddleware({ ...x.req }, { ...x.res }, getSavedMockNext(handlers1))
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const i of [1, 2, 0]) {
|
||||||
|
handlers1[i](noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permHistory, EXPECTED_HISTORIES.case3[0],
|
||||||
|
'should have expected history'
|
||||||
|
)
|
||||||
|
|
||||||
|
// make next round of requests
|
||||||
|
|
||||||
|
clock.tick(1)
|
||||||
|
|
||||||
|
const round2 = []
|
||||||
|
// we're just gonna process these in order
|
||||||
|
|
||||||
|
// first origin
|
||||||
|
round2.push({
|
||||||
|
req: RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
),
|
||||||
|
res: {
|
||||||
|
result: [ PERMS.granted.test_method() ],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// nothing for second origin
|
||||||
|
|
||||||
|
// third origin
|
||||||
|
round2.push({
|
||||||
|
req: RPC_REQUESTS.requestPermissions(ORIGINS.c, {
|
||||||
|
[PERM_NAMES.eth_accounts]: {},
|
||||||
|
}),
|
||||||
|
res: {
|
||||||
|
result: [
|
||||||
|
PERMS.granted.eth_accounts(ACCOUNT_ARRAYS.b),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// make requests
|
||||||
|
round2.forEach((x) => {
|
||||||
|
logMiddleware({ ...x.req }, { ...x.res })
|
||||||
|
})
|
||||||
|
|
||||||
|
// validate history
|
||||||
|
permHistory = permLog.getHistory()
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
permHistory, EXPECTED_HISTORIES.case3[1],
|
||||||
|
'should have expected history'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('instance method edge cases', function () {
|
||||||
|
|
||||||
|
it('logAccountExposure errors on invalid params', function () {
|
||||||
|
|
||||||
|
const permLog = initPermLog()
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => {
|
||||||
|
permLog.logAccountExposure('', ACCOUNT_ARRAYS.a)
|
||||||
|
},
|
||||||
|
ERRORS.logAccountExposure.invalidParams(),
|
||||||
|
'should throw expected error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => {
|
||||||
|
permLog.logAccountExposure(null, ACCOUNT_ARRAYS.a)
|
||||||
|
},
|
||||||
|
ERRORS.logAccountExposure.invalidParams(),
|
||||||
|
'should throw expected error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => {
|
||||||
|
permLog.logAccountExposure('foo', {})
|
||||||
|
},
|
||||||
|
ERRORS.logAccountExposure.invalidParams(),
|
||||||
|
'should throw expected error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => {
|
||||||
|
permLog.logAccountExposure('foo', [])
|
||||||
|
},
|
||||||
|
ERRORS.logAccountExposure.invalidParams(),
|
||||||
|
'should throw expected error'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,752 @@
|
|||||||
|
import { strict as assert } from 'assert'
|
||||||
|
|
||||||
|
import {
|
||||||
|
METADATA_STORE_KEY,
|
||||||
|
} from '../../../../../app/scripts/controllers/permissions/enums'
|
||||||
|
|
||||||
|
import {
|
||||||
|
PermissionsController,
|
||||||
|
} from '../../../../../app/scripts/controllers/permissions'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUserApprovalPromise,
|
||||||
|
grantPermissions,
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
import {
|
||||||
|
constants,
|
||||||
|
getters,
|
||||||
|
getPermControllerOpts,
|
||||||
|
getPermissionsMiddleware,
|
||||||
|
} from './mocks'
|
||||||
|
|
||||||
|
const {
|
||||||
|
CAVEATS,
|
||||||
|
ERRORS,
|
||||||
|
PERMS,
|
||||||
|
RPC_REQUESTS,
|
||||||
|
} = getters
|
||||||
|
|
||||||
|
const {
|
||||||
|
ACCOUNT_ARRAYS,
|
||||||
|
ORIGINS,
|
||||||
|
PERM_NAMES,
|
||||||
|
} = constants
|
||||||
|
|
||||||
|
const validatePermission = (perm, name, origin, caveats) => {
|
||||||
|
assert.equal(name, perm.parentCapability, 'should have expected permission name')
|
||||||
|
assert.equal(origin, perm.invoker, 'should have expected permission origin')
|
||||||
|
if (caveats) {
|
||||||
|
assert.deepEqual(caveats, perm.caveats, 'should have expected permission caveats')
|
||||||
|
} else {
|
||||||
|
assert.ok(!perm.caveats, 'should not have any caveats')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPermController = () => {
|
||||||
|
return new PermissionsController({
|
||||||
|
...getPermControllerOpts(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('permissions middleware', function () {
|
||||||
|
|
||||||
|
describe('wallet_requestPermissions', function () {
|
||||||
|
|
||||||
|
let permController
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permController = initPermController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('grants permissions on user approval', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const pendingApproval = assert.doesNotReject(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 1,
|
||||||
|
'perm controller should have single pending approval',
|
||||||
|
)
|
||||||
|
|
||||||
|
const id = permController.pendingApprovals.keys().next().value
|
||||||
|
const approvedReq = PERMS.approvedRequest(id, PERMS.requests.eth_accounts())
|
||||||
|
|
||||||
|
await permController.approvePermissionsRequest(approvedReq, ACCOUNT_ARRAYS.a)
|
||||||
|
await pendingApproval
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res.result && !res.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.result.length, 1,
|
||||||
|
'origin should have single approved permission'
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePermission(
|
||||||
|
res.result[0],
|
||||||
|
PERM_NAMES.eth_accounts,
|
||||||
|
ORIGINS.a,
|
||||||
|
[CAVEATS.eth_accounts(ACCOUNT_ARRAYS.a)]
|
||||||
|
)
|
||||||
|
|
||||||
|
const aAccounts = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
aAccounts, ACCOUNT_ARRAYS.a,
|
||||||
|
'origin should have correct accounts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles serial approved requests that overwrite existing permissions', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
// create first request
|
||||||
|
|
||||||
|
const req1 = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res1 = {}
|
||||||
|
|
||||||
|
// send, approve, and validate first request
|
||||||
|
// note use of ACCOUNT_ARRAYS.a
|
||||||
|
|
||||||
|
const pendingApproval1 = assert.doesNotReject(
|
||||||
|
aMiddleware(req1, res1),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
const id1 = permController.pendingApprovals.keys().next().value
|
||||||
|
const approvedReq1 = PERMS.approvedRequest(id1, PERMS.requests.eth_accounts())
|
||||||
|
|
||||||
|
await permController.approvePermissionsRequest(approvedReq1, ACCOUNT_ARRAYS.a)
|
||||||
|
await pendingApproval1
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res1.result && !res1.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res1.result.length, 1,
|
||||||
|
'origin should have single approved permission'
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePermission(
|
||||||
|
res1.result[0],
|
||||||
|
PERM_NAMES.eth_accounts,
|
||||||
|
ORIGINS.a,
|
||||||
|
[CAVEATS.eth_accounts(ACCOUNT_ARRAYS.a)]
|
||||||
|
)
|
||||||
|
|
||||||
|
const accounts1 = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
accounts1, ACCOUNT_ARRAYS.a,
|
||||||
|
'origin should have correct accounts'
|
||||||
|
)
|
||||||
|
|
||||||
|
// create second request
|
||||||
|
|
||||||
|
const requestedPerms2 = {
|
||||||
|
...PERMS.requests.eth_accounts(),
|
||||||
|
...PERMS.requests.test_method(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const req2 = RPC_REQUESTS.requestPermissions(
|
||||||
|
ORIGINS.a, { ...requestedPerms2 }
|
||||||
|
)
|
||||||
|
const res2 = {}
|
||||||
|
|
||||||
|
// send, approve, and validate second request
|
||||||
|
// note use of ACCOUNT_ARRAYS.b
|
||||||
|
|
||||||
|
const pendingApproval2 = assert.doesNotReject(
|
||||||
|
aMiddleware(req2, res2),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
const id2 = permController.pendingApprovals.keys().next().value
|
||||||
|
const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 })
|
||||||
|
|
||||||
|
await permController.approvePermissionsRequest(approvedReq2, ACCOUNT_ARRAYS.b)
|
||||||
|
await pendingApproval2
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res2.result && !res2.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res2.result.length, 2,
|
||||||
|
'origin should have single approved permission'
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePermission(
|
||||||
|
res2.result[0],
|
||||||
|
PERM_NAMES.eth_accounts,
|
||||||
|
ORIGINS.a,
|
||||||
|
[CAVEATS.eth_accounts(ACCOUNT_ARRAYS.b)]
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePermission(
|
||||||
|
res2.result[1],
|
||||||
|
PERM_NAMES.test_method,
|
||||||
|
ORIGINS.a,
|
||||||
|
)
|
||||||
|
|
||||||
|
const accounts2 = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
accounts2, ACCOUNT_ARRAYS.b,
|
||||||
|
'origin should have correct accounts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects permissions on user rejection', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.eth_accounts
|
||||||
|
)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const expectedError = ERRORS.rejectPermissionsRequest.rejection()
|
||||||
|
|
||||||
|
const requestRejection = assert.rejects(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
expectedError,
|
||||||
|
'request should be rejected with correct error',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 1,
|
||||||
|
'perm controller should have single pending approval',
|
||||||
|
)
|
||||||
|
|
||||||
|
const id = permController.pendingApprovals.keys().next().value
|
||||||
|
|
||||||
|
await permController.rejectPermissionsRequest(id)
|
||||||
|
await requestRejection
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
!res.result && res.error &&
|
||||||
|
res.error.message === expectedError.message
|
||||||
|
),
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
|
||||||
|
const aAccounts = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
aAccounts, [], 'origin should have have correct accounts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects requests with unknown permissions', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.requestPermissions(
|
||||||
|
ORIGINS.a, {
|
||||||
|
...PERMS.requests.does_not_exist(),
|
||||||
|
...PERMS.requests.test_method(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const expectedError = ERRORS.rejectPermissionsRequest.methodNotFound(
|
||||||
|
PERM_NAMES.does_not_exist
|
||||||
|
)
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
expectedError,
|
||||||
|
'request should be rejected with correct error',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 0,
|
||||||
|
'perm controller should have no pending approvals',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
!res.result && res.error &&
|
||||||
|
res.error.message === expectedError.message
|
||||||
|
),
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts only a single pending permissions request per origin', async function () {
|
||||||
|
|
||||||
|
const expectedError = ERRORS.pendingApprovals.requestAlreadyPending()
|
||||||
|
|
||||||
|
// two middlewares for two origins
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
const bMiddleware = getPermissionsMiddleware(permController, ORIGINS.b)
|
||||||
|
|
||||||
|
// create and start processing first request for first origin
|
||||||
|
|
||||||
|
const reqA1 = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
const resA1 = {}
|
||||||
|
|
||||||
|
const requestApproval1 = assert.doesNotReject(
|
||||||
|
aMiddleware(reqA1, resA1),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
// create and start processing first request for second origin
|
||||||
|
|
||||||
|
const reqB1 = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.b, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
const resB1 = {}
|
||||||
|
|
||||||
|
const requestApproval2 = assert.doesNotReject(
|
||||||
|
bMiddleware(reqB1, resB1),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 2,
|
||||||
|
'perm controller should have expected number of pending approvals',
|
||||||
|
)
|
||||||
|
|
||||||
|
// create and start processing second request for first origin,
|
||||||
|
// which should throw
|
||||||
|
|
||||||
|
const reqA2 = RPC_REQUESTS.requestPermission(
|
||||||
|
ORIGINS.a, PERM_NAMES.test_method
|
||||||
|
)
|
||||||
|
const resA2 = {}
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
aMiddleware(reqA2, resA2),
|
||||||
|
expectedError,
|
||||||
|
'request should be rejected with correct error',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
!resA2.result && resA2.error &&
|
||||||
|
resA2.error.message === expectedError.message
|
||||||
|
),
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
|
||||||
|
// first requests for both origins should remain
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 2,
|
||||||
|
'perm controller should have expected number of pending approvals',
|
||||||
|
)
|
||||||
|
|
||||||
|
// now, remaining pending requests should be approved without issue
|
||||||
|
|
||||||
|
for (const id of permController.pendingApprovals.keys()) {
|
||||||
|
await permController.approvePermissionsRequest(
|
||||||
|
PERMS.approvedRequest(id, PERMS.requests.test_method())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await requestApproval1
|
||||||
|
await requestApproval2
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
resA1.result && !resA1.error,
|
||||||
|
'first response should have result and no error'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resA1.result.length, 1,
|
||||||
|
'first origin should have single approved permission'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
resB1.result && !resB1.error,
|
||||||
|
'second response should have result and no error'
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resB1.result.length, 1,
|
||||||
|
'second origin should have single approved permission'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 0,
|
||||||
|
'perm controller should have expected number of pending approvals',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('restricted methods', function () {
|
||||||
|
|
||||||
|
let permController
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permController = initPermController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prevents restricted method access for unpermitted domain', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.test_method(ORIGINS.a)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const expectedError = ERRORS.rpcCap.unauthorized()
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
expectedError,
|
||||||
|
'request should be rejected with correct error',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
!res.result && res.error &&
|
||||||
|
res.error.code === expectedError.code
|
||||||
|
),
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows restricted method access for permitted domain', async function () {
|
||||||
|
|
||||||
|
const bMiddleware = getPermissionsMiddleware(permController, ORIGINS.b)
|
||||||
|
|
||||||
|
grantPermissions(permController, ORIGINS.b, PERMS.finalizedRequests.test_method())
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.test_method(ORIGINS.b, true)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
bMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res.result && res.result === 1,
|
||||||
|
'response should have correct result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('eth_accounts', function () {
|
||||||
|
|
||||||
|
let permController
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permController = initPermController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array for non-permitted domain', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.eth_accounts(ORIGINS.a)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res.result && !res.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
res.result, [],
|
||||||
|
'response should have correct result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns correct accounts for permitted domain', async function () {
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
grantPermissions(
|
||||||
|
permController, ORIGINS.a,
|
||||||
|
PERMS.finalizedRequests.eth_accounts(ACCOUNT_ARRAYS.a)
|
||||||
|
)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.eth_accounts(ORIGINS.a)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res.result && !res.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
res.result, ACCOUNT_ARRAYS.a,
|
||||||
|
'response should have correct result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('eth_requestAccounts', function () {
|
||||||
|
|
||||||
|
let permController
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permController = initPermController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests accounts for unpermitted origin, and approves on user approval', async function () {
|
||||||
|
|
||||||
|
const userApprovalPromise = getUserApprovalPromise(permController)
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.a)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const pendingApproval = assert.doesNotReject(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
'should not reject permissions request'
|
||||||
|
)
|
||||||
|
|
||||||
|
await userApprovalPromise
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 1,
|
||||||
|
'perm controller should have single pending approval',
|
||||||
|
)
|
||||||
|
|
||||||
|
const id = permController.pendingApprovals.keys().next().value
|
||||||
|
const approvedReq = PERMS.approvedRequest(id, PERMS.requests.eth_accounts())
|
||||||
|
|
||||||
|
await permController.approvePermissionsRequest(approvedReq, ACCOUNT_ARRAYS.a)
|
||||||
|
|
||||||
|
// at this point, the permission should have been granted
|
||||||
|
const perms = permController.permissions.getPermissionsForDomain(ORIGINS.a)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
perms.length, 1,
|
||||||
|
'domain should have correct number of permissions'
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePermission(
|
||||||
|
perms[0],
|
||||||
|
PERM_NAMES.eth_accounts,
|
||||||
|
ORIGINS.a,
|
||||||
|
[CAVEATS.eth_accounts(ACCOUNT_ARRAYS.a)]
|
||||||
|
)
|
||||||
|
|
||||||
|
await pendingApproval
|
||||||
|
|
||||||
|
// we should also see the accounts on the response
|
||||||
|
assert.ok(
|
||||||
|
res.result && !res.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
res.result, ACCOUNT_ARRAYS.a,
|
||||||
|
'result should have correct accounts'
|
||||||
|
)
|
||||||
|
|
||||||
|
// we should also be able to get the accounts independently
|
||||||
|
const aAccounts = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
aAccounts, ACCOUNT_ARRAYS.a, 'origin should have have correct accounts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests accounts for unpermitted origin, and rejects on user rejection', async function () {
|
||||||
|
|
||||||
|
const userApprovalPromise = getUserApprovalPromise(permController)
|
||||||
|
|
||||||
|
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.a)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
const expectedError = ERRORS.rejectPermissionsRequest.rejection()
|
||||||
|
|
||||||
|
const requestRejection = assert.rejects(
|
||||||
|
aMiddleware(req, res),
|
||||||
|
expectedError,
|
||||||
|
'request should be rejected with correct error',
|
||||||
|
)
|
||||||
|
|
||||||
|
await userApprovalPromise
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
permController.pendingApprovals.size, 1,
|
||||||
|
'perm controller should have single pending approval',
|
||||||
|
)
|
||||||
|
|
||||||
|
const id = permController.pendingApprovals.keys().next().value
|
||||||
|
|
||||||
|
await permController.rejectPermissionsRequest(id)
|
||||||
|
await requestRejection
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
(
|
||||||
|
!res.result && res.error &&
|
||||||
|
res.error.message === expectedError.message
|
||||||
|
),
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
|
||||||
|
const aAccounts = await permController.getAccounts(ORIGINS.a)
|
||||||
|
assert.deepEqual(
|
||||||
|
aAccounts, [], 'origin should have have correct accounts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('directly returns accounts for permitted domain', async function () {
|
||||||
|
|
||||||
|
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
|
||||||
|
|
||||||
|
grantPermissions(
|
||||||
|
permController, ORIGINS.c,
|
||||||
|
PERMS.finalizedRequests.eth_accounts(ACCOUNT_ARRAYS.c)
|
||||||
|
)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.c)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
cMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
res.result && !res.error,
|
||||||
|
'response should have result and no error'
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
res.result, ACCOUNT_ARRAYS.c,
|
||||||
|
'response should have correct result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('wallet_sendDomainMetadata', function () {
|
||||||
|
|
||||||
|
let permController
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
permController = initPermController()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('records domain metadata', async function () {
|
||||||
|
|
||||||
|
const name = 'BAZ'
|
||||||
|
|
||||||
|
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
cMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(res.result, 'result should be true')
|
||||||
|
|
||||||
|
const metadataStore = permController.store.getState()[METADATA_STORE_KEY]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
metadataStore,
|
||||||
|
{ [ORIGINS.c]: { name, extensionId: undefined } },
|
||||||
|
'metadata should have been added to store'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('records domain metadata and preserves extensionId', async function () {
|
||||||
|
|
||||||
|
const extensionId = 'fooExtension'
|
||||||
|
|
||||||
|
const name = 'BAZ'
|
||||||
|
|
||||||
|
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c, extensionId)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
cMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(res.result, 'result should be true')
|
||||||
|
|
||||||
|
const metadataStore = permController.store.getState()[METADATA_STORE_KEY]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
metadataStore,
|
||||||
|
{ [ORIGINS.c]: { name, extensionId } },
|
||||||
|
'metadata should have been added to store'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not record domain metadata if no name', async function () {
|
||||||
|
|
||||||
|
const name = null
|
||||||
|
|
||||||
|
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name)
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
cMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(res.result, 'result should be true')
|
||||||
|
|
||||||
|
const metadataStore = permController.store.getState()[METADATA_STORE_KEY]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
metadataStore, {},
|
||||||
|
'metadata should not have been added to store'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not record domain metadata if no metadata', async function () {
|
||||||
|
|
||||||
|
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
|
||||||
|
|
||||||
|
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c)
|
||||||
|
delete req.domainMetadata
|
||||||
|
const res = {}
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
cMiddleware(req, res),
|
||||||
|
'should not reject'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(res.result, 'result should be true')
|
||||||
|
|
||||||
|
const metadataStore = permController.store.getState()[METADATA_STORE_KEY]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
metadataStore, {},
|
||||||
|
'metadata should not have been added to store'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,35 @@
|
|||||||
|
import { strict as assert } from 'assert'
|
||||||
|
|
||||||
|
import getRestrictedMethods
|
||||||
|
from '../../../../../app/scripts/controllers/permissions/restrictedMethods'
|
||||||
|
|
||||||
|
describe('restricted methods', function () {
|
||||||
|
|
||||||
|
// this method is tested extensively in other permissions tests
|
||||||
|
describe('eth_accounts', function () {
|
||||||
|
|
||||||
|
it('handles failure', async function () {
|
||||||
|
const restrictedMethods = getRestrictedMethods({
|
||||||
|
getKeyringAccounts: async () => {
|
||||||
|
throw new Error('foo')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = {}
|
||||||
|
restrictedMethods.eth_accounts.method(null, res, null, (err) => {
|
||||||
|
|
||||||
|
const fooError = new Error('foo')
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
err, fooError,
|
||||||
|
'should end with expected error'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
res, { error: fooError },
|
||||||
|
'response should have expected error and no result'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -24638,10 +24638,10 @@ rn-host-detect@^1.1.5:
|
|||||||
resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.1.5.tgz#fbecb982b73932f34529e97932b9a63e58d8deb6"
|
resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.1.5.tgz#fbecb982b73932f34529e97932b9a63e58d8deb6"
|
||||||
integrity sha512-ufk2dFT3QeP9HyZ/xTuMtW27KnFy815CYitJMqQm+pgG3ZAtHBsrU8nXizNKkqXGy3bQmhEoloVbrfbvMJMqkg==
|
integrity sha512-ufk2dFT3QeP9HyZ/xTuMtW27KnFy815CYitJMqQm+pgG3ZAtHBsrU8nXizNKkqXGy3bQmhEoloVbrfbvMJMqkg==
|
||||||
|
|
||||||
rpc-cap@^1.0.5:
|
rpc-cap@^2.0.0:
|
||||||
version "1.0.5"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/rpc-cap/-/rpc-cap-1.0.5.tgz#6f5ec41a7f0f85eb9aca8ccc7e618625109e6430"
|
resolved "https://registry.yarnpkg.com/rpc-cap/-/rpc-cap-2.0.0.tgz#575ff22417bdea9526292b8989c7b7af5e664bfa"
|
||||||
integrity sha512-uZLjb609EbR+STeiLg27CVRWSKn/iKWeSkVYH5D1dj34ffpYcq6ByQBT7IN0AXozLUxmLulgpETB3gOS7fsH2w==
|
integrity sha512-P78tE+fIOxIkxcfZ8S1ge+Mt02AH4vMXdcFjr2uWLOdouDosgJOvJP5oROYx6qiUYbECuyKrsmETU7e+MA8vpg==
|
||||||
dependencies:
|
dependencies:
|
||||||
clone "^2.1.2"
|
clone "^2.1.2"
|
||||||
eth-json-rpc-errors "^2.0.2"
|
eth-json-rpc-errors "^2.0.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user