mirror of
https://github.com/bitwarden/server
synced 2026-01-12 13:33:24 +00:00
Merge branch 'main' into PM-19147_3
This commit is contained in:
@@ -8,6 +8,9 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -55,8 +58,11 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -79,8 +85,11 @@ public class OrganizationUsersController : Controller
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService,
|
||||
IPricingClient pricingClient)
|
||||
IPricingClient pricingClient,
|
||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -102,8 +111,11 @@ public class OrganizationUsersController : Controller
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -303,7 +315,7 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
|
||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||
}
|
||||
|
||||
[HttpPost("{organizationUserId}/accept")]
|
||||
@@ -315,11 +327,13 @@ public class OrganizationUsersController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId);
|
||||
var useMasterPasswordPolicy = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id)).AutoEnrollEnabled(orgId)
|
||||
: await ShouldHandleResetPasswordAsync(orgId);
|
||||
|
||||
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||
{
|
||||
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
|
||||
throw new BadRequestException("Master Password reset is required, but not provided.");
|
||||
}
|
||||
|
||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||
@@ -357,7 +371,7 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
|
||||
var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("confirm")]
|
||||
@@ -371,7 +385,7 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
|
||||
var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
@@ -620,14 +634,14 @@ public class OrganizationUsersController : Controller
|
||||
[HttpPut("{id}/restore")]
|
||||
public async Task RestoreAsync(Guid orgId, Guid id)
|
||||
{
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId));
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
|
||||
}
|
||||
|
||||
[HttpPatch("restore")]
|
||||
[HttpPut("restore")]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _organizationService.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
||||
}
|
||||
|
||||
[HttpPatch("enable-secrets-manager")]
|
||||
|
||||
@@ -16,6 +16,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -61,6 +63,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationsController(
|
||||
@@ -84,6 +87,7 @@ public class OrganizationsController : Controller
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
||||
IOrganizationDeleteCommand organizationDeleteCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@@ -106,6 +110,7 @@ public class OrganizationsController : Controller
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
|
||||
_organizationDeleteCommand = organizationDeleteCommand;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
@@ -163,8 +168,13 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var resetPasswordPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));
|
||||
}
|
||||
|
||||
var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
||||
{
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
||||
@@ -172,6 +182,7 @@ public class OrganizationsController : Controller
|
||||
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
|
||||
@@ -355,6 +355,7 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[Obsolete("Replaced by the safer rotate-user-account-keys endpoint.")]
|
||||
[HttpPost("key")]
|
||||
public async Task PostKey([FromBody] UpdateKeyRequestModel model)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class MasterPasswordUnlockDataModel : IValidatableObject
|
||||
{
|
||||
public required KdfType KdfType { get; set; }
|
||||
public required int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
[StrictEmailAddress]
|
||||
[StringLength(256)]
|
||||
public required string Email { get; set; }
|
||||
[StringLength(300)]
|
||||
public required string MasterKeyAuthenticationHash { get; set; }
|
||||
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; set; }
|
||||
[StringLength(50)]
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (KdfType == KdfType.PBKDF2_SHA256)
|
||||
{
|
||||
if (KdfMemory.HasValue || KdfParallelism.HasValue)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory and KdfParallelism must be null for PBKDF2_SHA256", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
|
||||
}
|
||||
}
|
||||
else if (KdfType == KdfType.Argon2id)
|
||||
{
|
||||
if (!KdfMemory.HasValue || !KdfParallelism.HasValue)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory and KdfParallelism must have values for Argon2id", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ValidationResult("Invalid KdfType", new[] { nameof(KdfType) });
|
||||
}
|
||||
}
|
||||
|
||||
public MasterPasswordUnlockData ToUnlockData()
|
||||
{
|
||||
var data = new MasterPasswordUnlockData
|
||||
{
|
||||
KdfType = KdfType,
|
||||
KdfIterations = KdfIterations,
|
||||
KdfMemory = KdfMemory,
|
||||
KdfParallelism = KdfParallelism,
|
||||
|
||||
Email = Email,
|
||||
|
||||
MasterKeyAuthenticationHash = MasterKeyAuthenticationHash,
|
||||
MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey,
|
||||
MasterPasswordHint = MasterPasswordHint
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -76,6 +76,13 @@ public class OrganizationSponsorshipsController : Controller
|
||||
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
||||
{
|
||||
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
|
||||
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
||||
sponsoringOrg,
|
||||
@@ -89,6 +96,14 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
|
||||
{
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
|
||||
var sponsoringOrgUser = await _organizationUserRepository
|
||||
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
||||
|
||||
@@ -135,6 +150,14 @@ public class OrganizationSponsorshipsController : Controller
|
||||
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
||||
}
|
||||
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(
|
||||
model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
|
||||
await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
|
||||
sponsorship,
|
||||
await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -19,18 +32,45 @@ public class AccountsKeyManagementController : Controller
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IRotateUserAccountKeysCommand _rotateUserAccountKeysCommand;
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
|
||||
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
|
||||
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
|
||||
_emergencyAccessValidator;
|
||||
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||
IReadOnlyList<OrganizationUser>>
|
||||
_organizationUserValidator;
|
||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
_webauthnKeyValidator;
|
||||
|
||||
public AccountsKeyManagementController(IUserService userService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand)
|
||||
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,
|
||||
IRotateUserAccountKeysCommand rotateUserKeyCommandV2,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
|
||||
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
|
||||
emergencyAccessValidator,
|
||||
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
||||
organizationUserValidator,
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator)
|
||||
{
|
||||
_userService = userService;
|
||||
_featureService = featureService;
|
||||
_regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_rotateUserAccountKeysCommand = rotateUserKeyCommandV2;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
_sendValidator = sendValidator;
|
||||
_emergencyAccessValidator = emergencyAccessValidator;
|
||||
_organizationUserValidator = organizationUserValidator;
|
||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||
}
|
||||
|
||||
[HttpPost("regenerate-keys")]
|
||||
@@ -47,4 +87,45 @@ public class AccountsKeyManagementController : Controller
|
||||
await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),
|
||||
usersOrganizationAccounts, designatedEmergencyAccess);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("rotate-user-account-keys")]
|
||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var dataModel = new RotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||
|
||||
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
AccountPublicKey = model.AccountKeys.AccountPublicKey,
|
||||
|
||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
||||
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),
|
||||
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),
|
||||
|
||||
Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),
|
||||
Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),
|
||||
Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends),
|
||||
};
|
||||
|
||||
var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class AccountKeysRequestModel
|
||||
{
|
||||
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||
public required string AccountPublicKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class RotateUserAccountKeysAndDataRequestModel
|
||||
{
|
||||
[StringLength(300)]
|
||||
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||
public required UnlockDataRequestModel AccountUnlockData { get; set; }
|
||||
public required AccountKeysRequestModel AccountKeys { get; set; }
|
||||
public required AccountDataRequestModel AccountData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class UnlockDataRequestModel
|
||||
{
|
||||
// All methods to get to the userkey
|
||||
public required MasterPasswordUnlockDataModel MasterPasswordUnlockData { get; set; }
|
||||
public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; }
|
||||
public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class AccountDataRequestModel
|
||||
{
|
||||
public required IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }
|
||||
public required IEnumerable<FolderWithIdRequestModel> Folders { get; set; }
|
||||
public required IEnumerable<SendWithIdRequestModel> Sends { get; set; }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Bit.Core;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Commands.Interfaces;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -89,11 +90,28 @@ public class SecurityTaskController : Controller
|
||||
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
|
||||
[FromBody] BulkCreateSecurityTasksRequestModel model)
|
||||
{
|
||||
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks);
|
||||
// Retrieve existing pending security tasks for the organization
|
||||
var pendingSecurityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(orgId, SecurityTaskStatus.Pending);
|
||||
|
||||
await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks);
|
||||
// Get the security tasks that are already associated with a cipher within the submitted model
|
||||
var existingTasks = pendingSecurityTasks.Where(x => model.Tasks.Any(y => y.CipherId == x.CipherId)).ToList();
|
||||
|
||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
// Get tasks that need to be created
|
||||
var tasksToCreateFromModel = model.Tasks.Where(x => !existingTasks.Any(y => y.CipherId == x.CipherId)).ToList();
|
||||
|
||||
ICollection<SecurityTask> newSecurityTasks = new List<SecurityTask>();
|
||||
|
||||
if (tasksToCreateFromModel.Count != 0)
|
||||
{
|
||||
newSecurityTasks = await _createManyTasksCommand.CreateAsync(orgId, tasksToCreateFromModel);
|
||||
}
|
||||
|
||||
// Combine existing tasks and newly created tasks
|
||||
var allTasks = existingTasks.Concat(newSecurityTasks);
|
||||
|
||||
await _createManyTaskNotificationsCommand.CreateAsync(orgId, allTasks);
|
||||
|
||||
var response = allTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS9113</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " />
|
||||
|
||||
18
src/Core/AdminConsole/Entities/OrganizationIntegration.cs
Normal file
18
src/Core/AdminConsole/Entities/OrganizationIntegration.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.AdminConsole.Entities;
|
||||
|
||||
public class OrganizationIntegration : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public IntegrationType Type { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.AdminConsole.Entities;
|
||||
|
||||
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationIntegrationId { get; set; }
|
||||
public EventType EventType { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public string? Template { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
7
src/Core/AdminConsole/Enums/IntegrationType.cs
Normal file
7
src/Core/AdminConsole/Enums/IntegrationType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum IntegrationType : int
|
||||
{
|
||||
Slack = 1,
|
||||
Webhook = 2,
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IPushRegistrationService _pushRegistrationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
|
||||
public ConfirmOrganizationUserCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IUserRepository userRepository,
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IPolicyService policyService,
|
||||
IDeviceRepository deviceRepository)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_userRepository = userRepository;
|
||||
_eventService = eventService;
|
||||
_mailService = mailService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_pushRegistrationService = pushRegistrationService;
|
||||
_policyService = policyService;
|
||||
_deviceRepository = deviceRepository;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId)
|
||||
{
|
||||
var result = await ConfirmUsersAsync(
|
||||
organizationId,
|
||||
new Dictionary<Guid, string>() { { organizationUserId, key } },
|
||||
confirmingUserId);
|
||||
|
||||
if (!result.Any())
|
||||
{
|
||||
throw new BadRequestException("User not valid.");
|
||||
}
|
||||
|
||||
var (orgUser, error) = result[0];
|
||||
if (error != "")
|
||||
{
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId)
|
||||
{
|
||||
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
||||
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
||||
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
||||
.ToList();
|
||||
|
||||
if (!validSelectedOrganizationUsers.Any())
|
||||
{
|
||||
return new List<Tuple<OrganizationUser, string>>();
|
||||
}
|
||||
|
||||
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
||||
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
||||
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
||||
|
||||
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
||||
.ToDictionary(u => u.Key, u => u.ToList());
|
||||
|
||||
var succeededUsers = new List<OrganizationUser>();
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var orgUser = keyedFilteredUsers[user.Id];
|
||||
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
||||
try
|
||||
{
|
||||
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
||||
|| orgUser.Type == OrganizationUserType.Owner))
|
||||
{
|
||||
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
||||
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser.Key = keys[orgUser.Id];
|
||||
orgUser.Email = null;
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||
succeededUsers.Add(orgUser);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
||||
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
||||
{
|
||||
// Enforce Two Factor Authentication Policy for this organization
|
||||
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
||||
.Any(p => p.OrganizationId == organizationId);
|
||||
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
||||
{
|
||||
throw new BadRequestException("User does not have two-step login enabled.");
|
||||
}
|
||||
|
||||
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||
var otherSingleOrgPolicies =
|
||||
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||
// Enforce Single Organization Policy for this organization
|
||||
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
||||
{
|
||||
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
||||
}
|
||||
// Enforce Single Organization Policy of other organizations user is a member of
|
||||
if (otherSingleOrgPolicies.Any())
|
||||
{
|
||||
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var devices = await GetUserDeviceIdsAsync(userId);
|
||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
||||
organizationId.ToString());
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||
{
|
||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||
return devices
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => d.Id.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command to confirm organization users who have accepted their invitations.
|
||||
/// </summary>
|
||||
public interface IConfirmOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms a single organization user who has accepted their invitation.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="organizationUserId">The ID of the organization user to confirm.</param>
|
||||
/// <param name="key">The encrypted organization key for the user.</param>
|
||||
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
||||
/// <returns>The confirmed organization user.</returns>
|
||||
/// <exception cref="BadRequestException">Thrown when the user is not valid or cannot be confirmed.</exception>
|
||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Confirms multiple organization users who have accepted their invitations.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="keys">A dictionary mapping organization user IDs to their encrypted organization keys.</param>
|
||||
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
||||
/// <returns>A list of tuples containing the organization user and an error message (if any).</returns>
|
||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
|
||||
/// <summary>
|
||||
/// Restores a user back to their previous status.
|
||||
/// </summary>
|
||||
public interface IRestoreOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||
/// <param name="systemUser">System that is performing the action on behalf of the organization (Public API, SCIM, etc.)</param>
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">Organization the users should be restored to.</param>
|
||||
/// <param name="organizationUserIds">List of organization user ids to restore to previous status.</param>
|
||||
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||
/// <param name="userService">Passed in from caller to avoid circular dependency</param>
|
||||
/// <returns>List of organization user Ids and strings. A successful restoration will have an empty string.
|
||||
/// If an error occurs, the error message will be provided.</returns>
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
|
||||
public class RestoreOrganizationUserCommand(
|
||||
ICurrentContext currentContext,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPolicyService policyService,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
|
||||
{
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
{
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!await currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored,
|
||||
systemUser);
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
await organizationService.AutoAddSeatsAsync(organization, 1); // Hooray
|
||||
}
|
||||
|
||||
var userTwoFactorIsEnabled = false;
|
||||
// Only check 2FA status if the user is linked to a user account
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
userTwoFactorIsEnabled =
|
||||
(await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([organizationUser.UserId.Value]))
|
||||
.FirstOrDefault()
|
||||
.twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
||||
|
||||
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
|
||||
organizationUser.Status = status;
|
||||
}
|
||||
|
||||
private async Task CheckUserForOtherFreeOrganizationOwnershipAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
var relatedOrgUsersFromOtherOrgs = await organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value);
|
||||
var otherOrgs = await organizationRepository.GetManyByUserIdAsync(organizationUser.UserId.Value);
|
||||
|
||||
var orgOrgUserDict = relatedOrgUsersFromOtherOrgs
|
||||
.Where(x => x.Id != organizationUser.Id)
|
||||
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||
|
||||
CheckForOtherFreeOrganizationOwnership(organizationUser, orgOrgUserDict);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<OrganizationUser, Organization>> GetRelatedOrganizationUsersAndOrganizations(
|
||||
IEnumerable<OrganizationUser> organizationUsers)
|
||||
{
|
||||
var allUserIds = organizationUsers.Select(x => x.UserId.Value);
|
||||
|
||||
var otherOrganizationUsers = (await organizationUserRepository.GetManyByManyUsersAsync(allUserIds))
|
||||
.Where(x => organizationUsers.Any(y => y.Id == x.Id) == false);
|
||||
|
||||
var otherOrgs = await organizationRepository.GetManyByIdsAsync(otherOrganizationUsers
|
||||
.Select(x => x.OrganizationId)
|
||||
.Distinct());
|
||||
|
||||
return otherOrganizationUsers
|
||||
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||
}
|
||||
|
||||
private static void CheckForOtherFreeOrganizationOwnership(OrganizationUser organizationUser,
|
||||
Dictionary<OrganizationUser, Organization> otherOrgUsersAndOrgs)
|
||||
{
|
||||
var ownerOrAdminList = new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||
if (otherOrgUsersAndOrgs.Any(x =>
|
||||
x.Key.UserId == organizationUser.UserId &&
|
||||
ownerOrAdminList.Any(userType => userType == x.Key.Type) &&
|
||||
x.Key.Status == OrganizationUserStatusType.Confirmed &&
|
||||
x.Value.PlanType == PlanType.Free))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||
{
|
||||
var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
.ToList();
|
||||
|
||||
if (filteredUsers.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (restoringUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
// Query Two Factor Authentication status for all users in the organization
|
||||
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||
var organizationUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
||||
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
||||
|
||||
var orgUsersAndOrgs = await GetRelatedOrganizationUsersAndOrganizations(filteredUsers);
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var organizationUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
||||
&& organizationUsersTwoFactorEnabled
|
||||
.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value)
|
||||
.twoFactorIsEnabled;
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
||||
|
||||
CheckForOtherFreeOrganizationOwnership(organizationUser, orgUsersAndOrgs);
|
||||
|
||||
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(organizationUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
||||
{
|
||||
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
||||
// The user will be subject to the same checks when they try to accept the invite
|
||||
if (OrganizationService.GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = orgUser.UserId.Value;
|
||||
|
||||
// Enforce Single Organization Policy of organization user is being restored to
|
||||
var allOrgUsers = await organizationUserRepository.GetManyByUserAsync(userId);
|
||||
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
||||
var singleOrgPoliciesApplyingToRevokedUsers = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
|
||||
var singleOrgPolicyApplies =
|
||||
singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
|
||||
|
||||
var singleOrgCompliant = true;
|
||||
var belongsToOtherOrgCompliant = true;
|
||||
var twoFactorCompliant = true;
|
||||
|
||||
if (hasOtherOrgs && singleOrgPolicyApplies)
|
||||
{
|
||||
singleOrgCompliant = false;
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of other organizations user is a member of
|
||||
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
belongsToOtherOrgCompliant = false;
|
||||
}
|
||||
|
||||
// Enforce 2FA Policy of organization user is trying to join
|
||||
if (!userHasTwoFactorEnabled)
|
||||
{
|
||||
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
{
|
||||
twoFactorCompliant = false;
|
||||
}
|
||||
}
|
||||
|
||||
var user = await userRepository.GetByIdAsync(userId);
|
||||
|
||||
if (!singleOrgCompliant && !twoFactorCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email +
|
||||
" is not compliant with the single organization and two-step login policy");
|
||||
}
|
||||
else if (!singleOrgCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
|
||||
}
|
||||
else if (!belongsToOtherOrgCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email +
|
||||
" belongs to an organization that doesn't allow them to join multiple organizations");
|
||||
}
|
||||
else if (!twoFactorCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Disable Personal Ownership policy.
|
||||
/// </summary>
|
||||
public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization.
|
||||
/// </summary>
|
||||
public bool DisablePersonalOwnership { get; init; }
|
||||
}
|
||||
|
||||
public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.PersonalOwnership;
|
||||
|
||||
public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Account recovery administration policy.
|
||||
/// </summary>
|
||||
public class ResetPasswordPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// List of Organization Ids that require automatic enrollment in password recovery.
|
||||
/// </summary>
|
||||
private IEnumerable<Guid> _autoEnrollOrganizations;
|
||||
public IEnumerable<Guid> AutoEnrollOrganizations { init => _autoEnrollOrganizations = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if provided organizationId requires automatic enrollment in password recovery.
|
||||
/// </summary>
|
||||
public bool AutoEnrollEnabled(Guid organizationId)
|
||||
{
|
||||
return _autoEnrollOrganizations.Contains(organizationId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class ResetPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<ResetPasswordPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.ResetPassword;
|
||||
|
||||
protected override bool ExemptProviders => false;
|
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
|
||||
|
||||
public override ResetPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = policyDetails
|
||||
.Where(p => p.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled)
|
||||
.Select(p => p.OrganizationId)
|
||||
.ToHashSet();
|
||||
|
||||
return new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = result };
|
||||
}
|
||||
}
|
||||
@@ -33,5 +33,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,5 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
/// </summary>
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ public interface IOrganizationService
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||
@@ -51,10 +48,6 @@ public interface IOrganizationService
|
||||
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||
/// <summary>
|
||||
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
||||
|
||||
@@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -17,7 +19,6 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -73,9 +74,9 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -109,9 +110,9 @@ public class OrganizationService : IOrganizationService
|
||||
IProviderRepository providerRepository,
|
||||
IFeatureService featureService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient)
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -144,9 +145,9 @@ public class OrganizationService : IOrganizationService
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||
@@ -1122,98 +1123,6 @@ public class OrganizationService : IOrganizationService
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId)
|
||||
{
|
||||
var result = await ConfirmUsersAsync(
|
||||
organizationId,
|
||||
new Dictionary<Guid, string>() { { organizationUserId, key } },
|
||||
confirmingUserId);
|
||||
|
||||
if (!result.Any())
|
||||
{
|
||||
throw new BadRequestException("User not valid.");
|
||||
}
|
||||
|
||||
var (orgUser, error) = result[0];
|
||||
if (error != "")
|
||||
{
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId)
|
||||
{
|
||||
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
||||
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
||||
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
||||
.ToList();
|
||||
|
||||
if (!validSelectedOrganizationUsers.Any())
|
||||
{
|
||||
return new List<Tuple<OrganizationUser, string>>();
|
||||
}
|
||||
|
||||
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||
|
||||
var organization = await GetOrgById(organizationId);
|
||||
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
||||
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
||||
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
||||
|
||||
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
||||
.ToDictionary(u => u.Key, u => u.ToList());
|
||||
|
||||
var succeededUsers = new List<OrganizationUser>();
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var orgUser = keyedFilteredUsers[user.Id];
|
||||
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
||||
try
|
||||
{
|
||||
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
||||
|| orgUser.Type == OrganizationUserType.Owner))
|
||||
{
|
||||
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
||||
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser.Key = keys[orgUser.Id];
|
||||
orgUser.Email = null;
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||
succeededUsers.Add(orgUser);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
||||
Organization organization,
|
||||
int seatsToAdd)
|
||||
@@ -1300,32 +1209,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
||||
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
||||
{
|
||||
// Enforce Two Factor Authentication Policy for this organization
|
||||
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
||||
.Any(p => p.OrganizationId == organizationId);
|
||||
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
||||
{
|
||||
throw new BadRequestException("User does not have two-step login enabled.");
|
||||
}
|
||||
|
||||
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||
var otherSingleOrgPolicies =
|
||||
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||
// Enforce Single Organization Policy for this organization
|
||||
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
||||
{
|
||||
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
||||
}
|
||||
// Enforce Single Organization Policy of other organizations user is a member of
|
||||
if (otherSingleOrgPolicies.Any())
|
||||
{
|
||||
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
|
||||
{
|
||||
@@ -1353,13 +1237,25 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
// Block the user from withdrawal if auto enrollment is enabled
|
||||
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||
|
||||
if (data?.AutoEnrollEnabled ?? false)
|
||||
var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);
|
||||
if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId))
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from Password Reset.");
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||
|
||||
if (data?.AutoEnrollEnabled ?? false)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,15 +1519,6 @@ public class OrganizationService : IOrganizationService
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
||||
}
|
||||
|
||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var devices = await GetUserDeviceIdsAsync(userId);
|
||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
||||
organizationId.ToString());
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
|
||||
|
||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||
{
|
||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||
@@ -2000,144 +1887,6 @@ public class OrganizationService : IOrganizationService
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
{
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!await _currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
await AutoAddSeatsAsync(organization, 1);
|
||||
}
|
||||
|
||||
var userTwoFactorIsEnabled = false;
|
||||
// Only check Two Factor Authentication status if the user is linked to a user account
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
||||
|
||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
.ToList();
|
||||
|
||||
if (!filteredUsers.Any())
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (restoringUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
// Query Two Factor Authentication status for all users in the organization
|
||||
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
||||
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var organizationUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue && !deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
||||
&& organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
||||
|
||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(organizationUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
||||
{
|
||||
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
||||
@@ -2204,7 +1953,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||
{
|
||||
// Determine status to revert back to
|
||||
var status = OrganizationUserStatusType.Invited;
|
||||
|
||||
@@ -32,6 +32,7 @@ public static class UserServiceCollectionExtensions
|
||||
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||
{
|
||||
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
|
||||
services.AddScoped<IRotateUserAccountKeysCommand, RotateUserAccountKeysCommand>();
|
||||
}
|
||||
|
||||
private static void AddUserPasswordCommands(this IServiceCollection services)
|
||||
|
||||
@@ -36,7 +36,7 @@ public class PremiumUserBillingService(
|
||||
var customer = await subscriberService.GetCustomer(user);
|
||||
|
||||
// Negative credit represents a balance and all Stripe denomination is in cents.
|
||||
var credit = (long)amount * -100;
|
||||
var credit = (long)(amount * -100);
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
|
||||
@@ -106,9 +106,82 @@ public static class FeatureFlagKeys
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
||||
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||
public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests";
|
||||
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
||||
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
||||
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
|
||||
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
||||
public const string NotificationRefresh = "notification-refresh";
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
|
||||
/* Billing Team */
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
|
||||
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
|
||||
public const string AndroidMutualTls = "mutual-tls";
|
||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
|
||||
/* Platform Team */
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
public const string WebPush = "web-push";
|
||||
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
||||
|
||||
/* Tools Team */
|
||||
public const string ItemShare = "item-share";
|
||||
@@ -116,6 +189,7 @@ public static class FeatureFlagKeys
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||
@@ -125,66 +199,7 @@ public static class FeatureFlagKeys
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
||||
public const string NotificationRefresh = "notification-refresh";
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
||||
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
|
||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string AndroidMutualTls = "mutual-tls";
|
||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
||||
public const string WebPush = "web-push";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS8602;CS9113;CS1998;CS8604</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.28" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
@@ -61,7 +61,7 @@
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.7.0" />
|
||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class MasterPasswordUnlockData
|
||||
{
|
||||
public KdfType KdfType { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
public required string Email { get; set; }
|
||||
public required string MasterKeyAuthenticationHash { get; set; }
|
||||
public required string MasterKeyEncryptedUserKey { get; set; }
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
public bool ValidateForUser(User user)
|
||||
{
|
||||
if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (Email != user.Email)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class RotateUserAccountKeysData
|
||||
{
|
||||
// Authentication for this requests
|
||||
public string OldMasterKeyAuthenticationHash { get; set; }
|
||||
|
||||
// Other keys encrypted by the userkey
|
||||
public string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||
public string AccountPublicKey { get; set; }
|
||||
|
||||
// All methods to get to the userkey
|
||||
public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; }
|
||||
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
public IEnumerable<Cipher> Ciphers { get; set; }
|
||||
public IEnumerable<Folder> Folders { get; set; }
|
||||
public IReadOnlyList<Send> Sends { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.KeyManagement.UserKey;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for rotation of a user key and updating database with re-encrypted data
|
||||
/// </summary>
|
||||
public interface IRotateUserAccountKeysCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a new user key and updates all encrypted data.
|
||||
/// </summary>
|
||||
/// <param name="model">All necessary information for rotation. If data is not included, this will lead to the change being rejected.</param>
|
||||
/// <returns>An IdentityResult for verification of the master password hash</returns>
|
||||
/// <exception cref="ArgumentNullException">User must be provided.</exception>
|
||||
/// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception>
|
||||
Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IFolderRepository _folderRepository;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
|
||||
/// </summary>
|
||||
/// <param name="userService">Master password hash validation</param>
|
||||
/// <param name="userRepository">Updates user keys and re-encrypted data if needed</param>
|
||||
/// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param>
|
||||
/// <param name="folderRepository">Provides a method to update re-encrypted folder data</param>
|
||||
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
|
||||
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
|
||||
/// <param name="organizationUserRepository">Provides a method to update re-encrypted organization user data</param>
|
||||
/// <param name="passwordHasher">Hashes the new master password</param>
|
||||
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
|
||||
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
|
||||
/// <param name="credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param>
|
||||
public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
_sendRepository = sendRepository;
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_pushService = pushService;
|
||||
_identityErrorDescriber = errors;
|
||||
_credentialRepository = credentialRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (!await _userService.CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash))
|
||||
{
|
||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
user.RevisionDate = user.AccountRevisionDate = now;
|
||||
user.LastKeyRotationDate = now;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
if (
|
||||
!model.MasterPasswordUnlockData.ValidateForUser(user)
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
||||
}
|
||||
if (
|
||||
model.AccountPublicKey != user.PublicKey
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation.");
|
||||
}
|
||||
|
||||
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
|
||||
user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
|
||||
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
|
||||
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
|
||||
if (model.Ciphers.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
|
||||
}
|
||||
|
||||
if (model.Folders.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
|
||||
}
|
||||
|
||||
if (model.Sends.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
|
||||
}
|
||||
|
||||
if (model.EmergencyAccesses.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
||||
}
|
||||
|
||||
if (model.OrganizationUsers.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
||||
}
|
||||
|
||||
if (model.WebAuthnKeys.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys));
|
||||
}
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,21 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
||||
style="display: table; width:100%; padding-bottom: 35px; text-align: center;" align="center">
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;" align="center">
|
||||
<tr>
|
||||
<td display="display: table-cell">
|
||||
<td>
|
||||
<a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank"
|
||||
style="display: inline-block; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
style="display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Review at-risk passwords
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;" align="center">
|
||||
<tr>
|
||||
<td display="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;">
|
||||
{{formatAdminOwnerEmails AdminOwnerEmails}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/SecurityTasksHtmlLayout}}
|
||||
|
||||
@@ -5,4 +5,15 @@ breach.
|
||||
Launch the Bitwarden extension to review your at-risk passwords.
|
||||
|
||||
Review at-risk passwords ({{{ReviewPasswordsUrl}}})
|
||||
|
||||
{{#if AdminOwnerEmails.[0]}}
|
||||
{{#if AdminOwnerEmails.[1]}}
|
||||
This request was initiated by
|
||||
{{#each AdminOwnerEmails}}
|
||||
{{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}}
|
||||
{{/each}}.
|
||||
{{else}}
|
||||
This request was initiated by {{AdminOwnerEmails.[0]}}.
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/SecurityTasksHtmlLayout}}
|
||||
|
||||
@@ -8,5 +8,7 @@ public class SecurityTaskNotificationViewModel : BaseMailModel
|
||||
|
||||
public bool TaskCountPlural => TaskCount != 1;
|
||||
|
||||
public List<string> AdminOwnerEmails { get; set; }
|
||||
|
||||
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
@@ -116,6 +117,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||
@@ -167,6 +169,8 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();
|
||||
services.AddScoped<IGetOrganizationUsersManagementStatusQuery, GetOrganizationUsersManagementStatusQuery>();
|
||||
|
||||
services.AddScoped<IRestoreOrganizationUserCommand, RestoreOrganizationUserCommand>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
|
||||
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
|
||||
|
||||
@@ -32,5 +32,7 @@ public interface IUserRepository : IRepository<User, Guid>
|
||||
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
|
||||
Task UpdateUserKeyAndEncryptedDataAsync(User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
||||
Task UpdateUserKeyAndEncryptedDataV2Async(User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
||||
Task DeleteManyAsync(IEnumerable<User> users);
|
||||
}
|
||||
|
||||
@@ -99,5 +99,5 @@ public interface IMailService
|
||||
string organizationName);
|
||||
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
|
||||
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName);
|
||||
Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications);
|
||||
Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Mail;
|
||||
@@ -740,6 +741,59 @@ public class HandlebarsMailService : IMailService
|
||||
var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty);
|
||||
writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>");
|
||||
});
|
||||
|
||||
// Construct markup for admin and owner email addresses.
|
||||
// Using conditionals within the handlebar syntax was including extra spaces around
|
||||
// concatenated strings, which this helper avoids.
|
||||
Handlebars.RegisterHelper("formatAdminOwnerEmails", (writer, context, parameters) =>
|
||||
{
|
||||
if (parameters.Length == 0)
|
||||
{
|
||||
writer.WriteSafeString(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var emailList = new List<string>();
|
||||
if (parameters[0] is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
emailList = jsonElement.EnumerateArray().Select(e => e.GetString()).ToList();
|
||||
}
|
||||
else if (parameters[0] is IEnumerable<string> emails)
|
||||
{
|
||||
emailList = emails.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteSafeString(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailList.Count == 0)
|
||||
{
|
||||
writer.WriteSafeString(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
string constructAnchorElement(string email)
|
||||
{
|
||||
return $"<a style=\"color: #175DDC\" href=\"mailto:{email}\">{email}</a>";
|
||||
}
|
||||
|
||||
var outputMessage = "This request was initiated by ";
|
||||
|
||||
if (emailList.Count == 1)
|
||||
{
|
||||
outputMessage += $"{constructAnchorElement(emailList[0])}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
outputMessage += string.Join(", ", emailList.Take(emailList.Count - 1)
|
||||
.Select(email => constructAnchorElement(email)));
|
||||
outputMessage += $" and {constructAnchorElement(emailList.Last())}.";
|
||||
}
|
||||
|
||||
writer.WriteSafeString($"{outputMessage}");
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||
@@ -1201,7 +1255,7 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications)
|
||||
public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)
|
||||
{
|
||||
MailQueueMessage CreateMessage(UserSecurityTasksCount notification)
|
||||
{
|
||||
@@ -1211,6 +1265,7 @@ public class HandlebarsMailService : IMailService
|
||||
{
|
||||
OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),
|
||||
TaskCount = notification.TaskCount,
|
||||
AdminOwnerEmails = adminOwnerEmails.ToList(),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
};
|
||||
message.Category = "SecurityTasksNotification";
|
||||
|
||||
@@ -324,7 +324,7 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications)
|
||||
public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.ImportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
@@ -26,7 +29,8 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public ImportCiphersCommand(
|
||||
ICipherRepository cipherRepository,
|
||||
@@ -37,7 +41,9 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
||||
IPushNotificationService pushService,
|
||||
IPolicyService policyService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
@@ -48,9 +54,10 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
||||
_policyService = policyService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
|
||||
public async Task ImportIntoIndividualVaultAsync(
|
||||
List<Folder> folders,
|
||||
List<CipherDetails> ciphers,
|
||||
@@ -58,8 +65,11 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
||||
Guid importingUserId)
|
||||
{
|
||||
// Make sure the user can save new ciphers to their personal vault
|
||||
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
||||
if (anyPersonalOwnershipPolicies)
|
||||
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)).DisablePersonalOwnership
|
||||
: await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
||||
|
||||
if (isPersonalVaultRestricted)
|
||||
{
|
||||
throw new BadRequestException("You cannot import items into your personal vault because you are " +
|
||||
"a member of an organization which forbids it.");
|
||||
|
||||
@@ -17,19 +17,22 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ICreateNotificationCommand _createNotificationCommand;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public CreateManyTaskNotificationsCommand(
|
||||
IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IMailService mailService,
|
||||
ICreateNotificationCommand createNotificationCommand,
|
||||
IPushNotificationService pushNotificationService)
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;
|
||||
_organizationRepository = organizationRepository;
|
||||
_mailService = mailService;
|
||||
_createNotificationCommand = createNotificationCommand;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
public async Task CreateAsync(Guid orgId, IEnumerable<SecurityTask> securityTasks)
|
||||
@@ -45,8 +48,18 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
|
||||
}).ToList();
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
var orgAdminEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin))
|
||||
.Select(u => u.Email)
|
||||
.ToList();
|
||||
|
||||
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount);
|
||||
var orgOwnerEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner))
|
||||
.Select(u => u.Email)
|
||||
.ToList();
|
||||
|
||||
// Ensure proper deserialization of emails
|
||||
var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Distinct().ToList();
|
||||
|
||||
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);
|
||||
|
||||
// Break securityTaskCiphers into separate lists by user Id
|
||||
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@@ -41,6 +43,8 @@ public class CipherService : ICipherService
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CipherService(
|
||||
ICipherRepository cipherRepository,
|
||||
@@ -58,7 +62,9 @@ public class CipherService : ICipherService
|
||||
GlobalSettings globalSettings,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
|
||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
@@ -76,6 +82,8 @@ public class CipherService : ICipherService
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||
@@ -143,9 +151,11 @@ public class CipherService : ICipherService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure the user can save new ciphers to their personal vault
|
||||
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
||||
if (anyPersonalOwnershipPolicies)
|
||||
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)).DisablePersonalOwnership
|
||||
: await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
||||
|
||||
if (isPersonalVaultRestricted)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.");
|
||||
}
|
||||
|
||||
@@ -196,4 +196,15 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
return (await connection.QueryAsync<Organization>(
|
||||
$"[{Schema}].[{Table}_ReadManyByIds]",
|
||||
new { OrganizationIds = ids.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618;CS4014</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -254,6 +254,42 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateUserKeyAndEncryptedDataV2Async(
|
||||
User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
connection.Open();
|
||||
|
||||
await using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
user.AccountRevisionDate = user.RevisionDate;
|
||||
|
||||
ProtectData(user);
|
||||
await connection.ExecuteAsync(
|
||||
$"[{Schema}].[{Table}_Update]",
|
||||
user,
|
||||
transaction: transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
// Update re-encrypted data
|
||||
foreach (var action in updateDataActions)
|
||||
{
|
||||
await action(connection, transaction);
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
UnprotectData(user);
|
||||
throw;
|
||||
}
|
||||
UnprotectData(user);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
@@ -295,6 +331,18 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
var originalKey = user.Key;
|
||||
|
||||
// Protect values
|
||||
ProtectData(user);
|
||||
|
||||
// Save
|
||||
await saveTask();
|
||||
|
||||
// Restore original values
|
||||
user.MasterPassword = originalMasterPassword;
|
||||
user.Key = originalKey;
|
||||
}
|
||||
|
||||
private void ProtectData(User user)
|
||||
{
|
||||
if (!user.MasterPassword?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
{
|
||||
user.MasterPassword = string.Concat(Constants.DatabaseFieldProtectedPrefix,
|
||||
@@ -306,13 +354,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
user.Key = string.Concat(Constants.DatabaseFieldProtectedPrefix,
|
||||
_dataProtector.Protect(user.Key!));
|
||||
}
|
||||
|
||||
// Save
|
||||
await saveTask();
|
||||
|
||||
// Restore original values
|
||||
user.MasterPassword = originalMasterPassword;
|
||||
user.Key = originalKey;
|
||||
}
|
||||
|
||||
private void UnprotectData(User? user)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegrationConfiguration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrganizationIntegrationConfiguration> builder)
|
||||
{
|
||||
builder
|
||||
.Property(p => p.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.ToTable(nameof(OrganizationIntegrationConfiguration));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
|
||||
|
||||
public class OrganizationIntegrationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrganizationIntegration> builder)
|
||||
{
|
||||
builder
|
||||
.Property(p => p.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder
|
||||
.HasIndex(p => p.OrganizationId)
|
||||
.IsClustered(false);
|
||||
|
||||
builder
|
||||
.HasIndex(p => new { p.OrganizationId, p.Type })
|
||||
.IsUnique()
|
||||
.IsClustered(false);
|
||||
|
||||
builder.ToTable(nameof(OrganizationIntegration));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
|
||||
public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration
|
||||
{
|
||||
public virtual Organization Organization { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationIntegrationMapperProfile : Profile
|
||||
{
|
||||
public OrganizationIntegrationMapperProfile()
|
||||
{
|
||||
CreateMap<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration>().ReverseMap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
|
||||
public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration
|
||||
{
|
||||
public virtual OrganizationIntegration OrganizationIntegration { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationIntegrationConfigurationMapperProfile : Profile
|
||||
{
|
||||
public OrganizationIntegrationConfigurationMapperProfile()
|
||||
{
|
||||
CreateMap<Core.AdminConsole.Entities.OrganizationIntegrationConfiguration, OrganizationIntegrationConfiguration>().ReverseMap();
|
||||
}
|
||||
}
|
||||
@@ -354,6 +354,19 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = from organization in dbContext.Organizations
|
||||
where ids.Contains(organization.Id)
|
||||
select organization;
|
||||
|
||||
return await query.ToArrayAsync();
|
||||
}
|
||||
|
||||
public Task EnableCollectionEnhancements(Guid organizationId)
|
||||
{
|
||||
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
||||
|
||||
@@ -68,12 +68,11 @@ public class WebAuthnCredentialRepository : Repository<Core.Auth.Entities.WebAut
|
||||
var newCreds = credentials.ToList();
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var userWebauthnCredentials = await GetDbSet(dbContext)
|
||||
.Where(wc => wc.Id == wc.Id)
|
||||
|
||||
var newCredIds = newCreds.Select(nwc => nwc.Id).ToList();
|
||||
var validUserWebauthnCredentials = await GetDbSet(dbContext)
|
||||
.Where(wc => wc.UserId == userId && newCredIds.Contains(wc.Id))
|
||||
.ToListAsync();
|
||||
var validUserWebauthnCredentials = userWebauthnCredentials
|
||||
.Where(wc => newCreds.Any(nwc => nwc.Id == wc.Id))
|
||||
.Where(wc => wc.UserId == userId);
|
||||
|
||||
foreach (var wc in validUserWebauthnCredentials)
|
||||
{
|
||||
|
||||
@@ -170,6 +170,7 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
||||
|
||||
entity.SecurityStamp = user.SecurityStamp;
|
||||
entity.Key = user.Key;
|
||||
|
||||
entity.PrivateKey = user.PrivateKey;
|
||||
entity.LastKeyRotationDate = user.LastKeyRotationDate;
|
||||
entity.AccountRevisionDate = user.AccountRevisionDate;
|
||||
@@ -194,6 +195,52 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
// Update user
|
||||
var userEntity = await dbContext.Users.FindAsync(user.Id);
|
||||
if (userEntity == null)
|
||||
{
|
||||
throw new ArgumentException("User not found", nameof(user));
|
||||
}
|
||||
|
||||
userEntity.SecurityStamp = user.SecurityStamp;
|
||||
userEntity.Key = user.Key;
|
||||
userEntity.PrivateKey = user.PrivateKey;
|
||||
|
||||
userEntity.Kdf = user.Kdf;
|
||||
userEntity.KdfIterations = user.KdfIterations;
|
||||
userEntity.KdfMemory = user.KdfMemory;
|
||||
userEntity.KdfParallelism = user.KdfParallelism;
|
||||
|
||||
userEntity.Email = user.Email;
|
||||
|
||||
userEntity.MasterPassword = user.MasterPassword;
|
||||
userEntity.MasterPasswordHint = user.MasterPasswordHint;
|
||||
|
||||
userEntity.LastKeyRotationDate = user.LastKeyRotationDate;
|
||||
userEntity.AccountRevisionDate = user.AccountRevisionDate;
|
||||
userEntity.RevisionDate = user.RevisionDate;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Update re-encrypted data
|
||||
foreach (var action in updateDataActions)
|
||||
{
|
||||
// connection and transaction aren't used in EF
|
||||
await action();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -25,15 +24,6 @@ public sealed class RequestLoggingMiddleware
|
||||
|
||||
public Task Invoke(HttpContext context, IFeatureService featureService)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.RemoveServerVersionHeader))
|
||||
{
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers.Append("Server-Version", AssemblyHelpers.GetVersion());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
using (_logger.BeginScope(
|
||||
new RequestLogScope(context.GetIpAddress(_globalSettings),
|
||||
GetHeaderValue(context, "user-agent"),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||
@EventType SMALLINT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@IntegrationType SMALLINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
oic.*
|
||||
FROM
|
||||
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||
WHERE
|
||||
oic.[EventType] = @EventType
|
||||
AND
|
||||
oic.[OrganizationId] = @OrganizationId
|
||||
AND
|
||||
oic.[IntegrationType] = @IntegrationType
|
||||
END
|
||||
GO
|
||||
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal file
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal file
@@ -0,0 +1,67 @@
|
||||
CREATE PROCEDURE [dbo].[Organization_ReadManyByIds] @OrganizationIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT o.[Id],
|
||||
o.[Identifier],
|
||||
o.[Name],
|
||||
o.[BusinessName],
|
||||
o.[BusinessAddress1],
|
||||
o.[BusinessAddress2],
|
||||
o.[BusinessAddress3],
|
||||
o.[BusinessCountry],
|
||||
o.[BusinessTaxNumber],
|
||||
o.[BillingEmail],
|
||||
o.[Plan],
|
||||
o.[PlanType],
|
||||
o.[Seats],
|
||||
o.[MaxCollections],
|
||||
o.[UsePolicies],
|
||||
o.[UseSso],
|
||||
o.[UseGroups],
|
||||
o.[UseDirectory],
|
||||
o.[UseEvents],
|
||||
o.[UseTotp],
|
||||
o.[Use2fa],
|
||||
o.[UseApi],
|
||||
o.[UseResetPassword],
|
||||
o.[SelfHost],
|
||||
o.[UsersGetPremium],
|
||||
o.[Storage],
|
||||
o.[MaxStorageGb],
|
||||
o.[Gateway],
|
||||
o.[GatewayCustomerId],
|
||||
o.[GatewaySubscriptionId],
|
||||
o.[ReferenceData],
|
||||
o.[Enabled],
|
||||
o.[LicenseKey],
|
||||
o.[PublicKey],
|
||||
o.[PrivateKey],
|
||||
o.[TwoFactorProviders],
|
||||
o.[ExpirationDate],
|
||||
o.[CreationDate],
|
||||
o.[RevisionDate],
|
||||
o.[OwnersNotifiedOfAutoscaling],
|
||||
o.[MaxAutoscaleSeats],
|
||||
o.[UseKeyConnector],
|
||||
o.[UseScim],
|
||||
o.[UseCustomPermissions],
|
||||
o.[UseSecretsManager],
|
||||
o.[Status],
|
||||
o.[UsePasswordManager],
|
||||
o.[SmSeats],
|
||||
o.[SmServiceAccounts],
|
||||
o.[MaxAutoscaleSmSeats],
|
||||
o.[MaxAutoscaleSmServiceAccounts],
|
||||
o.[SecretsManagerBeta],
|
||||
o.[LimitCollectionCreation],
|
||||
o.[LimitCollectionDeletion],
|
||||
o.[LimitItemDeletion],
|
||||
o.[AllowAdminAccessToAllCollectionItems],
|
||||
o.[UseRiskInsights]
|
||||
FROM [dbo].[OrganizationView] o
|
||||
INNER JOIN @OrganizationIds ids ON o.[Id] = ids.[Id]
|
||||
|
||||
END
|
||||
|
||||
20
src/Sql/dbo/Tables/OrganizationIntegration.sql
Normal file
20
src/Sql/dbo/Tables/OrganizationIntegration.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE [dbo].[OrganizationIntegration]
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[Type] SMALLINT NOT NULL,
|
||||
[Configuration] VARCHAR (MAX) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]
|
||||
ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);
|
||||
GO
|
||||
|
||||
CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]
|
||||
ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);
|
||||
GO
|
||||
13
src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql
Normal file
13
src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[EventType] SMALLINT NOT NULL,
|
||||
[Configuration] VARCHAR (MAX) NULL,
|
||||
[Template] VARCHAR (MAX) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id])
|
||||
);
|
||||
GO
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
oi.[OrganizationId],
|
||||
oi.[Type] AS [IntegrationType],
|
||||
oic.[EventType],
|
||||
oic.[Configuration],
|
||||
oi.[Configuration] AS [IntegrationConfiguration],
|
||||
oic.[Template]
|
||||
FROM
|
||||
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OrganizationIntegrationConfiguration]
|
||||
6
src/Sql/dbo/Views/OrganizationIntegrationView.sql
Normal file
6
src/Sql/dbo/Views/OrganizationIntegrationView.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE VIEW [dbo].[OrganizationIntegrationView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OrganizationIntegration]
|
||||
Reference in New Issue
Block a user