1
0
mirror of https://github.com/bitwarden/server synced 2025-12-24 20:23:21 +00:00

[PM-20348] Add pending auth request endpoint (#5957)

* Feat(pm-20348): 
  * Add migration scripts for Read Pending Auth Requests by UserId stored procedure and new `view` for pending AuthRequest. 
  * View only returns the most recent pending authRequest, or none at all if the most recent is answered.
  * Implement stored procedure in AuthRequestRepository for both Dapper and Entity Framework.
  * Update AuthRequestController to query the new View to get a user's most recent pending auth requests response includes the requesting deviceId.

* Doc: 
  * Move summary xml comments to interface.
  * Added comments for the AuthRequestService.

* Test: 
  * Added testing for AuthRequestsController.
  * Added testing for repositories. 
  * Added integration tests for multiple auth requests but only returning the most recent.
This commit is contained in:
Ike
2025-06-30 13:17:51 -04:00
committed by GitHub
parent 899ff1b660
commit 20bf1455cf
14 changed files with 752 additions and 50 deletions

View File

@@ -3,6 +3,7 @@ using AutoMapper.QueryableExtensions;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.EntityFramework.Auth.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
@@ -14,9 +15,13 @@ namespace Bit.Infrastructure.EntityFramework.Auth.Repositories;
public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest, AuthRequest, Guid>, IAuthRequestRepository
{
public AuthRequestRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.AuthRequests)
{ }
private readonly IGlobalSettings _globalSettings;
public AuthRequestRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper, IGlobalSettings globalSettings)
: base(serviceScopeFactory, mapper, context => context.AuthRequests)
{
_globalSettings = globalSettings;
}
public async Task<int> DeleteExpiredAsync(
TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration)
{
@@ -57,6 +62,32 @@ public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest,
}
}
public async Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId)
{
var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var mostRecentAuthRequests = await
(from authRequest in dbContext.AuthRequests
where authRequest.Type == AuthRequestType.AuthenticateAndUnlock
|| authRequest.Type == AuthRequestType.Unlock
where authRequest.UserId == userId
where authRequest.CreationDate.AddMinutes(expirationMinutes) >= DateTime.UtcNow
group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests
select
(from r in groupedAuthRequests
join d in dbContext.Devices on new { r.RequestDeviceIdentifier, r.UserId }
equals new { RequestDeviceIdentifier = d.Identifier, d.UserId } into deviceJoin
from dj in deviceJoin.DefaultIfEmpty() // This creates a left join allowing null for devices
orderby r.CreationDate descending
select new PendingAuthRequestDetails(r, dj.Id)).First()
).ToListAsync();
mostRecentAuthRequests.RemoveAll(a => a.Approved != null);
return mostRecentAuthRequests;
}
public async Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(
Guid organizationId,
IEnumerable<Guid> ids)