diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 4a1becc0bf67..83e4d5f9486f 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -1,7 +1,11 @@ using Bit.Api.AdminConsole.Models.Request; +using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.Models.Response; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Api.Response; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; @@ -16,7 +20,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; -using AdminConsoleEntities = Bit.Core.AdminConsole.Entities; namespace Bit.Api.AdminConsole.Controllers; @@ -32,6 +35,8 @@ public class PoliciesController : Controller private readonly GlobalSettings _globalSettings; private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public PoliciesController( IPolicyRepository policyRepository, @@ -41,7 +46,9 @@ public PoliciesController( ICurrentContext currentContext, GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _policyRepository = policyRepository; _policyService = policyService; @@ -53,10 +60,12 @@ public PoliciesController( "OrganizationServiceDataProtector"); _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } [HttpGet("{type}")] - public async Task Get(Guid orgId, int type) + public async Task Get(Guid orgId, int type) { if (!await _currentContext.ManagePolicies(orgId)) { @@ -65,10 +74,15 @@ public async Task Get(Guid orgId, int type) var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type); if (policy == null) { - return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false }); + return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); } - return new PolicyResponseModel(policy); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) + { + return await PolicyDetailResponses.GetSingleOrgPolicyDetailResponseAsync(policy, _organizationHasVerifiedDomainsQuery); + } + + return new PolicyDetailResponseModel(policy); } [HttpGet("")] @@ -81,8 +95,8 @@ public async Task> Get(string orgId) } var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid); - var responses = policies.Select(p => new PolicyResponseModel(p)); - return new ListResponseModel(responses); + + return new ListResponseModel(policies.Select(p => new PolicyResponseModel(p))); } [AllowAnonymous] diff --git a/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs b/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs new file mode 100644 index 000000000000..0a26b0a8d970 --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Api.Response; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; + +namespace Bit.Api.AdminConsole.Models.Response.Helpers; + +public static class PolicyDetailResponses +{ + public static async Task GetSingleOrgPolicyDetailResponseAsync(Policy policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery) + { + if (policy.Type is not PolicyType.SingleOrg) + { + throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy)); + } + + return new PolicyDetailResponseModel(policy, await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId)); + } +} diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index 71e03a547ec7..f2e7c35d2466 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -41,14 +41,13 @@ public PoliciesController( [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(PolicyType type) { - var policy = await _policyRepository.GetByOrganizationIdTypeAsync( - _currentContext.OrganizationId.Value, type); + var policy = await _policyRepository.GetByOrganizationIdTypeAsync(_currentContext.OrganizationId.Value, type); if (policy == null) { return new NotFoundResult(); } - var response = new PolicyResponseModel(policy); - return new JsonResult(response); + + return new JsonResult(new PolicyResponseModel(policy)); } /// @@ -62,9 +61,8 @@ public async Task Get(PolicyType type) public async Task List() { var policies = await _policyRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value); - var policyResponses = policies.Select(p => new PolicyResponseModel(p)); - var response = new ListResponseModel(policyResponses); - return new JsonResult(response); + + return new JsonResult(new ListResponseModel(policies.Select(p => new PolicyResponseModel(p)))); } /// diff --git a/src/Core/AdminConsole/Models/Api/Response/PolicyDetailResponseModel.cs b/src/Core/AdminConsole/Models/Api/Response/PolicyDetailResponseModel.cs new file mode 100644 index 000000000000..dd440190006d --- /dev/null +++ b/src/Core/AdminConsole/Models/Api/Response/PolicyDetailResponseModel.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.Models.Api.Response; + +public class PolicyDetailResponseModel : PolicyResponseModel +{ + public PolicyDetailResponseModel(Policy policy, string obj = "policy") : base(policy, obj) + { + } + + public PolicyDetailResponseModel(Policy policy, bool canToggleState) : base(policy) + { + CanToggleState = canToggleState; + } + + /// + /// Indicates whether the Policy can be enabled/disabled + /// + public bool CanToggleState { get; set; } = true; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 4a597a290c86..870fa72aa709 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand private readonly IGlobalSettings _globalSettings; private readonly IPolicyService _policyService; private readonly IFeatureService _featureService; - private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public VerifyOrganizationDomainCommand( @@ -30,7 +29,6 @@ public VerifyOrganizationDomainCommand( IGlobalSettings globalSettings, IPolicyService policyService, IFeatureService featureService, - IOrganizationService organizationService, ILogger logger) { _organizationDomainRepository = organizationDomainRepository; @@ -39,7 +37,6 @@ public VerifyOrganizationDomainCommand( _globalSettings = globalSettings; _policyService = policyService; _featureService = featureService; - _organizationService = organizationService; _logger = logger; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index 01ffce2cc670..f193aeabd1f6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -87,8 +87,7 @@ private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate po if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) { var missingRequiredPolicyTypes = validator.RequiredPolicies - .Where(requiredPolicyType => - savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) + .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) .ToList(); if (missingRequiredPolicyTypes.Count != 0) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index 3e1f8d26c81d..cc6971f9462a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Auth.Enums; @@ -23,7 +24,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator private readonly IOrganizationRepository _organizationRepository; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public SingleOrgPolicyValidator( IOrganizationUserRepository organizationUserRepository, @@ -31,14 +34,18 @@ public SingleOrgPolicyValidator( IOrganizationRepository organizationRepository, ISsoConfigRepository ssoConfigRepository, ICurrentContext currentContext, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IFeatureService featureService, + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _organizationUserRepository = organizationUserRepository; _mailService = mailService; _organizationRepository = organizationRepository; _ssoConfigRepository = ssoConfigRepository; _currentContext = currentContext; + _featureService = featureService; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } public IEnumerable RequiredPolicies => []; @@ -93,9 +100,21 @@ public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? curre if (policyUpdate is not { Enabled: true }) { var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId); - return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + var validateDecryptionErrorMessage = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + if (!string.IsNullOrWhiteSpace(validateDecryptionErrorMessage)) + { + return validateDecryptionErrorMessage; + } + + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) + { + return "The Single organization policy is required for organizations that have enabled domain verification."; + } } - return ""; + return string.Empty; } } diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 072aa8283489..42655040a3c1 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -289,7 +289,7 @@ private async Task HasVerifiedDomainsAsync(Organization org) if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id)) { - throw new BadRequestException("Organization has verified domains."); + throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification."); } } diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index 77cc5ea02c99..d88d9ab46a03 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -157,7 +157,7 @@ public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExisting var result = await sutProvider.Sut.Get(orgId, type); // Assert - Assert.IsType(result); + Assert.IsType(result); Assert.Equal(policy.Id, result.Id); Assert.Equal(policy.Type, result.Type); Assert.Equal(policy.Enabled, result.Enabled); @@ -182,7 +182,7 @@ public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefau var result = await sutProvider.Sut.Get(orgId, type); // Assert - Assert.IsType(result); + Assert.IsType(result); Assert.Equal(result.Type, (PolicyType)type); Assert.False(result.Enabled); } diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index da3f2b26779c..68f36e37ce83 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -842,6 +842,6 @@ public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_ var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, null)); - Assert.Equal("Organization has verified domains.", badRequestException.Message); + Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message); } }