Skip to content

Commit

Permalink
Add generic caveat validation utility (#2122)
Browse files Browse the repository at this point in the history
Add a utility function for validating caveat types for a permission.
This lets us generically validate permissions with multiple caveat types
without duplicating validation code across the permission
specifications. This is useful long-term as we add more permissions that
have more than 1 caveat type.

This is a prerequisite for #2113
and #2098
  • Loading branch information
FrederikBolding authored Jan 22, 2024
1 parent cf1fa9a commit f1903db
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 4 deletions.
8 changes: 4 additions & 4 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 90.77,
"functions": 96.6,
"lines": 97.84,
"statements": 97.54
"branches": 90.87,
"functions": 96.7,
"lines": 97.86,
"statements": 97.57
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { PermissionConstraint } from '@metamask/permission-controller';
import { SnapCaveatType } from '@metamask/snaps-utils';
import { MOCK_ORIGIN } from '@metamask/snaps-utils/test-utils';

import { createGenericPermissionValidator } from './generic';

const MOCK_PERMISSION: PermissionConstraint = {
caveats: null,
date: 1664187844588,
id: 'izn0WGUO8cvq_jqvLQuQP',
invoker: MOCK_ORIGIN,
parentCapability: 'snap_dialog',
};

describe('createGenericPermissionValidator', () => {
it('fails if required caveats are not specified', () => {
const validator = createGenericPermissionValidator([
{ type: SnapCaveatType.ChainIds },
{ type: SnapCaveatType.RpcOrigin },
]);

expect(() =>
validator({
...MOCK_PERMISSION,
caveats: [{ type: SnapCaveatType.ChainIds, value: null }],
}),
).toThrow('Expected the following caveats: "chainIds", "rpcOrigin".');
});

it('fails if caveats are of the wrong type', () => {
const validator = createGenericPermissionValidator([
{ type: SnapCaveatType.ChainIds },
{ type: SnapCaveatType.RpcOrigin },
]);

expect(() =>
validator({
...MOCK_PERMISSION,
caveats: [
{ type: SnapCaveatType.KeyringOrigin, value: null },
{ type: SnapCaveatType.SnapIds, value: null },
],
}),
).toThrow(
'Expected the following caveats: "chainIds", "rpcOrigin", received "keyringOrigin", "snapIds".',
);
});

it('fails if too many caveats specified', () => {
const validator = createGenericPermissionValidator([
{ type: SnapCaveatType.ChainIds },
{ type: SnapCaveatType.RpcOrigin },
]);

expect(() =>
validator({
...MOCK_PERMISSION,
caveats: [
{ type: SnapCaveatType.ChainIds, value: null },
{ type: SnapCaveatType.ChainIds, value: null },
{ type: SnapCaveatType.RpcOrigin, value: null },
],
}),
).toThrow('Duplicate caveats are not allowed.');
});

it('does not fail if optional caveat is missing', () => {
const validator = createGenericPermissionValidator([
{ type: SnapCaveatType.ChainIds },
{ type: SnapCaveatType.RpcOrigin, optional: true },
]);

expect(() =>
validator({
...MOCK_PERMISSION,
caveats: [{ type: SnapCaveatType.ChainIds, value: null }],
}),
).not.toThrow();
});

it('does not fail if optional caveats is missing', () => {
const validator = createGenericPermissionValidator([
{ type: SnapCaveatType.ChainIds, optional: true },
{ type: SnapCaveatType.RpcOrigin, optional: true },
]);

expect(() =>
validator({
...MOCK_PERMISSION,
caveats: [{ type: SnapCaveatType.ChainIds, value: null }],
}),
).not.toThrow();

expect(() => validator(MOCK_PERMISSION)).not.toThrow();
});
});
60 changes: 60 additions & 0 deletions packages/snaps-controllers/src/snaps/endowments/caveats/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { PermissionValidatorConstraint } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';

/**
* Create a generic permission validator that validates the presence of certain caveats.
*
* This validator only validates the types of the caveats, not the values.
*
* @param caveatsToValidate - A list of objects that represent caveats.
* @param caveatsToValidate.type - The string defining the caveat type.
* @param caveatsToValidate.optional - An optional boolean flag that defines
* whether the caveat is optional or not.
* @returns A function that validates a permission.
*/
export function createGenericPermissionValidator(
caveatsToValidate: {
type: string;
optional?: boolean;
}[],
): PermissionValidatorConstraint {
const validCaveatTypes = new Set(
caveatsToValidate.map((caveat) => caveat.type),
);
const requiredCaveats = caveatsToValidate.filter(
(caveat) => !caveat.optional,
);

return function ({ caveats }) {
const actualCaveats = caveats ?? [];
const passedCaveatTypes = actualCaveats.map((caveat) => caveat.type);
const passedCaveatsSet = new Set(passedCaveatTypes);

// Disallow duplicates
if (passedCaveatsSet.size !== passedCaveatTypes.length) {
throw rpcErrors.invalidParams({
message: 'Duplicate caveats are not allowed.',
});
}

// Disallow caveats that don't match expected types
if (!actualCaveats.every((caveat) => validCaveatTypes.has(caveat.type))) {
throw rpcErrors.invalidParams({
message: `Expected the following caveats: ${caveatsToValidate
.map((caveat) => `"${caveat.type}"`)
.join(', ')}, received ${actualCaveats
.map((caveat) => `"${caveat.type}"`)
.join(', ')}.`,
});
}

// Fail if not all required caveats are specified
if (!requiredCaveats.every((caveat) => passedCaveatsSet.has(caveat.type))) {
throw rpcErrors.invalidParams({
message: `Expected the following caveats: ${requiredCaveats
.map((caveat) => `"${caveat.type}"`)
.join(', ')}.`,
});
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './generic';

0 comments on commit f1903db

Please sign in to comment.