1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-20 11:04:31 -05:00

Added account delete.

This commit is contained in:
Kyle Spearrin 2015-12-27 00:14:56 -05:00
parent 8d7178bc74
commit 55be0c739e
8 changed files with 166 additions and 10 deletions

View File

@ -218,5 +218,31 @@ namespace Bit.Api.Controllers
model.Sites.Select(s => s.ToSite(User.GetUserId())).ToList(), model.Sites.Select(s => s.ToSite(User.GetUserId())).ToList(),
model.SiteRelationships); model.SiteRelationships);
} }
[HttpPost("delete")]
public async Task PostDelete([FromBody]DeleteAccountRequestModel model)
{
var user = _currentContext.User;
if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash))
{
ModelState.AddModelError("MasterPasswordHash", "Invalid password.");
await Task.Delay(2000);
}
else
{
var result = await _userService.DeleteAsync(user);
if(result.Succeeded)
{
return;
}
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
throw new BadRequestException(ModelState);
}
} }
} }

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class DeleteAccountRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -18,10 +18,14 @@ namespace Bit.Core.Identity
IUserSecurityStampStore<User> IUserSecurityStampStore<User>
{ {
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly CurrentContext _currentContext;
public UserStore(IUserRepository userRepository) public UserStore(
IUserRepository userRepository,
CurrentContext currentContext)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_currentContext = currentContext;
} }
public void Dispose() { } public void Dispose() { }
@ -40,16 +44,31 @@ namespace Bit.Core.Identity
public async Task<User> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) public async Task<User> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{ {
if(_currentContext?.User != null && _currentContext.User.Email == normalizedEmail)
{
return _currentContext.User;
}
return await _userRepository.GetByEmailAsync(normalizedEmail); return await _userRepository.GetByEmailAsync(normalizedEmail);
} }
public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{ {
if(_currentContext?.User != null && _currentContext.User.Id == userId)
{
return _currentContext.User;
}
return await _userRepository.GetByIdAsync(userId); return await _userRepository.GetByIdAsync(userId);
} }
public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{ {
if(_currentContext?.User != null && _currentContext.User.Email == normalizedUserName)
{
return _currentContext.User;
}
return await _userRepository.GetByEmailAsync(normalizedUserName); return await _userRepository.GetByEmailAsync(normalizedUserName);
} }

View File

@ -16,7 +16,7 @@ namespace Bit.Core.Repositories.DocumentDB
public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers) public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers)
{ {
await DocumentDBHelpers.QueryWithRetryAsync(async () => await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{ {
// Make sure we are dealing with cipher types since we accept any via dynamic. // Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher); var cleanedCiphers = ciphers.Where(c => c is Cipher);
@ -42,7 +42,7 @@ namespace Bit.Core.Repositories.DocumentDB
public async Task CreateAsync(IEnumerable<dynamic> ciphers) public async Task CreateAsync(IEnumerable<dynamic> ciphers)
{ {
await DocumentDBHelpers.QueryWithRetryAsync(async () => await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{ {
// Make sure we are dealing with cipher types since we accept any via dynamic. // Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher); var cleanedCiphers = ciphers.Where(c => c is Cipher);

View File

@ -0,0 +1,81 @@
/**
* A DocumentDB stored procedure that bulk deletes documents for a given query.<br/>
* Note: You may need to execute this sproc multiple times (depending whether the sproc is able to delete every document within the execution timeout limit).
*
* @function
* @param {string} query - A query that provides the documents to be deleted (e.g. "SELECT * FROM c WHERE c.founded_year = 2008")
* @returns {Object.<number, boolean>} Returns an object with the two properties:<br/>
* deleted - contains a count of documents deleted<br/>
* continuation - a boolean whether you should execute the sproc again (true if there are more documents to delete; false otherwise).
*/
function bulkDelete(query) {
var collection = getContext().getCollection();
var collectionLink = collection.getSelfLink();
var response = getContext().getResponse();
var responseBody = {
deleted: 0,
continuation: true
};
// Validate input.
if (!query) throw new Error('The query is undefined or null.');
tryQueryAndDelete();
// Recursively runs the query w/ support for continuation tokens.
// Calls tryDelete(documents) as soon as the query returns documents.
function tryQueryAndDelete(continuation) {
var requestOptions = { continuation: continuation };
var isAccepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, retrievedDocs, responseOptions) {
if (err) throw err;
if (retrievedDocs.length > 0) {
// Begin deleting documents as soon as documents are returned form the query results.
// tryDelete() resumes querying after deleting; no need to page through continuation tokens.
// - this is to prioritize writes over reads given timeout constraints.
tryDelete(retrievedDocs);
}
else if (responseOptions.continuation) {
// Else if the query came back empty, but with a continuation token; repeat the query w/ the token.
tryQueryAndDelete(responseOptions.continuation);
}
else {
// Else if there are no more documents and no continuation token - we are finished deleting documents.
responseBody.continuation = false;
response.setBody(responseBody);
}
});
// If we hit execution bounds - return continuation: true.
if (!isAccepted) {
response.setBody(responseBody);
}
}
// Recursively deletes documents passed in as an array argument.
// Attempts to query for more on empty array.
function tryDelete(documents) {
if (documents.length > 0) {
// Delete the first document in the array.
var isAccepted = collection.deleteDocument(documents[0]._self, {}, function (err, responseOptions) {
if (err) throw err;
responseBody.deleted++;
documents.shift();
// Delete the next document in the array.
tryDelete(documents);
});
// If we hit execution bounds - return continuation: true.
if (!isAccepted) {
response.setBody(responseBody);
}
}
else {
// If the document array is empty, query for more documents.
tryQueryAndDelete();
}
}
}

View File

@ -28,15 +28,35 @@ namespace Bit.Core.Repositories.DocumentDB
public async Task ReplaceAndDirtyCiphersAsync(Domains.User user) public async Task ReplaceAndDirtyCiphersAsync(Domains.User user)
{ {
await DocumentDBHelpers.QueryWithRetryAsync(async () => await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{ {
await Client.ExecuteStoredProcedureAsync<Domains.User>(ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), user); await Client.ExecuteStoredProcedureAsync<Domains.User>(
ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"),
user);
}); });
} }
public override async Task DeleteAsync(Domains.User user)
{
await DeleteByIdAsync(user.Id);
}
public override async Task DeleteByIdAsync(string id) public override async Task DeleteByIdAsync(string id)
{ {
await DeleteByPartitionIdAsync(id); await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
while(true)
{
StoredProcedureResponse<dynamic> sprocResponse = await Client.ExecuteStoredProcedureAsync<dynamic>(
ResolveSprocIdLink(id, "bulkDelete"),
string.Format("SELECT * FROM c WHERE c.id = '{0}' OR c.UserId = '{0}'", id));
if(!(bool)sprocResponse.Response.continuation)
{
break;
}
}
});
} }
} }
} }

View File

@ -31,15 +31,14 @@ namespace Bit.Core.Repositories.DocumentDB.Utilities
return client; return client;
} }
public static async Task QueryWithRetryAsync(Func<Task> func) public static async Task ExecuteWithRetryAsync(Func<Task> func)
{ {
var queryComplete = false; while(true)
while(!queryComplete)
{ {
try try
{ {
await func(); await func();
queryComplete = true; break;
} }
catch(DocumentClientException e) catch(DocumentClientException e)
{ {

View File

@ -18,5 +18,6 @@ namespace Bit.Core.Services
Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers); Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash); Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider); Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider);
Task<IdentityResult> DeleteAsync(User user);
} }
} }