mirror of
https://github.com/bitwarden/server.git
synced 2025-04-20 12:38:15 -05:00
[PM-19883] Add untrust devices endpoint (#5619)
* Add untrust devices endpoint * Fix tests * Update src/Core/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommand.cs Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> * Fix whitespace --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
parent
19b5431177
commit
0a4f97b50e
11
src/Api/Auth/Models/Request/UntrustDevicesModel.cs
Normal file
11
src/Api/Auth/Models/Request/UntrustDevicesModel.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Api.Auth.Models.Request;
|
||||||
|
|
||||||
|
public class UntrustDevicesRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public IEnumerable<Guid> Devices { get; set; } = null!;
|
||||||
|
}
|
@ -4,6 +4,7 @@ using Bit.Api.Models.Request;
|
|||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
using Bit.Core.Auth.Models.Api.Response;
|
using Bit.Core.Auth.Models.Api.Response;
|
||||||
|
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -21,6 +22,7 @@ public class DevicesController : Controller
|
|||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IDeviceService _deviceService;
|
private readonly IDeviceService _deviceService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IUntrustDevicesCommand _untrustDevicesCommand;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ILogger<DevicesController> _logger;
|
private readonly ILogger<DevicesController> _logger;
|
||||||
@ -29,6 +31,7 @@ public class DevicesController : Controller
|
|||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
IDeviceService deviceService,
|
IDeviceService deviceService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
|
IUntrustDevicesCommand untrustDevicesCommand,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<DevicesController> logger)
|
ILogger<DevicesController> logger)
|
||||||
@ -36,6 +39,7 @@ public class DevicesController : Controller
|
|||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_deviceService = deviceService;
|
_deviceService = deviceService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_untrustDevicesCommand = untrustDevicesCommand;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -165,6 +169,19 @@ public class DevicesController : Controller
|
|||||||
model.OtherDevices ?? Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>());
|
model.OtherDevices ?? Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("untrust")]
|
||||||
|
public async Task PostUntrust([FromBody] UntrustDevicesRequestModel model)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _untrustDevicesCommand.UntrustDevices(user, model.Devices);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("identifier/{identifier}/token")]
|
[HttpPut("identifier/{identifier}/token")]
|
||||||
[HttpPost("identifier/{identifier}/token")]
|
[HttpPost("identifier/{identifier}/token")]
|
||||||
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
|
|
||||||
|
public interface IUntrustDevicesCommand
|
||||||
|
{
|
||||||
|
public Task UntrustDevices(User user, IEnumerable<Guid> devicesToUntrust);
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
|
|
||||||
|
public class UntrustDevicesCommand : IUntrustDevicesCommand
|
||||||
|
{
|
||||||
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
|
||||||
|
public UntrustDevicesCommand(
|
||||||
|
IDeviceRepository deviceRepository)
|
||||||
|
{
|
||||||
|
_deviceRepository = deviceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UntrustDevices(User user, IEnumerable<Guid> devicesToUntrust)
|
||||||
|
{
|
||||||
|
var userDevices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
var deviceIdDict = userDevices.ToDictionary(device => device.Id);
|
||||||
|
|
||||||
|
// Validate that the user owns all devices that they passed in
|
||||||
|
foreach (var deviceId in devicesToUntrust)
|
||||||
|
{
|
||||||
|
if (!deviceIdDict.ContainsKey(deviceId))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException($"User {user.Id} does not have access to device {deviceId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var deviceId in devicesToUntrust)
|
||||||
|
{
|
||||||
|
var device = deviceIdDict[deviceId];
|
||||||
|
device.EncryptedPrivateKey = null;
|
||||||
|
device.EncryptedPublicKey = null;
|
||||||
|
device.EncryptedUserKey = null;
|
||||||
|
await _deviceRepository.UpsertAsync(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
@ -22,6 +23,7 @@ public static class UserServiceCollectionExtensions
|
|||||||
public static void AddUserServices(this IServiceCollection services, IGlobalSettings globalSettings)
|
public static void AddUserServices(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
services.AddScoped<IUserService, UserService>();
|
services.AddScoped<IUserService, UserService>();
|
||||||
|
services.AddDeviceTrustCommands();
|
||||||
services.AddUserPasswordCommands();
|
services.AddUserPasswordCommands();
|
||||||
services.AddUserRegistrationCommands();
|
services.AddUserRegistrationCommands();
|
||||||
services.AddWebAuthnLoginCommands();
|
services.AddWebAuthnLoginCommands();
|
||||||
@ -29,6 +31,11 @@ public static class UserServiceCollectionExtensions
|
|||||||
services.AddTwoFactorQueries();
|
services.AddTwoFactorQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUntrustDevicesCommand, UntrustDevicesCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
|
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Auth.Models.Api.Response;
|
using Bit.Core.Auth.Models.Api.Response;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -19,6 +20,7 @@ public class DevicesControllerTest
|
|||||||
private readonly IDeviceRepository _deviceRepositoryMock;
|
private readonly IDeviceRepository _deviceRepositoryMock;
|
||||||
private readonly IDeviceService _deviceServiceMock;
|
private readonly IDeviceService _deviceServiceMock;
|
||||||
private readonly IUserService _userServiceMock;
|
private readonly IUserService _userServiceMock;
|
||||||
|
private readonly IUntrustDevicesCommand _untrustDevicesCommand;
|
||||||
private readonly IUserRepository _userRepositoryMock;
|
private readonly IUserRepository _userRepositoryMock;
|
||||||
private readonly ICurrentContext _currentContextMock;
|
private readonly ICurrentContext _currentContextMock;
|
||||||
private readonly IGlobalSettings _globalSettingsMock;
|
private readonly IGlobalSettings _globalSettingsMock;
|
||||||
@ -30,6 +32,7 @@ public class DevicesControllerTest
|
|||||||
_deviceRepositoryMock = Substitute.For<IDeviceRepository>();
|
_deviceRepositoryMock = Substitute.For<IDeviceRepository>();
|
||||||
_deviceServiceMock = Substitute.For<IDeviceService>();
|
_deviceServiceMock = Substitute.For<IDeviceService>();
|
||||||
_userServiceMock = Substitute.For<IUserService>();
|
_userServiceMock = Substitute.For<IUserService>();
|
||||||
|
_untrustDevicesCommand = Substitute.For<IUntrustDevicesCommand>();
|
||||||
_userRepositoryMock = Substitute.For<IUserRepository>();
|
_userRepositoryMock = Substitute.For<IUserRepository>();
|
||||||
_currentContextMock = Substitute.For<ICurrentContext>();
|
_currentContextMock = Substitute.For<ICurrentContext>();
|
||||||
_loggerMock = Substitute.For<ILogger<DevicesController>>();
|
_loggerMock = Substitute.For<ILogger<DevicesController>>();
|
||||||
@ -38,6 +41,7 @@ public class DevicesControllerTest
|
|||||||
_deviceRepositoryMock,
|
_deviceRepositoryMock,
|
||||||
_deviceServiceMock,
|
_deviceServiceMock,
|
||||||
_userServiceMock,
|
_userServiceMock,
|
||||||
|
_untrustDevicesCommand,
|
||||||
_userRepositoryMock,
|
_userRepositoryMock,
|
||||||
_currentContextMock,
|
_currentContextMock,
|
||||||
_loggerMock);
|
_loggerMock);
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class UntrustDevicesCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetsKeysToNull(SutProvider<UntrustDevicesCommand> sutProvider, User user)
|
||||||
|
{
|
||||||
|
var deviceId = Guid.NewGuid();
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns([new Device
|
||||||
|
{
|
||||||
|
Id = deviceId,
|
||||||
|
EncryptedPrivateKey = "encryptedPrivateKey",
|
||||||
|
EncryptedPublicKey = "encryptedPublicKey",
|
||||||
|
EncryptedUserKey = "encryptedUserKey"
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UntrustDevices(user, new List<Guid> { deviceId });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.Received()
|
||||||
|
.UpsertAsync(Arg.Is<Device>(d =>
|
||||||
|
d.Id == deviceId &&
|
||||||
|
d.EncryptedPrivateKey == null &&
|
||||||
|
d.EncryptedPublicKey == null &&
|
||||||
|
d.EncryptedUserKey == null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RejectsWrongUser(SutProvider<UntrustDevicesCommand> sutProvider, User user)
|
||||||
|
{
|
||||||
|
var deviceId = Guid.NewGuid();
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IDeviceRepository>()
|
||||||
|
.GetManyByUserIdAsync(user.Id)
|
||||||
|
.Returns([]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
||||||
|
await sutProvider.Sut.UntrustDevices(user, new List<Guid> { deviceId }));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user