From 436ce47c8e2f1a67cec0497c7a4ef4ccd33d189e Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Thu, 24 Oct 2024 10:49:44 -0400 Subject: [PATCH] feat: begin hooking up new permissions --- .../src/Approvals/ApproveDisconnection.tsx | 7 ++- .../src/background/approvals/handler.test.ts | 1 + .../src/background/approvals/handler.ts | 3 +- .../src/background/approvals/messages.ts | 3 +- .../src/background/approvals/service.test.ts | 23 +++++--- .../src/background/approvals/service.ts | 28 ++++++---- .../src/background/permissions/service.ts | 13 +++++ apps/extension/src/storage/LocalStorage.ts | 56 +++++-------------- .../src/App/Setup/ExtensionLoader.tsx | 15 +++-- 9 files changed, 80 insertions(+), 69 deletions(-) diff --git a/apps/extension/src/Approvals/ApproveDisconnection.tsx b/apps/extension/src/Approvals/ApproveDisconnection.tsx index 0c8817723..29e320e34 100644 --- a/apps/extension/src/Approvals/ApproveDisconnection.tsx +++ b/apps/extension/src/Approvals/ApproveDisconnection.tsx @@ -10,12 +10,17 @@ export const ApproveDisconnection: React.FC = () => { const requester = useRequester(); const params = useQuery(); const interfaceOrigin = params.get("interfaceOrigin"); + const chainId = params.get("chainId")!; const handleResponse = async (revokeConnection: boolean): Promise => { if (interfaceOrigin) { await requester.sendMessage( Ports.Background, - new DisconnectInterfaceResponseMsg(interfaceOrigin, revokeConnection) + new DisconnectInterfaceResponseMsg( + interfaceOrigin, + chainId, + revokeConnection + ) ); await closeCurrentTab(); } diff --git a/apps/extension/src/background/approvals/handler.test.ts b/apps/extension/src/background/approvals/handler.test.ts index 85c7b53c4..d7fec2cf5 100644 --- a/apps/extension/src/background/approvals/handler.test.ts +++ b/apps/extension/src/background/approvals/handler.test.ts @@ -91,6 +91,7 @@ describe("approvals handler", () => { const disconnectInterfaceResponseMsg = new DisconnectInterfaceResponseMsg( "", + "chainId", true ); handler(env, disconnectInterfaceResponseMsg); diff --git a/apps/extension/src/background/approvals/handler.ts b/apps/extension/src/background/approvals/handler.ts index cb967f094..d0f17aa45 100644 --- a/apps/extension/src/background/approvals/handler.ts +++ b/apps/extension/src/background/approvals/handler.ts @@ -159,11 +159,12 @@ const handleDisconnectInterfaceResponseMsg: ( ) => InternalHandler = (service) => { return async ( { senderTabId: popupTabId }, - { interfaceOrigin, revokeConnection } + { interfaceOrigin, chainId, revokeConnection } ) => { return await service.approveDisconnectionResponse( popupTabId, interfaceOrigin, + chainId, revokeConnection ); }; diff --git a/apps/extension/src/background/approvals/messages.ts b/apps/extension/src/background/approvals/messages.ts index dc0921c81..8e263e97b 100644 --- a/apps/extension/src/background/approvals/messages.ts +++ b/apps/extension/src/background/approvals/messages.ts @@ -173,13 +173,14 @@ export class DisconnectInterfaceResponseMsg extends Message { constructor( public readonly interfaceOrigin: string, + public readonly chainId: string, public readonly revokeConnection: boolean ) { super(); } validate(): void { - validateProps(this, ["interfaceOrigin", "revokeConnection"]); + validateProps(this, ["interfaceOrigin", "chainId", "revokeConnection"]); } route(): string { diff --git a/apps/extension/src/background/approvals/service.test.ts b/apps/extension/src/background/approvals/service.test.ts index a2e4f969d..735ed35d5 100644 --- a/apps/extension/src/background/approvals/service.test.ts +++ b/apps/extension/src/background/approvals/service.test.ts @@ -317,7 +317,7 @@ describe("approvals service", () => { reject: jest.fn(), }, }; - jest.spyOn(localStorage, "addApprovedOrigin").mockResolvedValue(); + jest.spyOn(permissionsService, "enablePermissions").mockResolvedValue(); await service.approveConnectionResponse( popupTabId, @@ -327,8 +327,9 @@ describe("approvals service", () => { ); expect(service["resolverMap"][popupTabId].resolve).toHaveBeenCalled(); - expect(localStorage.addApprovedOrigin).toHaveBeenCalledWith( - interfaceOrigin + expect(permissionsService.enablePermissions).toHaveBeenCalledWith( + interfaceOrigin, + chainId ); }); @@ -412,6 +413,7 @@ describe("approvals service", () => { describe("approveDisconnectionResponse", () => { it("should approve disconnection response", async () => { const interfaceOrigin = "origin"; + const chainId = "chainId"; const popupTabId = 1; service["resolverMap"] = { [popupTabId]: { @@ -424,6 +426,7 @@ describe("approvals service", () => { await service.approveDisconnectionResponse( popupTabId, interfaceOrigin, + chainId, true ); @@ -457,10 +460,12 @@ describe("approvals service", () => { it("should reject connection response", async () => { const originToRevoke = "origin"; - jest.spyOn(localStorage, "removeApprovedOrigin").mockResolvedValue(); + jest + .spyOn(permissionsService, "revokeDomainPermissions") + .mockResolvedValue(); await service.revokeConnection(originToRevoke); - expect(localStorage.removeApprovedOrigin).toHaveBeenCalledWith( + expect(permissionsService.revokeDomainPermissions).toHaveBeenCalledWith( originToRevoke ); }); @@ -569,7 +574,7 @@ describe("approvals service", () => { const chainId = chains.namada.chainId; jest.spyOn(chainService, "getChain").mockResolvedValue(chains.namada); jest - .spyOn(localStorage, "getApprovedOrigins") + .spyOn(permissionsService, "getApprovedOrigins") .mockResolvedValue([origin]); await expect(service.isConnectionApproved(origin, chainId)).resolves.toBe( @@ -581,7 +586,9 @@ describe("approvals service", () => { const origin = "origin"; const chainId = "chainId"; jest.spyOn(chainService, "getChain").mockResolvedValue(chains.namada); - jest.spyOn(localStorage, "getApprovedOrigins").mockResolvedValue([]); + jest + .spyOn(permissionsService, "getApprovedOrigins") + .mockResolvedValue([]); await expect(service.isConnectionApproved(origin, chainId)).resolves.toBe( false @@ -593,7 +600,7 @@ describe("approvals service", () => { const chainId = "chainId"; jest.spyOn(chainService, "getChain").mockResolvedValue(chains.namada); jest - .spyOn(localStorage, "getApprovedOrigins") + .spyOn(permissionsService, "getApprovedOrigins") .mockResolvedValue(undefined); await expect(service.isConnectionApproved(origin, chainId)).resolves.toBe( diff --git a/apps/extension/src/background/approvals/service.ts b/apps/extension/src/background/approvals/service.ts index 50e63c7b6..9247a8e26 100644 --- a/apps/extension/src/background/approvals/service.ts +++ b/apps/extension/src/background/approvals/service.ts @@ -190,15 +190,14 @@ export class ApprovalsService { interfaceOrigin: string, chainId: string ): Promise { - const approvedOrigins = - (await this.localStorage.getApprovedOrigins()) || []; - - const chain = await this.chainService.getChain(); - if (chain.chainId !== chainId) { + const permission = await this.permissionsService.permissionsByChain( + interfaceOrigin, + chainId + ); + if (!permission || !permission.length) { return false; } - - return approvedOrigins.includes(interfaceOrigin); + return true; } async approveConnection( @@ -231,7 +230,11 @@ export class ApprovalsService { if (allowConnection) { try { - await this.localStorage.addApprovedOrigin(interfaceOrigin); + await this.permissionsService.enablePermissions( + interfaceOrigin, + chainId, + ["accounts", "proofGenKeys", "signing"] + ); // Enable signing for this chain await this.chainService.updateChain(chainId); } catch (e) { @@ -255,6 +258,7 @@ export class ApprovalsService { if (isConnected) { return this.launchApprovalPopup(TopLevelRoute.ApproveDisconnection, { interfaceOrigin, + chainId, }); } @@ -265,13 +269,17 @@ export class ApprovalsService { async approveDisconnectionResponse( popupTabId: number, interfaceOrigin: string, + chainId: string, revokeConnection: boolean ): Promise { const resolvers = this.getResolver(popupTabId); if (revokeConnection) { try { - await this.revokeConnection(interfaceOrigin); + await this.permissionsService.revokeChainPermissions( + interfaceOrigin, + chainId + ); } catch (e) { resolvers.reject(e); } @@ -282,7 +290,7 @@ export class ApprovalsService { } async revokeConnection(originToRevoke: string): Promise { - await this.localStorage.removeApprovedOrigin(originToRevoke); + await this.permissionsService.revokeDomainPermissions(originToRevoke); await this.broadcaster.revokeConnection(); } diff --git a/apps/extension/src/background/permissions/service.ts b/apps/extension/src/background/permissions/service.ts index 994f752e9..6f17d77b1 100644 --- a/apps/extension/src/background/permissions/service.ts +++ b/apps/extension/src/background/permissions/service.ts @@ -37,6 +37,15 @@ export class PermissionsService { await this.localStorage.setPermissions(updatedPermissions); } + async revokeDomainPermissions(domain: string): Promise { + const updatedPermissions = await this.localStorage.getPermissions(); + if (!updatedPermissions || !updatedPermissions[domain]) { + return; + } + delete updatedPermissions[domain]; + await this.localStorage.setPermissions(updatedPermissions); + } + async permissionsByDomain( domain: string ): Promise | undefined> { @@ -56,4 +65,8 @@ export class PermissionsService { return permissions[domain][chainId]; } } + + async getApprovedOrigins(): Promise { + return Object.keys((await this.localStorage.getPermissions()) || {}); + } } diff --git a/apps/extension/src/storage/LocalStorage.ts b/apps/extension/src/storage/LocalStorage.ts index a98fa751f..02252cb80 100644 --- a/apps/extension/src/storage/LocalStorage.ts +++ b/apps/extension/src/storage/LocalStorage.ts @@ -53,12 +53,6 @@ const Chain = t.intersection([ ]); type ChainType = t.TypeOf; -// TODO: Remove the folllowing once NamadaKeychainPermissions is working! -const NamadaExtensionApprovedOrigins = t.array(t.string); -type NamadaExtensionApprovedOriginsType = t.TypeOf< - typeof NamadaExtensionApprovedOrigins ->; - export type PermissionKind = "accounts" | "signing" | "proofGenKeys"; export const KeychainPermissions: Record< @@ -94,12 +88,11 @@ type NamadaExtensionRouterIdType = t.TypeOf; type LocalStorageTypes = | ChainType - | NamadaExtensionApprovedOriginsType + | NamadaKeychainPermissionsType | NamadaExtensionRouterIdType; type LocalStorageSchemas = | typeof Chain - | typeof NamadaExtensionApprovedOrigins | typeof NamadaExtensionRouterId | typeof NamadaKeychainPermissions; @@ -112,7 +105,6 @@ export type LocalStorageKeys = const schemasMap = new Map([ [Chain, "chains"], - [NamadaExtensionApprovedOrigins, "namadaExtensionApprovedOrigins"], [NamadaExtensionRouterId, "namadaExtensionRouterId"], [NamadaKeychainPermissions, "namadaKeychainPermissions"], ]); @@ -139,33 +131,6 @@ export class LocalStorage extends ExtStorage { await this.setRaw(this.getKey(Chain), chain); } - async getApprovedOrigins(): Promise< - NamadaExtensionApprovedOriginsType | undefined - > { - const data = await this.getRaw(this.getKey(NamadaExtensionApprovedOrigins)); - - const Schema = t.union([NamadaExtensionApprovedOrigins, t.undefined]); - const decodedData = Schema.decode(data); - - if (E.isLeft(decodedData)) { - throw new Error("Approved Origins are not valid"); - } - - return decodedData.right; - } - - async addApprovedOrigin(originToAdd: string): Promise { - const data = (await this.getApprovedOrigins()) || []; - await this.setApprovedOrigins([...data, originToAdd]); - } - - async removeApprovedOrigin(originToRemove: string): Promise { - const data = (await this.getApprovedOrigins()) || []; - await this.setApprovedOrigins( - data.filter((origin) => origin !== originToRemove) - ); - } - async getRouterId(): Promise { const data = await this.getRaw(this.getKey(NamadaExtensionRouterId)); @@ -183,12 +148,6 @@ export class LocalStorage extends ExtStorage { await this.setRaw(this.getKey(NamadaExtensionRouterId), id); } - private async setApprovedOrigins( - origins: NamadaExtensionApprovedOriginsType - ): Promise { - await this.setRaw(this.getKey(NamadaExtensionApprovedOrigins), origins); - } - async getPermissions(): Promise { const data = await this.getRaw(this.getKey(NamadaKeychainPermissions)); const Schema = t.union([NamadaKeychainPermissions, t.undefined]); @@ -217,6 +176,19 @@ export class LocalStorage extends ExtStorage { ); } + async getApprovedOrigins(): Promise { + const data = await this.getRaw(this.getKey(NamadaKeychainPermissions)); + + const Schema = t.union([NamadaKeychainPermissions, t.undefined]); + const decodedData = Schema.decode(data); + + if (E.isLeft(decodedData)) { + throw new Error("Stored Keychain permissions are not valid!"); + } + + return Object.keys(decodedData.right || {}); + } + private getKey(schema: S): LocalStorageKeys { const key = schemasMap.get(schema); if (!key) { diff --git a/apps/namadillo/src/App/Setup/ExtensionLoader.tsx b/apps/namadillo/src/App/Setup/ExtensionLoader.tsx index 2a324a49f..ca5709258 100644 --- a/apps/namadillo/src/App/Setup/ExtensionLoader.tsx +++ b/apps/namadillo/src/App/Setup/ExtensionLoader.tsx @@ -1,9 +1,10 @@ import { useIntegration } from "@namada/integrations"; +import { chainParametersAtom } from "atoms/chain"; import { namadaExtensionAttachStatus, namadaExtensionConnectionStatus, } from "atoms/settings"; -import { useAtom, useSetAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { ReactNode, useEffect } from "react"; import { PageLoader } from "../Common/PageLoader"; @@ -15,15 +16,17 @@ export const ExtensionLoader = ({ const [attachStatus, setAttachStatus] = useAtom(namadaExtensionAttachStatus); const setConnectionStatus = useSetAtom(namadaExtensionConnectionStatus); const integration = useIntegration("namada"); + const { data: chain } = useAtomValue(chainParametersAtom); useEffect(() => { setAttachStatus(integration.detect() ? "attached" : "detached"); - integration.isConnected().then((isConnected) => { - if (isConnected) { - setConnectionStatus("connected"); - } - }); + chain?.chainId && + integration.isConnected(chain.chainId).then((isConnected) => { + if (isConnected) { + setConnectionStatus("connected"); + } + }); }, [integration]); if (attachStatus === "pending") {