1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 05:28:15 -05:00

Transactionally safe user password and email change updates.

This commit is contained in:
Kyle Spearrin 2016-02-21 00:15:17 -05:00
parent 1da53f0ecc
commit 1b3acec905
5 changed files with 53 additions and 39 deletions

View File

@ -1,12 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories namespace Bit.Core.Repositories
{ {
public interface ICipherRepository public interface ICipherRepository
{ {
Task DirtyCiphersAsync(string userId); Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<dynamic> ciphers);
Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers);
Task CreateAsync(IEnumerable<dynamic> ciphers); Task CreateAsync(IEnumerable<dynamic> ciphers);
} }
} }

View File

@ -16,12 +16,7 @@ namespace Bit.Core.Repositories.SqlServer
: base(connectionString) : base(connectionString)
{ } { }
public Task DirtyCiphersAsync(string userId) public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<dynamic> ciphers)
{
return Task.FromResult(0);
}
public Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers)
{ {
var cleanedCiphers = ciphers.Where(c => c is Cipher); var cleanedCiphers = ciphers.Where(c => c is Cipher);
if(cleanedCiphers.Count() == 0) if(cleanedCiphers.Count() == 0)
@ -29,9 +24,6 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0); return Task.FromResult(0);
} }
// Get the id of the expected user
var userId = ((Cipher)ciphers.First()).UserId;
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
connection.Open(); connection.Open();
@ -40,7 +32,19 @@ namespace Bit.Core.Repositories.SqlServer
{ {
try try
{ {
// 1. Create temp tables to bulk copy into. // 1. Update user.
using(var cmd = new SqlCommand("[dbo].[User_UpdateEmailPassword]", connection, transaction))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = new Guid(user.Id);
cmd.Parameters.Add("@Email", SqlDbType.NVarChar).Value = user.Email;
cmd.Parameters.Add("@MasterPassword", SqlDbType.NVarChar).Value = user.MasterPassword;
cmd.Parameters.Add("@SecurityStamp", SqlDbType.NVarChar).Value = user.SecurityStamp;
cmd.ExecuteNonQuery();
}
// 2. Create temp tables to bulk copy into.
var sqlCreateTemp = @" var sqlCreateTemp = @"
SELECT TOP 0 * SELECT TOP 0 *
@ -56,7 +60,7 @@ namespace Bit.Core.Repositories.SqlServer
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
// 2. Bulk bopy into temp tables. // 3. Bulk bopy into temp tables.
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{ {
@ -82,7 +86,7 @@ namespace Bit.Core.Repositories.SqlServer
bulkCopy.WriteToServer(dataTable); bulkCopy.WriteToServer(dataTable);
} }
// 3. Insert into real tables from temp tables and clean up. // 4. Insert into real tables from temp tables and clean up.
var sqlUpdate = @" var sqlUpdate = @"
UPDATE UPDATE
@ -123,7 +127,7 @@ namespace Bit.Core.Repositories.SqlServer
using(var cmd = new SqlCommand(sqlUpdate, connection, transaction)) using(var cmd = new SqlCommand(sqlUpdate, connection, transaction))
{ {
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = new Guid(userId); cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = new Guid(user.Id);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }

View File

@ -128,50 +128,43 @@ namespace Bit.Core.Services
} }
var existingUser = await _userRepository.GetByEmailAsync(newEmail); var existingUser = await _userRepository.GetByEmailAsync(newEmail);
if(existingUser != null) if(existingUser != null && existingUser.Id != user.Id)
{ {
return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail)); return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));
} }
var result = await UpdatePasswordHash(user, newMasterPassword);
if(!result.Succeeded)
{
return result;
}
user.Email = newEmail; user.Email = newEmail;
user.MasterPassword = _passwordHasher.HashPassword(user, newMasterPassword); await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers);
user.SecurityStamp = Guid.NewGuid().ToString();
await _cipherRepository.DirtyCiphersAsync(user.Id);
await _userRepository.ReplaceAsync(user);
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
// TODO: what if something fails? rollback?
return IdentityResult.Success; return IdentityResult.Success;
} }
public override Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash) public override Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers) public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, IEnumerable<dynamic> ciphers)
{ {
if(user == null) if(user == null)
{ {
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
if(await base.CheckPasswordAsync(user, currentMasterPasswordHash)) if(await base.CheckPasswordAsync(user, masterPassword))
{ {
var result = await UpdatePasswordHash(user, newMasterPasswordHash); var result = await UpdatePasswordHash(user, newMasterPassword);
if(!result.Succeeded) if(!result.Succeeded)
{ {
return result; return result;
} }
await _cipherRepository.DirtyCiphersAsync(user.Id); await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers);
await _userRepository.ReplaceAsync(user);
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
// TODO: what if something fails? rollback?
return IdentityResult.Success; return IdentityResult.Success;
} }
@ -179,14 +172,14 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
} }
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash) public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPassword)
{ {
if(user == null) if(user == null)
{ {
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
if(await base.CheckPasswordAsync(user, masterPasswordHash)) if(await base.CheckPasswordAsync(user, masterPassword))
{ {
var result = await base.UpdateSecurityStampAsync(user); var result = await base.UpdateSecurityStampAsync(user);
if(!result.Succeeded) if(!result.Succeeded)

View File

@ -87,5 +87,6 @@
<Build Include="dbo\Stored Procedures\Site_Update.sql" /> <Build Include="dbo\Stored Procedures\Site_Update.sql" />
<Build Include="dbo\Stored Procedures\Folder_Update.sql" /> <Build Include="dbo\Stored Procedures\Folder_Update.sql" />
<Build Include="dbo\Stored Procedures\Cipher_ReadByUserId.sql" /> <Build Include="dbo\Stored Procedures\Cipher_ReadByUserId.sql" />
<Build Include="dbo\Stored Procedures\User_UpdateEmailPassword.sql" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,16 @@
CREATE PROCEDURE [dbo].[User_UpdateEmailPassword]
@Id UNIQUEIDENTIFIER,
@Email NVARCHAR(50),
@MasterPassword NVARCHAR(300),
@SecurityStamp NVARCHAR(50)
AS
BEGIN
UPDATE
[dbo].[User]
SET
[Email] = @Email,
[MasterPassword] = @MasterPassword,
[SecurityStamp] = @SecurityStamp
WHERE
[Id] = @Id
END