Skip to content

[py][bidi]: implement bidi permissions module #15830

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

Merged
merged 6 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions py/selenium/webdriver/common/bidi/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you 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 typing import Optional, Union

from selenium.webdriver.common.bidi.common import command_builder


class PermissionState:
"""Represents the possible permission states."""

GRANTED = "granted"
DENIED = "denied"
PROMPT = "prompt"


class PermissionDescriptor:
"""Represents a permission descriptor."""

def __init__(self, name: str):
self.name = name

def to_dict(self) -> dict:
return {"name": self.name}


class Permissions:
"""
BiDi implementation of the permissions module.
"""

def __init__(self, conn):
self.conn = conn

def set_permission(
self,
descriptor: Union[str, PermissionDescriptor],
state: str,
origin: str,
user_context: Optional[str] = None,
) -> None:
"""Sets a permission state for a given permission descriptor.

Parameters:
-----------
descriptor: The permission name (str) or PermissionDescriptor object.
Examples: "geolocation", "camera", "microphone"
state: The permission state (granted, denied, prompt).
origin: The origin for which the permission is set.
user_context: The user context id (optional).

Raises:
------
ValueError: If the permission state is invalid.
"""
if state not in [PermissionState.GRANTED, PermissionState.DENIED, PermissionState.PROMPT]:
valid_states = f"{PermissionState.GRANTED}, {PermissionState.DENIED}, {PermissionState.PROMPT}"
raise ValueError(f"Invalid permission state. Must be one of: {valid_states}")

if isinstance(descriptor, str):
permission_descriptor = PermissionDescriptor(descriptor)
else:
permission_descriptor = descriptor

params = {
"descriptor": permission_descriptor.to_dict(),
"state": state,
"origin": origin,
}

if user_context is not None:
params["userContext"] = user_context

self.conn.execute(command_builder("permissions.setPermission", params))
24 changes: 24 additions & 0 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from selenium.webdriver.common.bidi.browser import Browser
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
from selenium.webdriver.common.bidi.network import Network
from selenium.webdriver.common.bidi.permissions import Permissions
from selenium.webdriver.common.bidi.script import Script
from selenium.webdriver.common.bidi.session import Session
from selenium.webdriver.common.bidi.storage import Storage
Expand Down Expand Up @@ -265,6 +266,7 @@ def __init__(
self._browsing_context = None
self._storage = None
self._webextension = None
self._permissions = None

def __repr__(self):
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
Expand Down Expand Up @@ -1339,6 +1341,28 @@ def storage(self):

return self._storage

@property
def permissions(self):
"""Returns a permissions module object for BiDi permissions commands.

Returns:
--------
Permissions: an object containing access to BiDi permissions commands.

Examples:
---------
>>> from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState
>>> descriptor = PermissionDescriptor("geolocation")
>>> driver.permissions.set_permission(descriptor, PermissionState.GRANTED, "https://example.com")
"""
if not self._websocket_connection:
self._start_bidi()

if self._permissions is None:
self._permissions = Permissions(self._websocket_connection)

return self._permissions

@property
def webextension(self):
"""Returns a webextension module object for BiDi webextension commands.
Expand Down
142 changes: 142 additions & 0 deletions py/test/selenium/webdriver/common/bidi_permissions_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you 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 selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState
from selenium.webdriver.common.window import WindowTypes


def get_origin(driver):
"""Get the current window origin."""
return driver.execute_script("return window.location.origin;")


def get_geolocation_permission(driver):
"""Get the geolocation permission state."""
script = """
const callback = arguments[arguments.length - 1];
navigator.permissions.query({ name: 'geolocation' }).then(permission => {
callback(permission.state);
}).catch(error => {
callback(null);
});
"""
return driver.execute_async_script(script)


def test_permissions_initialized(driver):
"""Test that the permissions module is initialized properly."""
assert driver.permissions is not None


def test_can_set_permission_to_granted(driver, pages):
"""Test setting permission to granted state."""
pages.load("blank.html")

origin = get_origin(driver)

# Set geolocation permission to granted
driver.permissions.set_permission("geolocation", PermissionState.GRANTED, origin)

result = get_geolocation_permission(driver)
assert result == PermissionState.GRANTED


def test_can_set_permission_to_denied(driver, pages):
"""Test setting permission to denied state."""
pages.load("blank.html")

origin = get_origin(driver)

# Set geolocation permission to denied
driver.permissions.set_permission("geolocation", PermissionState.DENIED, origin)

result = get_geolocation_permission(driver)
assert result == PermissionState.DENIED


def test_can_set_permission_to_prompt(driver, pages):
"""Test setting permission to prompt state."""
pages.load("blank.html")

origin = get_origin(driver)

# First set to denied, then to prompt since most of the time the default state is prompt
driver.permissions.set_permission("geolocation", PermissionState.DENIED, origin)
driver.permissions.set_permission("geolocation", PermissionState.PROMPT, origin)

result = get_geolocation_permission(driver)
assert result == PermissionState.PROMPT


def test_can_set_permission_for_user_context(driver, pages):
"""Test setting permission for a specific user context."""
# Create a user context
user_context = driver.browser.create_user_context()

context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context)

# Navigate both contexts to the same page
pages.load("blank.html")
original_window = driver.current_window_handle
driver.switch_to.window(context_id)
pages.load("blank.html")

origin = get_origin(driver)

# Get original permission states
driver.switch_to.window(original_window)
original_permission = get_geolocation_permission(driver)

driver.switch_to.window(context_id)

# Set permission only for the user context using PermissionDescriptor
descriptor = PermissionDescriptor("geolocation")
driver.permissions.set_permission(descriptor, PermissionState.GRANTED, origin, user_context)

# Check that the original window's permission hasn't changed
driver.switch_to.window(original_window)
updated_original_permission = get_geolocation_permission(driver)
assert updated_original_permission == original_permission

# Check that the new context's permission was updated
driver.switch_to.window(context_id)
updated_new_context_permission = get_geolocation_permission(driver)
assert updated_new_context_permission == PermissionState.GRANTED

driver.browsing_context.close(context_id)
driver.browser.remove_user_context(user_context)


def test_invalid_permission_state_raises_error(driver, pages):
"""Test that invalid permission state raises ValueError."""
pages.load("blank.html")
origin = get_origin(driver)

# set permission using PermissionDescriptor
descriptor = PermissionDescriptor("geolocation")

with pytest.raises(ValueError, match="Invalid permission state"):
driver.permissions.set_permission(descriptor, "invalid_state", origin)


def test_permission_states_constants():
"""Test that permission state constants are correctly defined."""
assert PermissionState.GRANTED == "granted"
assert PermissionState.DENIED == "denied"
assert PermissionState.PROMPT == "prompt"