1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-02 04:11:04 +01:00

Stored procedure and entity framework query written. Still have cleanup to do and questions to answer.

This commit is contained in:
Patrick Pimentel 2024-12-04 13:57:37 -05:00
parent bca5eff1a8
commit e916fafd89
No known key found for this signature in database
GPG Key ID: 4B27FC74C6422186
12 changed files with 159 additions and 86 deletions

View File

@ -72,16 +72,14 @@ public class DevicesController : Controller
return response;
}
// working here
[HttpGet("")]
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
{
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<DeviceAuthRequestResponseModel>(responses);
var response = new ListResponseModel<DeviceAuthRequestResponseModel>(devices);
return response;
}
// end working here
[HttpPost("")]
public async Task<DeviceResponseModel> Post([FromBody] DeviceRequestModel model)

View File

@ -1,41 +0,0 @@
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; }
}
}

View File

@ -1,5 +1,12 @@
namespace Bit.Core.Auth.Enums;
/**
* The type of auth request.
*
* Note:
* Used by the Device_ReadActiveWithPendingAuthRequestsByUserId.sql stored procedure.
* If the enum changes be aware of this reference.
*/
public enum AuthRequestType : byte
{
AuthenticateAndUnlock = 0,

View File

@ -0,0 +1,84 @@
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Core.Auth.Models.Api.Response;
public class DeviceAuthRequestResponseModel : ResponseModel
{
public DeviceAuthRequestResponseModel(
Device device,
Guid authRequestId,
DateTime authRequestCreationDate)
: 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();
if (authRequestId != Guid.Empty && authRequestCreationDate != DateTime.MinValue)
{
DevicePendingAuthRequest = new PendingAuthRequest { Id = authRequestId, CreationDate = authRequestCreationDate };
}
}
/**
* Is there a better way to do this in Dapper so that I don't need to explicitly
* enumerate all the properties in the constructor for mapping?
*/
public DeviceAuthRequestResponseModel(
Guid id,
Guid userId,
string name,
short type,
string identifier,
string pushToken,
DateTime creationDate,
DateTime revisionDate,
string encryptedUserKey,
string encryptedPublicKey,
string encryptedPrivateKey,
bool active,
Guid authRequestId,
DateTime authRequestCreationDate)
: base("device")
{
Id = id;
Name = name;
Type = (DeviceType)type;
Identifier = identifier;
CreationDate = creationDate;
IsTrusted = active;
if (authRequestId != Guid.Empty && authRequestCreationDate != DateTime.MinValue)
{
DevicePendingAuthRequest = new PendingAuthRequest()
{
Id = authRequestId,
CreationDate = authRequestCreationDate,
};
}
}
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; }
}
}

View File

@ -2,5 +2,4 @@
public interface IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery
{
}

View File

@ -1,6 +1,6 @@
namespace Bit.Core.Auth.UserFeatures.DeviceReadActiveWithPendingAuthRequestsByUserId;
public class DeviceReadActiveWithPendingAuthRequestsByUserIdQuery
public class DeviceReadActiveWithPendingAuthRequestsByUserIdQuery : IDeviceReadActiveWithPendingAuthRequestsByUserIdQuery
{
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Entities;
#nullable enable
@ -10,6 +11,6 @@ public interface IDeviceRepository : IRepository<Device, Guid>
Task<Device?> GetByIdentifierAsync(string identifier);
Task<Device?> GetByIdentifierAsync(string identifier, Guid userId);
Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId);
Task<ICollection<Device>> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes);
Task<ICollection<DeviceAuthRequestResponseModel>> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes);
Task ClearPushTokenAsync(Guid id);
}

View File

@ -1,4 +1,5 @@
using System.Data;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@ -76,11 +77,11 @@ public class DeviceRepository : Repository<Device, Guid>, IDeviceRepository
}
}
public async Task<ICollection<Device>> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes)
public async Task<ICollection<DeviceAuthRequestResponseModel>> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<Device>(
var results = await connection.QueryAsync<DeviceAuthRequestResponseModel>(
$"[{Schema}].[{Table}_ReadActiveWithPendingAuthRequestsByUserId]",
new
{

View File

@ -1,4 +1,6 @@
using AutoMapper;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
@ -70,26 +72,34 @@ public class DeviceRepository : Repository<Core.Entities.Device, Device, Guid>,
}
}
public async Task<ICollection<Core.Entities.Device>> GetManyByUserIdWithDeviceAuth(Guid userId, int expirationMinutes)
public async Task<ICollection<DeviceAuthRequestResponseModel>> 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 currentTime = DateTime.UtcNow;
var query = from device in dbContext.Devices
join authRequest in dbContext.AuthRequests
on device.UserId equals authRequest.UserId
on device.Identifier equals authRequest.RequestDeviceIdentifier
into deviceRequests
from authRequest in deviceRequests
.Where(ar => ar.Type == AuthRequestType.AuthenticateAndUnlock || ar.Type == AuthRequestType.Unlock)
.Where(ar => ar.Approved == null)
.Where(ar => ar.CreationDate.AddMinutes(expirationMinutes) > currentTime)
.OrderByDescending(ar => ar.CreationDate)
.Take(1)
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,
Device = device,
AuthRequestId = authRequest.Id,
AuthRequestCreationDate = authRequest.CreationDate
};
var devices = await query.ToListAsync();
return Mapper.Map<List<Core.Entities.Device>>(devices);
return Mapper.Map<List<DeviceAuthRequestResponseModel>>(devices);
}
}
}

View File

@ -3,16 +3,23 @@ CREATE PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId]
@ExpirationMinutes INT
AS
BEGIN
SELECT D.*,
AR.Id as AuthRequestId,
AR.CreationDate as AuthRequestCreationDate
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
LEFT OUTER JOIN (
SELECT TOP 1 -- Take only the top record sorted by auth request creation date
AR.Id,
AR.CreationDate,
AR.RequestDeviceIdentifier
FROM [dbo].[AuthRequestView] AR
WHERE AR.Type IN (0, 1) -- Include only specific types, excluding Admin Approval (type 2)
AND DATEADD(mi, @ExpirationMinutes, AR.CreationDate) > GETUTCDATE() -- Ensure the request hasn't expired
AND AR.Approved IS NULL -- Include only requests that haven't been acknowledged or approved
ORDER BY AR.CreationDate DESC
) AR ON D.Identifier = AR.RequestDeviceIdentifier
WHERE
D.UserId = @UserId
AND D.Active = 1 -- Include only active devices
END

View File

@ -1,18 +0,0 @@
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

View File

@ -0,0 +1,25 @@
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 (
SELECT TOP 1 -- Take only the top record sorted by auth request creation date
AR.Id,
AR.CreationDate,
AR.RequestDeviceIdentifier
FROM [dbo].[AuthRequestView] AR
WHERE AR.Type IN (0, 1) -- Include only specific types, excluding Admin Approval (type 2)
AND DATEADD(mi, @ExpirationMinutes, AR.CreationDate) > GETUTCDATE() -- Ensure the request hasn't expired
AND AR.Approved IS NULL -- Include only requests that haven't been acknowledged or approved
ORDER BY AR.CreationDate DESC
) AR ON D.Identifier = AR.RequestDeviceIdentifier
WHERE
D.UserId = @UserId
AND D.Active = 1 -- Include only active devices
END