From c7f88ae43038a3458910772cd226a64cde9063c5 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 19 May 2021 09:40:32 -0500 Subject: [PATCH] [Reset Password] Get/Post Org Keys and API updates (#1323) * [Reset Password] Organization Keys APIs * Updated details response to include private key and added more security checks for reset password methods * Added org type and policy security checks to the enrollment api * Updated based on PR feedback * Added org user type permission checks * Added TODO for email to user * Removed unecessary policyRepository object --- .../OrganizationUsersController.cs | 31 +++++------ .../Controllers/OrganizationsController.cs | 26 +++++++++ .../Response/OrganizationKeysResponseModel.cs | 22 ++++++++ .../Response/OrganizationUserResponseModel.cs | 2 + .../OrganizationUserResetPasswordDetails.cs | 9 +++- src/Core/Services/IOrganizationService.cs | 2 + src/Core/Services/IUserService.cs | 2 +- .../Implementations/OrganizationService.cs | 54 +++++++++++++++++-- .../Services/Implementations/UserService.cs | 54 ++++++++++++++++++- 9 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 src/Core/Models/Api/Response/OrganizationKeysResponseModel.cs diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index e71951d5b..2a0eaa21a 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -107,14 +107,21 @@ namespace Bit.Api.Controllers } // Retrieve data necessary for response (KDF, KDF Iterations, ResetPasswordKey) - // TODO Revisit this and create SPROC to reduce DB calls + // TODO Reset Password - Revisit this and create SPROC to reduce DB calls var user = await _userService.GetUserByIdAsync(organizationUser.UserId.Value); if (user == null) { throw new NotFoundException(); } + + // Retrieve Encrypted Private Key from organization + var org = await _organizationRepository.GetByIdAsync(orgGuidId); + if (org == null) + { + throw new NotFoundException(); + } - return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user)); + return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, org)); } [HttpPost("invite")] @@ -233,29 +240,23 @@ namespace Bit.Api.Controllers [HttpPut("{id}/reset-password")] public async Task PutResetPassword(string orgId, string id, [FromBody]OrganizationUserResetPasswordRequestModel model) { + var orgGuidId = new Guid(orgId); + // Calling user must have Manage Reset Password permission if (!_currentContext.ManageResetPassword(orgGuidId)) { throw new NotFoundException(); } - var orgUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed || - orgUser.OrganizationId != orgGuidId || string.IsNullOrEmpty(orgUser.ResetPasswordKey) || - !orgUser.UserId.HasValue) - { - throw new BadRequestException("Organization User not valid"); - } - - var user = await _userService.GetUserByIdAsync(orgUser.UserId.Value); - if (user == null) + // Get the calling user's Type for this organization and pass it along + var orgType = _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgGuidId)?.Type; + if (orgType == null) { throw new NotFoundException(); } - - var result = await _userService.AdminResetPasswordAsync(user, model.NewMasterPasswordHash, model.Key); + var result = await _userService.AdminResetPasswordAsync(orgType.Value, orgGuidId, new Guid(id), model.NewMasterPasswordHash, model.Key); if (result.Succeeded) { return; @@ -268,7 +269,7 @@ namespace Bit.Api.Controllers await Task.Delay(2000); throw new BadRequestException(ModelState); - } + } [HttpDelete("{id}")] [HttpPost("{id}/delete")] diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 613e9532d..340cfb895 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -11,6 +11,7 @@ using Bit.Core.Services; using Bit.Core.Context; using Bit.Api.Utilities; using Bit.Core.Models.Business; +using Bit.Core.Models.Data; using Bit.Core.Utilities; using Bit.Core.Settings; @@ -555,5 +556,30 @@ namespace Bit.Api.Controllers }; await _paymentService.SaveTaxInfoAsync(organization, taxInfo); } + + [HttpGet("{id}/keys")] + public async Task GetKeys(string id) + { + var org = await _organizationRepository.GetByIdAsync(new Guid(id)); + if (org == null) + { + throw new NotFoundException(); + } + + return new OrganizationKeysResponseModel(org); + } + + [HttpPost("{id}/keys")] + public async Task PostKeys(string id, [FromBody]OrganizationKeysRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var org = await _organizationService.UpdateOrganizationKeysAsync(user.Id, new Guid(id), model.PublicKey, model.EncryptedPrivateKey); + return new OrganizationKeysResponseModel(org); + } } } diff --git a/src/Core/Models/Api/Response/OrganizationKeysResponseModel.cs b/src/Core/Models/Api/Response/OrganizationKeysResponseModel.cs new file mode 100644 index 000000000..ca7b4fdea --- /dev/null +++ b/src/Core/Models/Api/Response/OrganizationKeysResponseModel.cs @@ -0,0 +1,22 @@ +using System; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Api +{ + public class OrganizationKeysResponseModel : ResponseModel + { + public OrganizationKeysResponseModel(Organization org) : base("organizationKeys") + { + if (org == null) + { + throw new ArgumentNullException(nameof(org)); + } + + PublicKey = org.PublicKey; + PrivateKey = org.PrivateKey; + } + + public string PublicKey { get; set; } + public string PrivateKey { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs b/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs index 8f029ae11..f3a632f2b 100644 --- a/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs @@ -100,10 +100,12 @@ namespace Bit.Core.Models.Api Kdf = orgUser.Kdf; KdfIterations = orgUser.KdfIterations; ResetPasswordKey = orgUser.ResetPasswordKey; + EncryptedPrivateKey = orgUser.EncryptedPrivateKey; } public KdfType Kdf { get; set; } public int KdfIterations { get; set; } public string ResetPasswordKey { get; set; } + public string EncryptedPrivateKey { get; set; } } } diff --git a/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs b/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs index 40ec1408d..1d4327f8d 100644 --- a/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs +++ b/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs @@ -6,7 +6,7 @@ namespace Bit.Core.Models.Data { public class OrganizationUserResetPasswordDetails { - public OrganizationUserResetPasswordDetails(OrganizationUser orgUser, User user) + public OrganizationUserResetPasswordDetails(OrganizationUser orgUser, User user, Organization org) { if (orgUser == null) { @@ -17,13 +17,20 @@ namespace Bit.Core.Models.Data { throw new ArgumentNullException(nameof(user)); } + + if (org == null) + { + throw new ArgumentNullException(nameof(org)); + } Kdf = user.Kdf; KdfIterations = user.KdfIterations; ResetPasswordKey = orgUser.ResetPasswordKey; + EncryptedPrivateKey = org.PrivateKey; } public KdfType Kdf { get; set; } public int KdfIterations { get; set; } public string ResetPasswordKey { get; set; } + public string EncryptedPrivateKey { get; set; } } } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 801c18371..d6d62e4da 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -4,6 +4,7 @@ using Bit.Core.Models.Table; using System; using System.Collections.Generic; using Bit.Core.Enums; +using Bit.Core.Models.Api; using Bit.Core.Models.Data; namespace Bit.Core.Services @@ -54,5 +55,6 @@ namespace Bit.Core.Services bool overwriteExisting); Task RotateApiKeyAsync(Organization organization); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); + Task UpdateOrganizationKeysAsync(Guid userId, Guid orgId, string publicKey, string privateKey); } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index ebc18dd2e..8fdafbb6f 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -34,7 +34,7 @@ namespace Bit.Core.Services string token, string key); Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key); Task SetPasswordAsync(User user, string newMasterPassword, string key, string orgIdentifier = null); - Task AdminResetPasswordAsync(User user, string newMasterPassword, string key); + Task AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key); Task ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key, KdfType kdf, int kdfIterations); Task UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 6d90710e0..02a6b98b8 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -15,6 +15,7 @@ using Bit.Core.Settings; using System.IO; using Newtonsoft.Json; using System.Text.Json; +using Bit.Core.Models.Api; namespace Bit.Core.Services { @@ -1588,16 +1589,29 @@ namespace Bit.Core.Services public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId) { + // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, organizationUserId); - if (!callingUserId.HasValue || orgUser == null || orgUser.UserId != callingUserId.Value || - orgUser.Status != OrganizationUserStatusType.Confirmed || + if (!callingUserId.HasValue || orgUser == null || orgUser.UserId != callingUserId.Value || orgUser.OrganizationId != organizationId) { throw new BadRequestException("User not valid."); } - // TODO Reset Password - Block certain org types from using this feature? + // Make sure the organization has the ability to use password reset + var org = await _organizationRepository.GetByIdAsync(organizationId); + if (org == null || !org.UseResetPassword) + { + throw new BadRequestException("Organization does not allow password reset enrollment."); + } + // Make sure the organization has the policy enabled + var resetPasswordPolicy = + await _policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.ResetPassword); + if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled) + { + throw new BadRequestException("Organization does not have the password reset policy enabled."); + } + orgUser.ResetPasswordKey = resetPasswordKey; await _organizationUserRepository.ReplaceAsync(orgUser); await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ? @@ -1849,6 +1863,40 @@ namespace Bit.Core.Services } } + public async Task UpdateOrganizationKeysAsync(Guid userId, Guid orgId, string publicKey, string privateKey) + { + // Only Owners/Admins/Custom (w/ ManageResetPassword) can create org keys + var orgUser = await _organizationUserRepository.GetDetailsByUserAsync(userId, orgId); + if (orgUser == null || orgUser.Type != OrganizationUserType.Admin && + orgUser.Type != OrganizationUserType.Owner && orgUser.Type != OrganizationUserType.Custom) + { + throw new UnauthorizedAccessException(); + } + + if (orgUser.Type == OrganizationUserType.Custom) + { + var permissions = CoreHelpers.LoadClassFromJsonData(orgUser.Permissions); + if (permissions == null || !permissions.ManageResetPassword) + { + throw new UnauthorizedAccessException(); + } + } + + // If the keys already exist, error out + var org = await _organizationRepository.GetByIdAsync(orgId); + if (org.PublicKey != null && org.PrivateKey != null) + { + throw new BadRequestException("Organization Keys already exist"); + } + + // Update org with generated public/private key + org.PublicKey = publicKey; + org.PrivateKey = privateKey; + await UpdateAsync(org); + + return org; + } + private async Task UpdateUsersAsync(Group group, HashSet groupUsers, Dictionary existingUsersIdDict, HashSet existingUsers = null) { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index e409563b5..26f9388ca 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -626,8 +626,59 @@ namespace Bit.Core.Services return IdentityResult.Success; } - public async Task AdminResetPasswordAsync(User user, string newMasterPassword, string key) + public async Task AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key) { + // Org must be able to use reset password + var org = await _organizationRepository.GetByIdAsync(orgId); + if (org == null || !org.UseResetPassword) + { + throw new BadRequestException("Organization does not allow password reset."); + } + + // Enterprise policy must be enabled + var resetPasswordPolicy = + await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled) + { + throw new BadRequestException("Organization does not have the password reset policy enabled."); + } + + // Org User must be confirmed and have a ResetPasswordKey + var orgUser = await _organizationUserRepository.GetByIdAsync(id); + if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed || + orgUser.OrganizationId != orgId || string.IsNullOrEmpty(orgUser.ResetPasswordKey) || + !orgUser.UserId.HasValue) + { + throw new BadRequestException("Organization User not valid"); + } + + // Calling User must be of higher/equal user type to reset user's password + var canAdjustPassword = false; + switch (callingUserType) + { + case OrganizationUserType.Owner: + canAdjustPassword = true; + break; + case OrganizationUserType.Admin: + canAdjustPassword = orgUser.Type != OrganizationUserType.Owner; + break; + case OrganizationUserType.Custom: + canAdjustPassword = orgUser.Type != OrganizationUserType.Owner && + orgUser.Type != OrganizationUserType.Admin; + break; + } + + if (!canAdjustPassword) + { + throw new BadRequestException("Calling user does not have permission to reset this user's master password"); + } + + var user = await GetUserByIdAsync(orgUser.UserId.Value); + if (user == null) + { + throw new NotFoundException(); + } + var result = await UpdatePasswordHash(user, newMasterPassword); if (!result.Succeeded) { @@ -638,6 +689,7 @@ namespace Bit.Core.Services user.Key = key; await _userRepository.ReplaceAsync(user); + // TODO Reset Password - Send email alerting user of changed password await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); await _pushService.PushLogOutAsync(user.Id);