mirror of
https://github.com/bitwarden/server.git
synced 2025-05-20 19:14:32 -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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user