diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-07 20:39:18 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-07 20:39:18 -0500 |
commit | c438ee90e3be4e5e01ae3d045d6b841a03bd46eb (patch) | |
tree | 41c0b2ee815ce979e2af0e79f8fde9b58f5f4627 /extension/src/features | |
parent | e87c4b69036e32b4fcf3df89e8158fb52df6a4e0 (diff) |
Diffstat (limited to 'extension/src/features')
-rw-r--r-- | extension/src/features/framework/index.ts | 2 | ||||
-rw-r--r-- | extension/src/features/history.ts | 2 | ||||
-rw-r--r-- | extension/src/features/identity-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/index.ts | 7 | ||||
-rw-r--r-- | extension/src/features/mfa-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/nip07allow-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/nostr-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/permissions.ts | 371 | ||||
-rw-r--r-- | extension/src/features/pki-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/server-api/endpoints.ts | 2 | ||||
-rw-r--r-- | extension/src/features/server-api/index.ts | 2 | ||||
-rw-r--r-- | extension/src/features/settings.ts | 2 | ||||
-rw-r--r-- | extension/src/features/tagfilter-api.ts | 2 | ||||
-rw-r--r-- | extension/src/features/types.ts | 2 | ||||
-rw-r--r-- | extension/src/features/util.ts | 10 |
15 files changed, 352 insertions, 60 deletions
diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts index 2d9cad5..b545335 100644 --- a/extension/src/features/framework/index.ts +++ b/extension/src/features/framework/index.ts @@ -1,5 +1,5 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/history.ts b/extension/src/features/history.ts index ff7c267..82f31c4 100644 --- a/extension/src/features/history.ts +++ b/extension/src/features/history.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts index d909162..0b8973d 100644 --- a/extension/src/features/identity-api.ts +++ b/extension/src/features/identity-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts index f05de9b..0a8e182 100644 --- a/extension/src/features/index.ts +++ b/extension/src/features/index.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as @@ -21,6 +21,8 @@ export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './pki-api' export type { NostrApi } from './nostr-api' export type { UserApi } from './auth-api' export type { IdentityApi } from './identity-api' +export type { MfaUpdateResult } from './mfa-api' +export type { PermissionRequest, PrType, AutoAllowRule } from './permissions' export { useBackgroundFeatures, useForegoundFeatures } from './framework' export { useLocalPki, usePkiApi } from './pki-api' @@ -32,4 +34,5 @@ export { useHistoryApi } from './history' export { useEventTagFilterApi } from './tagfilter-api' export { useInjectAllowList } from './nip07allow-api' export { onWatchableChange } from './util' -export { useMfaConfigApi, type MfaUpdateResult } from './mfa-api'
\ No newline at end of file +export { useMfaConfigApi } from './mfa-api' +export { usePermissionApi, PrStatus } from './permissions'
\ No newline at end of file diff --git a/extension/src/features/mfa-api.ts b/extension/src/features/mfa-api.ts index fc6d51a..85ff49e 100644 --- a/extension/src/features/mfa-api.ts +++ b/extension/src/features/mfa-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts index 8c08d5e..4787437 100644 --- a/extension/src/features/nip07allow-api.ts +++ b/extension/src/features/nip07allow-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts index 046ccea..4aee660 100644 --- a/extension/src/features/nostr-api.ts +++ b/extension/src/features/nostr-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/permissions.ts b/extension/src/features/permissions.ts index c06257b..5473962 100644 --- a/extension/src/features/permissions.ts +++ b/extension/src/features/permissions.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as @@ -13,68 +13,353 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -import { useStorageAsync } from "@vueuse/core"; -import { find, isEmpty, merge, remove } from "lodash"; -import { storage } from "webextension-polyfill"; -import { useAuthApi } from "./auth-api"; -import { useSettingsApi } from "./settings"; +import { Mutable, get, set, toRefs } from "@vueuse/core"; +import { Ref } from "vue"; +import { defaultTo, defaults, defer, filter, find, forEach, isEqual, isNil } from "lodash"; +import { nanoid } from "nanoid"; +import { useSession } from "@vnuge/vnlib.browser"; +import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, optionsOnly } from "./framework"; +import { waitForChangeFn, waitOne } from "./util"; +import { windows, runtime, Windows, tabs } from "webextension-polyfill"; +import type { TotpUpdateMessage, Watchable } from "./types"; +import type { AppSettings } from "./settings"; -const permissions = useStorageAsync("permissions", [], storage.local); +export interface AutoAllowRule{ + origin: string + type: string + readonly timestamp: number +} -export const setAutoAllow = async (origin, mKind, keyId) => { - permissions.value.push({ origin, mKind, keyId, }) +export type PrType = string +export enum PrStatus{ + Pending, + Approved, + Denied } -/** - * Determines if the user has previously allowed the origin to use the key to sign events - * of the desired kind - * @param {*} origin The site origin requesting the permission - * @param {*} mKind The kind of message being signed - * @param {*} keyId The keyId of the key being used to sign the message - */ -export const isAutoAllow = async (origin, mKind, keyId) => { - return find(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId) !== undefined +export interface PermissionRequest{ + readonly uuid: string + readonly origin: string + readonly requestType: PrType + readonly timestamp: number + readonly status: PrStatus } -/** - * Removes the auto allow permission from the list - * @param {*} origin The site origin requesting the permission - * @param {*} mKind The message kind being signed - * @param {*} keyId The keyId of the key being used to sign the message - */ -export const removeAutoAllow = async (origin, mKind, keyId) => { - //Remove the permission from the list - remove(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId); +export type MfaUpdateResult = TotpUpdateMessage + +export interface PermissionApi extends FeatureApi, Watchable { + getRequests(): Promise<PermissionRequest[]> + allow(requestId: string, addRule: boolean): Promise<void> + deny(requestId: string): Promise<void> + clearRequests(): Promise<void> + requestAndWaitResult(request: Partial<PermissionRequest>): Promise<PrStatus> + + getRules(): Promise<AutoAllowRule[]> + deleteRule(rule: AutoAllowRule): Promise<void> + addRule(rule: AutoAllowRule): Promise<void> } +interface PermissionSlot { + requests: PermissionRequest[] +} -export const useSitePermissions = (() => { +interface RuleSlot{ + rules: AutoAllowRule[] +} - const { apiCall, handleProtectedMessage } = useAuthApi(); - const { currentConfig } = useSettingsApi(); +const useRuleSet = (slot: Ref<RuleSlot>) => { + defaults(slot.value, { rules: [] }) + const { rules } = toRefs(slot) + + return{ + isAllowed: (request: PermissionRequest): boolean => { + //find existing rule + const rule = find(get(rules), r => isEqual(r.origin, request.origin) && isEqual(r.type, request.requestType)) + return !isNil(rule) + }, + addRule: (rule: Partial<AutoAllowRule>) => { + const current = defaultTo(get(rules), []) + + //see if rule aready exists + if (find(current, r => isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) { + return; + } + + //add rule to head of store + current.unshift({ + ...rule, + timestamp: Date.now() + } as AutoAllowRule) + + set(rules, current) + }, + deleteRule (rule: AutoAllowRule) { + //Filter all non matching rules + const wo = filter(get(rules), r => !(isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) + set(rules, wo) + }, + getRules:(): AutoAllowRule[] =>get(rules) + } +} + +const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useRuleSet>) => { + + const permPopupUrl = runtime.getURL("src/entries/contentScript/auth-popup.html") + + defaults(slot.value, { rules: [] }) + const { requests } = toRefs(slot) + + const drawWindow = async ({ uuid }: Partial<PermissionRequest>): Promise<Windows.CreateCreateDataType> => { + const current = await windows.getCurrent() + + const minWidth = 350 + const minHeight = 180 + + const maxWidth = 500 + const maxHeight = 250 - const getCurrentPerms = async () => { - const { permissions } = await storage.local.get('permissions'); + const width = Math.min(Math.max(current.width! - 100, minWidth), maxWidth) + const height = Math.min(Math.max(current.height! - 100, minHeight), maxHeight) - //Store a default config if none exists - if (isEmpty(permissions)) { - await storage.local.set({ siteConfig: defaultConfig }); + //draw half way across screen minus half its width + const left = current.left! + (current.width! / 2) - (width / 2) + + return { + url: `${permPopupUrl}?uuid=${uuid}&closeable`, + type: "popup", + height: height, + width: width, + focused: true, + allowScriptsToClose: true, + top: 100, + //try to center popup + left: left, } + } + + const activePopups = new Map<number, PermissionRequest>() - //Merge the default config with the site config - return merge(defaultConfig, siteConfig) + const getRequest = (requestId: string): PermissionRequest | undefined => { + return find(get(requests), r => r.uuid === requestId) } - const onIsSiteEnabled = handleProtectedMessage(async (data) => { + const updateRequest = (request: PermissionRequest, addRule: boolean) => { + const current = get(requests) - }) + const index = current.findIndex(r => r.uuid === request.uuid) + if (index === -1) { + throw new Error("Request not found") + } + + //Set request state + current[index] = request + + //Update storage + set(requests, current) - return () => { + //Add rule if needed + if (addRule) { + rules.addRule({ origin: request.origin, type: request.requestType }) + } + } + + const initNewRequest = (request: Partial<PermissionRequest>): PermissionRequest => { return { - onCreateIdentity, - onUpdateIdentity + ...request, + uuid: nanoid(), + status: PrStatus.Pending, + timestamp: Date.now() + } as PermissionRequest + } + + //Listen for popup close to cleanup request + windows.onRemoved.addListener(async (id) => { + const req = activePopups.get(id) + if (req && req.status === PrStatus.Pending) { + //set denied + (req as Mutable<PermissionRequest>).status = PrStatus.Denied + //popup closed, set to denied + updateRequest(req, false) } + }) + + //Watch for changes to the current tab + tabs.onRemoved.addListener(async (tabId) => { + const tab = await tabs.get(tabId) + const { origin } = new URL(tab.url!) + + //Find ally pending requests for the origin + const pending = filter(requests.value, r => r.status == PrStatus.Pending && r.origin == origin) + + //update all pending requests to denied + forEach(pending, r => updateRequest(r, false)) + }) + + return{ + getRequest, + + async showPermsWindow (request: PermissionRequest): Promise<void> { + const windowsArgs = await drawWindow(request) + const { id } = await windows.create(windowsArgs) + activePopups.set(id!, request) + }, + + pushRequest (request: Partial<PermissionRequest>, showPopup: boolean): PermissionRequest { + //Create new request + const req = initNewRequest(request) + + //See if allowed + if(rules.isAllowed(req)){ + //Set to approved + (req as Mutable<PermissionRequest>).status = PrStatus.Approved + //No need to show popup + showPopup = false + } + + const current = get(requests) + current.unshift(req) + set(requests, current) + + //Show popup if needed + if (showPopup) { + this.showPermsWindow(req) + } + + return req + }, + + allow (requestId: string, addRule: boolean): void { + const request = getRequest(requestId) + if(!request){ + throw new Error("Request not found") + } + //set approved + (request as Mutable<PermissionRequest>).status = PrStatus.Approved + //update request + updateRequest(request, addRule) + }, + + deny(requestId: string): void { + const request = getRequest(requestId) + if (!request) { + throw new Error("Request not found") + } + //set denied + (request as Mutable<PermissionRequest>).status = PrStatus.Denied + //update request + updateRequest(request, false) + }, + + clearAll: () => { + //notify pending requests + forEach(filter(requests.value, r => r.status == PrStatus.Pending), r => { + //set denied + (r as Mutable<PermissionRequest>).status = PrStatus.Denied + //update request + updateRequest(r, false) + }) + + //Then defer clear + defer(() => set(requests, [])) + }, + getAll: () => get(requests) } +} + +export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> => { + + return { + background: ({ state }: BgRuntime<AppSettings>): PermissionApi => { + const { loggedIn } = useSession(); + const { currentConfig } = state + + //Open storage slot for permissions + const reqStore = state.useStorageSlot<PermissionSlot>("permissions", { requests: [] }) + const ruleStore = state.useStorageSlot<RuleSlot>("rules", { rules: [] }) + + //init rules api + const ruleSet = useRuleSet(ruleStore) + const permissions = usePermissions(reqStore, ruleSet) -})()
\ No newline at end of file + return { + waitForChange: waitForChangeFn([currentConfig, loggedIn, reqStore, ruleStore]), + + getRequests: () => Promise.resolve(permissions.getAll()), + + deny(requestId: string) { + permissions.deny(requestId) + return Promise.resolve() + }, + + allow(requestId: string, addRule: boolean) { + permissions.allow(requestId, addRule) + return Promise.resolve() + }, + + clearRequests: optionsOnly(() => { + //clear stored requests + permissions.clearAll() + return Promise.resolve() + }), + + async requestAndWaitResult(request: Partial<PermissionRequest>) { + //push request + const req = permissions.pushRequest(request, true) + + //See if pending + if(req.status !== PrStatus.Pending){ + //completed already, return status + return req.status + } + + do { + + //wait for a change + await waitOne([reqStore]) + + //check if request was approved + const status = permissions.getRequest(req.uuid); + + switch(status?.status){ + case PrStatus.Approved: + return PrStatus.Approved; + case PrStatus.Denied: + return PrStatus.Denied; + case PrStatus.Pending: + //continue waiting + break; + default: + throw new Error("Request was rejected or deleted") + } + + //continue to wait for pending status + } while(true) + }, + + getRules: () => Promise.resolve(ruleSet.getRules()), + + deleteRule: optionsOnly((rule: AutoAllowRule) => { + ruleSet.deleteRule(rule) + return Promise.resolve() + }), + + addRule: optionsOnly((rule: AutoAllowRule) => { + ruleSet.addRule(rule) + return Promise.resolve() + }), + } + }, + foreground: exportForegroundApi<PermissionApi>([ + 'waitForChange', + 'getRequests', + 'clearRequests', + 'requestAndWaitResult', + 'getRules', + 'deleteRule', + 'addRule', + 'allow', + 'deny' + ]), + } +}
\ No newline at end of file diff --git a/extension/src/features/pki-api.ts b/extension/src/features/pki-api.ts index 41fbd48..07fb3df 100644 --- a/extension/src/features/pki-api.ts +++ b/extension/src/features/pki-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/server-api/endpoints.ts b/extension/src/features/server-api/endpoints.ts index 9c73866..5fa1bf4 100644 --- a/extension/src/features/server-api/endpoints.ts +++ b/extension/src/features/server-api/endpoints.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts index cd67242..b9524ed 100644 --- a/extension/src/features/server-api/index.ts +++ b/extension/src/features/server-api/index.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts index 9a3c32d..ca714a5 100644 --- a/extension/src/features/settings.ts +++ b/extension/src/features/settings.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts index f5f1b6c..22369d0 100644 --- a/extension/src/features/tagfilter-api.ts +++ b/extension/src/features/tagfilter-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts index 92cf6cf..fe59011 100644 --- a/extension/src/features/types.ts +++ b/extension/src/features/types.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as diff --git a/extension/src/features/util.ts b/extension/src/features/util.ts index e9147bc..6ec8f15 100644 --- a/extension/src/features/util.ts +++ b/extension/src/features/util.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as @@ -20,15 +20,19 @@ import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw } from "vue"; import type { Watchable } from "./types"; export const waitForChange = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]):Promise<void> => { - return new Promise((resolve) => watchOnce(source, () => resolve())) + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) } export const waitForChangeFn = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]) => { return (): Promise<void> => { - return new Promise((resolve) => watchOnce(source, () => resolve())) + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), {deep: true})) } } +export const waitOne = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]): Promise<void> => { + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) +} + export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, initialValue: MaybeRefOrGetter<T>): RemovableRef<T> => { const wrapper: StorageLikeAsync = { |