mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
Auth/pm 2996/add auth request data to devices response model (#5152)
fix(auth): [PM-2996] Add Pending Auth Request Data to Devices Response - New stored procedure to fetch the appropriate data. - Updated devices controller to respond with the new data. - Tests written at the controller and repository level. Resolves PM-2996
This commit is contained in:

committed by
GitHub

parent
5ae232e336
commit
cc96e35072
@ -6,7 +6,6 @@ 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;
|
||||
@ -70,11 +69,17 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<DeviceResponseModel>> Get()
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
|
||||
{
|
||||
ICollection<Device> devices = await _deviceRepository.GetManyByUserIdAsync(_userService.GetProperUserId(User).Value);
|
||||
var responses = devices.Select(d => new DeviceResponseModel(d));
|
||||
return new ListResponseModel<DeviceResponseModel>(responses);
|
||||
var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
|
||||
|
||||
// Convert from DeviceAuthDetails to DeviceAuthRequestResponseModel
|
||||
var deviceAuthRequestResponseList = devicesWithPendingAuthData
|
||||
.Select(DeviceAuthRequestResponseModel.From)
|
||||
.ToList();
|
||||
|
||||
var response = new ListResponseModel<DeviceAuthRequestResponseModel>(deviceAuthRequestResponseList);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,51 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
public class DeviceAuthRequestResponseModel : ResponseModel
|
||||
{
|
||||
public DeviceAuthRequestResponseModel()
|
||||
: base("device") { }
|
||||
|
||||
public static DeviceAuthRequestResponseModel From(DeviceAuthDetails deviceAuthDetails)
|
||||
{
|
||||
var converted = new DeviceAuthRequestResponseModel
|
||||
{
|
||||
Id = deviceAuthDetails.Id,
|
||||
Name = deviceAuthDetails.Name,
|
||||
Type = deviceAuthDetails.Type,
|
||||
Identifier = deviceAuthDetails.Identifier,
|
||||
CreationDate = deviceAuthDetails.CreationDate,
|
||||
IsTrusted = deviceAuthDetails.IsTrusted()
|
||||
};
|
||||
|
||||
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
|
||||
{
|
||||
converted.DevicePendingAuthRequest = new PendingAuthRequest
|
||||
{
|
||||
Id = (Guid)deviceAuthDetails.AuthRequestId,
|
||||
CreationDate = (DateTime)deviceAuthDetails.AuthRequestCreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
81
src/Core/Auth/Models/Data/DeviceAuthDetails.cs
Normal file
81
src/Core/Auth/Models/Data/DeviceAuthDetails.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
|
||||
public class DeviceAuthDetails : Device
|
||||
{
|
||||
public bool IsTrusted { get; set; }
|
||||
public Guid? AuthRequestId { get; set; }
|
||||
public DateTime? AuthRequestCreatedAt { get; set; }
|
||||
|
||||
/**
|
||||
* Constructor for EF response.
|
||||
*/
|
||||
public DeviceAuthDetails(
|
||||
Device device,
|
||||
Guid? authRequestId,
|
||||
DateTime? authRequestCreationDate)
|
||||
{
|
||||
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();
|
||||
AuthRequestId = authRequestId;
|
||||
AuthRequestCreatedAt = authRequestCreationDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for dapper response.
|
||||
* Note: if the authRequestId or authRequestCreationDate is null it comes back as
|
||||
* an empty guid and a min value for datetime. That could change if the stored
|
||||
* procedure runs on a different kind of db.
|
||||
*/
|
||||
public DeviceAuthDetails(
|
||||
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)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Type = (DeviceType)type;
|
||||
Identifier = identifier;
|
||||
CreationDate = creationDate;
|
||||
IsTrusted = new Device
|
||||
{
|
||||
Id = id,
|
||||
UserId = userId,
|
||||
Name = name,
|
||||
Type = (DeviceType)type,
|
||||
Identifier = identifier,
|
||||
PushToken = pushToken,
|
||||
RevisionDate = revisionDate,
|
||||
EncryptedUserKey = encryptedUserKey,
|
||||
EncryptedPublicKey = encryptedPublicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey,
|
||||
Active = active
|
||||
}.IsTrusted();
|
||||
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
|
||||
AuthRequestCreatedAt =
|
||||
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
|
||||
|
@ -23,7 +23,6 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
|
||||
public class RegisterUserCommand : IRegisterUserCommand
|
||||
{
|
||||
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -10,5 +11,9 @@ public interface IDeviceRepository : IRepository<Device, Guid>
|
||||
Task<Device?> GetByIdentifierAsync(string identifier);
|
||||
Task<Device?> GetByIdentifierAsync(string identifier, Guid userId);
|
||||
Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId);
|
||||
// DeviceAuthDetails is passed back to decouple the response model from the
|
||||
// repository in case more fields are ever added to the details response for
|
||||
// other requests.
|
||||
Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId);
|
||||
Task ClearPushTokenAsync(Guid id);
|
||||
}
|
||||
|
@ -24,5 +24,7 @@ public interface IGlobalSettings
|
||||
IPasswordlessAuthSettings PasswordlessAuth { get; set; }
|
||||
IDomainVerificationSettings DomainVerification { get; set; }
|
||||
ILaunchDarklySettings LaunchDarkly { get; set; }
|
||||
string DatabaseProvider { get; set; }
|
||||
GlobalSettings.SqlSettings SqlServer { get; set; }
|
||||
string DevelopmentDirectory { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Data;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@ -11,9 +12,13 @@ namespace Bit.Infrastructure.Dapper.Repositories;
|
||||
|
||||
public class DeviceRepository : Repository<Device, Guid>, IDeviceRepository
|
||||
{
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public DeviceRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public DeviceRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
@ -76,6 +81,24 @@ public class DeviceRepository : Repository<Device, Guid>, IDeviceRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId)
|
||||
{
|
||||
var expirationMinutes = _globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<DeviceAuthDetails>(
|
||||
$"[{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))
|
||||
|
@ -51,7 +51,7 @@ public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.AddDynamicParams(obj);
|
||||
parameters.Add("Id", obj.Id, direction: ParameterDirection.InputOutput);
|
||||
var results = await connection.ExecuteAsync(
|
||||
await connection.ExecuteAsync(
|
||||
$"[{Schema}].[{Table}_Create]",
|
||||
parameters,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
@ -64,7 +64,7 @@ public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
await connection.ExecuteAsync(
|
||||
$"[{Schema}].[{Table}_Update]",
|
||||
obj,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
@ -0,0 +1,38 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;
|
||||
|
||||
public class DeviceWithPendingAuthByUserIdQuery
|
||||
{
|
||||
public IQueryable<DeviceAuthDetails> GetQuery(
|
||||
DatabaseContext dbContext,
|
||||
Guid userId,
|
||||
int expirationMinutes)
|
||||
{
|
||||
var devicesWithAuthQuery = (
|
||||
from device in dbContext.Devices
|
||||
where device.UserId == userId && device.Active
|
||||
select new
|
||||
{
|
||||
device,
|
||||
authRequest =
|
||||
(
|
||||
from authRequest in dbContext.AuthRequests
|
||||
where authRequest.RequestDeviceIdentifier == device.Identifier
|
||||
where authRequest.Type == AuthRequestType.AuthenticateAndUnlock || authRequest.Type == AuthRequestType.Unlock
|
||||
where authRequest.Approved == null
|
||||
where authRequest.UserId == userId
|
||||
where authRequest.CreationDate.AddMinutes(expirationMinutes) > DateTime.UtcNow
|
||||
orderby authRequest.CreationDate descending
|
||||
select authRequest
|
||||
).First()
|
||||
}).Select(deviceWithAuthRequest => new DeviceAuthDetails(
|
||||
deviceWithAuthRequest.device,
|
||||
deviceWithAuthRequest.authRequest.Id,
|
||||
deviceWithAuthRequest.authRequest.CreationDate));
|
||||
|
||||
return devicesWithAuthQuery;
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -10,9 +13,17 @@ namespace Bit.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
public class DeviceRepository : Repository<Core.Entities.Device, Device, Guid>, IDeviceRepository
|
||||
{
|
||||
public DeviceRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public DeviceRepository(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IMapper mapper,
|
||||
IGlobalSettings globalSettings
|
||||
)
|
||||
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Devices)
|
||||
{ }
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task ClearPushTokenAsync(Guid id)
|
||||
{
|
||||
@ -69,4 +80,15 @@ public class DeviceRepository : Repository<Core.Entities.Device, Device, Guid>,
|
||||
return Mapper.Map<List<Core.Entities.Device>>(devices);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId)
|
||||
{
|
||||
var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = new DeviceWithPendingAuthByUserIdQuery();
|
||||
return await query.GetQuery(dbContext, userId, expirationMinutes).ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
CREATE PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId]
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@ExpirationMinutes INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
D.*,
|
||||
AR.Id as AuthRequestId,
|
||||
AR.CreationDate as AuthRequestCreationDate
|
||||
FROM dbo.DeviceView D
|
||||
LEFT JOIN (
|
||||
SELECT TOP 1 -- Take only the top record sorted by auth request creation date
|
||||
Id,
|
||||
CreationDate,
|
||||
RequestDeviceIdentifier
|
||||
FROM dbo.AuthRequestView
|
||||
WHERE Type IN (0, 1) -- Include only AuthenticateAndUnlock and Unlock types, excluding Admin Approval (type 2)
|
||||
AND CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) -- Ensure the request hasn't expired
|
||||
AND Approved IS NULL -- Include only requests that haven't been acknowledged or approved
|
||||
ORDER BY CreationDate DESC
|
||||
) AR ON D.Identifier = AR.RequestDeviceIdentifier
|
||||
WHERE
|
||||
D.UserId = @UserId
|
||||
AND D.Active = 1; -- Include only active devices
|
||||
END;
|
Reference in New Issue
Block a user