Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-10319] - Revoke Non Complaint Users for 2FA and Single Org Policy Enablement #5037

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0a4c88a
Revoking users when enabling single org and 2fa policies. Fixing tests.
jrmccannon Nov 7, 2024
ff61b83
Added migration.
jrmccannon Nov 7, 2024
49620c7
Wrote tests and fixed bugs found.
jrmccannon Nov 11, 2024
e7a9a4f
Patch build process
withinfocus Nov 11, 2024
8b534b0
Fixing tests.
jrmccannon Nov 11, 2024
6a36767
Added unit test around disabling the feature flag.
jrmccannon Nov 12, 2024
f2f2a62
Updated error message to be public and added test for validating the โ€ฆ
jrmccannon Nov 12, 2024
f009db0
formatting
jrmccannon Nov 12, 2024
595e4b9
Added some tests for single org policy validator.
jrmccannon Nov 12, 2024
d662fff
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 12, 2024
d64032c
Fix issues from merge.
jrmccannon Nov 12, 2024
abbd4f5
Added sending emails to revoked non-compliant users.
jrmccannon Nov 12, 2024
3917114
Fixing name. Adding two factor policy email.
jrmccannon Nov 13, 2024
da02c89
Send email when user has been revoked.
jrmccannon Nov 13, 2024
1c3e4d8
Correcting migration name.
jrmccannon Nov 13, 2024
35643c1
Fixing templates and logic issue in Revoke command.
jrmccannon Nov 13, 2024
d3172c0
Moving interface into its own file.
jrmccannon Nov 13, 2024
fe5af90
Correcting namespaces for email templates.
jrmccannon Nov 13, 2024
95bb1f4
correcting logic that would not allow normal users to revoke non owners.
jrmccannon Nov 13, 2024
eb693a8
Actually correcting the test and logic.
jrmccannon Nov 13, 2024
58702d2
dotnet format. Added exec to bottom of bulk sproc
jrmccannon Nov 13, 2024
47cc545
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 14, 2024
9427b9f
Update src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Rโ€ฆ
jrmccannon Nov 15, 2024
99f98f2
Updated OrgIds to be a json string
jrmccannon Nov 15, 2024
78cd03d
Fixing errors.
jrmccannon Nov 15, 2024
436bebd
Updating test
jrmccannon Nov 15, 2024
8b89ad6
Moving command result.
jrmccannon Nov 15, 2024
0014d5c
Formatting and request rename
jrmccannon Nov 15, 2024
82b65e0
Realized this would throw a null error from the system domain verificโ€ฆ
jrmccannon Nov 15, 2024
51d0b12
Code review changes
jrmccannon Nov 19, 2024
d4bd369
Removing todos
jrmccannon Nov 19, 2024
1d77345
Corrected test name.
jrmccannon Nov 19, 2024
37ca7ff
Syncing filename to record name.
jrmccannon Nov 20, 2024
d0dd776
Fixing up the tests.
jrmccannon Nov 20, 2024
273754a
Added happy path test
jrmccannon Nov 20, 2024
2f3da19
Naming corrections. And corrected EF query.
jrmccannon Nov 20, 2024
1e7c17f
added check against event service
jrmccannon Nov 20, 2024
66d5831
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
withinfocus Nov 21, 2024
772b057
Code review changes.
jrmccannon Nov 22, 2024
0253c6f
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 22, 2024
8669e90
Fixing tests.
jrmccannon Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Core/AdminConsole/Models/Data/IActingUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace Bit.Core.AdminConsole.Models.Data;
public interface IActingUser
{
Guid? UserId { get; }
bool IsOrganizationOwner { get; }
bool IsOrganizationOwnerOrProvider { get; }
EventSystemUser? SystemUserType { get; }
}
4 changes: 2 additions & 2 deletions src/Core/AdminConsole/Models/Data/StandardUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
public StandardUser(Guid userId, bool isOrganizationOwner)
{
UserId = userId;
IsOrganizationOwner = isOrganizationOwner;
IsOrganizationOwnerOrProvider = isOrganizationOwner;
}

public Guid? UserId { get; }
public bool IsOrganizationOwner { get; }
public bool IsOrganizationOwnerOrProvider { get; }
public EventSystemUser? SystemUserType => throw new Exception($"{nameof(StandardUser)} does not have a {nameof(SystemUserType)}");

Check warning on line 15 in src/Core/AdminConsole/Models/Data/StandardUser.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Data/StandardUser.cs#L15

Added line #L15 was not covered by tests
}
2 changes: 1 addition & 1 deletion src/Core/AdminConsole/Models/Data/SystemUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
SystemUserType = systemUser;
}

public Guid? UserId => throw new Exception($"{nameof(SystemUserType)} does not have a {nameof(UserId)}.");

Check warning on line 12 in src/Core/AdminConsole/Models/Data/SystemUser.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Data/SystemUser.cs#L12

Added line #L12 was not covered by tests

public bool IsOrganizationOwner => false;
public bool IsOrganizationOwnerOrProvider => false;
public EventSystemUser? SystemUserType { get; }

Check warning on line 15 in src/Core/AdminConsole/Models/Data/SystemUser.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Data/SystemUser.cs#L14-L15

Added lines #L14 - L15 were not covered by tests
}
eliykat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -30,56 +30,57 @@
return validationResult;
}

await organizationUserRepository.SetOrganizationUsersStatusAsync(request.OrganizationUsers.Select(x => x.Id),
OrganizationUserStatusType.Revoked);
await organizationUserRepository.RevokeOrganizationUserAsync(request.OrganizationUsers.Select(x => x.Id));

if (request.ActionPerformedBy.GetType() == typeof(StandardUser))
{
await eventService.LogOrganizationUserEventsAsync(
request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, timeProvider.GetUtcNow())));
}
else if (request.ActionPerformedBy is SystemUser { SystemUserType: not null } loggableSystem)
var now = timeProvider.GetUtcNow();

switch (request.ActionPerformedBy)
{
await eventService.LogOrganizationUserEventsAsync(
request.OrganizationUsers.Select(x =>
GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value,
timeProvider.GetUtcNow())));
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;

Check warning on line 47 in src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs#L44-L47

Added lines #L44 - L47 were not covered by tests
}

return validationResult;
}

private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple(
OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) => new(organizationUser,
EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);
OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) =>
new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);

Check warning on line 55 in src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs#L55

Added line #L55 was not covered by tests

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);

Check warning on line 59 in src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs#L58-L59

Added lines #L58 - L59 were not covered by tests

private async Task<CommandResult> ValidateAsync(RevokeOrganizationUsersRequest request)
{
if (!PerformedByIsAnExpectedType(request.ActionPerformedBy))
{
return new CommandResult([ErrorRequestedByWasNotValid]);
return new CommandResult(ErrorRequestedByWasNotValid);
}

if (request.ActionPerformedBy is StandardUser loggableUser
&& request.OrganizationUsers.Any(x => x.UserId == loggableUser.UserId))
if (request.ActionPerformedBy is StandardUser user
&& request.OrganizationUsers.Any(x => x.UserId == user.UserId))
{
return new CommandResult([ErrorCannotRevokeSelf]);
return new CommandResult(ErrorCannotRevokeSelf);
}

if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId))
{
return new CommandResult([ErrorInvalidUsers]);
return new CommandResult(ErrorInvalidUsers);
}

if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
request.OrganizationId,
request.OrganizationUsers.Select(x => x.Id)))
{
return new CommandResult([ErrorOrgMustHaveAtLeastOneOwner]);
return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner);
}

return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) =>
Expand Down Expand Up @@ -107,9 +108,5 @@

private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser,
IActingUser actingUser) =>
actingUser is StandardUser { IsOrganizationOwner: false } && organizationUser.Type == OrganizationUserType.Owner;

private static bool IsActingUserAllowedToRevokeOwner(OrganizationUserUserDetails organizationUser,
StandardUser requestingOrganizationUser) => organizationUser is { Type: OrganizationUserType.Owner }
&& requestingOrganizationUser.IsOrganizationOwner;
actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
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;
Expand Down Expand Up @@ -59,10 +61,55 @@
{
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{

Check warning on line 65 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L65

Added line #L65 was not covered by tests
var currentUser = _currentContext.UserId ?? Guid.Empty;
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);

Check warning on line 67 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L67

Added line #L67 was not covered by tests
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}

Check warning on line 69 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L69

Added line #L69 was not covered by tests
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
}
}

private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);

Check warning on line 79 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L78-L79

Added lines #L78 - L79 were not covered by tests

if (organization is null)
{
throw new NotFoundException(OrganizationNotFoundErrorMessage);

Check warning on line 83 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L82-L83

Added lines #L82 - L83 were not covered by tests
}

var currentActiveRevocableOrganizationUsers =
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))

Check warning on line 87 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L86-L87

Added lines #L86 - L87 were not covered by tests
.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)
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
.ToList();

Check warning on line 94 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L89-L94

Added lines #L89 - L94 were not covered by tests

if (currentActiveRevocableOrganizationUsers.Count == 0)
{
return;

Check warning on line 98 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L97-L98

Added lines #L97 - L98 were not covered by tests
}

var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy));

Check warning on line 102 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L101-L102

Added lines #L101 - L102 were not covered by tests

if (commandResult.HasErrors)
{
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));

Check warning on line 106 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L105-L106

Added lines #L105 - L106 were not covered by tests
}

await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
}

Check warning on line 111 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L109-L111

Added lines #L109 - L111 were not covered by tests

private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
// Remove non-compliant users
Expand All @@ -72,7 +119,7 @@
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException("Organization not found.");
throw new NotFoundException(OrganizationNotFoundErrorMessage);

Check warning on line 122 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L122

Added line #L122 was not covered by tests
}

var removableOrgUsers = orgUsers.Where(ou =>
Expand All @@ -86,41 +133,16 @@
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId!.Value));

if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
foreach (var orgUser in removableOrgUsers)
{
var isOwner = await _currentContext.OrganizationOwner(organizationId);

var revocableUsers = removableOrgUsers.Where(nonCompliantUser => orgUsers.Any(orgUser =>
nonCompliantUser.UserId == orgUser.UserId
&& nonCompliantUser.OrganizationId != org.Id
&& nonCompliantUser.Status != OrganizationUserStatusType.Invited))
.ToList();

var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(organizationId, revocableUsers, new StandardUser(savingUserId ?? Guid.Empty, isOwner)));

if (commandResult.HasErrors)
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
}
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);

await Task.WhenAll(revocableUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(org.DisplayName(), x.Email)));
}
else
{
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);

await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
}
Expand All @@ -141,7 +163,7 @@
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;

Check warning on line 166 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs#L166

Added line #L166 was not covered by tests
}
}

Expand Down
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,58 @@
{
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));
}

Check warning on line 64 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs#L64

Added line #L64 was not covered by tests
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)
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
.ToList();

if (currentActiveRevocableOrganizationUsers.Count == 0)
{
return;

Check warning on line 88 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs#L87-L88

Added lines #L87 - L88 were not covered by tests
}

var organizationUsersTwoFactorEnabled =
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers);

if (DoMembersNotHaveAPasswordWithTwoFactorAuthenticationEnabled(currentActiveRevocableOrganizationUsers, organizationUsersTwoFactorEnabled))
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
{
throw new BadRequestException(NonCompliantMembersWillLoseAccess);
}

var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy));

Check warning on line 100 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs#L99-L100

Added lines #L99 - L100 were not covered by tests

if (commandResult.HasErrors)
{
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));

Check warning on line 104 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs#L103-L104

Added lines #L103 - L104 were not covered by tests
}

await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Run validation

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Setup, ./util)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (MsSqlMigratorUtility, ./util, true)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build MSSQL migrator utility (linux-x64)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build MSSQL migrator utility (win-x64)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Icons, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build MSSQL migrator utility (osx-x64)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Scim, ./bitwarden_license/src, true)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Billing, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Identity, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (EventsProcessor, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Api, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Notifications, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Events, ./src)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Sso, ./bitwarden_license/src, true)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Admin, ./src, true)

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Quality scan

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Run tests

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View workflow job for this annotation

GitHub Actions / Upload

Dereference of a possibly null reference.

Check warning on line 108 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs#L107-L108

Added lines #L107 - L108 were not covered by tests
}

private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
Expand All @@ -68,54 +116,32 @@
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);

// this can get moved into the private method after AccountDeprovisiong flag check is removed.
var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers)).ToList();
var organizationUsersTwoFactorEnabled =
(await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers)).ToList();

var revocableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId)
.ToList();

if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in revocableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
if (DoMembersNotHaveAPasswordWithTwoFactorAuthenticationEnabled(revocableOrgUsers, organizationUsersTwoFactorEnabled))
{
throw new BadRequestException(NonCompliantMembersWillLoseAccess);
}

var result = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(organizationId, revocableOrgUsers,
new StandardUser(savingUserId ?? Guid.Empty,
await _currentContext.OrganizationOwner(organizationId))));

if (result.HasErrors)
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled
.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
if (!userTwoFactorEnabled)
{
throw new BadRequestException(string.Join(", ", result.ErrorMessages));
}

await Task.WhenAll(revocableOrgUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(org!.DisplayName(), x.Email)));
}
else
{
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in revocableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled
.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
if (!userTwoFactorEnabled)
if (!orgUser.HasMasterPassword)
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(NonCompliantMembersWillLoseAccess);
}
throw new BadRequestException(NonCompliantMembersWillLoseAccess);
}

await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);

await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org!.DisplayName(), orgUser.Email);
}
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org!.DisplayName(), orgUser.Email);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
/// </summary>
Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);

Task SetOrganizationUsersStatusAsync(IEnumerable<Guid> userIds, OrganizationUserStatusType status);
Task RevokeOrganizationUserAsync(IEnumerable<Guid> userIds);
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions src/Core/Models/Commands/CommandResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public class CommandResult(IEnumerable<string> errors)
{
public CommandResult(string error) : this([error]) { }

public bool Success => ErrorMessages.Count == 0;
public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList();
Expand Down
Loading
Loading