import { strict as assert } from 'assert' import { ObservableStore } from '@metamask/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 { PERMS, RPC_REQUESTS } = getters const { ACCOUNTS, EXPECTED_HISTORIES, DOMAINS, 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 = () => { // useFakeTimers, is in fact, not a react-hook // eslint-disable-next-line 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(DOMAINS.a.origin) 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(DOMAINS.b.origin) 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(DOMAINS.c.origin) req.id = REQUEST_IDS.c res = { result: ACCOUNTS.c.permitted } 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(DOMAINS.a.origin) 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(DOMAINS.a.origin) // 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(DOMAINS.a.origin) 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(DOMAINS.b.origin) req.id = REQUEST_IDS.b res = { result: ACCOUNTS.b.permitted } 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.metamask_sendDomainMetadata( DOMAINS.c.origin, 'foobar', ) const req2 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'eth_getBlockNumber') const req3 = RPC_REQUESTS.custom(DOMAINS.b.origin, '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(DOMAINS.a.origin) 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( DOMAINS.a.origin, 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[DOMAINS.a.origin]), 'history should have expected origin', ) }) it('ignores malformed permissions requests', function () { const req = RPC_REQUESTS.requestPermission( DOMAINS.a.origin, 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( DOMAINS.a.origin, PERM_NAMES.eth_accounts, ) const res = { result: [PERMS.granted.eth_accounts(ACCOUNTS.a.permitted)], } 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([ACCOUNTS.a.permitted[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( DOMAINS.a.origin, PERM_NAMES.eth_accounts, ) const res = { result: [PERMS.granted.eth_accounts(ACCOUNTS.a.permitted)], } 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( DOMAINS.a.origin, PERM_NAMES.eth_accounts, ) const res = { result: [PERMS.granted.eth_accounts(ACCOUNTS.a.permitted)], } 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( DOMAINS.a.origin, PERM_NAMES.eth_accounts, ) const res = { result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted), 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( DOMAINS.a.origin, 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( DOMAINS.a.origin, 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( DOMAINS.a.origin, PERM_NAMES.test_method, ), res: { result: [PERMS.granted.test_method()], }, }) // second origin round1.push({ req: RPC_REQUESTS.requestPermission( DOMAINS.b.origin, PERM_NAMES.eth_accounts, ), res: { result: [PERMS.granted.eth_accounts(ACCOUNTS.b.permitted)], }, }) // third origin round1.push({ req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, { [PERM_NAMES.test_method]: {}, [PERM_NAMES.eth_accounts]: {}, }), res: { result: [ PERMS.granted.test_method(), PERMS.granted.eth_accounts(ACCOUNTS.c.permitted), ], }, }) // 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( DOMAINS.a.origin, PERM_NAMES.test_method, ), res: { result: [PERMS.granted.test_method()], }, }) // nothing for second origin // third origin round2.push({ req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, { [PERM_NAMES.eth_accounts]: {}, }), res: { result: [PERMS.granted.eth_accounts(ACCOUNTS.b.permitted)], }, }) // 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', ) }) }) })