diff --git a/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDi.cs b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDi.cs
new file mode 100644
index 0000000000000..c70ed1812c91b
--- /dev/null
+++ b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDi.cs
@@ -0,0 +1,45 @@
+//
+// 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.
+//
+
+using OpenQA.Selenium.BiDi.Communication;
+using System;
+using System.Threading.Tasks;
+
+namespace OpenQA.Selenium.BiDi.Extensions.Permissions;
+
+public class PermissionsBiDi : BiDi
+{
+ private readonly Lazy _permissionsModule;
+
+ public PermissionsModule Permissions => _permissionsModule.Value;
+
+ private PermissionsBiDi(string url)
+ : base(url)
+ {
+ Broker.ProvideCustomSerializationContext(PermissionsModule.SerializerContext);
+ _permissionsModule = new Lazy(() => new PermissionsModule(Broker));
+ }
+
+ public static async Task ConnectAsync(string webSocketUrl)
+ {
+ var bidi = new PermissionsBiDi(webSocketUrl);
+ await bidi.Broker.ConnectAsync();
+ return bidi;
+ }
+}
diff --git a/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDiJsonSerializerContext.cs b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDiJsonSerializerContext.cs
new file mode 100644
index 0000000000000..45aa7fb66d809
--- /dev/null
+++ b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsBiDiJsonSerializerContext.cs
@@ -0,0 +1,25 @@
+//
+// 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.
+//
+
+using System.Text.Json.Serialization;
+
+namespace OpenQA.Selenium.BiDi.Extensions.Permissions;
+
+[JsonSerializable(typeof(SetPermissionCommand), TypeInfoPropertyName = "Permissions_SetPermissionCommand")]
+internal partial class PermissionsBiDiJsonSerializerContext : JsonSerializerContext;
diff --git a/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsExtensions.cs b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsExtensions.cs
new file mode 100644
index 0000000000000..ee09601f11af5
--- /dev/null
+++ b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsExtensions.cs
@@ -0,0 +1,44 @@
+//
+// 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.
+//
+
+using System;
+using System.Threading.Tasks;
+
+namespace OpenQA.Selenium.BiDi.Extensions.Permissions;
+
+public static class PermissionsExtensions
+{
+ public static async Task AsBiDiPermissionsAsync(this IWebDriver webDriver)
+ {
+ if (webDriver is null) throw new ArgumentNullException(nameof(webDriver));
+
+ string? webSocketUrl = null;
+
+ if (webDriver is IHasCapabilities hasCapabilities)
+ {
+ webSocketUrl = hasCapabilities.Capabilities.GetCapability("webSocketUrl")?.ToString();
+ }
+
+ if (webSocketUrl is null) throw new BiDiException("The driver is not compatible with bidirectional protocol or \"webSocketUrl\" not enabled in driver options.");
+
+ var bidi = await PermissionsBiDi.ConnectAsync(webSocketUrl).ConfigureAwait(false);
+
+ return bidi;
+ }
+}
diff --git a/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsModule.cs b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsModule.cs
new file mode 100644
index 0000000000000..3a7c81021f6fe
--- /dev/null
+++ b/dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsModule.cs
@@ -0,0 +1,38 @@
+//
+// 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.
+//
+
+using OpenQA.Selenium.BiDi.Communication;
+using OpenQA.Selenium.BiDi.Modules;
+using OpenQA.Selenium.BiDi.Modules.Browser;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace OpenQA.Selenium.BiDi.Extensions.Permissions;
+
+public class PermissionsModule(Broker broker) : Module(broker)
+{
+ public static JsonSerializerContext SerializerContext => PermissionsBiDiJsonSerializerContext.Default;
+
+ public async Task SetPermissionAsync(string permissionName, PermissionState state, string origin, UserContext? userContext, SetPermissionOptions? options = null)
+ {
+ var @params = new SetPermissionCommandParameters(new PermissionDescriptor(permissionName), state, origin, userContext);
+
+ await Broker.ExecuteCommandAsync(new SetPermissionCommand(@params), options).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/src/webdriver/BiDi/Extensions/Permissions/SetPermissionCommand.cs b/dotnet/src/webdriver/BiDi/Extensions/Permissions/SetPermissionCommand.cs
new file mode 100644
index 0000000000000..2bff4df15220b
--- /dev/null
+++ b/dotnet/src/webdriver/BiDi/Extensions/Permissions/SetPermissionCommand.cs
@@ -0,0 +1,39 @@
+//
+// 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.
+//
+
+using OpenQA.Selenium.BiDi.Communication;
+using OpenQA.Selenium.BiDi.Modules.Browser;
+
+namespace OpenQA.Selenium.BiDi.Extensions.Permissions;
+
+internal class SetPermissionCommand(SetPermissionCommandParameters @params)
+ : Command(@params, "permissions.setPermission");
+
+public record SetPermissionOptions : CommandOptions;
+
+internal record SetPermissionCommandParameters(PermissionDescriptor Descriptor, PermissionState State, string Origin, UserContext? UserContext) : CommandParameters;
+
+internal record PermissionDescriptor(string Name);
+
+public enum PermissionState
+{
+ Granted,
+ Denied,
+ Prompt
+}
diff --git a/dotnet/test/common/BiDi/BiDiFixture.cs b/dotnet/test/common/BiDi/BiDiFixture.cs
index d81501a3df668..68de58ebfb657 100644
--- a/dotnet/test/common/BiDi/BiDiFixture.cs
+++ b/dotnet/test/common/BiDi/BiDiFixture.cs
@@ -44,11 +44,16 @@ public async Task BiDiSetUp()
driver = EnvironmentManager.Instance.CreateDriverInstance(options);
- bidi = await driver.AsBiDiAsync();
+ bidi = await CreateBiDi(driver);
context = (await bidi.BrowsingContext.GetTreeAsync())[0].Context;
}
+ protected virtual async Task CreateBiDi(IWebDriver driver)
+ {
+ return await driver.AsBiDiAsync();
+ }
+
[TearDown]
public async Task BiDiTearDown()
{
diff --git a/dotnet/test/common/BiDi/Permissions/PermissionsTests.cs b/dotnet/test/common/BiDi/Permissions/PermissionsTests.cs
new file mode 100644
index 0000000000000..cc00f10aa90dc
--- /dev/null
+++ b/dotnet/test/common/BiDi/Permissions/PermissionsTests.cs
@@ -0,0 +1,70 @@
+//
+// 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.
+//
+
+using NUnit.Framework;
+using OpenQA.Selenium.BiDi.Extensions.Permissions;
+using OpenQA.Selenium.BiDi.Modules.BrowsingContext;
+using OpenQA.Selenium.BiDi.Modules.Script;
+using OpenQA.Selenium.Environment;
+using System;
+using System.Threading.Tasks;
+
+namespace OpenQA.Selenium.BiDi.Permissions
+{
+ internal class PermissionsTests : BiDiTestFixture
+ {
+ private new PermissionsBiDi bidi => (PermissionsBiDi)base.bidi;
+
+ protected override async Task CreateBiDi(IWebDriver driver)
+ {
+ return await driver.AsBiDiPermissionsAsync();
+ }
+
+ [Test]
+ public async Task SettingPermissionsTest()
+ {
+ var userContext = await bidi.Browser.CreateUserContextAsync();
+ var window = await bidi.BrowsingContext.CreateAsync(ContextType.Window, new()
+ {
+ ReferenceContext = context,
+ UserContext = userContext.UserContext,
+ Background = true
+ });
+
+ var newPage = EnvironmentManager.Instance.UrlBuilder.CreateInlinePage(new InlinePage()
+ .WithBody("new page
"));
+
+ await window.NavigateAsync(newPage);
+
+ var before = await window.Script.CallFunctionAsync("""
+ async () => (await navigator.permissions.query({ name: "geolocation" })).state
+ """, awaitPromise: true, new() { UserActivation = true, });
+
+ Assert.That(before.Result, Is.EqualTo(new RemoteValue.String("prompt")));
+
+ await bidi.Permissions.SetPermissionAsync("geolocation", PermissionState.Denied, newPage, userContext.UserContext);
+
+ var after = await window.Script.CallFunctionAsync("""
+ async () => (await navigator.permissions.query({ name: "geolocation" })).state
+ """, awaitPromise: true, new() { UserActivation = true });
+
+ Assert.That(after.Result, Is.EqualTo(new RemoteValue.String("denied")));
+ }
+ }
+}