diff --git a/bitwarden-server.slnx b/bitwarden-server.slnx
index 28f4406b31fc..835e4b2b57f8 100644
--- a/bitwarden-server.slnx
+++ b/bitwarden-server.slnx
@@ -19,6 +19,7 @@
+
@@ -26,6 +27,7 @@
+
@@ -37,11 +39,15 @@
+
+
+
+
diff --git a/bitwarden_license/src/Commercial.Core/packages.lock.json b/bitwarden_license/src/Commercial.Core/packages.lock.json
index 5f90fe68bef3..67418c84e1d9 100644
--- a/bitwarden_license/src/Commercial.Core/packages.lock.json
+++ b/bitwarden_license/src/Commercial.Core/packages.lock.json
@@ -1159,6 +1159,7 @@
"BitPay.Light": "[1.0.1907, 1.0.1907]",
"Braintree": "[5.36.0, 5.36.0]",
"CsvHelper": "[33.1.0, 33.1.0]",
+ "Data": "[2026.6.1, )",
"DnsClient": "[1.8.0, 1.8.0]",
"Duende.IdentityServer": "[7.4.6, 7.4.6]",
"DuoUniversal": "[1.3.1, 1.3.1]",
@@ -1183,6 +1184,7 @@
"Newtonsoft.Json": "[13.0.3, 13.0.3]",
"OneOf": "[3.0.271, 3.0.271]",
"Otp.NET": "[1.4.0, 1.4.0]",
+ "Pam.Domain": "[2026.6.1, )",
"Quartz": "[3.15.1, 3.15.1]",
"Quartz.Extensions.DependencyInjection": "[3.15.1, 3.15.1]",
"Quartz.Extensions.Hosting": "[3.15.1, 3.15.1]",
@@ -1195,6 +1197,15 @@
"ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]",
"ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]"
}
+ },
+ "data": {
+ "type": "Project"
+ },
+ "pam.domain": {
+ "type": "Project",
+ "dependencies": {
+ "Data": "[2026.6.1, )"
+ }
}
}
}
diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json
index 1423baf938d1..f7e8b634d243 100644
--- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json
+++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json
@@ -1315,6 +1315,7 @@
"BitPay.Light": "[1.0.1907, 1.0.1907]",
"Braintree": "[5.36.0, 5.36.0]",
"CsvHelper": "[33.1.0, 33.1.0]",
+ "Data": "[2026.6.1, )",
"DnsClient": "[1.8.0, 1.8.0]",
"Duende.IdentityServer": "[7.4.6, 7.4.6]",
"DuoUniversal": "[1.3.1, 1.3.1]",
@@ -1339,6 +1340,7 @@
"Newtonsoft.Json": "[13.0.3, 13.0.3]",
"OneOf": "[3.0.271, 3.0.271]",
"Otp.NET": "[1.4.0, 1.4.0]",
+ "Pam.Domain": "[2026.6.1, )",
"Quartz": "[3.15.1, 3.15.1]",
"Quartz.Extensions.DependencyInjection": "[3.15.1, 3.15.1]",
"Quartz.Extensions.Hosting": "[3.15.1, 3.15.1]",
@@ -1352,19 +1354,29 @@
"ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]"
}
},
+ "data": {
+ "type": "Project"
+ },
"infrastructure.entityframework": {
"type": "Project",
"dependencies": {
"AutoMapper": "[14.0.0, 14.0.0]",
- "Core": "[2026.6.0, )",
+ "Core": "[2026.6.1, )",
"Microsoft.EntityFrameworkCore.Relational": "[8.0.8, 8.0.8]",
"Microsoft.EntityFrameworkCore.SqlServer": "[8.0.8, 8.0.8]",
"Microsoft.EntityFrameworkCore.Sqlite": "[8.0.8, 8.0.8]",
"Npgsql.EntityFrameworkCore.PostgreSQL": "[8.0.4, 8.0.4]",
+ "Pam.Domain": "[2026.6.1, )",
"Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]",
"linq2db": "[5.4.1, 5.4.1]",
"linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]"
}
+ },
+ "pam.domain": {
+ "type": "Project",
+ "dependencies": {
+ "Data": "[2026.6.1, )"
+ }
}
}
}
diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs
new file mode 100644
index 000000000000..7f6cd2a210dd
--- /dev/null
+++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs
@@ -0,0 +1,43 @@
+using System.Security.Claims;
+using Bit.Commercial.Pam.Api.Endpoints.Handlers;
+using Bit.Commercial.Pam.Api.Models.Request;
+
+namespace Bit.Commercial.Pam.Api.Endpoints;
+
+///
+/// The access-requests resource: lease requests through their lifecycle (the requester's own queue plus the
+/// approver's queue and decision). Mirrors the routes the former AccessRequestsController served.
+///
+internal static class AccessRequestEndpoints
+{
+ public static RouteGroupBuilder MapAccessRequestEndpoints(this RouteGroupBuilder group)
+ {
+ group.MapGet("inbox", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetInbox(user))
+ .WithName("Pam_AccessRequests_GetInbox");
+
+ group.MapGet("history", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetHistory(user))
+ .WithName("Pam_AccessRequests_GetHistory");
+
+ group.MapGet("mine", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetMine(user))
+ .WithName("Pam_AccessRequests_GetMine");
+
+ group.MapPost("{id:guid}/decision",
+ (Guid id, AccessDecisionRequestModel model, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) =>
+ handler.Decide(user, id, model))
+ .WithName("Pam_AccessRequests_Decide");
+
+ group.MapPost("{id:guid}/activate",
+ (Guid id, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.Activate(user, id))
+ .WithName("Pam_AccessRequests_Activate");
+
+ group.MapPost("{id:guid}/revoke",
+ async (Guid id, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) =>
+ {
+ await handler.Revoke(user, id);
+ return TypedResults.NoContent();
+ })
+ .WithName("Pam_AccessRequests_Revoke");
+
+ return group;
+ }
+}
diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs
new file mode 100644
index 000000000000..05247c97f21f
--- /dev/null
+++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs
@@ -0,0 +1,36 @@
+using Bit.Commercial.Pam.Api.Endpoints.Handlers;
+using Bit.Commercial.Pam.Api.Models.Request;
+
+namespace Bit.Commercial.Pam.Api.Endpoints;
+
+///
+/// The organizations/{orgId}/access-rules resource: rule CRUD scoped to an organization. Mirrors the routes
+/// the former AccessRulesController served. orgId is bound from the group's route prefix.
+///
+internal static class AccessRuleEndpoints
+{
+ public static RouteGroupBuilder MapAccessRuleEndpoints(this RouteGroupBuilder group)
+ {
+ group.MapGet("", (Guid orgId, AccessRuleEndpointsHandler handler) => handler.GetAll(orgId))
+ .WithName("Pam_AccessRules_GetAll");
+
+ group.MapGet("{id:guid}", (Guid orgId, Guid id, AccessRuleEndpointsHandler handler) => handler.Get(orgId, id))
+ .WithName("Pam_AccessRules_Get");
+
+ group.MapPost("", (Guid orgId, AccessRuleRequestModel model, AccessRuleEndpointsHandler handler) => handler.Post(orgId, model))
+ .WithName("Pam_AccessRules_Post");
+
+ group.MapPut("{id:guid}", (Guid orgId, Guid id, AccessRuleRequestModel model, AccessRuleEndpointsHandler handler) => handler.Put(orgId, id, model))
+ .WithName("Pam_AccessRules_Put");
+
+ group.MapDelete("{id:guid}",
+ async (Guid orgId, Guid id, AccessRuleEndpointsHandler handler) =>
+ {
+ await handler.Delete(orgId, id);
+ return TypedResults.NoContent();
+ })
+ .WithName("Pam_AccessRules_Delete");
+
+ return group;
+ }
+}
diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs
new file mode 100644
index 000000000000..0e2c84b38f49
--- /dev/null
+++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs
@@ -0,0 +1,30 @@
+using System.Security.Claims;
+using Bit.Commercial.Pam.Api.Endpoints.Handlers;
+using Bit.Commercial.Pam.Api.Models.Request;
+
+namespace Bit.Commercial.Pam.Api.Endpoints;
+
+///
+/// The ciphers/{id}/lease resource: the per-cipher leasing entry points (pre-check, state, submit).
+/// Mirrors the routes the former CipherLeaseController served. id is bound from the group's route
+/// prefix. The deprecated GET …/lease/cipher read-back is hosted by a small MVC controller in the Api
+/// project (it depends on the Api Vault response models).
+///
+internal static class CipherLeaseEndpoints
+{
+ public static RouteGroupBuilder MapCipherLeaseEndpoints(this RouteGroupBuilder group)
+ {
+ group.MapGet("pre-check", (Guid id, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.PreCheck(user, id))
+ .WithName("Pam_CipherLease_PreCheck");
+
+ group.MapGet("state", (Guid id, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.State(user, id))
+ .WithName("Pam_CipherLease_State");
+
+ group.MapPost("",
+ (Guid id, AccessRequestCreateRequestModel model, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) =>
+ handler.Post(user, id, model))
+ .WithName("Pam_CipherLease_Post");
+
+ return group;
+ }
+}
diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs
new file mode 100644
index 000000000000..e3a110a9d257
--- /dev/null
+++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs
@@ -0,0 +1,92 @@
+using Bit.Core.Exceptions;
+using Bit.Core.Models.Api;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Bit.Commercial.Pam.Api.Endpoints.Filters;
+
+///
+/// Minimal API equivalent of the internal-API branch of Bit.Api.Utilities.ExceptionHandlerFilterAttribute.
+/// Minimal API endpoints do not run MVC exception filters and src/Api has no exception-handling middleware,
+/// so this filter translates thrown exceptions into Bitwarden's with the same
+/// status codes the controllers produced (e.g. → 404 "Resource not found.").
+///
+public class PamExceptionHandlerEndpointFilter : IEndpointFilter
+{
+ public async ValueTask