Skip to content

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

V00D00-child
Copy link
Member

@V00D00-child V00D00-child commented Jun 25, 2025

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

  • Serialization Strategy: The controller 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.
  • Default Permission Structure: The controller initializes with an empty structure for all three permission types, ensuring consistent state even when no permissions are configured

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 the GatorPermissionsController 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

graph TD
    %% dApp flow for storing permissions
    A[dApp<br/>client side RPC] -->|RPC| GPS[gator-permissions-snap]
    C -->|WRITE| D[(permissions stored<br/>across all sites)]
    
    %% User flow for reading permissions
    E[user<br/>permissions page] -->|UI| F[MM client]
    F -->|submitRequestToBackground| G[GatorPermissionsController]
    G --> MSYS[messagingSystem]
    MSYS -->|handleRequest| SC[SnapController]
    SC -->|RPC| GPS
    C -->|READ| D

    %% SRP Auth
    GPS -->|OAuth 2.0 Auth| MS[message-signing-snap]
    MS -->|SRP identifier & signature| C[profile sync service]
    
    %% Styling
    classDef dappStyle fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef userStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef serviceStyle fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef dataStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef authStyle fill:#ffebee,stroke:#c62828,stroke-width:2px
    classDef systemStyle fill:#fce4ec,stroke:#ad1457,stroke-width:2px
    
    class A dappStyle
    class E,F userStyle
    class GPS,C serviceStyle
    class D dataStyle
    class MS authStyle
    class G,MSYS,SC systemStyle
Loading

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

Copy link
Contributor

@jeffsmale90 jeffsmale90 left a 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.

@V00D00-child V00D00-child changed the title Add GatorPermissionsController package feat: add gator permissions controller Jun 26, 2025
@V00D00-child V00D00-child added the team-delegation MetaMask Delegation Team label Jun 26, 2025
@V00D00-child V00D00-child force-pushed the feat/add-gator-permissions-controller branch from cbf1238 to 22ab42d Compare June 30, 2025 15:56
@V00D00-child V00D00-child marked this pull request as ready for review June 30, 2025 16:23
@V00D00-child V00D00-child requested review from a team as code owners June 30, 2025 16:23
@V00D00-child V00D00-child requested a review from jeffsmale90 June 30, 2025 16:24
cursor[bot]

This comment was marked as outdated.

mathieuartu
mathieuartu previously approved these changes Jul 4, 2025
Copy link
Contributor

@mathieuartu mathieuartu left a 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.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@V00D00-child V00D00-child requested a review from a team as a code owner July 10, 2025 16:11
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

const defaultGatorPermissionsList: GatorPermissionsList = {
'native-token-stream': {},
'native-token-periodic': {},
'erc20-token-stream': {},
Copy link

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?

Copy link
Member Author

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;
Copy link

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}`,
Copy link

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.

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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;
Copy link

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

Copy link
Member Author

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.

Copy link
Contributor

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).

@github-project-automation github-project-automation bot moved this from Needs dev review to Needs more work from the author in PR review queue Jul 18, 2025
Copy link

@cursor cursor bot left a 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

*/
export type PermissionTypes =
| NativeTokenStreamPermission
| NativeTokenPeriodicPermission
| Erc20TokenStreamPermission;

packages/gator-permissions-controller/src/types.ts#L178-L198

*/
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
>[];
};
};

Fix in CursorFix in Web


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

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}`,
);
}

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

};

/**
* Represents a list of gator permissions filtered by permission type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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}`,
Copy link
Contributor

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 = {
Copy link
Contributor

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.

  1. It's actually a much more complex object - map of permission type, to map of chainId to permissions
  2. The leaf on the data type isn't really a list, it's an array

Copy link
Contributor

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,
Copy link
Contributor

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}`,
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor

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 = {
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor

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;
Copy link
Contributor

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,
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team-delegation MetaMask Delegation Team
Projects
Status: Needs more work from the author
Development

Successfully merging this pull request may close these issues.

4 participants