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:
parent
8d7178bc74
commit
55be0c739e
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
src/Api/Models/Request/Accounts/DeleteAccountRequestModel.cs
Normal file
10
src/Api/Models/Request/Accounts/DeleteAccountRequestModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Models
|
||||
{
|
||||
public class DeleteAccountRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user