diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba5dbb1b56..648b3d17a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -569,10 +569,9 @@ jobs: && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main with: - ephemeral_env_branch: process.env.GITHUB_HEAD_REF project: server - sync_environment: true pull_request_number: ${{ github.event.number }} + create_branch: true secrets: inherit check-failures: diff --git a/.github/workflows/ephemeral-environment.yml b/.github/workflows/ephemeral-environment.yml index c784d48354..699a28c6fb 100644 --- a/.github/workflows/ephemeral-environment.yml +++ b/.github/workflows/ephemeral-environment.yml @@ -5,34 +5,13 @@ on: types: [labeled] jobs: - trigger-ee-updates: - name: Trigger Ephemeral Environment updates - runs-on: ubuntu-24.04 + setup-ephemeral-environment: + name: Setup Ephemeral Environment if: github.event.label.name == 'ephemeral-environment' - steps: - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve GitHub PAT secrets - id: retrieve-secret-pat - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - - name: Trigger Ephemeral Environment update - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: 'bitwarden', - repo: 'devops', - workflow_id: '_update_ephemeral_tags.yml', - ref: 'main', - inputs: { - ephemeral_env_branch: process.env.GITHUB_HEAD_REF - } - }) + uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main + with: + project: server + pull_request_number: ${{ github.event.number }} + sync_environment: true + create_branch: true + secrets: inherit diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 6b7d031a00..c856c8ab91 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -65,6 +65,7 @@ public class OrganizationsController : Controller private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; + private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -88,7 +89,8 @@ public class OrganizationsController : Controller ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, IOrganizationDeleteCommand organizationDeleteCommand, IPolicyRequirementQuery policyRequirementQuery, - IPricingClient pricingClient) + IPricingClient pricingClient, + IOrganizationUpdateKeysCommand organizationUpdateKeysCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -112,6 +114,7 @@ public class OrganizationsController : Controller _organizationDeleteCommand = organizationDeleteCommand; _policyRequirementQuery = policyRequirementQuery; _pricingClient = pricingClient; + _organizationUpdateKeysCommand = organizationUpdateKeysCommand; } [HttpGet("{id}")] @@ -490,7 +493,7 @@ public class OrganizationsController : Controller } [HttpPost("{id}/keys")] - public async Task PostKeys(string id, [FromBody] OrganizationKeysRequestModel model) + public async Task PostKeys(Guid id, [FromBody] OrganizationKeysRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -498,7 +501,7 @@ public class OrganizationsController : Controller throw new UnauthorizedAccessException(); } - var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey, + var org = await _organizationUpdateKeysCommand.UpdateOrganizationKeysAsync(id, model.PublicKey, model.EncryptedPrivateKey); return new OrganizationKeysResponseModel(org); } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index f9e5193045..4e869f59b1 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -67,10 +67,12 @@ public class OrganizationUserDetailsResponseModel : OrganizationUserResponseMode public OrganizationUserDetailsResponseModel( OrganizationUser organizationUser, bool claimedByOrganization, + string ssoExternalId, IEnumerable collections) : base(organizationUser, "organizationUserDetails") { ClaimedByOrganization = claimedByOrganization; + SsoExternalId = ssoExternalId; Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); } @@ -80,6 +82,7 @@ public class OrganizationUserDetailsResponseModel : OrganizationUserResponseMode : base(organizationUser, "organizationUserDetails") { ClaimedByOrganization = claimedByOrganization; + SsoExternalId = organizationUser.SsoExternalId; Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); } @@ -90,6 +93,7 @@ public class OrganizationUserDetailsResponseModel : OrganizationUserResponseMode set => ClaimedByOrganization = value; } public bool ClaimedByOrganization { get; set; } + public string SsoExternalId { get; set; } public IEnumerable Collections { get; set; } diff --git a/src/Api/Auth/Models/Request/UntrustDevicesModel.cs b/src/Api/Auth/Models/Request/UntrustDevicesModel.cs new file mode 100644 index 0000000000..ca4f0ad2e7 --- /dev/null +++ b/src/Api/Auth/Models/Request/UntrustDevicesModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +#nullable enable + +namespace Bit.Api.Auth.Models.Request; + +public class UntrustDevicesRequestModel +{ + [Required] + public IEnumerable Devices { get; set; } = null!; +} diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index 4e21b5e9dc..6851aed5de 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -4,6 +4,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Response; +using Bit.Core.Auth.UserFeatures.DeviceTrust; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -21,6 +22,7 @@ public class DevicesController : Controller private readonly IDeviceRepository _deviceRepository; private readonly IDeviceService _deviceService; private readonly IUserService _userService; + private readonly IUntrustDevicesCommand _untrustDevicesCommand; private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; @@ -29,6 +31,7 @@ public class DevicesController : Controller IDeviceRepository deviceRepository, IDeviceService deviceService, IUserService userService, + IUntrustDevicesCommand untrustDevicesCommand, IUserRepository userRepository, ICurrentContext currentContext, ILogger logger) @@ -36,6 +39,7 @@ public class DevicesController : Controller _deviceRepository = deviceRepository; _deviceService = deviceService; _userService = userService; + _untrustDevicesCommand = untrustDevicesCommand; _userRepository = userRepository; _currentContext = currentContext; _logger = logger; @@ -165,6 +169,19 @@ public class DevicesController : Controller model.OtherDevices ?? Enumerable.Empty()); } + [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")] [HttpPost("identifier/{identifier}/token")] public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model) diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index d2ca7fa9bf..fe5021c827 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,6 +1,5 @@ using Bit.Billing.Constants; using Bit.Billing.Jobs; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -24,7 +23,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; - private readonly IFeatureService _featureService; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; @@ -39,7 +37,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IPushNotificationService pushNotificationService, IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, - IFeatureService featureService, IOrganizationEnableCommand organizationEnableCommand, IOrganizationDisableCommand organizationDisableCommand, IPricingClient pricingClient) @@ -53,7 +50,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _pushNotificationService = pushNotificationService; _organizationRepository = organizationRepository; _schedulerFactory = schedulerFactory; - _featureService = featureService; _organizationEnableCommand = organizationEnableCommand; _organizationDisableCommand = organizationDisableCommand; _pricingClient = pricingClient; @@ -227,12 +223,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId) { - var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert); - if (!isResellerManagedOrgAlertEnabled) - { - return; - } - var scheduler = await _schedulerFactory.GetScheduler(); var job = JobBuilder.Create() diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationUpdateKeysCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationUpdateKeysCommand.cs new file mode 100644 index 0000000000..2d01a5e4e3 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationUpdateKeysCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Entities; + +public interface IOrganizationUpdateKeysCommand +{ + /// + /// Update the keys for an organization. + /// + /// The ID of the organization to update. + /// The public key for the organization. + /// The private key for the organization. + /// The updated organization. + Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs new file mode 100644 index 0000000000..aa85c7e2a4 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs @@ -0,0 +1,47 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; + +public class OrganizationUpdateKeysCommand : IOrganizationUpdateKeysCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationService _organizationService; + + public const string OrganizationKeysAlreadyExistErrorMessage = "Organization Keys already exist."; + + public OrganizationUpdateKeysCommand( + ICurrentContext currentContext, + IOrganizationRepository organizationRepository, + IOrganizationService organizationService) + { + _currentContext = currentContext; + _organizationRepository = organizationRepository; + _organizationService = organizationService; + } + + public async Task UpdateOrganizationKeysAsync(Guid organizationId, string publicKey, string privateKey) + { + if (!await _currentContext.ManageResetPassword(organizationId)) + { + throw new UnauthorizedAccessException(); + } + + // If the keys already exist, error out + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization.PublicKey != null && organization.PrivateKey != null) + { + throw new BadRequestException(OrganizationKeysAlreadyExistErrorMessage); + } + + // Update org with generated public/private key + organization.PublicKey = publicKey; + organization.PrivateKey = privateKey; + + await _organizationService.UpdateAsync(organization); + + return organization; + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index e0088f1f74..8d2997bbc6 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -43,7 +43,6 @@ public interface IOrganizationService IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting, EventSystemUser eventSystemUser); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); - Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey); Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RevokeUsersAsync(Guid organizationId, diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index c9027b8030..c9b38b3e30 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1418,28 +1418,6 @@ public class OrganizationService : IOrganizationService } } - public async Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey) - { - if (!await _currentContext.ManageResetPassword(orgId)) - { - throw new UnauthorizedAccessException(); - } - - // If the keys already exist, error out - var org = await _organizationRepository.GetByIdAsync(orgId); - if (org.PublicKey != null && org.PrivateKey != null) - { - throw new BadRequestException("Organization Keys already exist"); - } - - // Update org with generated public/private key - org.PublicKey = publicKey; - org.PrivateKey = privateKey; - await UpdateAsync(org); - - return org; - } - private async Task UpdateUsersAsync(Group group, HashSet groupUsers, Dictionary existingUsersIdDict, HashSet existingUsers = null) { diff --git a/src/Core/Auth/UserFeatures/DeviceTrust/Interfaces/IUntrustDevicesCommand.cs b/src/Core/Auth/UserFeatures/DeviceTrust/Interfaces/IUntrustDevicesCommand.cs new file mode 100644 index 0000000000..860490ce1a --- /dev/null +++ b/src/Core/Auth/UserFeatures/DeviceTrust/Interfaces/IUntrustDevicesCommand.cs @@ -0,0 +1,8 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Auth.UserFeatures.DeviceTrust; + +public interface IUntrustDevicesCommand +{ + public Task UntrustDevices(User user, IEnumerable devicesToUntrust); +} diff --git a/src/Core/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommand.cs b/src/Core/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommand.cs new file mode 100644 index 0000000000..1f6f49753a --- /dev/null +++ b/src/Core/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommand.cs @@ -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 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); + } + } +} diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index 16a0ef9805..7731e04af2 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -1,5 +1,6 @@  +using Bit.Core.Auth.UserFeatures.DeviceTrust; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.Registration.Implementations; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; @@ -22,6 +23,7 @@ public static class UserServiceCollectionExtensions public static void AddUserServices(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped(); + services.AddDeviceTrustCommands(); services.AddUserPasswordCommands(); services.AddUserRegistrationCommands(); services.AddWebAuthnLoginCommands(); @@ -29,6 +31,11 @@ public static class UserServiceCollectionExtensions services.AddTwoFactorQueries(); } + public static void AddDeviceTrustCommands(this IServiceCollection services) + { + services.AddScoped(); + } + public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped(); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0eaa6cd85f..33dec32e34 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -141,7 +141,6 @@ public static class FeatureFlagKeys /* Billing Team */ public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string TrialPayment = "PM-8163-trial-payment"; - public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; public const string UsePricingService = "use-pricing-service"; public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; @@ -169,14 +168,16 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; - public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias"; + public const string EnablePMFlightRecorder = "enable-pm-flight-recorder"; + public const string MobileErrorReporting = "mobile-error-reporting"; /* Platform Team */ public const string PersistPopupView = "persist-popup-view"; public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string WebPush = "web-push"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; + public const string IpcChannelFramework = "ipc-channel-framework"; /* Tools Team */ public const string ItemShare = "item-share"; diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 96d9095c1a..164710d522 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -60,6 +60,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); + services.AddOrganizationUpdateCommands(); services.AddOrganizationEnableCommands(); services.AddOrganizationDisableCommands(); services.AddOrganizationAuthCommands(); @@ -77,6 +78,11 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } + private static void AddOrganizationUpdateCommands(this IServiceCollection services) + { + services.AddScoped(); + } + private static void AddOrganizationEnableCommands(this IServiceCollection services) => services.AddScoped(); diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 3c06c78392..867f8f8ec6 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -60,6 +60,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; + private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand; private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -86,6 +87,7 @@ public class OrganizationsControllerTests : IDisposable _organizationDeleteCommand = Substitute.For(); _policyRequirementQuery = Substitute.For(); _pricingClient = Substitute.For(); + _organizationUpdateKeysCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -109,7 +111,8 @@ public class OrganizationsControllerTests : IDisposable _cloudOrganizationSignUpCommand, _organizationDeleteCommand, _policyRequirementQuery, - _pricingClient); + _pricingClient, + _organizationUpdateKeysCommand); } public void Dispose() diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index 3dcf2016c4..74f00be866 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -2,6 +2,7 @@ using Bit.Api.Models.Response; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.DeviceTrust; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -19,6 +20,7 @@ public class DevicesControllerTest private readonly IDeviceRepository _deviceRepositoryMock; private readonly IDeviceService _deviceServiceMock; private readonly IUserService _userServiceMock; + private readonly IUntrustDevicesCommand _untrustDevicesCommand; private readonly IUserRepository _userRepositoryMock; private readonly ICurrentContext _currentContextMock; private readonly IGlobalSettings _globalSettingsMock; @@ -30,6 +32,7 @@ public class DevicesControllerTest _deviceRepositoryMock = Substitute.For(); _deviceServiceMock = Substitute.For(); _userServiceMock = Substitute.For(); + _untrustDevicesCommand = Substitute.For(); _userRepositoryMock = Substitute.For(); _currentContextMock = Substitute.For(); _loggerMock = Substitute.For>(); @@ -38,6 +41,7 @@ public class DevicesControllerTest _deviceRepositoryMock, _deviceServiceMock, _userServiceMock, + _untrustDevicesCommand, _userRepositoryMock, _currentContextMock, _loggerMock); diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index a6ac7e9512..9c58bbdbf7 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Billing.Enums; @@ -31,7 +30,6 @@ public class SubscriptionUpdatedHandlerTests private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; - private readonly IFeatureService _featureService; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; @@ -49,7 +47,6 @@ public class SubscriptionUpdatedHandlerTests _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); _schedulerFactory = Substitute.For(); - _featureService = Substitute.For(); _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); _pricingClient = Substitute.For(); @@ -67,7 +64,6 @@ public class SubscriptionUpdatedHandlerTests _pushNotificationService, _organizationRepository, _schedulerFactory, - _featureService, _organizationEnableCommand, _organizationDisableCommand, _pricingClient); @@ -97,9 +93,6 @@ public class SubscriptionUpdatedHandlerTests _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(organizationId, null, null)); - _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert) - .Returns(true); - // Act await _sut.HandleAsync(parsedEvent); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommandTests.cs new file mode 100644 index 0000000000..91ab9214e1 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommandTests.cs @@ -0,0 +1,75 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class OrganizationUpdateKeysCommandTests +{ + [Theory, BitAutoData] + public async Task UpdateOrganizationKeysAsync_WithoutManageResetPasswordPermission_ThrowsUnauthorizedException( + Guid orgId, string publicKey, string privateKey, SutProvider sutProvider) + { + sutProvider.GetDependency() + .ManageResetPassword(orgId) + .Returns(false); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey)); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationKeysAsync_WhenKeysAlreadyExist_ThrowsBadRequestException( + Organization organization, string publicKey, string privateKey, + SutProvider sutProvider) + { + organization.PublicKey = "existingPublicKey"; + organization.PrivateKey = "existingPrivateKey"; + + sutProvider.GetDependency() + .ManageResetPassword(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey)); + + Assert.Equal(OrganizationUpdateKeysCommand.OrganizationKeysAlreadyExistErrorMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationKeysAsync_WhenKeysDoNotExist_UpdatesOrganization( + Organization organization, string publicKey, string privateKey, + SutProvider sutProvider) + { + organization.PublicKey = null; + organization.PrivateKey = null; + + sutProvider.GetDependency() + .ManageResetPassword(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var result = await sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey); + + Assert.Equal(publicKey, result.PublicKey); + Assert.Equal(privateKey, result.PrivateKey); + + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync(organization); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index e45643435d..c138cfac2e 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -814,48 +814,6 @@ public class OrganizationServiceTests sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); } - [Theory, BitAutoData] - public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey, - string privateKey, SutProvider sutProvider) - { - var currentContext = Substitute.For(); - currentContext.ManageResetPassword(orgId).Returns(false); - - await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey)); - } - - [Theory, BitAutoData] - public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Throws(Organization org, string publicKey, - string privateKey, SutProvider sutProvider) - { - var currentContext = sutProvider.GetDependency(); - currentContext.ManageResetPassword(org.Id).Returns(true); - - var organizationRepository = sutProvider.GetDependency(); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey)); - Assert.Contains("Organization Keys already exist", exception.Message); - } - - [Theory, BitAutoData] - public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Success(Organization org, string publicKey, - string privateKey, SutProvider sutProvider) - { - org.PublicKey = null; - org.PrivateKey = null; - - var currentContext = sutProvider.GetDependency(); - currentContext.ManageResetPassword(org.Id).Returns(true); - - var organizationRepository = sutProvider.GetDependency(); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - - await sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey); - } - [Theory] [PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)] [BitAutoData("Cannot set max seat autoscaling below seat count", 1, 0, 2, 2)] diff --git a/test/Core.Test/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommandTests.cs b/test/Core.Test/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommandTests.cs new file mode 100644 index 0000000000..c4714be63b --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommandTests.cs @@ -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 sutProvider, User user) + { + var deviceId = Guid.NewGuid(); + // Arrange + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns([new Device + { + Id = deviceId, + EncryptedPrivateKey = "encryptedPrivateKey", + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }]); + + // Act + await sutProvider.Sut.UntrustDevices(user, new List { deviceId }); + + // Assert + await sutProvider.GetDependency() + .Received() + .UpsertAsync(Arg.Is(d => + d.Id == deviceId && + d.EncryptedPrivateKey == null && + d.EncryptedPublicKey == null && + d.EncryptedUserKey == null)); + } + + [Theory, BitAutoData] + public async Task RejectsWrongUser(SutProvider sutProvider, User user) + { + var deviceId = Guid.NewGuid(); + // Arrange + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns([]); + + // Act + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UntrustDevices(user, new List { deviceId })); + } +}