diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 46d4e3527..c86c0f425 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -11,6 +11,7 @@ namespace Bit.Core.Repositories { Task GetCountByOrganizationIdAsync(Guid organizationId); Task GetCountByFreeOrganizationAdminUserAsync(Guid userId); + Task GetCountByOrganizationOwnerUserAsync(Guid userId); Task> GetManyByUserAsync(Guid userId); Task> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type); Task GetByOrganizationAsync(Guid organizationId, string email); diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs index f5e6cb1dc..daa00e682 100644 --- a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -47,6 +47,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task GetCountByOrganizationOwnerUserAsync(Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteScalarAsync( + "[dbo].[OrganizationUser_ReadCountByOrganizationOwnerUser]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results; + } + } + public async Task GetByOrganizationAsync(Guid organizationId, string email) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 584af5846..32692096b 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -18,6 +18,7 @@ namespace Bit.Core.Services { private readonly IUserRepository _userRepository; private readonly ICipherRepository _cipherRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly IPushService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; @@ -29,6 +30,7 @@ namespace Bit.Core.Services public UserService( IUserRepository userRepository, ICipherRepository cipherRepository, + IOrganizationUserRepository organizationUserRepository, IMailService mailService, IPushService pushService, IUserStore store, @@ -54,6 +56,7 @@ namespace Bit.Core.Services { _userRepository = userRepository; _cipherRepository = cipherRepository; + _organizationUserRepository = organizationUserRepository; _mailService = mailService; _pushService = pushService; _identityOptions = optionsAccessor?.Value ?? new IdentityOptions(); @@ -133,6 +136,22 @@ namespace Bit.Core.Services await _pushService.PushSyncSettingsAsync(user.Id); } + public override async Task DeleteAsync(User user) + { + // Check if user is the owner of any organizations. + var organizationOwnerCount = await _organizationUserRepository.GetCountByOrganizationOwnerUserAsync(user.Id); + if(organizationOwnerCount > 0) + { + return IdentityResult.Failed(new IdentityError + { + Description = "You must leave or delete any organizations that you are the owner of first." + }); + } + + await _userRepository.DeleteAsync(user); + return IdentityResult.Success; + } + public async Task RegisterUserAsync(User user, string masterPassword) { var result = await base.CreateAsync(user, masterPassword); @@ -184,7 +203,7 @@ namespace Bit.Core.Services return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } - if(!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider, + if(!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider, GetChangeEmailTokenPurpose(newEmail), token)) { return IdentityResult.Failed(_identityErrorDescriber.InvalidToken()); @@ -224,7 +243,7 @@ namespace Bit.Core.Services throw new NotImplementedException(); } - public async Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, + public async Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, IEnumerable ciphers, IEnumerable folders, string privateKey) { if(user == null) @@ -373,7 +392,7 @@ namespace Bit.Core.Services if(errors.Count > 0) { - Logger.LogWarning("User {userId} password validation failed: {errors}.", await GetUserIdAsync(user), + Logger.LogWarning("User {userId} password validation failed: {errors}.", await GetUserIdAsync(user), string.Join(";", errors.Select(e => e.Code))); return IdentityResult.Failed(errors.ToArray()); } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 1bcd66b72..90e88383a 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -179,5 +179,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOrganizationOwnerUser.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOrganizationOwnerUser.sql new file mode 100644 index 000000000..33ba249be --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOrganizationOwnerUser.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadCountByOrganizationOwnerUser] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(1) + FROM + [dbo].[OrganizationUser] OU + WHERE + OU.[UserId] = @UserId + AND OU.[Type] = 0 +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/User_DeleteById.sql b/src/Sql/dbo/Stored Procedures/User_DeleteById.sql index 9ceac6033..bcdf0a81d 100644 --- a/src/Sql/dbo/Stored Procedures/User_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/User_DeleteById.sql @@ -6,6 +6,7 @@ BEGIN SET NOCOUNT ON DECLARE @BatchSize INT = 100 + -- Delete ciphers WHILE @BatchSize > 0 BEGIN BEGIN TRANSACTION User_DeleteById_Ciphers @@ -23,12 +24,25 @@ BEGIN BEGIN TRANSACTION User_DeleteById + -- Delete collection users + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE + OU.[UserId] = @Id + + -- Delete organization users DELETE FROM - [dbo].[Device] + [dbo].[OrganizationUser] WHERE [UserId] = @Id + AND [Type] != 0 -- 0 = owner + -- Finally, delete the user DELETE FROM [dbo].[User]