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' ) }) }) })