mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[PM-10319] - Revoke Non Complaint Users for 2FA and Single Org Policy Enablement (#5037)
- Revoking users when enabling single org and 2fa policies. - Updated emails sent when users are revoked via 2FA or Single Organization policy enablement Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -12,124 +14,121 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
public class VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
: IVerifyOrganizationDomainCommand
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
if (currentContext.UserId is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(UserVerifyOrganizationDomainAsync)} can only be called by a user. " +
|
||||
$"Please call {nameof(SystemVerifyOrganizationDomainAsync)} for system users.");
|
||||
}
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
var actingUser = new StandardUser(currentContext.UserId.Value, await currentContext.OrganizationOwner(organizationDomain.OrganizationId));
|
||||
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);
|
||||
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
domainVerificationResult.VerifiedDate != null
|
||||
? EventType.OrganizationDomain_Verified
|
||||
: EventType.OrganizationDomain_NotVerified);
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
await organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
var actingUser = new SystemUser(EventSystemUser.DomainVerification);
|
||||
|
||||
organizationDomain.SetJobRunCount();
|
||||
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);
|
||||
|
||||
if (domainVerificationResult.VerifiedDate is not null)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_Verified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
else
|
||||
{
|
||||
domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
domainVerificationResult.SetNextRunDate(globalSettings.DomainVerification.VerificationInterval);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Verification for organization {OrgId} with domain {Domain} failed",
|
||||
domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);
|
||||
}
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
await organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
||||
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
|
||||
if (domain.VerifiedDate is not null)
|
||||
{
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
await organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("Domain has already been verified.");
|
||||
}
|
||||
|
||||
var claimedDomain =
|
||||
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
await organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
|
||||
if (claimedDomain.Count > 0)
|
||||
{
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
await organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("The domain is not available to be claimed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
if (await dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
{
|
||||
domain.SetVerifiedDate();
|
||||
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}",
|
||||
logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}",
|
||||
domain.DomainName, e.Message);
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId)
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _policyService.SaveAsync(
|
||||
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null);
|
||||
await policyService.SaveAsync(
|
||||
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true },
|
||||
savingUserId: actingUser is StandardUser standardUser ? standardUser.UserId : null,
|
||||
eventSystemUser: actingUser is SystemUser systemUser ? systemUser.SystemUserType : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Models.Commands;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IRevokeNonCompliantOrganizationUserCommand
|
||||
{
|
||||
Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
public record RevokeOrganizationUsersRequest(
|
||||
Guid OrganizationId,
|
||||
IEnumerable<OrganizationUserUserDetails> OrganizationUsers,
|
||||
IActingUser ActionPerformedBy)
|
||||
{
|
||||
public RevokeOrganizationUsersRequest(Guid organizationId, OrganizationUserUserDetails organizationUser, IActingUser actionPerformedBy)
|
||||
: this(organizationId, [organizationUser], actionPerformedBy) { }
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class RevokeNonCompliantOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IHasConfirmedOwnersExceptQuery confirmedOwnersExceptQuery,
|
||||
TimeProvider timeProvider) : IRevokeNonCompliantOrganizationUserCommand
|
||||
{
|
||||
public const string ErrorCannotRevokeSelf = "You cannot revoke yourself.";
|
||||
public const string ErrorOnlyOwnersCanRevokeOtherOwners = "Only owners can revoke other owners.";
|
||||
public const string ErrorUserAlreadyRevoked = "User is already revoked.";
|
||||
public const string ErrorOrgMustHaveAtLeastOneOwner = "Organization must have at least one confirmed owner.";
|
||||
public const string ErrorInvalidUsers = "Invalid users.";
|
||||
public const string ErrorRequestedByWasNotValid = "Action was performed by an unexpected type.";
|
||||
|
||||
public async Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
var validationResult = await ValidateAsync(request);
|
||||
|
||||
if (validationResult.HasErrors)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
await organizationUserRepository.RevokeManyByIdAsync(request.OrganizationUsers.Select(x => x.Id));
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
switch (request.ActionPerformedBy)
|
||||
{
|
||||
case StandardUser:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, now)));
|
||||
break;
|
||||
case SystemUser { SystemUserType: not null } loggableSystem:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x =>
|
||||
GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value, now)));
|
||||
break;
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple(
|
||||
OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) =>
|
||||
new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, EventSystemUser eventSystemUser, DateTime? time) GetRevokedUserEventBySystemUserTuple(
|
||||
OrganizationUserUserDetails organizationUser, EventSystemUser systemUser, DateTimeOffset dateTimeOffset) => new(organizationUser,
|
||||
EventType.OrganizationUser_Revoked, systemUser, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private async Task<CommandResult> ValidateAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
if (!PerformedByIsAnExpectedType(request.ActionPerformedBy))
|
||||
{
|
||||
return new CommandResult(ErrorRequestedByWasNotValid);
|
||||
}
|
||||
|
||||
if (request.ActionPerformedBy is StandardUser user
|
||||
&& request.OrganizationUsers.Any(x => x.UserId == user.UserId))
|
||||
{
|
||||
return new CommandResult(ErrorCannotRevokeSelf);
|
||||
}
|
||||
|
||||
if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId))
|
||||
{
|
||||
return new CommandResult(ErrorInvalidUsers);
|
||||
}
|
||||
|
||||
if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||
request.OrganizationId,
|
||||
request.OrganizationUsers.Select(x => x.Id)))
|
||||
{
|
||||
return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner);
|
||||
}
|
||||
|
||||
return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) =>
|
||||
{
|
||||
if (IsAlreadyRevoked(userToRevoke))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (NonOwnersCannotRevokeOwners(userToRevoke, request.ActionPerformedBy))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorOnlyOwnersCanRevokeOtherOwners}");
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static bool PerformedByIsAnExpectedType(IActingUser entity) => entity is SystemUser or StandardUser;
|
||||
|
||||
private static bool IsAlreadyRevoked(OrganizationUserUserDetails organizationUser) =>
|
||||
organizationUser is { Status: OrganizationUserStatusType.Revoked };
|
||||
|
||||
private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser,
|
||||
IActingUser actingUser) =>
|
||||
actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@ -15,6 +16,7 @@ public record PolicyUpdate
|
||||
public PolicyType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public IActingUser? PerformedBy { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -18,6 +20,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
private const string OrganizationNotFoundErrorMessage = "Organization not found.";
|
||||
private const string ClaimedDomainSingleOrganizationRequiredErrorMessage = "The Single organization policy is required for organizations that have enabled domain verification.";
|
||||
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
@ -27,6 +31,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public SingleOrgPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -36,7 +41,8 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
@ -46,6 +52,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@ -54,10 +61,54 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization is null)
|
||||
{
|
||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
var currentActiveRevocableOrganizationUsers =
|
||||
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
|
||||
.Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
!(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))
|
||||
.ToList();
|
||||
|
||||
if (currentActiveRevocableOrganizationUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy));
|
||||
|
||||
if (commandResult.HasErrors)
|
||||
{
|
||||
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
||||
}
|
||||
|
||||
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
|
||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
// Remove non-compliant users
|
||||
@ -67,7 +118,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException("Organization not found.");
|
||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
@ -76,18 +127,17 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId
|
||||
).ToList();
|
||||
).ToList();
|
||||
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
||||
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))
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
@ -111,7 +161,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
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 ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,15 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
@ -21,6 +24,10 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public const string NonCompliantMembersWillLoseAccessMessage = "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.";
|
||||
|
||||
public PolicyType Type => PolicyType.TwoFactorAuthentication;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@ -31,7 +38,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
@ -39,16 +48,65 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
var currentActiveRevocableOrganizationUsers =
|
||||
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
|
||||
.Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
!(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))
|
||||
.ToList();
|
||||
|
||||
if (currentActiveRevocableOrganizationUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationUsersTwoFactorEnabled =
|
||||
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers);
|
||||
|
||||
if (NonCompliantMembersWillLoseAccess(currentActiveRevocableOrganizationUsers, organizationUsersTwoFactorEnabled))
|
||||
{
|
||||
throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage);
|
||||
}
|
||||
|
||||
var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy));
|
||||
|
||||
if (commandResult.HasErrors)
|
||||
{
|
||||
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
||||
}
|
||||
|
||||
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
|
||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
@ -83,5 +141,12 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static bool NonCompliantMembersWillLoseAccess(
|
||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||
orgUserDetails.Any(x =>
|
||||
!x.HasMasterPassword && !organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == x.Id)
|
||||
.isTwoFactorEnabled);
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
}
|
||||
|
Reference in New Issue
Block a user