diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5fd91090773f..d3f1f18a3597 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -63,6 +63,7 @@ public class OrganizationUsersController : Controller private readonly IPricingClient _pricingClient; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; + private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -89,7 +90,8 @@ public OrganizationUsersController( IFeatureService featureService, IPricingClient pricingClient, IConfirmOrganizationUserCommand confirmOrganizationUserCommand, - IRestoreOrganizationUserCommand restoreOrganizationUserCommand) + IRestoreOrganizationUserCommand restoreOrganizationUserCommand, + IInitPendingOrganizationCommand initPendingOrganizationCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -116,6 +118,7 @@ public OrganizationUsersController( _pricingClient = pricingClient; _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; + _initPendingOrganizationCommand = initPendingOrganizationCommand; } [HttpGet("{id}")] @@ -313,7 +316,7 @@ public async Task AcceptInit(Guid orgId, Guid organizationUserId, [FromBody] Org throw new UnauthorizedAccessException(); } - await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); + await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs new file mode 100644 index 000000000000..7a9e5f7c72ae --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs @@ -0,0 +1,76 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand +{ + + private readonly IOrganizationService _organizationService; + private readonly ICollectionRepository _collectionRepository; + private readonly IOrganizationRepository _organizationRepository; + + public InitPendingOrganizationCommand( + IOrganizationService organizationService, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository + ) + { + _organizationService = organizationService; + _collectionRepository = collectionRepository; + _organizationRepository = organizationRepository; + } + + public async Task InitPendingOrganizationAsync(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName) + { + await _organizationService.ValidateSignUpPoliciesAsync(userId); + + var org = await _organizationRepository.GetByIdAsync(organizationId); + + if (org.Enabled) + { + throw new BadRequestException("Organization is already enabled."); + } + + if (org.Status != OrganizationStatusType.Pending) + { + throw new BadRequestException("Organization is not on a Pending status."); + } + + if (!string.IsNullOrEmpty(org.PublicKey)) + { + throw new BadRequestException("Organization already has a Public Key."); + } + + if (!string.IsNullOrEmpty(org.PrivateKey)) + { + throw new BadRequestException("Organization already has a Private Key."); + } + + org.Enabled = true; + org.Status = OrganizationStatusType.Created; + org.PublicKey = publicKey; + org.PrivateKey = privateKey; + + await _organizationService.UpdateAsync(org); + + if (!string.IsNullOrWhiteSpace(collectionName)) + { + // give the owner Can Manage access over the default collection + List defaultOwnerAccess = + [new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }]; + + var defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = org.Id + }; + await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs new file mode 100644 index 000000000000..4806ab981bf8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IInitPendingOrganizationCommand +{ + /// + /// Accept an invitation to initialize and join an organization created via the Admin Portal + /// + Task InitPendingOrganizationAsync(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index e0088f1f7459..c4058b92c81b 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -55,7 +55,6 @@ Task>> RevokeUsersAsync(Guid organizationId /// /// This method must target a disabled Organization that has null keys and status as 'Pending'. /// - Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); @@ -63,4 +62,5 @@ Task>> RevokeUsersAsync(Guid organizationId Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions); Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType); + Task ValidateSignUpPoliciesAsync(Guid ownerId); } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 64bd434327b8..82ec7c958e8a 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -496,7 +496,7 @@ await _referenceEventService.RaiseEventAsync( return returnValue; } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + public async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); if (anySingleOrgPolicies) @@ -1999,52 +1999,4 @@ await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventTy SalesAssistedTrialStarted = salesAssistedTrialStarted, }); } - - public async Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName) - { - await ValidateSignUpPoliciesAsync(userId); - - var org = await GetOrgById(organizationId); - - if (org.Enabled) - { - throw new BadRequestException("Organization is already enabled."); - } - - if (org.Status != OrganizationStatusType.Pending) - { - throw new BadRequestException("Organization is not on a Pending status."); - } - - if (!string.IsNullOrEmpty(org.PublicKey)) - { - throw new BadRequestException("Organization already has a Public Key."); - } - - if (!string.IsNullOrEmpty(org.PrivateKey)) - { - throw new BadRequestException("Organization already has a Private Key."); - } - - org.Enabled = true; - org.Status = OrganizationStatusType.Created; - org.PublicKey = publicKey; - org.PrivateKey = privateKey; - - await UpdateAsync(org); - - if (!string.IsNullOrWhiteSpace(collectionName)) - { - // give the owner Can Manage access over the default collection - List defaultOwnerAccess = - [new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }]; - - var defaultCollection = new Collection - { - Name = collectionName, - OrganizationId = org.Id - }; - await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); - } - } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs new file mode 100644 index 000000000000..0b3d0d4b620e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs @@ -0,0 +1,131 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +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 InitPendingOrganizationCommandTests +{ + [Theory, BitAutoData] + public async Task Init_Organization_Success(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org) + { + org.PrivateKey = null; + org.PublicKey = null; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, ""); + + await organizationServcie.Received().ValidateSignUpPoliciesAsync(userId); + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + await collectionRepository.DidNotReceiveWithAnyArgs().CreateAsync(default); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_With_CollectionName_Success(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, string collectionName) + { + org.PrivateKey = null; + org.PublicKey = null; + org.Id = orgId; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, collectionName); + + await organizationServcie.Received().ValidateSignUpPoliciesAsync(userId); + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + + await collectionRepository.Received().CreateAsync( + Arg.Any(), + Arg.Is>(l => l == null), + Arg.Is>(l => l.Any(i => i.Manage == true))); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Enabled(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org) + + { + org.Enabled = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, "")); + + Assert.Equal("Organization is already enabled.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Not_Pending(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org) + + { + org.Status = Enums.OrganizationStatusType.Created; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, "")); + + Assert.Equal("Organization is not on a Pending status.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Public_Key(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org) + + { + org.PublicKey = publicKey; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, "")); + + Assert.Equal("Organization already has a Public Key.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Private_Key(Guid userId, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org) + + { + org.PublicKey = null; + org.PrivateKey = privateKey; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(userId, orgId, orgUserId, publicKey, privateKey, "")); + + Assert.Equal("Organization already has a Private Key.", exception.Message); + + } +}