1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-10 15:13:29 +01:00

Remove PolicyService.SaveAsync and use command instead

This commit is contained in:
Thomas Rittson 2024-11-27 15:02:08 +10:00
parent 1b75e35c31
commit 4de9a00612
No known key found for this signature in database
GPG Key ID: CDDDA03861C35E27
14 changed files with 123 additions and 1092 deletions

View File

@ -6,8 +6,8 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -28,7 +28,6 @@ namespace Bit.Api.AdminConsole.Controllers;
public class PoliciesController : Controller public class PoliciesController : Controller
{ {
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
@ -37,10 +36,10 @@ public class PoliciesController : Controller
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly ISavePolicyCommand _savePolicyCommand;
public PoliciesController( public PoliciesController(
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IUserService userService, IUserService userService,
ICurrentContext currentContext, ICurrentContext currentContext,
@ -48,10 +47,10 @@ public class PoliciesController : Controller
IDataProtectionProvider dataProtectionProvider, IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory, IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
ISavePolicyCommand savePolicyCommand)
{ {
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
@ -62,6 +61,7 @@ public class PoliciesController : Controller
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService; _featureService = featureService;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_savePolicyCommand = savePolicyCommand;
} }
[HttpGet("{type}")] [HttpGet("{type}")]
@ -178,25 +178,20 @@ public class PoliciesController : Controller
} }
[HttpPut("{type}")] [HttpPut("{type}")]
public async Task<PolicyResponseModel> Put(string orgId, int type, [FromBody] PolicyRequestModel model) public async Task<PolicyResponseModel> Put(Guid orgId, PolicyType type, [FromBody] PolicyRequestModel model)
{ {
var orgIdGuid = new Guid(orgId); if (!await _currentContext.ManagePolicies(orgId))
if (!await _currentContext.ManagePolicies(orgIdGuid))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(new Guid(orgId), (PolicyType)type);
if (policy == null) if (type != model.Type)
{ {
policy = model.ToPolicy(orgIdGuid); throw new BadRequestException("Mismatched policy type");
}
else
{
policy = model.ToPolicy(policy);
} }
var userId = _userService.GetProperUserId(User); var policyUpdate = await model.ToPolicyUpdateAsync(orgId, _currentContext);
await _policyService.SaveAsync(policy, userId); var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
return new PolicyResponseModel(policy); return new PolicyResponseModel(policy);
} }
} }

View File

@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context;
namespace Bit.Api.AdminConsole.Models.Request; namespace Bit.Api.AdminConsole.Models.Request;
@ -13,19 +15,12 @@ public class PolicyRequestModel
public bool? Enabled { get; set; } public bool? Enabled { get; set; }
public Dictionary<string, object> Data { get; set; } public Dictionary<string, object> Data { get; set; }
public Policy ToPolicy(Guid orgId) public async Task<PolicyUpdate> ToPolicyUpdateAsync(Guid organizationId, ICurrentContext currentContext) => new()
{ {
return ToPolicy(new Policy Type = Type!.Value,
{ OrganizationId = organizationId,
Type = Type.Value, Data = Data != null ? JsonSerializer.Serialize(Data) : null,
OrganizationId = orgId Enabled = Enabled.GetValueOrDefault(),
}); PerformedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId))
} };
public Policy ToPolicy(Policy existingPolicy)
{
existingPolicy.Enabled = Enabled.GetValueOrDefault();
existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null;
return existingPolicy;
}
} }

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response; using Bit.Api.Models.Public.Response;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -18,15 +19,18 @@ public class PoliciesController : Controller
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ISavePolicyCommand _savePolicyCommand;
public PoliciesController( public PoliciesController(
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
ICurrentContext currentContext) ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand)
{ {
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_currentContext = currentContext; _currentContext = currentContext;
_savePolicyCommand = savePolicyCommand;
} }
/// <summary> /// <summary>
@ -80,17 +84,9 @@ public class PoliciesController : Controller
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model) public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)
{ {
var policy = await _policyRepository.GetByOrganizationIdTypeAsync( var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
_currentContext.OrganizationId.Value, type); var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
if (policy == null)
{
policy = model.ToPolicy(_currentContext.OrganizationId.Value, type);
}
else
{
policy = model.ToPolicy(policy);
}
await _policyService.SaveAsync(policy, null);
var response = new PolicyResponseModel(policy); var response = new PolicyResponseModel(policy);
return new JsonResult(response); return new JsonResult(response);
} }

View File

@ -1,26 +1,19 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Enums;
namespace Bit.Api.AdminConsole.Public.Models.Request; namespace Bit.Api.AdminConsole.Public.Models.Request;
public class PolicyUpdateRequestModel : PolicyBaseModel public class PolicyUpdateRequestModel : PolicyBaseModel
{ {
public Policy ToPolicy(Guid orgId, PolicyType type) public PolicyUpdate ToPolicyUpdate(Guid organizationId, PolicyType type) => new()
{ {
return ToPolicy(new Policy Type = type,
{ OrganizationId = organizationId,
OrganizationId = orgId, Data = Data != null ? JsonSerializer.Serialize(Data) : null,
Enabled = Enabled.GetValueOrDefault(), Enabled = Enabled.GetValueOrDefault(),
Data = Data != null ? JsonSerializer.Serialize(Data) : null, PerformedBy = new SystemUser(EventSystemUser.PublicApi)
Type = type };
});
}
public virtual Policy ToPolicy(Policy existingPolicy)
{
existingPolicy.Enabled = Enabled.GetValueOrDefault();
existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null;
return existingPolicy;
}
} }

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
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,9 +19,9 @@ public class VerifyOrganizationDomainCommand(
IDnsResolverService dnsResolverService, IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IPolicyService policyService,
IFeatureService featureService, IFeatureService featureService,
ICurrentContext currentContext, ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand,
ILogger<VerifyOrganizationDomainCommand> logger) ILogger<VerifyOrganizationDomainCommand> logger)
: IVerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
{ {
@ -125,10 +125,15 @@ public class VerifyOrganizationDomainCommand(
{ {
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{ {
await policyService.SaveAsync( var policyUpdate = new PolicyUpdate
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, {
savingUserId: actingUser is StandardUser standardUser ? standardUser.UserId : null, OrganizationId = organizationId,
eventSystemUser: actingUser is SystemUser systemUser ? systemUser.SystemUserType : null); Type = PolicyType.SingleOrg,
Enabled = true,
PerformedBy = actingUser
};
await savePolicyCommand.SaveAsync(policyUpdate);
} }
} }
} }

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public interface ISavePolicyCommand public interface ISavePolicyCommand
{ {
Task SaveAsync(PolicyUpdate policy); Task<Policy> SaveAsync(PolicyUpdate policy);
} }

View File

@ -42,7 +42,7 @@ public class SavePolicyCommand : ISavePolicyCommand
_policyValidators = policyValidatorsDict; _policyValidators = policyValidatorsDict;
} }
public async Task SaveAsync(PolicyUpdate policyUpdate) public async Task<Policy> SaveAsync(PolicyUpdate policyUpdate)
{ {
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId); var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId);
if (org == null) if (org == null)
@ -74,6 +74,8 @@ public class SavePolicyCommand : ISavePolicyCommand
await _policyRepository.UpsertAsync(policy); await _policyRepository.UpsertAsync(policy);
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
return policy;
} }
private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate) private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -9,8 +8,6 @@ namespace Bit.Core.AdminConsole.Services;
public interface IPolicyService public interface IPolicyService
{ {
Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null);
/// <summary> /// <summary>
/// Get the combined master password policy options for the specified user. /// Get the combined master password policy options for the specified user.
/// </summary> /// </summary>

View File

@ -1,19 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -24,107 +13,20 @@ namespace Bit.Core.AdminConsole.Services.Implementations;
public class PolicyService : IPolicyService public class PolicyService : IPolicyService
{ {
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IEventService _eventService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IMailService _mailService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly ISavePolicyCommand _savePolicyCommand;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly ICurrentContext _currentContext;
public PolicyService( public PolicyService(
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IEventService eventService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository, GlobalSettings globalSettings)
IMailService mailService,
GlobalSettings globalSettings,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
ISavePolicyCommand savePolicyCommand,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
ICurrentContext currentContext)
{ {
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_eventService = eventService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository;
_mailService = mailService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_savePolicyCommand = savePolicyCommand;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_currentContext = currentContext;
}
public async Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null)
{
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
{
// Transitional mapping - this will be moved to callers once the feature flag is removed
// TODO make sure to populate with SystemUser if not an actual user
var policyUpdate = new PolicyUpdate
{
OrganizationId = policy.OrganizationId,
Type = policy.Type,
Enabled = policy.Enabled,
Data = policy.Data,
PerformedBy = savingUserId.HasValue
? new StandardUser(savingUserId.Value, await _currentContext.OrganizationOwner(policy.OrganizationId))
: new SystemUser(eventSystemUser ?? EventSystemUser.Unknown)
};
await _savePolicyCommand.SaveAsync(policyUpdate);
return;
}
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
if (org == null)
{
throw new BadRequestException("Organization not found");
}
if (!org.UsePolicies)
{
throw new BadRequestException("This organization cannot use policies.");
}
// FIXME: This method will throw a bunch of errors based on if the
// policy that is being applied requires some other policy that is
// not enabled. It may be advisable to refactor this into a domain
// object and get this kind of stuff out of the service.
await HandleDependentPoliciesAsync(policy, org);
var now = DateTime.UtcNow;
if (policy.Id == default(Guid))
{
policy.CreationDate = now;
}
policy.RevisionDate = now;
// We can exit early for disable operations, because they are
// simpler.
if (!policy.Enabled)
{
await SetPolicyConfiguration(policy);
return;
}
await EnablePolicyAsync(policy, org, savingUserId);
} }
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user) public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
@ -190,178 +92,4 @@ public class PolicyService : IPolicyService
return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin }; return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
} }
private async Task DependsOnSingleOrgAsync(Organization org)
{
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
if (singleOrg?.Enabled != true)
{
throw new BadRequestException("Single Organization policy not enabled.");
}
}
private async Task RequiredBySsoAsync(Organization org)
{
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
if (requireSso?.Enabled == true)
{
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
}
}
private async Task RequiredByKeyConnectorAsync(Organization org)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("Key Connector is enabled.");
}
}
private async Task RequiredByAccountRecoveryAsync(Organization org)
{
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword);
if (requireSso?.Enabled == true)
{
throw new BadRequestException("Account recovery policy is enabled.");
}
}
private async Task RequiredByVaultTimeoutAsync(Organization org)
{
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
if (vaultTimeout?.Enabled == true)
{
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
}
}
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
{
throw new BadRequestException("Trusted device encryption is on and requires this policy.");
}
}
private async Task HandleDependentPoliciesAsync(Policy policy, Organization org)
{
switch (policy.Type)
{
case PolicyType.SingleOrg:
if (!policy.Enabled)
{
await HasVerifiedDomainsAsync(org);
await RequiredBySsoAsync(org);
await RequiredByVaultTimeoutAsync(org);
await RequiredByKeyConnectorAsync(org);
await RequiredByAccountRecoveryAsync(org);
}
break;
case PolicyType.RequireSso:
if (policy.Enabled)
{
await DependsOnSingleOrgAsync(org);
}
else
{
await RequiredByKeyConnectorAsync(org);
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
}
break;
case PolicyType.ResetPassword:
if (!policy.Enabled || policy.GetDataModel<ResetPasswordDataModel>()?.AutoEnrollEnabled == false)
{
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
}
if (policy.Enabled)
{
await DependsOnSingleOrgAsync(org);
}
break;
case PolicyType.MaximumVaultTimeout:
if (policy.Enabled)
{
await DependsOnSingleOrgAsync(org);
}
break;
}
}
private async Task HasVerifiedDomainsAsync(Organization org)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
{
throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification.");
}
}
private async Task SetPolicyConfiguration(Policy policy)
{
await _policyRepository.UpsertAsync(policy);
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
}
private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId)
{
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
if (!currentPolicy?.Enabled ?? true)
{
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId);
switch (policy.Type)
{
case PolicyType.TwoFactorAuthentication:
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
if (!userTwoFactorEnabled)
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
break;
case PolicyType.SingleOrg:
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId.Value));
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
break;
default:
break;
}
}
await SetPolicyConfiguration(policy);
}
} }

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
@ -17,25 +18,25 @@ public class SsoConfigService : ISsoConfigService
{ {
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly ISavePolicyCommand _savePolicyCommand;
public SsoConfigService( public SsoConfigService(
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IEventService eventService) IEventService eventService,
ISavePolicyCommand savePolicyCommand)
{ {
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_eventService = eventService; _eventService = eventService;
_savePolicyCommand = savePolicyCommand;
} }
public async Task SaveAsync(SsoConfig config, Organization organization) public async Task SaveAsync(SsoConfig config, Organization organization)
@ -63,25 +64,29 @@ public class SsoConfigService : ISsoConfigService
// Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected // Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected
if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption) if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
{ {
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.SingleOrg };
singleOrgPolicy.Enabled = true; await _savePolicyCommand.SaveAsync(new()
{
OrganizationId = config.OrganizationId,
Type = PolicyType.SingleOrg,
Enabled = true
});
await _policyService.SaveAsync(singleOrgPolicy, null); var resetPasswordPolicy = new PolicyUpdate
{
OrganizationId = config.OrganizationId,
Type = PolicyType.ResetPassword,
Enabled = true,
};
resetPasswordPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
await _savePolicyCommand.SaveAsync(resetPasswordPolicy);
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? await _savePolicyCommand.SaveAsync(new()
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; {
OrganizationId = config.OrganizationId,
resetPolicy.Enabled = true; Type = PolicyType.RequireSso,
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); Enabled = true
await _policyService.SaveAsync(resetPolicy, null); });
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
ssoRequiredPolicy.Enabled = true;
await _policyService.SaveAsync(ssoRequiredPolicy, null);
} }
await LogEventsAsync(config, oldConfig); await LogEventsAsync(config, oldConfig);

View File

@ -8,5 +8,5 @@ public class CommandResult(IEnumerable<string> errors)
public bool HasErrors => ErrorMessages.Count > 0; public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList(); public List<string> ErrorMessages { get; } = errors.ToList();
public CommandResult() : this([]) { } public CommandResult() : this((IEnumerable<string>)[]) { }
} }

View File

@ -1,7 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -182,9 +183,13 @@ public class VerifyOrganizationDomainCommandTests
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>() await sutProvider.GetDependency<ISavePolicyCommand>()
.Received(1) .Received(1)
.SaveAsync(Arg.Is<Policy>(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), userId); .SaveAsync(Arg.Is<PolicyUpdate>(x => x.Type == PolicyType.SingleOrg &&
x.OrganizationId == domain.OrganizationId &&
x.Enabled &&
x.PerformedBy is StandardUser &&
x.PerformedBy.UserId == userId));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -208,9 +213,9 @@ public class VerifyOrganizationDomainCommandTests
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>() await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive() .DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null); .SaveAsync(Arg.Any<PolicyUpdate>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -234,10 +239,9 @@ public class VerifyOrganizationDomainCommandTests
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>() await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive() .DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null); .SaveAsync(Arg.Any<PolicyUpdate>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -261,8 +265,8 @@ public class VerifyOrganizationDomainCommandTests
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>() await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive() .DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null); .SaveAsync(Arg.Any<PolicyUpdate>());
} }
} }

View File

@ -1,25 +1,13 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
using AdminConsoleFixtures = Bit.Core.Test.AdminConsole.AutoFixture;
using GlobalSettings = Bit.Core.Settings.GlobalSettings; using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.AdminConsole.Services; namespace Bit.Core.Test.AdminConsole.Services;
@ -27,667 +15,6 @@ namespace Bit.Core.Test.AdminConsole.Services;
[SutProviderCustomize] [SutProviderCustomize]
public class PolicyServiceTests public class PolicyServiceTests
{ {
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider<PolicyService> sutProvider)
{
SetupOrg(sutProvider, policy.OrganizationId, null);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider<PolicyService> sutProvider)
{
var orgId = Guid.NewGuid();
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
UsePolicies = false,
});
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_RequireSsoEnabled_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.RequireSso)
.Returns(Task.FromResult(new Policy { Enabled = true }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Sign-On Authentication policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_VaultTimeoutEnabled_ThrowsBadRequest([AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.MaximumVaultTimeout)
.Returns(new Policy { Enabled = true });
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Maximum Vault Timeout policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory]
[BitAutoData(PolicyType.SingleOrg)]
[BitAutoData(PolicyType.RequireSso)]
public async Task SaveAsync_PolicyRequiredByKeyConnector_DisablePolicy_ThrowsBadRequest(
PolicyType policyType,
Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
policy.Type = policyType;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
var data = new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector };
ssoConfig.SetData(data);
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData]
public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_NewPolicy_Created(
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Id = default;
policy.Data = null;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = true }));
var utcNow = DateTime.UtcNow;
await sutProvider.Sut.SaveAsync(policy, Guid.NewGuid());
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, BitAutoData]
public async Task SaveAsync_VaultTimeoutPolicy_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.MaximumVaultTimeout)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor(
Organization organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
organization.UsePolicies = true;
policy.OrganizationId = organization.Id;
SetupOrg(sutProvider, organization.Id, organization);
sutProvider.GetDependency<IPolicyRepository>()
.GetByIdAsync(policy.Id)
.Returns(new Policy
{
Id = policy.Id,
Type = PolicyType.TwoFactorAuthentication,
Enabled = false
});
var orgUserDetailUserInvited = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserInvited,
orgUserDetailUserAcceptedWith2FA,
orgUserDetailUserAcceptedWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserInvited, false),
(orgUserDetailUserAcceptedWith2FA, true),
(orgUserDetailUserAcceptedWithout2FA, false),
(orgUserDetailAdmin, false),
});
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, savingUserId);
await removeOrganizationUserCommand.Received()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().Received()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, BitAutoData]
public async Task SaveAsync_EnableTwoFactor_WithoutMasterPasswordOr2FA_ThrowsBadRequest(
Organization organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
organization.UsePolicies = true;
policy.OrganizationId = organization.Id;
SetupOrg(sutProvider, organization.Id, organization);
var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserWith2FAAndMP,
orgUserDetailUserWith2FANoMP,
orgUserDetailUserWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
(orgUserDetailUserWithout2FA.UserId.Value, false),
(orgUserDetailAdmin.UserId.Value, false),
});
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var savingUserId = Guid.NewGuid();
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, savingUserId));
Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs()
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default);
await sutProvider.GetDependency<IPolicyRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData]
public async Task SaveAsync_ExistingPolicy_UpdateSingleOrg(
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy, SutProvider<PolicyService> sutProvider)
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
var org = new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
Name = "TEST",
};
SetupOrg(sutProvider, policy.OrganizationId, org);
sutProvider.GetDependency<IPolicyRepository>()
.GetByIdAsync(policy.Id)
.Returns(new Policy
{
Id = policy.Id,
Type = PolicyType.SingleOrg,
Enabled = false,
});
var orgUserDetail = new Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "test@bitwarden.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails>
{
orgUserDetail,
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserDetail.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetail.UserId.Value, false),
});
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, savingUserId);
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory]
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task SaveAsync_ResetPasswordPolicyRequiredByTrustedDeviceEncryption_DisablePolicyOrDisableAutomaticEnrollment_ThrowsBadRequest(
bool policyEnabled,
bool autoEnrollEnabled,
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = policyEnabled;
policy.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = autoEnrollEnabled
});
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_RequireSsoPolicyRequiredByTrustedDeviceEncryption_DisablePolicy_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
policy.SetDataModel(new ResetPasswordDataModel());
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_AccountRecoveryEnabled_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.ResetPassword)
.Returns(new Policy { Enabled = true });
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider) public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider)
{ {
@ -816,32 +143,4 @@ public class PolicyServiceTests
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }
}); });
} }
[Theory, BitAutoData]
public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider<PolicyService> sutProvider)
{
org.Id = policy.OrganizationId;
org.UsePolicies = true;
policy.Enabled = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policy.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(org.Id)
.Returns(true);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, null));
Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message);
}
} }

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
@ -338,16 +339,26 @@ public class SsoConfigServiceTests
await sutProvider.Sut.SaveAsync(ssoConfig, organization); await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<IPolicyService>().Received(1) await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync( .SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.SingleOrg), Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.SingleOrg &&
null t.OrganizationId == organization.Id &&
t.Enabled)
); );
await sutProvider.GetDependency<IPolicyService>().Received(1) await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync( .SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.ResetPassword && t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled), Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.ResetPassword &&
null t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled &&
t.OrganizationId == organization.Id &&
t.Enabled)
);
await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync(
Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.RequireSso &&
t.OrganizationId == organization.Id &&
t.Enabled)
); );
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs() await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()