mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
b1d090ac4d
* 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>
691 lines
17 KiB
JavaScript
691 lines
17 KiB
JavaScript
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'
|
|
)
|
|
})
|
|
})
|
|
})
|