diff --git a/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/AccessConnectorEndpoints.cs b/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/AccessConnectorEndpoints.cs
index 0fc894dde419..2de4232083c9 100644
--- a/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/AccessConnectorEndpoints.cs
+++ b/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/AccessConnectorEndpoints.cs
@@ -1,13 +1,11 @@
-using Bit.Services.Pam.AccessConnector.Api.Endpoints.Handlers;
+using System.Security.Claims;
+using Bit.Services.Pam.AccessConnector.Api.Endpoints.Handlers;
using Bit.Services.Pam.AccessConnector.Api.Models.Request;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Services.Pam.AccessConnector.Api.Endpoints;
-///
-/// The organizations/{orgId}/access-connectors resource: fleet registration, enable/disable, deletion, and
-/// target assignment. orgId is bound from the group's route prefix.
-///
+/// The organizations/{orgId}/access-connectors resource.
internal static class AccessConnectorEndpoints
{
public static RouteGroupBuilder MapAccessConnectorEndpoints(this RouteGroupBuilder group)
@@ -23,25 +21,25 @@ public static RouteGroupBuilder MapAccessConnectorEndpoints(this RouteGroupBuild
"Returns one access connector with its recent rotation activity -- the jobs it has worked and " +
"the attempts it recorded against them, newest first.");
- group.MapPost("", ([FromRoute] Guid orgId, RegisterAccessConnectorRequestModel model, AccessConnectorEndpointsHandler handler) => handler.Post(orgId, model))
+ group.MapPost("", ([FromRoute] Guid orgId, RegisterAccessConnectorRequestModel model, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) => handler.Post(user, orgId, model))
.WithName("Pam_AccessConnectors_Post")
.WithDescription(
"Registers an access connector and returns its client secret. The secret is shown exactly once here " +
"-- the server hashes it for storage and cannot return it again.");
group.MapPost("{id:guid}/enable",
- async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler) =>
+ async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) =>
{
- await handler.Enable(orgId, id);
+ await handler.Enable(user, orgId, id);
return TypedResults.NoContent();
})
.WithName("Pam_AccessConnectors_Enable")
.WithDescription("Re-enables a disabled access connector so it can authenticate and claim jobs again.");
group.MapPost("{id:guid}/disable",
- async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler) =>
+ async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) =>
{
- await handler.Disable(orgId, id);
+ await handler.Disable(user, orgId, id);
return TypedResults.NoContent();
})
.WithName("Pam_AccessConnectors_Disable")
@@ -50,9 +48,9 @@ public static RouteGroupBuilder MapAccessConnectorEndpoints(this RouteGroupBuild
"are released, but its credential is retained so it can be re-enabled later.");
group.MapDelete("{id:guid}",
- async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler) =>
+ async ([FromRoute] Guid orgId, Guid id, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) =>
{
- await handler.Delete(orgId, id);
+ await handler.Delete(user, orgId, id);
return TypedResults.NoContent();
})
.WithName("Pam_AccessConnectors_Delete")
@@ -62,17 +60,17 @@ public static RouteGroupBuilder MapAccessConnectorEndpoints(this RouteGroupBuild
"remediation for a suspected compromise.");
group.MapPost("{id:guid}/assignments",
- async ([FromRoute] Guid orgId, Guid id, AssignAccessConnectorTargetRequestModel model, AccessConnectorEndpointsHandler handler) =>
+ async ([FromRoute] Guid orgId, Guid id, AssignAccessConnectorTargetRequestModel model, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) =>
{
- await handler.AssignTarget(orgId, id, model);
+ await handler.AssignTarget(user, orgId, id, model);
return TypedResults.NoContent();
})
.WithName("Pam_AccessConnectors_AssignTarget");
group.MapDelete("{id:guid}/assignments/{targetSystemId:guid}",
- async ([FromRoute] Guid orgId, Guid id, Guid targetSystemId, AccessConnectorEndpointsHandler handler) =>
+ async ([FromRoute] Guid orgId, Guid id, Guid targetSystemId, AccessConnectorEndpointsHandler handler, ClaimsPrincipal user) =>
{
- await handler.UnassignTarget(orgId, id, targetSystemId);
+ await handler.UnassignTarget(user, orgId, id, targetSystemId);
return TypedResults.NoContent();
})
.WithName("Pam_AccessConnectors_UnassignTarget");
diff --git a/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/Filters/AccessConnectorHeartbeatEndpointFilter.cs b/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/Filters/AccessConnectorHeartbeatEndpointFilter.cs
new file mode 100644
index 000000000000..b2b7d521797a
--- /dev/null
+++ b/bitwarden_license/src/Services/Pam/AccessConnector/Api/Endpoints/Filters/AccessConnectorHeartbeatEndpointFilter.cs
@@ -0,0 +1,32 @@
+using Bit.Core.Context;
+using Bit.Core.Exceptions;
+using Bit.Pam.Entities;
+using Bit.Pam.Repositories;
+using Microsoft.Extensions.Options;
+
+namespace Bit.Services.Pam.AccessConnector.Api.Endpoints.Filters;
+
+///
+/// Bumps on every connector-facing route, at most once per
+/// HeartbeatMinInterval since the repository write is conditional. Authorizes nothing; token issuance and the
+/// work queries check eligibility.
+///
+public class AccessConnectorHeartbeatEndpointFilter : IEndpointFilter
+{
+ public async ValueTask