From bca5eff1a8b293273fc492a387fd4e868157ffd3 Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 29 Nov 2024 19:15:14 -0500 Subject: [PATCH] Have the dapper query working. Working on the ef query. --- src/Api/Controllers/DevicesController.cs | 16 +++++--- .../DeviceAuthRequestResponseModel.cs | 41 +++++++++++++++++++ ...iveWithPendingAuthRequestsByUserIdQuery.cs | 6 +++ ...iveWithPendingAuthRequestsByUserIdQuery.cs | 6 +++ src/Core/Repositories/IDeviceRepository.cs | 1 + .../Repositories/DeviceRepository.cs | 17 ++++++++ .../Repositories/DeviceRepository.cs | 23 +++++++++++ ...dActiveWithPendingAuthRequestsByUserId.sql | 18 ++++++++ ..._00_AddDeviceReadActiveWithPendingAuth.sql | 18 ++++++++ 9 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/Api/Models/Response/DeviceAuthRequestResponseModel.cs create mode 100644 src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs create mode 100644 src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/Implementations/DeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs create mode 100644 src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql create mode 100644 util/Migrator/DbScripts/2024-11-27_00_AddDeviceReadActiveWithPendingAuth.sql diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index f55b30eb2..880105ccc 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -6,10 +6,10 @@ using Bit.Api.Models.Response; using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -25,6 +25,7 @@ public class DevicesController : Controller private readonly IUserService _userService; private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; + private readonly IGlobalSettings _globalSettings; private readonly ILogger _logger; public DevicesController( @@ -33,6 +34,7 @@ public class DevicesController : Controller IUserService userService, IUserRepository userRepository, ICurrentContext currentContext, + IGlobalSettings globalSettings, ILogger logger) { _deviceRepository = deviceRepository; @@ -40,6 +42,7 @@ public class DevicesController : Controller _userService = userService; _userRepository = userRepository; _currentContext = currentContext; + _globalSettings = globalSettings; _logger = logger; } @@ -69,13 +72,16 @@ public class DevicesController : Controller return response; } + // working here [HttpGet("")] - public async Task> Get() + public async Task> Get() { - ICollection devices = await _deviceRepository.GetManyByUserIdAsync(_userService.GetProperUserId(User).Value); - var responses = devices.Select(d => new DeviceResponseModel(d)); - return new ListResponseModel(responses); + var expirationMinutes = _globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes; + var devices = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value, (int)expirationMinutes); + var responses = devices.Select(d => new DeviceAuthRequestResponseModel(d)); + return new ListResponseModel(responses); } + // end working here [HttpPost("")] public async Task Post([FromBody] DeviceRequestModel model) diff --git a/src/Api/Models/Response/DeviceAuthRequestResponseModel.cs b/src/Api/Models/Response/DeviceAuthRequestResponseModel.cs new file mode 100644 index 000000000..a206a9246 --- /dev/null +++ b/src/Api/Models/Response/DeviceAuthRequestResponseModel.cs @@ -0,0 +1,41 @@ +using Bit.Core.Auth.Utilities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Api.Models.Response; + +public class DeviceAuthRequestResponseModel : ResponseModel +{ + public DeviceAuthRequestResponseModel(Device device) + : base("device") + { + if (device == null) + { + throw new ArgumentNullException(nameof(device)); + } + + Id = device.Id; + Name = device.Name; + Type = device.Type; + Identifier = device.Identifier; + CreationDate = device.CreationDate; + IsTrusted = device.IsTrusted(); + DevicePendingAuthRequest = new PendingAuthRequest(); + } + + public Guid Id { get; set; } + public string Name { get; set; } + public DeviceType Type { get; set; } + public string Identifier { get; set; } + public DateTime CreationDate { get; set; } + public bool IsTrusted { get; set; } + + public PendingAuthRequest DevicePendingAuthRequest { get; set; } + + public class PendingAuthRequest + { + public Guid Id { get; set; } + public DateTime CreationDate { get; set; } + } +} diff --git a/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs b/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs new file mode 100644 index 000000000..4909858cb --- /dev/null +++ b/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Auth.UserFeatures.DeviceReadActiveWithPendingAuthRequestsByUserId; + +public interface IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery +{ + +} diff --git a/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/Implementations/DeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs b/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/Implementations/DeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs new file mode 100644 index 000000000..519dda5f4 --- /dev/null +++ b/src/Core/Auth/UserFeatures/DeviceReadActiveWithPendingAuthRequestsByUserId/Implementations/DeviceReadActiveWithPendingAuthRequestsByUserIdQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Auth.UserFeatures.DeviceReadActiveWithPendingAuthRequestsByUserId; + +public class DeviceReadActiveWithPendingAuthRequestsByUserIdQuery +{ + +} diff --git a/src/Core/Repositories/IDeviceRepository.cs b/src/Core/Repositories/IDeviceRepository.cs index c5d14a094..1dc5c656a 100644 --- a/src/Core/Repositories/IDeviceRepository.cs +++ b/src/Core/Repositories/IDeviceRepository.cs @@ -10,5 +10,6 @@ public interface IDeviceRepository : IRepository Task GetByIdentifierAsync(string identifier); Task GetByIdentifierAsync(string identifier, Guid userId); Task> GetManyByUserIdAsync(Guid userId); + Task> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes); Task ClearPushTokenAsync(Guid id); } diff --git a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs index 7216d87f5..de8d9270a 100644 --- a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs @@ -76,6 +76,23 @@ public class DeviceRepository : Repository, IDeviceRepository } } + public async Task> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadActiveWithPendingAuthRequestsByUserId]", + new + { + UserId = userId, + ExpirationMinutes = expirationMinutes + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task ClearPushTokenAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs b/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs index da82427cb..b73c7b709 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs @@ -69,4 +69,27 @@ public class DeviceRepository : Repository, return Mapper.Map>(devices); } } + + public async Task> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + // This is getting closer, need to keep work-shopping it. + var query = from device in dbContext.Devices + join authRequest in dbContext.AuthRequests + on device.UserId equals authRequest.UserId + into deviceRequests + where device.UserId == userId && device.Active == true + select new + { + AuthRequestId = from authRequest in deviceRequests select authRequest.Id, + AuthCreationDate = from authRequest in deviceRequests select authRequest.CreationDate, + }; + + var devices = await query.ToListAsync(); + return Mapper.Map>(devices); + } + } } diff --git a/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql b/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql new file mode 100644 index 000000000..a839cadff --- /dev/null +++ b/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId] + @UserId UNIQUEIDENTIFIER, + @ExpirationMinutes INT +AS +BEGIN + SELECT D.*, + AR.Id as AuthRequestId, + AR.CreationDate as AuthRequestCreationDate + FROM [dbo].[DeviceView] D + LEFT OUTER JOIN [dbo].[AuthRequestView] AR + ON D.userId = AR.userId + AND AR.RequestDeviceIdentifier = D.Identifier + AND AR.Type IN (0, 1) -- Exclude Admin Approval (type 2) + AND DATEADD(mi, @ExpirationMinutes, AR.CreationDate) < GETUTCDATE() -- This means it hasn't expired + AND AR.Approved IS NOT NULL -- This means it hasn't been approved already + WHERE D.UserId = @UserId + AND D.Active = 1 -- Device is active +END diff --git a/util/Migrator/DbScripts/2024-11-27_00_AddDeviceReadActiveWithPendingAuth.sql b/util/Migrator/DbScripts/2024-11-27_00_AddDeviceReadActiveWithPendingAuth.sql new file mode 100644 index 000000000..9b2d5fa32 --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-27_00_AddDeviceReadActiveWithPendingAuth.sql @@ -0,0 +1,18 @@ +CREATE OR ALTER PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId] + @UserId UNIQUEIDENTIFIER, + @ExpirationMinutes INT +AS +BEGIN +SELECT D.*, + AR.Id as AuthRequestId, + AR.CreationDate as AuthRequestCreationDate +FROM [dbo].[DeviceView] D + LEFT OUTER JOIN [dbo].[AuthRequestView] AR +ON D.userId = AR.userId + AND AR.RequestDeviceIdentifier = D.Identifier + AND AR.Type IN (0, 1) -- Exclude Admin Approval (type 2) + AND DATEADD(mi, @ExpirationMinutes, AR.CreationDate) < GETUTCDATE() -- This means it hasn't expired + AND AR.Approved IS NOT NULL -- This means it hasn't been approved already +WHERE D.UserId = @UserId + AND D.Active = 1 -- Device is active +END