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.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>
{
private readonly IUserRepository _userRepository;
private readonly CurrentContext _currentContext;
public UserStore(IUserRepository userRepository)
public UserStore(
IUserRepository userRepository,
CurrentContext currentContext)
{
_userRepository = userRepository;
_currentContext = currentContext;
}
public void Dispose() { }
@ -40,16 +44,31 @@ namespace Bit.Core.Identity
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);
}
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);
}
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);
}

View File

@ -16,7 +16,7 @@ namespace Bit.Core.Repositories.DocumentDB
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.
var cleanedCiphers = ciphers.Where(c => c is Cipher);
@ -42,7 +42,7 @@ namespace Bit.Core.Repositories.DocumentDB
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.
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)
{
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)
{
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;
}
public static async Task QueryWithRetryAsync(Func<Task> func)
public static async Task ExecuteWithRetryAsync(Func<Task> func)
{
var queryComplete = false;
while(!queryComplete)
while(true)
{
try
{
await func();
queryComplete = true;
break;
}
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> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider);
Task<IdentityResult> DeleteAsync(User user);
}
}