A Metamask fork with Infura removed and default networks editable
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
ciphermask/app/scripts/controllers/permissions/permissions-log-controller....

680 lines
17 KiB

import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import nanoid from 'nanoid';
import { useFakeTimers } from 'sinon';
import {
constants,
getters,
noop,
} from '../../../../test/mocks/permission-controller';
import { validateActivityEntry } from '../../../../test/helpers/permission-controller-helpers';
import PermissionsLogController from './permissionsLog';
import { LOG_LIMIT, LOG_METHOD_TYPES } from './enums';
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',
);
});
});
});