1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-15 07:20:49 -05:00

feat: add deviceId to response for pending auth requests; Modified queries to match join for new devices; Fixed tests to account for object changes.

This commit is contained in:
Ike Kottlowski 2025-06-14 00:06:09 -04:00
parent d525a1c93c
commit f944bd2a31
No known key found for this signature in database
GPG Key ID: C86308E3DCA6D76F
9 changed files with 129 additions and 16 deletions

View File

@ -52,12 +52,12 @@ public class AuthRequestsController(
[HttpGet("pending")]
[RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)]
public async Task<ListResponseModel<AuthRequestResponseModel>> GetPendingAuthRequestsAsync()
public async Task<ListResponseModel<PendingAuthRequestResponseModel>> GetPendingAuthRequestsAsync()
{
var userId = _userService.GetProperUserId(User).Value;
var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);
var responses = rawResponse.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault));
return new ListResponseModel<AuthRequestResponseModel>(responses);
var responses = rawResponse.Select(a => new PendingAuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault));
return new ListResponseModel<PendingAuthRequestResponseModel>(responses);
}
[HttpGet("{id}/response")]

View File

@ -0,0 +1,15 @@
using Bit.Core.Auth.Models.Data;
namespace Bit.Api.Auth.Models.Response;
public class PendingAuthRequestResponseModel : AuthRequestResponseModel
{
public PendingAuthRequestResponseModel(PendingAuthRequestDetails authRequest, string vaultUri, string obj = "auth-request")
: base(authRequest, vaultUri, obj)
{
ArgumentNullException.ThrowIfNull(authRequest);
RequestingDeviceId = authRequest.DeviceId;
}
public Guid? RequestingDeviceId { get; set; }
}

View File

@ -0,0 +1,89 @@

using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Enums;
namespace Bit.Core.Auth.Models.Data;
public class PendingAuthRequestDetails : AuthRequest
{
public Guid? DeviceId { get; set; }
/**
* Constructor for EF response.
*/
public PendingAuthRequestDetails(
AuthRequest authRequest,
Guid? deviceId)
{
ArgumentNullException.ThrowIfNull(authRequest);
Id = authRequest.Id;
UserId = authRequest.UserId;
OrganizationId = authRequest.OrganizationId;
Type = authRequest.Type;
RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;
RequestDeviceType = authRequest.RequestDeviceType;
RequestIpAddress = authRequest.RequestIpAddress;
RequestCountryName = authRequest.RequestCountryName;
ResponseDeviceId = authRequest.ResponseDeviceId;
AccessCode = authRequest.AccessCode;
PublicKey = authRequest.PublicKey;
Key = authRequest.Key;
MasterPasswordHash = authRequest.MasterPasswordHash;
Approved = authRequest.Approved;
CreationDate = authRequest.CreationDate;
ResponseDate = authRequest.ResponseDate;
AuthenticationDate = authRequest.AuthenticationDate;
DeviceId = deviceId;
}
/**
* Constructor for dapper response.
* Note: if the DeviceId is null it comes back as an empty guid That could change if the stored
* procedure runs on a different kind of db.
* In order to maintain the flexibility of the wildcard * in SQL the constrctor accepts a long "row number rn"
* parameter that was used to order the results in the SQL query. Also SQL complains about the constructor not
* having the same parameters as the SELECT statement.
*/
public PendingAuthRequestDetails(
Guid id,
Guid userId,
short type,
string requestDeviceIdentifier,
short requestDeviceType,
string requestIpAddress,
Guid? responseDeviceId,
string accessCode,
string publicKey,
string key,
string masterPasswordHash,
DateTime creationDate,
DateTime? responseDate,
DateTime? authenticationDate,
bool? approved,
Guid organizationId,
string requestCountryName,
Guid deviceId,
long rn) // see comment above about rn parameter
{
Id = id;
UserId = userId;
OrganizationId = organizationId;
Type = (AuthRequestType)type;
RequestDeviceIdentifier = requestDeviceIdentifier;
RequestDeviceType = (DeviceType)requestDeviceType;
RequestIpAddress = requestIpAddress;
RequestCountryName = requestCountryName;
ResponseDeviceId = responseDeviceId;
AccessCode = accessCode;
PublicKey = publicKey;
Key = key;
MasterPasswordHash = masterPasswordHash;
Approved = approved;
CreationDate = creationDate;
ResponseDate = responseDate;
AuthenticationDate = authenticationDate;
DeviceId = deviceId;
}
}

View File

@ -15,7 +15,7 @@ public interface IAuthRequestRepository : IRepository<AuthRequest, Guid>
/// </summary>
/// <param name="userId">UserId of the owner of the AuthRequests</param>
/// <returns>a collection Auth request details or null</returns>
Task<IEnumerable<AuthRequest>> GetManyPendingAuthRequestByUserId(Guid userId);
Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId);
Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId);
Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests);

View File

@ -51,11 +51,11 @@ public class AuthRequestRepository : Repository<AuthRequest, Guid>, IAuthRequest
}
}
public async Task<IEnumerable<AuthRequest>> GetManyPendingAuthRequestByUserId(Guid userId)
public async Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId)
{
var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<OrganizationAdminAuthRequest>(
var results = await connection.QueryAsync<PendingAuthRequestDetails>(
$"[{Schema}].[AuthRequest_ReadPendingByUserId]",
new { UserId = userId, ExpirationMinutes = expirationMinutes },
commandType: CommandType.StoredProcedure);

View File

@ -62,7 +62,7 @@ public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest,
}
}
public async Task<IEnumerable<Core.Auth.Entities.AuthRequest>> GetManyPendingAuthRequestByUserId(Guid userId)
public async Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId)
{
var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using var scope = ServiceScopeFactory.CreateScope();
@ -76,10 +76,12 @@ public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest,
group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests
select
(from r in groupedAuthRequests
join d in dbContext.Devices on r.RequestDeviceIdentifier equals d.Identifier into deviceJoin
from dj in deviceJoin.DefaultIfEmpty() // This accomplishes a left join allowing nulld for devices
orderby r.CreationDate descending
select r).First()).ToListAsync();
select new PendingAuthRequestDetails(r, dj.Id)).First()
).ToListAsync();
// Pending AuthRequests are those where Approved is null.
mostRecentAuthRequests.RemoveAll(a => a.Approved != null);
return mostRecentAuthRequests;

View File

@ -1,4 +1,4 @@
CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId]
CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId]
@UserId UNIQUEIDENTIFIER,
@ExpirationMinutes INT
AS
@ -8,9 +8,12 @@ BEGIN
;WITH PendingRequests AS (
SELECT
AR.*,
ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) AS rn
D.Id AS DeviceId,
ROW_NUMBER() OVER (PARTITION BY AR.RequestDeviceIdentifier ORDER BY AR.CreationDate DESC) AS rn
FROM dbo.AuthRequestView AR
WHERE Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock
LEFT JOIN
Device D ON AR.RequestDeviceIdentifier = D.Identifier
WHERE AR.Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock
AND AR.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE())
AND AR.UserId = @UserId
)

View File

@ -5,6 +5,7 @@ using Bit.Api.Models.Response;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
@ -100,7 +101,7 @@ public class AuthRequestsControllerTests
public async Task GetPending_ReturnsExpectedResult(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequest authRequest)
PendingAuthRequestDetails authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
@ -120,7 +121,7 @@ public class AuthRequestsControllerTests
Assert.NotNull(result);
var expectedCount = 1;
Assert.Equal(result.Data.Count(), expectedCount);
Assert.IsType<ListResponseModel<AuthRequestResponseModel>>(result);
Assert.IsType<ListResponseModel<PendingAuthRequestResponseModel>>(result);
}
[Theory, BitAutoData]

View File

@ -9,9 +9,12 @@ BEGIN
;WITH PendingRequests AS (
SELECT
AR.*,
ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) AS rn
D.Id AS DeviceId,
ROW_NUMBER() OVER (PARTITION BY AR.RequestDeviceIdentifier ORDER BY AR.CreationDate DESC) AS rn
FROM dbo.AuthRequestView AR
WHERE Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock
LEFT JOIN
Device D ON AR.RequestDeviceIdentifier = D.Identifier
WHERE AR.Type IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock
AND AR.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE())
AND AR.UserId = @UserId
)