From 29c7b0b6ce6fb1bcfc3053c92bd24c7c7d580fa4 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 17 Jan 2024 11:47:24 +0100 Subject: [PATCH] feat: implement permissions (#1645) Spec: https://www.w3.org/TR/permissions/#automation-webdriver-bidi Tests: https://github.com/web-platform-tests/wpt/pull/43802 --------- Co-authored-by: Maksim Sadym <69349599+sadym-chromium@users.noreply.github.com> --- src/bidiMapper/BidiNoOpParser.ts | 10 ++++ src/bidiMapper/BidiParser.ts | 8 +++ src/bidiMapper/CommandProcessor.ts | 11 ++++ .../permissions/PermissionsProcessor.ts | 56 ++++++++++++++++++ src/bidiTab/BidiParser.ts | 10 ++++ .../generated/webdriver-bidi-permissions.ts | 58 +++++++++++++++++++ src/protocol-parser/protocol-parser.ts | 12 ++++ src/protocol/chromium-bidi.ts | 11 +++- .../generated/webdriver-bidi-permissions.ts | 47 +++++++++++++++ src/protocol/protocol.ts | 1 + tests/permissions/__init__.py | 58 +++++++++++++++++++ tests/permissions/test_set_permission.py | 36 ++++++++++++ 12 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/bidiMapper/domains/permissions/PermissionsProcessor.ts create mode 100644 src/protocol-parser/generated/webdriver-bidi-permissions.ts create mode 100644 src/protocol/generated/webdriver-bidi-permissions.ts create mode 100644 tests/permissions/__init__.py create mode 100644 tests/permissions/test_set_permission.py diff --git a/src/bidiMapper/BidiNoOpParser.ts b/src/bidiMapper/BidiNoOpParser.ts index a2fe297540..69910f9d5e 100644 --- a/src/bidiMapper/BidiNoOpParser.ts +++ b/src/bidiMapper/BidiNoOpParser.ts @@ -23,6 +23,7 @@ import type { Script, Session, Storage, + Permissions, } from '../protocol/protocol.js'; import type {BidiCommandParameterParser} from './BidiParser.js'; @@ -157,6 +158,15 @@ export class BidiNoOpParser implements BidiCommandParameterParser { } // keep-sorted end + // Permissions domain + // keep-sorted start block=yes + parseSetPermissionsParams( + params: unknown + ): Permissions.SetPermissionParameters { + return params as Permissions.SetPermissionParameters; + } + // keep-sorted end + // Session domain // keep-sorted start block=yes parseSubscribeParams(params: unknown): Session.SubscriptionRequest { diff --git a/src/bidiMapper/BidiParser.ts b/src/bidiMapper/BidiParser.ts index ac4a2c62cb..d0ea0ee6e4 100644 --- a/src/bidiMapper/BidiParser.ts +++ b/src/bidiMapper/BidiParser.ts @@ -20,6 +20,7 @@ import type { Cdp, Input, Network, + Permissions, Script, Session, Storage, @@ -62,6 +63,13 @@ export interface BidiCommandParameterParser { parseSetFilesParams(params: unknown): Input.SetFilesParameters; // keep-sorted end + // PermissionsDomain domain + // keep-sorted start block=yes + parseSetPermissionsParams( + params: unknown + ): Permissions.SetPermissionParameters; + // keep-sorted end block=yes + // Network domain // keep-sorted start block=yes parseAddInterceptParams(params: unknown): Network.AddInterceptParameters; diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index 049bc4fd49..3e187f231a 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -37,6 +37,7 @@ import type {BrowsingContextStorage} from './domains/context/BrowsingContextStor import {InputProcessor} from './domains/input/InputProcessor.js'; import {NetworkProcessor} from './domains/network/NetworkProcessor.js'; import {NetworkStorage} from './domains/network/NetworkStorage.js'; +import {PermissionsProcessor} from './domains/permissions/PermissionsProcessor.js'; import {PreloadScriptStorage} from './domains/script/PreloadScriptStorage.js'; import type {RealmStorage} from './domains/script/RealmStorage.js'; import {ScriptProcessor} from './domains/script/ScriptProcessor.js'; @@ -63,6 +64,7 @@ export class CommandProcessor extends EventEmitter { #cdpProcessor: CdpProcessor; #inputProcessor: InputProcessor; #networkProcessor: NetworkProcessor; + #permissionsProcessor: PermissionsProcessor; #scriptProcessor: ScriptProcessor; #sessionProcessor: SessionProcessor; #storageProcessor: StorageProcessor; @@ -118,6 +120,7 @@ export class CommandProcessor extends EventEmitter { browsingContextStorage, networkStorage ); + this.#permissionsProcessor = new PermissionsProcessor(browserCdpClient); this.#scriptProcessor = new ScriptProcessor( browsingContextStorage, realmStorage, @@ -260,6 +263,14 @@ export class CommandProcessor extends EventEmitter { ); // keep-sorted end + // Permissions domain + // keep-sorted start block=yes + case 'permissions.setPermission': + return await this.#permissionsProcessor.setPermissions( + this.#parser.parseSetPermissionsParams(command.params) + ); + // keep-sorted end + // Script domain // keep-sorted start block=yes case 'script.addPreloadScript': diff --git a/src/bidiMapper/domains/permissions/PermissionsProcessor.ts b/src/bidiMapper/domains/permissions/PermissionsProcessor.ts new file mode 100644 index 0000000000..c629c95384 --- /dev/null +++ b/src/bidiMapper/domains/permissions/PermissionsProcessor.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2024 Google LLC. + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {CdpClient} from '../../../cdp/CdpClient.js'; +import { + InvalidArgumentException, + type EmptyResult, + type Permissions, +} from '../../../protocol/protocol.js'; + +export class PermissionsProcessor { + #browserCdpClient: CdpClient; + + constructor(browserCdpClient: CdpClient) { + this.#browserCdpClient = browserCdpClient; + } + + async setPermissions( + params: Permissions.SetPermissionParameters + ): Promise { + try { + await this.#browserCdpClient.sendCommand('Browser.setPermission', { + origin: params.origin, + permission: { + name: params.descriptor.name, + }, + setting: params.state, + }); + } catch (err) { + if ( + (err as Error).message === + `Permission can't be granted to opaque origins.` + ) { + // Return success if the origin is not valid (does not match any + // existing origins). + return {}; + } + throw new InvalidArgumentException((err as Error).message); + } + return {}; + } +} diff --git a/src/bidiTab/BidiParser.ts b/src/bidiTab/BidiParser.ts index bc6555dbfe..0c54113ae2 100644 --- a/src/bidiTab/BidiParser.ts +++ b/src/bidiTab/BidiParser.ts @@ -20,6 +20,7 @@ import type { Cdp, Input, Network, + Permissions, Script, Session, Storage, @@ -132,6 +133,15 @@ export class BidiParser implements BidiCommandParameterParser { } // keep-sorted end + // Permissions domain + // keep-sorted start block=yes + parseSetPermissionsParams( + params: unknown + ): Permissions.SetPermissionParameters { + return Parser.Permissions.parseSetPermissionsParams(params); + } + // keep-sorted end + // Script domain // keep-sorted start block=yes parseAddPreloadScriptParams( diff --git a/src/protocol-parser/generated/webdriver-bidi-permissions.ts b/src/protocol-parser/generated/webdriver-bidi-permissions.ts new file mode 100644 index 0000000000..7f34d20252 --- /dev/null +++ b/src/protocol-parser/generated/webdriver-bidi-permissions.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2024 Google LLC. + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * THIS FILE IS AUTOGENERATED. Run `npm run bidi-types` to regenerate. + * @see https://github.com/w3c/webdriver-bidi/blob/master/index.bs + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck Some types may be circular. + +import z from 'zod'; + +export const PermissionsCommandSchema = z.lazy( + () => Permissions.SetPermissionSchema +); +export namespace Permissions { + export const PermissionDescriptorSchema = z.lazy(() => + z.object({ + name: z.string(), + }) + ); +} +export namespace Permissions { + export const PermissionStateSchema = z.lazy(() => + z.enum(['granted', 'denied', 'prompt']) + ); +} +export namespace Permissions { + export const SetPermissionSchema = z.lazy(() => + z.object({ + method: z.literal('permissions.setPermission'), + params: Permissions.SetPermissionParametersSchema, + }) + ); +} +export namespace Permissions { + export const SetPermissionParametersSchema = z.lazy(() => + z.object({ + descriptor: Permissions.PermissionDescriptorSchema, + state: Permissions.PermissionStateSchema, + origin: z.string(), + }) + ); +} diff --git a/src/protocol-parser/protocol-parser.ts b/src/protocol-parser/protocol-parser.ts index 3a419cced7..58e29a197a 100644 --- a/src/protocol-parser/protocol-parser.ts +++ b/src/protocol-parser/protocol-parser.ts @@ -24,6 +24,7 @@ import {z, type ZodType} from 'zod'; import type * as Protocol from '../protocol/protocol.js'; import {InvalidArgumentException} from '../protocol/protocol.js'; +import * as WebDriverBidiPermissions from './generated/webdriver-bidi-permissions.js'; import * as WebDriverBidi from './generated/webdriver-bidi.js'; export function parseObject( @@ -344,3 +345,14 @@ export namespace Cdp { return parseObject(params, GetSessionRequestSchema); } } + +export namespace Permissions { + export function parseSetPermissionsParams( + params: unknown + ): Protocol.Permissions.SetPermissionParameters { + return parseObject( + params, + WebDriverBidiPermissions.Permissions.SetPermissionParametersSchema + ) as Protocol.Permissions.SetPermissionParameters; + } +} diff --git a/src/protocol/chromium-bidi.ts b/src/protocol/chromium-bidi.ts index e9db023cc3..24a27e2f72 100644 --- a/src/protocol/chromium-bidi.ts +++ b/src/protocol/chromium-bidi.ts @@ -16,6 +16,7 @@ */ import type * as Cdp from './cdp.js'; +import type * as WebDriverBidiPermissions from './generated/webdriver-bidi-permissions.js'; import type * as WebDriverBidi from './generated/webdriver-bidi.js'; export type EventNames = @@ -87,7 +88,15 @@ export namespace Network { } } -export type Command = (WebDriverBidi.Command | Cdp.Command) & { +export type Command = ( + | WebDriverBidi.Command + | Cdp.Command + | ({ + // id is defined by the main WebDriver BiDi spec and extension specs do + // not re-define it. Therefore, it's not part of generated types. + id: WebDriverBidi.JsUint; + } & WebDriverBidiPermissions.PermissionsCommand) +) & { channel?: WebDriverBidi.Script.Channel; }; diff --git a/src/protocol/generated/webdriver-bidi-permissions.ts b/src/protocol/generated/webdriver-bidi-permissions.ts new file mode 100644 index 0000000000..f237dc504e --- /dev/null +++ b/src/protocol/generated/webdriver-bidi-permissions.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2024 Google LLC. + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * THIS FILE IS AUTOGENERATED. Run `npm run bidi-types` to regenerate. + * @see https://github.com/w3c/webdriver-bidi/blob/master/index.bs + */ +export type PermissionsCommand = Permissions.SetPermission; +export namespace Permissions { + export type PermissionDescriptor = { + name: string; + }; +} +export namespace Permissions { + export const enum PermissionState { + Granted = 'granted', + Denied = 'denied', + Prompt = 'prompt', + } +} +export namespace Permissions { + export type SetPermission = { + method: 'permissions.setPermission'; + params: Permissions.SetPermissionParameters; + }; +} +export namespace Permissions { + export type SetPermissionParameters = { + descriptor: Permissions.PermissionDescriptor; + state: Permissions.PermissionState; + origin: string; + }; +} diff --git a/src/protocol/protocol.ts b/src/protocol/protocol.ts index 6ab2e0d530..6977ccf340 100644 --- a/src/protocol/protocol.ts +++ b/src/protocol/protocol.ts @@ -18,3 +18,4 @@ export * as Cdp from './cdp.js'; export * as ChromiumBidi from './chromium-bidi.js'; export * from './generated/webdriver-bidi.js'; export * from './ErrorResponse.js'; +export * from './generated/webdriver-bidi-permissions.js'; diff --git a/tests/permissions/__init__.py b/tests/permissions/__init__.py new file mode 100644 index 0000000000..6465f20c4a --- /dev/null +++ b/tests/permissions/__init__.py @@ -0,0 +1,58 @@ +# Copyright 2024 Google LLC. +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from urllib.parse import urlparse + +from test_helpers import execute_command + + +def get_origin(url): + """ Return the origin for the given url.""" + parts = urlparse(url) + return parts.scheme + '://' + parts.netloc + + +async def query_permission(websocket, context_id, name): + """ Queries a permission via the script.callFunction command.""" + + result = await execute_command( + websocket, { + "method": "script.callFunction", + "params": { + "functionDeclaration": """() => { + return navigator.permissions.query({ name: '%s' }) + .then(val => val.state, err => err.message) + }""" % name, + "target": { + "context": context_id + }, + "awaitPromise": True, + } + }) + + return result['result']['value'] + + +async def set_permission(websocket, origin, descriptor, state): + """ Set a permission via the permissions.setPermission command.""" + return await execute_command( + websocket, { + 'method': 'permissions.setPermission', + 'params': { + 'origin': origin, + 'descriptor': descriptor, + 'state': state + } + }) diff --git a/tests/permissions/test_set_permission.py b/tests/permissions/test_set_permission.py new file mode 100644 index 0000000000..de6239a234 --- /dev/null +++ b/tests/permissions/test_set_permission.py @@ -0,0 +1,36 @@ +# Copyright 2024 Google LLC. +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from permissions import get_origin, query_permission, set_permission +from test_helpers import goto_url + + +@pytest.mark.asyncio +async def test_permissions_set_permission(websocket, context_id, example_url): + origin = get_origin(example_url) + await goto_url(websocket, context_id, example_url) + assert await query_permission(websocket, context_id, + 'geolocation') == 'prompt' + resp = await set_permission(websocket, origin, {'name': 'geolocation'}, + 'granted') + assert resp == {} + assert await query_permission(websocket, context_id, + 'geolocation') == 'granted' + resp = await set_permission(websocket, origin, {'name': 'geolocation'}, + 'prompt') + assert resp == {} + assert await query_permission(websocket, context_id, + 'geolocation') == 'prompt'