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:
parent
d525a1c93c
commit
f944bd2a31
@ -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")]
|
||||||
|
@ -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; }
|
||||||
|
}
|
89
src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs
Normal file
89
src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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]
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user