Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add abstraction for Snaps permissions #25175

Merged
merged 5 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions shared/constants/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,45 @@ export const ConnectionPermission = Object.freeze({
connection_permission: 'connection_permission',
});

// This configuration specifies permission weight thresholds used to determine which
// permissions to show or hide on certain Snap-related flows (Install, Update, etc.)
export const PermissionWeightThreshold = Object.freeze({
snapInstall: 3 as const,
snapUpdateApprovedPermissions: 2 as const,
david0xd marked this conversation as resolved.
Show resolved Hide resolved
});

// Specify minimum number of permissions to be shown, when abstraction is applied
export const MinPermissionAbstractionDisplayCount = 3;

// Specify number of permissions used as threshold for permission abstraction logic to be applied
export const PermissionsAbstractionThreshold = 3;

export const PermissionWeight = Object.freeze({
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
eth_accounts: 3,
permittedChains: 3,
snap_dialog: 4,
snap_notify: 4,
snap_getBip32PublicKey: 3,
snap_getBip32Entropy: 1,
snap_getBip44Entropy: 1,
snap_getEntropy: 3,
snap_manageState: 4,
snap_getLocale: 4,
wallet_snap: 4,
endowment_networkAccess: 3,
david0xd marked this conversation as resolved.
Show resolved Hide resolved
endowment_webassembly: 4,
endowment_transactionInsight: 4,
endowment_cronjob: 4,
endowment_ethereumProvider: 4,
endowment_rpc: 4,
endowment_lifecycleHooks: 4,
endowment_pageHome: 4,
snap_manageAccounts: 3,
endowment_keyring: 4,
endowment_nameLookup: 3,
endowment_signatureInsight: 4,
connection_permission: 3,
unknown_permission: 3,
});

export * from './snaps/permissions';
48 changes: 48 additions & 0 deletions test/data/mock-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,54 @@
}
}
}
},
"subjects": {
"npm:@metamask/test-snap-bip44": {
"origin": "npm:@metamask/test-snap-bip44",
"permissions": {
"endowment:rpc": {
"caveats": [
{
"type": "rpcOrigin",
"value": {
"allowedOrigins": ["npm:@metamask/json-rpc-example-snap"],
"dapps": true
}
}
],
"date": 1718117256761,
"id": "MhjpHKQFfGpMzI6YzkPGU",
"invoker": "npm:@metamask/test-snap-bip44",
"parentCapability": "endowment:rpc"
},
"snap_dialog": {
"caveats": null,
"date": 1718117256761,
"id": "sBxmdvnow7QiN9aS4uSdn",
"invoker": "npm:@metamask/test-snap-bip44",
"parentCapability": "snap_dialog"
},
"snap_getBip44Entropy": {
"caveats": [
{
"type": "permittedCoinTypes",
"value": [
{
"coinType": 1
},
{
"coinType": 3
}
]
}
],
"date": 1718117256762,
"id": "R9tggB2pDzyCcbt6dIebN",
"invoker": "npm:@metamask/test-snap-bip44",
"parentCapability": "snap_getBip44Entropy"
}
}
}
}
},
"ramps": {
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/snaps/snap-permission-adapter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './snap-permission-adapter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import SnapPermissionCell from '../snap-permission-cell';

export default function SnapPermissionAdapter({
snapId,
permissions,
showOptions,
targetSubjectsMetadata,
revoked,
approved,
}) {
return permissions.map((permission, index) => (
david0xd marked this conversation as resolved.
Show resolved Hide resolved
<SnapPermissionCell
snapId={snapId}
showOptions={showOptions}
connectionSubjectMetadata={targetSubjectsMetadata[permission.connection]}
permission={permission}
index={index}
key={`permissionCellDisplay_${snapId}_${index}`}
revoked={revoked}
approved={approved}
/>
));
}

SnapPermissionAdapter.propTypes = {
snapId: PropTypes.string.isRequired,
snapName: PropTypes.string.isRequired,
permissions: PropTypes.array.isRequired,
showOptions: PropTypes.bool,
targetSubjectsMetadata: PropTypes.object,
weightThreshold: PropTypes.number,
revoked: PropTypes.bool,
approved: PropTypes.bool,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithProvider } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import SnapPermissionAdapter from './snap-permission-adapter';

describe('Snap Permission Adapter', () => {
const mockSnapId = 'mock-snap-id';
const mockSnapName = 'Snap Name';
const mockTargetSubjectMetadata = {
extensionId: null,
iconUrl: null,
name: 'TypeScript Example Snap',
origin: 'local:http://localhost:8080',
subjectType: 'snap',
version: '0.2.2',
};
const mockState = {
metamask: {
subjectMetadata: {
'npm:@metamask/notifications-example-snap': {
name: 'Notifications Example Snap',
version: '1.2.3',
subjectType: 'snap',
},
},
snaps: {
'npm:@metamask/notifications-example-snap': {
id: 'npm:@metamask/notifications-example-snap',
version: '1.2.3',
manifest: {
proposedName: 'Notifications Example Snap',
description: 'A snap',
},
},
},
},
};
const mockWeightedPermissions = [
{
leftIcon: 'hierarchy',
weight: 3,
label: 'Allow websites to communicate directly with Dialog Example Snap.',
description: {},
permissionName: 'endowment:rpc',
permissionValue: {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
},
},
],
},
},
{
label: 'Display dialog windows in MetaMask.',
description: {},
leftIcon: 'messages',
weight: 4,
permissionName: 'snap_dialog',
permissionValue: {},
},
];

const store = configureStore(mockState);

it('renders set of provided permissions', () => {
renderWithProvider(
<SnapPermissionAdapter
snapId={mockSnapId}
snapName={mockSnapName}
permissions={mockWeightedPermissions}
targetSubjectsMetadata={{ ...mockTargetSubjectMetadata }}
/>,
store,
);
expect(
screen.queryByText('Display dialog windows in MetaMask.'),
).toBeInTheDocument();
expect(
screen.queryByText(
'Allow websites to communicate directly with Dialog Example Snap.',
),
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,52 +1,97 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { getWeightedPermissions } from '../../../../helpers/utils/permission';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { Box } from '../../../component-library';
import { Box, ButtonLink } from '../../../component-library';
import {
getMultipleTargetsSubjectMetadata,
getSnapsMetadata,
} from '../../../../selectors';
import { getSnapName } from '../../../../helpers/utils/util';
import SnapPermissionCell from '../snap-permission-cell';
import {
Display,
FlexDirection,
JustifyContent,
} from '../../../../helpers/constants/design-system';
import {
MinPermissionAbstractionDisplayCount,
PermissionsAbstractionThreshold,
PermissionWeightThreshold,
} from '../../../../../shared/constants/permissions';
import {
getFilteredSnapPermissions,
getSnapName,
} from '../../../../helpers/utils/util';
import { getWeightedPermissions } from '../../../../helpers/utils/permission';
import SnapPermissionAdapter from '../snap-permission-adapter';

export default function SnapPermissionsList({
snapId,
snapName,
permissions,
connections,
showOptions,
showAllPermissions,
onShowAllPermissions,
}) {
const t = useI18nContext();
const snapsMetadata = useSelector(getSnapsMetadata);
const permissionsToShow = {
...permissions,
connection_permission: connections ?? {},
};

const combinedPermissions = useMemo(() => {
hmalik88 marked this conversation as resolved.
Show resolved Hide resolved
return { ...permissions, connection_permission: connections ?? {} };
}, [permissions, connections]);

const targetSubjectsMetadata = useSelector((state) =>
getMultipleTargetsSubjectMetadata(state, connections),
);

const snapsMetadata = useSelector(getSnapsMetadata);

const weightedPermissions = getWeightedPermissions({
t,
permissions: combinedPermissions,
subjectName: snapName,
getSubjectName: getSnapName(snapsMetadata),
});

const [showAll, setShowAll] = useState(
showAllPermissions ||
Object.keys(weightedPermissions).length <=
PermissionsAbstractionThreshold,
);

const filteredPermissions = getFilteredSnapPermissions(
weightedPermissions,
PermissionWeightThreshold.snapInstall,
MinPermissionAbstractionDisplayCount,
);

const onShowAllPermissionsHandler = () => {
onShowAllPermissions();
setShowAll(true);
};

return (
<Box className="snap-permissions-list">
{getWeightedPermissions({
t,
permissions: permissionsToShow,
subjectName: snapName,
getSubjectName: getSnapName(snapsMetadata),
}).map((permission, index) => (
<SnapPermissionCell
<Box display={Display.Flex} flexDirection={FlexDirection.Column}>
<Box className="snap-permissions-list">
<SnapPermissionAdapter
permissions={showAll ? weightedPermissions : filteredPermissions}
snapId={snapId}
snapName={snapName}
showOptions={showOptions}
connectionSubjectMetadata={
targetSubjectsMetadata[permission.connection]
}
permission={permission}
index={index}
key={`permissionCellDisplay_${snapId}_${index}`}
targetSubjectsMetadata={targetSubjectsMetadata}
/>
))}
</Box>
{showAll ? null : (
<Box
display={Display.Flex}
justifyContent={JustifyContent.center}
paddingTop={2}
paddingBottom={2}
>
<ButtonLink onClick={() => onShowAllPermissionsHandler()}>
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
{t('seeAllPermissions')}
</ButtonLink>
</Box>
)}
</Box>
);
}
Expand All @@ -57,4 +102,6 @@ SnapPermissionsList.propTypes = {
permissions: PropTypes.object.isRequired,
connections: PropTypes.object,
showOptions: PropTypes.bool,
showAllPermissions: PropTypes.bool,
onShowAllPermissions: PropTypes.func,
};
Loading