mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-11404] Account Management: Prevent a verified user from purging their vault (#4853)
* Add check for managed user before purging account * Rename IOrganizationRepository.GetByClaimedUserDomainAsync to GetByVerifiedUserEmailDomainAsync and refactor to return a list. Remove ManagedByOrganizationId from ProfileResponseMode. Add ManagesActiveUser to ProfileOrganizationResponseModel * Rename the property ManagesActiveUser to UserIsManagedByOrganization * Remove whole class #nullable enable and add it to specific places * Remove unnecessary .ToList() * Refactor IUserService methods GetOrganizationsManagingUserAsync and IsManagedByAnyOrganizationAsync to not return nullable objects. Update ProfileOrganizationResponseModel.UserIsManagedByOrganization to not be nullable * Update error message when unable to purge vault for managed account
This commit is contained in:
parent
245e2e4d52
commit
d6cd73cfcc
@ -124,7 +124,11 @@ public class OrganizationsController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,
|
var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,
|
||||||
OrganizationUserStatusType.Confirmed);
|
OrganizationUserStatusType.Confirmed);
|
||||||
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o));
|
|
||||||
|
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(userId);
|
||||||
|
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
|
||||||
|
|
||||||
|
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
|
||||||
return new ListResponseModel<ProfileOrganizationResponseModel>(responses);
|
return new ListResponseModel<ProfileOrganizationResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,10 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
{
|
{
|
||||||
public ProfileOrganizationResponseModel(string str) : base(str) { }
|
public ProfileOrganizationResponseModel(string str) : base(str) { }
|
||||||
|
|
||||||
public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails organization) : this("profileOrganization")
|
public ProfileOrganizationResponseModel(
|
||||||
|
OrganizationUserOrganizationDetails organization,
|
||||||
|
IEnumerable<Guid> organizationIdsManagingUser)
|
||||||
|
: this("profileOrganization")
|
||||||
{
|
{
|
||||||
Id = organization.OrganizationId;
|
Id = organization.OrganizationId;
|
||||||
Name = organization.Name;
|
Name = organization.Name;
|
||||||
@ -64,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
AccessSecretsManager = organization.AccessSecretsManager;
|
AccessSecretsManager = organization.AccessSecretsManager;
|
||||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
|
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
|
||||||
|
|
||||||
if (organization.SsoConfig != null)
|
if (organization.SsoConfig != null)
|
||||||
{
|
{
|
||||||
@ -122,4 +126,15 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool AccessSecretsManager { get; set; }
|
public bool AccessSecretsManager { get; set; }
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the organization manages the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// An organization manages a user if the user's email domain is verified by the organization and the user is a member of it.
|
||||||
|
/// The organization must be enabled and able to have verified domains.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// False if the Account Deprovisioning feature flag is disabled.
|
||||||
|
/// </returns>
|
||||||
|
public bool UserIsManagedByOrganization { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -443,11 +443,11 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
|
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, twoFactorEnabled,
|
providerUserOrganizationDetails, twoFactorEnabled,
|
||||||
hasPremiumFromOrg, managedByOrganizationId);
|
hasPremiumFromOrg, organizationIdsManagingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,7 +457,9 @@ public class AccountsController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,
|
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,
|
||||||
OrganizationUserStatusType.Confirmed);
|
OrganizationUserStatusType.Confirmed);
|
||||||
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o));
|
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(userId.Value);
|
||||||
|
|
||||||
|
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
|
||||||
return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);
|
return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,9 +477,9 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
|
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId);
|
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsManagingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,9 +496,9 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
|
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
|
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,9 +649,9 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
|
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||||
|
|
||||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
|
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
|
||||||
return new PaymentResponseModel
|
return new PaymentResponseModel
|
||||||
{
|
{
|
||||||
UserProfile = profile,
|
UserProfile = profile,
|
||||||
@ -937,14 +939,9 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user)
|
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);
|
||||||
{
|
return organizationManagingUser.Select(o => o.Id);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
|
|
||||||
return organizationManagingUser?.Id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,10 @@ public class OrganizationsController(
|
|||||||
var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
||||||
OrganizationUserStatusType.Confirmed);
|
OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
return new ProfileOrganizationResponseModel(organizationDetails);
|
var organizationManagingActiveUser = await userService.GetOrganizationsManagingUserAsync(userId);
|
||||||
|
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
|
||||||
|
|
||||||
|
return new ProfileOrganizationResponseModel(organizationDetails, organizationIdsManagingActiveUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/seat")]
|
[HttpPost("{id:guid}/seat")]
|
||||||
|
@ -15,7 +15,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||||
bool twoFactorEnabled,
|
bool twoFactorEnabled,
|
||||||
bool premiumFromOrganization,
|
bool premiumFromOrganization,
|
||||||
Guid? managedByOrganizationId) : base("profile")
|
IEnumerable<Guid> organizationIdsManagingUser) : base("profile")
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@ -37,11 +37,10 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
UsesKeyConnector = user.UsesKeyConnector;
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
AvatarColor = user.AvatarColor;
|
AvatarColor = user.AvatarColor;
|
||||||
CreationDate = user.CreationDate;
|
CreationDate = user.CreationDate;
|
||||||
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
|
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser));
|
||||||
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
||||||
ProviderOrganizations =
|
ProviderOrganizations =
|
||||||
providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po));
|
providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po));
|
||||||
ManagedByOrganizationId = managedByOrganizationId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileResponseModel() : base("profile")
|
public ProfileResponseModel() : base("profile")
|
||||||
@ -63,7 +62,6 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
public string AvatarColor { get; set; }
|
public string AvatarColor { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public Guid? ManagedByOrganizationId { get; set; }
|
|
||||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||||
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
||||||
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
||||||
|
@ -910,6 +910,13 @@ public class CiphersController : Controller
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(organizationId))
|
if (string.IsNullOrWhiteSpace(organizationId))
|
||||||
{
|
{
|
||||||
await _cipherRepository.DeleteByUserIdAsync(user.Id);
|
await _cipherRepository.DeleteByUserIdAsync(user.Id);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Api.Vault.Models.Response;
|
using Bit.Api.Vault.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -7,7 +6,6 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -95,23 +93,12 @@ public class SyncController : Controller
|
|||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails);
|
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id);
|
||||||
|
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
|
||||||
|
|
||||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization,
|
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization,
|
||||||
managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user, IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails)
|
|
||||||
{
|
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) ||
|
|
||||||
!organizationUserDetails.Any(o => o.Enabled && o.UseSso))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
|
|
||||||
return organizationManagingUser?.Id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel
|
|||||||
User user,
|
User user,
|
||||||
bool userTwoFactorEnabled,
|
bool userTwoFactorEnabled,
|
||||||
bool userHasPremiumFromOrganization,
|
bool userHasPremiumFromOrganization,
|
||||||
Guid? managedByOrganizationId,
|
IEnumerable<Guid> organizationIdsManagingUser,
|
||||||
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
|
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
|
||||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||||
@ -35,7 +35,7 @@ public class SyncResponseModel : ResponseModel
|
|||||||
: base("sync")
|
: base("sync")
|
||||||
{
|
{
|
||||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
|
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser);
|
||||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||||
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict));
|
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict));
|
||||||
Collections = collections?.Select(
|
Collections = collections?.Select(
|
||||||
|
@ -19,7 +19,7 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
|||||||
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the organization that has a claimed domain matching the user's email domain.
|
/// Gets the organizations that have a verified domain matching the user's email domain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Organization> GetByClaimedUserDomainAsync(Guid userId);
|
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||||
}
|
}
|
||||||
|
@ -90,14 +90,20 @@ public interface IUserService
|
|||||||
/// Indicates if the user is managed by any organization.
|
/// Indicates if the user is managed by any organization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A managed user is a user whose email domain matches one of the Organization's verified domains.
|
/// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
|
||||||
/// The organization must be enabled and be on an Enterprise plan.
|
/// The organization must be enabled and able to have verified domains.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// False if the Account Deprovisioning feature flag is disabled.
|
||||||
|
/// </returns>
|
||||||
Task<bool> IsManagedByAnyOrganizationAsync(Guid userId);
|
Task<bool> IsManagedByAnyOrganizationAsync(Guid userId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the organization that manages the user.
|
/// Gets the organizations that manage the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An empty collection if the Account Deprovisioning feature flag is disabled.
|
||||||
|
/// </returns>
|
||||||
/// <inheritdoc cref="IsManagedByAnyOrganizationAsync(Guid)"/>
|
/// <inheritdoc cref="IsManagedByAnyOrganizationAsync(Guid)"/>
|
||||||
Task<Organization> GetOrganizationManagingUserAsync(Guid userId);
|
Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId);
|
||||||
}
|
}
|
||||||
|
@ -1267,18 +1267,24 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
|
|
||||||
public async Task<bool> IsManagedByAnyOrganizationAsync(Guid userId)
|
public async Task<bool> IsManagedByAnyOrganizationAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var managingOrganization = await GetOrganizationManagingUserAsync(userId);
|
var managingOrganizations = await GetOrganizationsManagingUserAsync(userId);
|
||||||
return managingOrganization != null;
|
return managingOrganizations.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Organization> GetOrganizationManagingUserAsync(Guid userId)
|
public async Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
// Users can only be managed by an Organization that is enabled and can have organization domains
|
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||||
var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId);
|
{
|
||||||
|
return Enumerable.Empty<Organization>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all organizations that have verified the user's email domain.
|
||||||
|
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
|
||||||
|
|
||||||
|
// Organizations must be enabled and able to have verified domains.
|
||||||
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
|
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
|
||||||
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
|
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
|
||||||
return (organization is { Enabled: true, UseSso: true }) ? organization : null;
|
return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IsLegacyUser(string)"/>
|
/// <inheritdoc cref="IsLegacyUser(string)"/>
|
||||||
|
@ -168,7 +168,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Organization> GetByClaimedUserDomainAsync(Guid userId)
|
public async Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
@ -177,7 +177,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
new { UserId = userId },
|
new { UserId = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return result.SingleOrDefault();
|
return result.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
return await query.ToListAsync();
|
return await query.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Core.AdminConsole.Entities.Organization> GetByClaimedUserDomainAsync(Guid userId)
|
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
@ -291,7 +291,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
&& u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
|
&& u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
|
||||||
select o;
|
select o;
|
||||||
|
|
||||||
return await query.FirstOrDefaultAsync();
|
return await query.ToArrayAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReceivedExtensions;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
namespace Bit.Core.Test.Services;
|
||||||
@ -282,45 +281,69 @@ public class UserServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task IsManagedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue(
|
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse(
|
||||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
SutProvider<UserService> sutProvider, Guid userId)
|
||||||
{
|
{
|
||||||
organization.Enabled = true;
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
organization.UseSso = true;
|
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
.Returns(false);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByClaimedUserDomainAsync(userId)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
|
||||||
Assert.True(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse(
|
|
||||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
|
||||||
{
|
|
||||||
organization.Enabled = false;
|
|
||||||
organization.UseSso = true;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByClaimedUserDomainAsync(userId)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse(
|
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue(
|
||||||
|
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||||
|
{
|
||||||
|
organization.Enabled = true;
|
||||||
|
organization.UseSso = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||||
|
.Returns(new[] { organization });
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse(
|
||||||
|
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||||
|
{
|
||||||
|
organization.Enabled = false;
|
||||||
|
organization.UseSso = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||||
|
.Returns(new[] { organization });
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse(
|
||||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||||
{
|
{
|
||||||
organization.Enabled = true;
|
organization.Enabled = true;
|
||||||
organization.UseSso = false;
|
organization.UseSso = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByClaimedUserDomainAsync(userId)
|
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||||
.Returns(organization);
|
.Returns(new[] { organization });
|
||||||
|
|
||||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
|
@ -97,13 +97,160 @@ public class OrganizationRepositoryTests
|
|||||||
ResetPasswordKey = "resetpasswordkey1",
|
ResetPasswordKey = "resetpasswordkey1",
|
||||||
});
|
});
|
||||||
|
|
||||||
var user1Response = await organizationRepository.GetByClaimedUserDomainAsync(user1.Id);
|
var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
|
||||||
var user2Response = await organizationRepository.GetByClaimedUserDomainAsync(user2.Id);
|
var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
|
||||||
var user3Response = await organizationRepository.GetByClaimedUserDomainAsync(user3.Id);
|
var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id);
|
||||||
|
|
||||||
Assert.NotNull(user1Response);
|
Assert.NotEmpty(user1Response);
|
||||||
Assert.Equal(organization.Id, user1Response.Id);
|
Assert.Equal(organization.Id, user1Response.First().Id);
|
||||||
Assert.Null(user2Response);
|
Assert.Empty(user2Response);
|
||||||
Assert.Null(user3Response);
|
Assert.Empty(user3Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org {id}",
|
||||||
|
BillingEmail = user.Email,
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey",
|
||||||
|
});
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain.SetNextRunDate(12);
|
||||||
|
organizationDomain.SetJobRunCount();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = user.Id,
|
||||||
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
ResetPasswordKey = "resetpasswordkey",
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org 1 {id}",
|
||||||
|
BillingEmail = user.Email,
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey1",
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization2 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org 2 {id}",
|
||||||
|
BillingEmail = user.Email,
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey2",
|
||||||
|
});
|
||||||
|
|
||||||
|
var organizationDomain1 = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain1.SetNextRunDate(12);
|
||||||
|
organizationDomain1.SetJobRunCount();
|
||||||
|
organizationDomain1.SetVerifiedDate();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain1);
|
||||||
|
|
||||||
|
var organizationDomain2 = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization2.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+67890",
|
||||||
|
};
|
||||||
|
organizationDomain2.SetNextRunDate(12);
|
||||||
|
organizationDomain2.SetJobRunCount();
|
||||||
|
organizationDomain2.SetVerifiedDate();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain2);
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
UserId = user.Id,
|
||||||
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
ResetPasswordKey = "resetpasswordkey1",
|
||||||
|
});
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization2.Id,
|
||||||
|
UserId = user.Id,
|
||||||
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
ResetPasswordKey = "resetpasswordkey2",
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(result, org => org.Id == organization1.Id);
|
||||||
|
Assert.Contains(result, org => org.Id == organization2.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty(
|
||||||
|
IOrganizationRepository organizationRepository)
|
||||||
|
{
|
||||||
|
var nonExistentUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user