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")] [HttpGet("pending")]
[RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)] [RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)]
public async Task<ListResponseModel<AuthRequestResponseModel>> GetPendingAuthRequestsAsync() public async Task<ListResponseModel<PendingAuthRequestResponseModel>> GetPendingAuthRequestsAsync()
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId); var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);
var responses = rawResponse.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)); var responses = rawResponse.Select(a => new PendingAuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault));
return new ListResponseModel<AuthRequestResponseModel>(responses); return new ListResponseModel<PendingAuthRequestResponseModel>(responses);
} }
[HttpGet("{id}/response")] [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> /// </summary>
/// <param name="userId">UserId of the owner of the AuthRequests</param> /// <param name="userId">UserId of the owner of the AuthRequests</param>
/// <returns>a collection Auth request details or null</returns> /// <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>> GetManyPendingByOrganizationIdAsync(Guid organizationId);
Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids); Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests); 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; var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using var connection = new SqlConnection(ConnectionString); using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<OrganizationAdminAuthRequest>( var results = await connection.QueryAsync<PendingAuthRequestDetails>(
$"[{Schema}].[AuthRequest_ReadPendingByUserId]", $"[{Schema}].[AuthRequest_ReadPendingByUserId]",
new { UserId = userId, ExpirationMinutes = expirationMinutes }, new { UserId = userId, ExpirationMinutes = expirationMinutes },
commandType: CommandType.StoredProcedure); 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; var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();
@ -76,10 +76,12 @@ public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest,
group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests
select select
(from r in groupedAuthRequests (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 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); mostRecentAuthRequests.RemoveAll(a => a.Approved != null);
return mostRecentAuthRequests; return mostRecentAuthRequests;

View File

@ -1,4 +1,4 @@
CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId] CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId]
@UserId UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER,
@ExpirationMinutes INT @ExpirationMinutes INT
AS AS
@ -8,9 +8,12 @@ BEGIN
;WITH PendingRequests AS ( ;WITH PendingRequests AS (
SELECT SELECT
AR.*, 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 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.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE())
AND AR.UserId = @UserId AND AR.UserId = @UserId
) )

View File

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

View File

@ -9,9 +9,12 @@ BEGIN
;WITH PendingRequests AS ( ;WITH PendingRequests AS (
SELECT SELECT
AR.*, 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 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.CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE())
AND AR.UserId = @UserId AND AR.UserId = @UserId
) )