-
-
Notifications
You must be signed in to change notification settings - Fork 242
feat: add gator permissions controller #6033
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a high level glance through and this looks good 👍
Just one question regarding the long-term scope of this controller.
...es/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts
Outdated
Show resolved
Hide resolved
cbf1238
to
22ab42d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Owned files LGTM! Nice usage of our controllers, I'm very happy to see this.
…schema enforcement has been deprecated
…t directly to gator-snap
const defaultGatorPermissionsList: GatorPermissionsList = { | ||
'native-token-stream': {}, | ||
'native-token-periodic': {}, | ||
'erc20-token-stream': {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is erc20-token-periodic missing here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, need to add that type. Started building this controller before type was supported in gator-snap
.
Erc20TokenStreamPermission | ||
>, | ||
); | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
erc20-token-periodic missing here as well and at other instances where needed
break; | ||
default: | ||
throw new Error( | ||
`Unsupported permission type: ${permissionType as string}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will we always have to update the controller for a new permission type? I believe the plan is also to support developer creating custom permissions which will have type 'custom' and will then need to provide a name
. I think it still makes sense to save those permissions and display to the user."
Tagging @jeffsmale90 who should have more info on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Permission types defined by a third party would likely not be shown here.
It does feel like a reasonably large amount of overhead for surfacing permission types here for revocation.
A couple thoughts:
- can we generalise this logic so that we're not having to duplicate it for every type?
- do we want to have a case for permission types that don't fall into any of the "known" categories? It feels like there's a possibility that a user could grant a permission that the extension doesn't know how to format - we probably don't want to fail, or hide that permission. Maybe group them into "Other" permission types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will likely need to update this controller for any new permission type that we support in gator-snap
to keep this controller in sync.
can we generalise this logic so that we're not having to duplicate it for every type?
Yeah, I can take a look at that.
do we want to have a case for permission types that don't fall into any of the "known" categories? It feels like there's a possibility that a user could grant a permission that the extension doesn't know how to format - we probably don't want to fail, or hide that permission. Maybe group them into "Other" permission types?
I intentionally left that out, but I can update the default statement to serve as a catch-all for all custom types. The key in GatorPermissionsList
for the see custom permissions type can be "other" or "custom and will have a known
type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - yeah, I think framing these as "Legitimate permissions where we don't have details of the type" makes sense, so "other' works. I imagine we should be able to type the value as the base permission type at least.
For these permissions, we should figure out how to present enough information to the user that they can at least recognise the permission, and revoke it if they wish to.
/** | ||
* Boolean value that allows DApp to define whether the permission can be attenuated–adjusted to meet the user’s terms. | ||
*/ | ||
isAdjustmentAllowed: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
possibly a parameter we don't need to save
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We stored the entire permission response, so if we want to remove this data, we will need to make the change on gator-snap
first before making that change in this controller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not strictly true, we can remove it here, and it won't matter that it's included on any data.
These types only need to be sufficiently expressive to capture any data that we show to the user, or otherwise use in the controller.
If we don't show any of the permission specific typed data, we could tighten these up to be a single type shared across all permissions. (I suspect we will need to show permission specific data, such as for how much the allowance is).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: ERC20 Token Missing Periodic Permission Type
The Erc20TokenPeriodicPermission
type definition is missing and not included in the PermissionTypes
union. Consequently, the GatorPermissionsList
type also lacks the 'erc20-token-periodic'
key. This creates an inconsistency where native tokens support both stream and periodic permissions, but ERC20 tokens only support stream.
packages/gator-permissions-controller/src/types.ts#L56-L61
core/packages/gator-permissions-controller/src/types.ts
Lines 56 to 61 in 746a079
*/ | |
export type PermissionTypes = | |
| NativeTokenStreamPermission | |
| NativeTokenPeriodicPermission | |
| Erc20TokenStreamPermission; | |
packages/gator-permissions-controller/src/types.ts#L178-L198
core/packages/gator-permissions-controller/src/types.ts
Lines 178 to 198 in 746a079
*/ | |
export type GatorPermissionsList = { | |
'native-token-stream': { | |
[chainId: Hex]: StoredGatorPermission< | |
SignerParam, | |
NativeTokenStreamPermission | |
>[]; | |
}; | |
'native-token-periodic': { | |
[chainId: Hex]: StoredGatorPermission< | |
SignerParam, | |
NativeTokenPeriodicPermission | |
>[]; | |
}; | |
'erc20-token-stream': { | |
[chainId: Hex]: StoredGatorPermission< | |
SignerParam, | |
Erc20TokenStreamPermission | |
>[]; | |
}; | |
}; |
Bug: Missing Permission Type Handling
The #categorizePermissionsDataByTypeAndChainId
method in GatorPermissionsController.ts
is missing a case
for the 'erc20-token-periodic'
permission type in its switch
statement. This causes permissions of this type to fall into the default
case, resulting in an "Unsupported permission type" error.
packages/gator-permissions-controller/src/GatorPermissionsController.ts#L334-L373
core/packages/gator-permissions-controller/src/GatorPermissionsController.ts
Lines 334 to 373 in 746a079
switch (permissionType) { | |
case 'native-token-stream': | |
if (!gatorPermissionsList['native-token-stream'][chainId]) { | |
gatorPermissionsList['native-token-stream'][chainId] = []; | |
} | |
gatorPermissionsList['native-token-stream'][chainId].push( | |
storedGatorPermission as StoredGatorPermission< | |
SignerParam, | |
NativeTokenStreamPermission | |
>, | |
); | |
break; | |
case 'native-token-periodic': | |
if (!gatorPermissionsList['native-token-periodic'][chainId]) { | |
gatorPermissionsList['native-token-periodic'][chainId] = []; | |
} | |
gatorPermissionsList['native-token-periodic'][chainId].push( | |
storedGatorPermission as StoredGatorPermission< | |
SignerParam, | |
NativeTokenPeriodicPermission | |
>, | |
); | |
break; | |
case 'erc20-token-stream': | |
if (!gatorPermissionsList['erc20-token-stream'][chainId]) { | |
gatorPermissionsList['erc20-token-stream'][chainId] = []; | |
} | |
gatorPermissionsList['erc20-token-stream'][chainId].push( | |
storedGatorPermission as StoredGatorPermission< | |
SignerParam, | |
Erc20TokenStreamPermission | |
>, | |
); | |
break; | |
default: | |
throw new Error( | |
`Unsupported permission type: ${permissionType as string}`, | |
); | |
} |
Was this report helpful? Give feedback by reacting with 👍 or 👎
}; | ||
|
||
/** | ||
* Represents a list of gator permissions filtered by permission type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Represents a list of gator permissions filtered by permission type. | |
* Represents a list of gator permissions grouped by permission type. |
break; | ||
default: | ||
throw new Error( | ||
`Unsupported permission type: ${permissionType as string}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Permission types defined by a third party would likely not be shown here.
It does feel like a reasonably large amount of overhead for surfacing permission types here for revocation.
A couple thoughts:
- can we generalise this logic so that we're not having to duplicate it for every type?
- do we want to have a case for permission types that don't fall into any of the "known" categories? It feels like there's a possibility that a user could grant a permission that the extension doesn't know how to format - we probably don't want to fail, or hide that permission. Maybe group them into "Other" permission types?
/** | ||
* Represents a list of gator permissions filtered by permission type and chainId. | ||
*/ | ||
export type GatorPermissionsList = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Point 2 is a bit of a nit, but I think point 1 is more important. GatorPermissionsList
isn't really a list.
- It's actually a much more complex object - map of permission type, to map of chainId to permissions
- The leaf on the data type isn't really a list, it's an array
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also inverting the relationship between GatorPermissionsList
and SupportedGatorPermissionsType
will make this much more concise; you can define a map from SupportedGatorPermissionType
to chainId-> permission
once (maybe with a generic).
anonymous: false, | ||
}, | ||
gatorPermissionsListStringify: { | ||
persist: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that persist: true
means this data is stored locally (in some sort of extension storage?), and fetched from the profile sync as needed - given that, loading the "All Permissions" page is incredibly slow, whether navigating to the page for the first time, or navigating back from the specific permission.
Is my assumption correct? Given the above experience, is this working as expected?
break; | ||
default: | ||
throw new Error( | ||
`Unsupported permission type: ${permissionType as string}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - yeah, I think framing these as "Legitimate permissions where we don't have details of the type" makes sense, so "other' works. I imagine we should be able to type the value as the base permission type at least.
For these permissions, we should figure out how to present enough information to the user that they can at least recognise the permission, and revoke it if they wish to.
return JSON.stringify(gatorPermissionsList); | ||
} catch (error) { | ||
log('Failed to serialize gator permissions list', error); | ||
throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we throw a new Error with more details?
throw new Error("Failed to serialize gator permissions", { cause: error });
This will make it easier to track down the actual meaning of the error, without having to dig into the stack trace.
return JSON.parse(gatorPermissionsList); | ||
} catch (error) { | ||
log('Failed to deserialize gator permissions list', error); | ||
throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we throw a new Error with more details?
throw new Error("Failed to deserialize gator permissions", { cause: error });
This will make it easier to track down the actual meaning of the error, without having to dig into the stack trace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should come up with a minimization and mitigation strategy for corrupted permission data.
I believe we're storing the permissions separately in profile sync, which is a good start! Can we store them separately in the gator-permissions-controller state too, so that a single corrupted permission doesn't result in corrupting the entire dataset?
How do we recover from this error? Can we refetch from profile sync?
/** | ||
* Represents a list of gator permissions filtered by permission type and chainId. | ||
*/ | ||
export type GatorPermissionsList = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also inverting the relationship between GatorPermissionsList
and SupportedGatorPermissionsType
will make this much more concise; you can define a map from SupportedGatorPermissionType
to chainId-> permission
once (maybe with a generic).
/** | ||
* JSON serialized object containing gator permissions fetched from profile sync indexed by permission type | ||
*/ | ||
gatorPermissionsListStringify: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'm not a huge fan of Stringify
as a suffix to denote Serialized
. It's a verb, and just generally a big funny.
I think gatorPermissionsListSerialized
would be more meaningful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, must this be a string? Can we not store a complex object in the controller state?
/** | ||
* Boolean value that allows DApp to define whether the permission can be attenuated–adjusted to meet the user’s terms. | ||
*/ | ||
isAdjustmentAllowed: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not strictly true, we can remove it here, and it won't matter that it's included on any data.
These types only need to be sufficiently expressive to capture any data that we show to the user, or otherwise use in the controller.
If we don't show any of the permission specific typed data, we could tighten these up to be a single type shared across all permissions. (I suspect we will need to show permission specific data, such as for how much the allowance is).
}; | ||
|
||
const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< | ||
AccountSigner, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I enjoy generic parameters named with a T prefix denoting that they are a generic Type.
It can become harder to grok, especially when it's amongst concrete types.
I'll comment just here, but there's a couple of generic parameters to which this applies.
Explanation
@metamask/gator-permissions-controller
Current State and Why Change is Needed
MetaMask clients currently lack a dedicated system for managing gator permissions that are stored to profile sync via the
@metamask/gator-permissions-snap
.Solution and How It Works
This change introduces a new
@metamask/gator-permissions-controller
package that provides a comprehensive solution for managing gator permissions in MetaMask clients with gator-snap integration.Changes That Might Not Be Obvious
state
uses JSON serialization for storing permission data fetched from@metamask/gator-permissions-snap
, which allows for efficient storage and retrieval while maintaining data integrity. The deserialize permission data is represented as a list of gator permissions filtered by permission type and chainId.Package Dependencies and Integration
The new package depends on
@metamask/snaps-controllers
as a peer dependency, ensuring it can leverage sending RPC requests to an installed Metamask Snap. This integration allows theGatorPermissionsController
to forward requests to@metamask/gator-permissions-snap
to fetch users' Gator permissions that have been stored in the MetaMask Profile Sync service.The
@metamask/gator-permissions-snap
will take on the responsibility of authenticating with MetaMask Profile Sync service using anSRP
identifier via integration with@metamask/message-signing-snap
.No Dependency Upgrades Required
This is a new package that introduces new functionality without requiring changes to existing dependencies. The package uses the current stable versions of
@metamask/base-controller
,@metamask/utils
,@metamask/snaps-sdk
, and@metamask/snaps-utils
following the established patterns in the MetaMask codebase.References
Related to(MM snap-7715-permissions): Persisting Granted Permissions with MM Profile Sync
Requires(MM snap-7715-permissions):Add new permissionsProvider_getGrantedPermissions RPC
Required by(MM Extension): MetaMask/metamask-extension#33996
Gator Permissions Data Flow
Checklist