mirror of
https://github.com/bitwarden/server.git
synced 2025-04-28 00:02:26 -05:00
Merge branch 'main' into ac/pm-14613/feature-flag-removal---step-1-remove-flagged-logic-from-clients/server-and-clients-feature-flag
This commit is contained in:
commit
8a469f484e
13
.github/renovate.json5
vendored
13
.github/renovate.json5
vendored
@ -9,6 +9,19 @@
|
|||||||
"nuget",
|
"nuget",
|
||||||
],
|
],
|
||||||
packageRules: [
|
packageRules: [
|
||||||
|
{
|
||||||
|
// Group all release-related workflows for GitHub Actions together for BRE.
|
||||||
|
groupName: "github-action",
|
||||||
|
matchManagers: ["github-actions"],
|
||||||
|
matchFileNames: [
|
||||||
|
".github/workflows/publish.yml",
|
||||||
|
".github/workflows/release.yml",
|
||||||
|
".github/workflows/repository-management.yml"
|
||||||
|
],
|
||||||
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
|
reviewers: ["team:dept-bre"],
|
||||||
|
addLabels: ["hold"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
matchManagers: ["dockerfile"],
|
matchManagers: ["dockerfile"],
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.4.1</Version>
|
<Version>2025.4.2</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -494,7 +494,7 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
||||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
var isTdeEnrollment = ssoConfig != null && ssoConfig.Enabled && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||||
if (!isTdeEnrollment && !string.IsNullOrWhiteSpace(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
if (!isTdeEnrollment && !string.IsNullOrWhiteSpace(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Incorrect password");
|
throw new BadRequestException("Incorrect password");
|
||||||
|
@ -284,52 +284,6 @@ public class AccountsController : Controller
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("set-key-connector-key")]
|
|
||||||
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
|
||||||
{
|
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("convert-to-key-connector")]
|
|
||||||
public async Task PostConvertToKeyConnector()
|
|
||||||
{
|
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("kdf")]
|
[HttpPost("kdf")]
|
||||||
public async Task PostKdf([FromBody] KdfRequestModel model)
|
public async Task PostKdf([FromBody] KdfRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -128,6 +128,7 @@ public class DevicesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{identifier}/retrieve-keys")]
|
[HttpPost("{identifier}/retrieve-keys")]
|
||||||
|
[Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")]
|
||||||
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
@ -24,7 +24,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Controllers;
|
namespace Bit.Api.KeyManagement.Controllers;
|
||||||
|
|
||||||
[Route("accounts/key-management")]
|
[Route("accounts")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsKeyManagementController : Controller
|
public class AccountsKeyManagementController : Controller
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ public class AccountsKeyManagementController : Controller
|
|||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("regenerate-keys")]
|
[HttpPost("key-management/regenerate-keys")]
|
||||||
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
@ -93,7 +93,7 @@ public class AccountsKeyManagementController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("rotate-user-account-keys")]
|
[HttpPost("key-management/rotate-user-account-keys")]
|
||||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
@ -133,4 +133,50 @@ public class AccountsKeyManagementController : Controller
|
|||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("set-key-connector-key")]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("convert-to-key-connector")]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync()
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException(ModelState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
public class SetKeyConnectorKeyRequestModel
|
public class SetKeyConnectorKeyRequestModel
|
||||||
{
|
{
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.Models.Response;
|
namespace Bit.Api.Models.Response;
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ public class DeviceResponseModel : ResponseModel
|
|||||||
Identifier = device.Identifier;
|
Identifier = device.Identifier;
|
||||||
CreationDate = device.CreationDate;
|
CreationDate = device.CreationDate;
|
||||||
IsTrusted = device.IsTrusted();
|
IsTrusted = device.IsTrusted();
|
||||||
|
EncryptedUserKey = device.EncryptedUserKey;
|
||||||
|
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@ -29,4 +32,10 @@ public class DeviceResponseModel : ResponseModel
|
|||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public bool IsTrusted { get; set; }
|
public bool IsTrusted { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(2000)]
|
||||||
|
public string EncryptedUserKey { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(2000)]
|
||||||
|
public string EncryptedPublicKey { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -159,13 +159,13 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
|
|
||||||
private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0)
|
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { Seats: > 0, SeatsRequiredToAdd: > 0 })
|
||||||
{
|
{
|
||||||
// When reverting seats, we have to tell payments service that the seats are going back down by what we attempted to add.
|
|
||||||
// However, this might lead to a problem if we don't actually update stripe but throw any ways.
|
|
||||||
// stripe could not be updated, and then we would decrement the number of seats in stripe accidentally.
|
await paymentService.AdjustSeatsAsync(organization,
|
||||||
var seatsToRemove = validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd;
|
validatedResult.Value.InviteOrganization.Plan,
|
||||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, -seatsToRemove);
|
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats.Value);
|
||||||
|
|
||||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats;
|
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats;
|
||||||
|
|
||||||
@ -206,10 +206,26 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
|
|
||||||
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
await SendPasswordManagerMaxSeatLimitEmailsAsync(validatedResult, organization);
|
await NotifyOwnersIfAutoscaleOccursAsync(validatedResult, organization);
|
||||||
|
await NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(validatedResult, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task NotifyOwnersIfAutoscaleOccursAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
|
{
|
||||||
|
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0
|
||||||
|
&& !organization.OwnersNotifiedOfAutoscaling.HasValue)
|
||||||
|
{
|
||||||
|
await mailService.SendOrganizationAutoscaledEmailAsync(
|
||||||
|
organization,
|
||||||
|
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats!.Value,
|
||||||
|
await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization));
|
||||||
|
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = validatedResult.Value.PerformedAt.UtcDateTime;
|
||||||
|
await organizationRepository.UpsertAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
||||||
{
|
{
|
||||||
@ -258,12 +274,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
|
|
||||||
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd <= 0)
|
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { SeatsRequiredToAdd: > 0, UpdatedSeatTotal: > 0 })
|
||||||
{
|
{
|
||||||
return;
|
await paymentService.AdjustSeatsAsync(organization,
|
||||||
}
|
validatedResult.Value.InviteOrganization.Plan,
|
||||||
|
validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal.Value);
|
||||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd);
|
|
||||||
|
|
||||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;
|
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;
|
||||||
|
|
||||||
@ -279,4 +294,5 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats
|
PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models.Api.Response;
|
namespace Bit.Core.Auth.Models.Api.Response;
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
|||||||
Identifier = deviceAuthDetails.Identifier,
|
Identifier = deviceAuthDetails.Identifier,
|
||||||
CreationDate = deviceAuthDetails.CreationDate,
|
CreationDate = deviceAuthDetails.CreationDate,
|
||||||
IsTrusted = deviceAuthDetails.IsTrusted,
|
IsTrusted = deviceAuthDetails.IsTrusted,
|
||||||
|
EncryptedPublicKey = deviceAuthDetails.EncryptedPublicKey,
|
||||||
|
EncryptedUserKey = deviceAuthDetails.EncryptedUserKey
|
||||||
};
|
};
|
||||||
|
|
||||||
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
|
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
|
||||||
@ -39,6 +42,12 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
|||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public bool IsTrusted { get; set; }
|
public bool IsTrusted { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(2000)]
|
||||||
|
public string EncryptedUserKey { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(2000)]
|
||||||
|
public string EncryptedPublicKey { get; set; }
|
||||||
|
|
||||||
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
|
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ public class DeviceAuthDetails : Device
|
|||||||
Identifier = device.Identifier;
|
Identifier = device.Identifier;
|
||||||
CreationDate = device.CreationDate;
|
CreationDate = device.CreationDate;
|
||||||
IsTrusted = device.IsTrusted();
|
IsTrusted = device.IsTrusted();
|
||||||
|
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||||
|
EncryptedUserKey = device.EncryptedUserKey;
|
||||||
AuthRequestId = authRequestId;
|
AuthRequestId = authRequestId;
|
||||||
AuthRequestCreatedAt = authRequestCreationDate;
|
AuthRequestCreatedAt = authRequestCreationDate;
|
||||||
}
|
}
|
||||||
@ -74,6 +76,8 @@ public class DeviceAuthDetails : Device
|
|||||||
EncryptedPrivateKey = encryptedPrivateKey,
|
EncryptedPrivateKey = encryptedPrivateKey,
|
||||||
Active = active
|
Active = active
|
||||||
}.IsTrusted();
|
}.IsTrusted();
|
||||||
|
EncryptedPublicKey = encryptedPublicKey;
|
||||||
|
EncryptedUserKey = encryptedUserKey;
|
||||||
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
|
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
|
||||||
AuthRequestCreatedAt =
|
AuthRequestCreatedAt =
|
||||||
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
|
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
|
||||||
|
@ -112,7 +112,6 @@ public static class FeatureFlagKeys
|
|||||||
/* Auth Team */
|
/* Auth Team */
|
||||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-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 EmailVerification = "email-verification";
|
||||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||||
@ -176,6 +175,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||||
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
||||||
public const string MobileErrorReporting = "mobile-error-reporting";
|
public const string MobileErrorReporting = "mobile-error-reporting";
|
||||||
|
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
||||||
|
|
||||||
/* Platform Team */
|
/* Platform Team */
|
||||||
public const string PersistPopupView = "persist-popup-view";
|
public const string PersistPopupView = "persist-popup-view";
|
||||||
@ -215,9 +215,6 @@ public static class FeatureFlagKeys
|
|||||||
public static Dictionary<string, string> GetLocalOverrideFlagValues()
|
public static Dictionary<string, string> GetLocalOverrideFlagValues()
|
||||||
{
|
{
|
||||||
// place overriding values when needed locally (offline), or return null
|
// place overriding values when needed locally (offline), or return null
|
||||||
return new Dictionary<string, string>()
|
return null;
|
||||||
{
|
|
||||||
{ DuoRedirect, "true" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,24 +14,27 @@ BEGIN
|
|||||||
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||||
|
|
||||||
-- Groups
|
-- Groups
|
||||||
;WITH [AvailableGroupsCTE] AS(
|
-- Delete groups that are no longer in source
|
||||||
SELECT
|
DELETE cg
|
||||||
Id
|
FROM [dbo].[CollectionGroup] cg
|
||||||
FROM
|
LEFT JOIN @Groups g ON cg.GroupId = g.Id
|
||||||
[dbo].[Group]
|
WHERE cg.CollectionId = @Id
|
||||||
WHERE
|
AND g.Id IS NULL;
|
||||||
OrganizationId = @OrganizationId
|
|
||||||
)
|
-- Update existing groups
|
||||||
MERGE
|
UPDATE cg
|
||||||
[dbo].[CollectionGroup] AS [Target]
|
SET cg.ReadOnly = g.ReadOnly,
|
||||||
USING
|
cg.HidePasswords = g.HidePasswords,
|
||||||
@Groups AS [Source]
|
cg.Manage = g.Manage
|
||||||
ON
|
FROM [dbo].[CollectionGroup] cg
|
||||||
[Target].[CollectionId] = @Id
|
INNER JOIN @Groups g ON cg.GroupId = g.Id
|
||||||
AND [Target].[GroupId] = [Source].[Id]
|
WHERE cg.CollectionId = @Id
|
||||||
WHEN NOT MATCHED BY TARGET
|
AND (cg.ReadOnly != g.ReadOnly
|
||||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
OR cg.HidePasswords != g.HidePasswords
|
||||||
INSERT -- Add explicit column list
|
OR cg.Manage != g.Manage);
|
||||||
|
|
||||||
|
-- Insert new groups
|
||||||
|
INSERT INTO [dbo].[CollectionGroup]
|
||||||
(
|
(
|
||||||
[CollectionId],
|
[CollectionId],
|
||||||
[GroupId],
|
[GroupId],
|
||||||
@ -39,46 +42,41 @@ BEGIN
|
|||||||
[HidePasswords],
|
[HidePasswords],
|
||||||
[Manage]
|
[Manage]
|
||||||
)
|
)
|
||||||
VALUES
|
SELECT
|
||||||
(
|
|
||||||
@Id,
|
@Id,
|
||||||
[Source].[Id],
|
g.Id,
|
||||||
[Source].[ReadOnly],
|
g.ReadOnly,
|
||||||
[Source].[HidePasswords],
|
g.HidePasswords,
|
||||||
[Source].[Manage]
|
g.Manage
|
||||||
)
|
FROM @Groups g
|
||||||
WHEN MATCHED AND (
|
INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id
|
||||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
LEFT JOIN [dbo].[CollectionGroup] cg
|
||||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
ON cg.CollectionId = @Id AND cg.GroupId = g.Id
|
||||||
OR [Target].[Manage] != [Source].[Manage]
|
WHERE grp.OrganizationId = @OrganizationId
|
||||||
) THEN
|
AND cg.CollectionId IS NULL;
|
||||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
|
||||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
|
||||||
[Target].[Manage] = [Source].[Manage]
|
|
||||||
WHEN NOT MATCHED BY SOURCE
|
|
||||||
AND [Target].[CollectionId] = @Id THEN
|
|
||||||
DELETE
|
|
||||||
;
|
|
||||||
|
|
||||||
-- Users
|
-- Users
|
||||||
;WITH [AvailableGroupsCTE] AS(
|
-- Delete users that are no longer in source
|
||||||
SELECT
|
DELETE cu
|
||||||
Id
|
FROM [dbo].[CollectionUser] cu
|
||||||
FROM
|
LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||||
[dbo].[OrganizationUser]
|
WHERE cu.CollectionId = @Id
|
||||||
WHERE
|
AND u.Id IS NULL;
|
||||||
OrganizationId = @OrganizationId
|
|
||||||
)
|
-- Update existing users
|
||||||
MERGE
|
UPDATE cu
|
||||||
[dbo].[CollectionUser] AS [Target]
|
SET cu.ReadOnly = u.ReadOnly,
|
||||||
USING
|
cu.HidePasswords = u.HidePasswords,
|
||||||
@Users AS [Source]
|
cu.Manage = u.Manage
|
||||||
ON
|
FROM [dbo].[CollectionUser] cu
|
||||||
[Target].[CollectionId] = @Id
|
INNER JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||||
AND [Target].[OrganizationUserId] = [Source].[Id]
|
WHERE cu.CollectionId = @Id
|
||||||
WHEN NOT MATCHED BY TARGET
|
AND (cu.ReadOnly != u.ReadOnly
|
||||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
OR cu.HidePasswords != u.HidePasswords
|
||||||
INSERT
|
OR cu.Manage != u.Manage);
|
||||||
|
|
||||||
|
-- Insert new users
|
||||||
|
INSERT INTO [dbo].[CollectionUser]
|
||||||
(
|
(
|
||||||
[CollectionId],
|
[CollectionId],
|
||||||
[OrganizationUserId],
|
[OrganizationUserId],
|
||||||
@ -86,26 +84,18 @@ BEGIN
|
|||||||
[HidePasswords],
|
[HidePasswords],
|
||||||
[Manage]
|
[Manage]
|
||||||
)
|
)
|
||||||
VALUES
|
SELECT
|
||||||
(
|
|
||||||
@Id,
|
@Id,
|
||||||
[Source].[Id],
|
u.Id,
|
||||||
[Source].[ReadOnly],
|
u.ReadOnly,
|
||||||
[Source].[HidePasswords],
|
u.HidePasswords,
|
||||||
[Source].[Manage]
|
u.Manage
|
||||||
)
|
FROM @Users u
|
||||||
WHEN MATCHED AND (
|
INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id
|
||||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
LEFT JOIN [dbo].[CollectionUser] cu
|
||||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id
|
||||||
OR [Target].[Manage] != [Source].[Manage]
|
WHERE ou.OrganizationId = @OrganizationId
|
||||||
) THEN
|
AND cu.CollectionId IS NULL;
|
||||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
|
||||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
|
||||||
[Target].[Manage] = [Source].[Manage]
|
|
||||||
WHEN NOT MATCHED BY SOURCE
|
|
||||||
AND [Target].[CollectionId] = @Id THEN
|
|
||||||
DELETE
|
|
||||||
;
|
|
||||||
|
|
||||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||||
END
|
END
|
||||||
|
@ -9,3 +9,9 @@
|
|||||||
CONSTRAINT [FK_CollectionGroup_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE
|
CONSTRAINT [FK_CollectionGroup_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GO
|
||||||
|
CREATE NONCLUSTERED INDEX IX_CollectionGroup_GroupId
|
||||||
|
ON [dbo].[CollectionGroup] (GroupId)
|
||||||
|
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||||
|
|
||||||
|
GO
|
||||||
|
@ -9,3 +9,9 @@
|
|||||||
CONSTRAINT [FK_CollectionUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])
|
CONSTRAINT [FK_CollectionUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GO
|
||||||
|
CREATE NONCLUSTERED INDEX IX_CollectionUser_OrganizationUserId
|
||||||
|
ON [dbo].[CollectionUser] (OrganizationUserId)
|
||||||
|
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||||
|
|
||||||
|
GO
|
||||||
|
@ -59,7 +59,8 @@ public static class OrganizationTestHelpers
|
|||||||
string userEmail,
|
string userEmail,
|
||||||
OrganizationUserType type,
|
OrganizationUserType type,
|
||||||
bool accessSecretsManager = false,
|
bool accessSecretsManager = false,
|
||||||
Permissions? permissions = null
|
Permissions? permissions = null,
|
||||||
|
OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
var userRepository = factory.GetService<IUserRepository>();
|
var userRepository = factory.GetService<IUserRepository>();
|
||||||
@ -74,7 +75,7 @@ public static class OrganizationTestHelpers
|
|||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Key = null,
|
Key = null,
|
||||||
Type = type,
|
Type = type,
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
Status = userStatusType,
|
||||||
ExternalId = null,
|
ExternalId = null,
|
||||||
AccessSecretsManager = accessSecretsManager,
|
AccessSecretsManager = accessSecretsManager,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
#nullable enable
|
||||||
|
using System.Net;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.KeyManagement.Models.Requests;
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
@ -7,6 +8,7 @@ using Bit.Api.Vault.Models;
|
|||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -31,6 +33,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private string _ownerEmail = null!;
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||||
@ -45,6 +48,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
||||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||||
|
_organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
@ -174,7 +178,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(RotateUserAccountKeysAndDataRequestModel request)
|
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(
|
||||||
|
RotateUserAccountKeysAndDataRequestModel request)
|
||||||
{
|
{
|
||||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||||
|
|
||||||
@ -256,4 +261,97 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_NotLoggedIn_Unauthorized(SetKeyConnectorKeyRequestModel request)
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
|
||||||
|
SetKeyConnectorKeyRequestModel request)
|
||||||
|
{
|
||||||
|
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||||
|
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||||
|
paymentMethod: PaymentMethodType.Card);
|
||||||
|
organization.UseKeyConnector = true;
|
||||||
|
organization.UseSso = true;
|
||||||
|
organization.Identifier = organizationSsoIdentifier;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||||
|
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||||
|
|
||||||
|
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||||
|
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);
|
||||||
|
|
||||||
|
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(ssoUser);
|
||||||
|
|
||||||
|
request.Keys = new KeysRequestModel
|
||||||
|
{
|
||||||
|
PublicKey = ssoUser.PublicKey,
|
||||||
|
EncryptedPrivateKey = ssoUser.PrivateKey
|
||||||
|
};
|
||||||
|
request.Key = _mockEncryptedString;
|
||||||
|
request.OrgIdentifier = organizationSsoIdentifier;
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(request.Key, user.Key);
|
||||||
|
Assert.True(user.UsesKeyConnector);
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
var ssoOrganizationUser =
|
||||||
|
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||||
|
Assert.NotNull(ssoOrganizationUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||||
|
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||||
|
Assert.Null(ssoOrganizationUser.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_NotLoggedIn_Unauthorized()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_Success()
|
||||||
|
{
|
||||||
|
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||||
|
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||||
|
paymentMethod: PaymentMethodType.Card);
|
||||||
|
organization.UseKeyConnector = true;
|
||||||
|
organization.UseSso = true;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||||
|
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||||
|
|
||||||
|
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||||
|
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { });
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Null(user.MasterPassword);
|
||||||
|
Assert.True(user.UsesKeyConnector);
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,9 @@ public class DevicesControllerTest
|
|||||||
UserId = userId,
|
UserId = userId,
|
||||||
Name = "chrome",
|
Name = "chrome",
|
||||||
Type = DeviceType.ChromeBrowser,
|
Type = DeviceType.ChromeBrowser,
|
||||||
Identifier = Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString()
|
Identifier = Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString(),
|
||||||
|
EncryptedPublicKey = "PublicKey",
|
||||||
|
EncryptedUserKey = "UserKey",
|
||||||
},
|
},
|
||||||
Guid.Parse("E09D6943-D574-49E5-AC85-C3F12B4E019E"),
|
Guid.Parse("E09D6943-D574-49E5-AC85-C3F12B4E019E"),
|
||||||
authDateTimeResponse)
|
authDateTimeResponse)
|
||||||
@ -78,6 +80,13 @@ public class DevicesControllerTest
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.IsType<ListResponseModel<DeviceAuthRequestResponseModel>>(result);
|
Assert.IsType<ListResponseModel<DeviceAuthRequestResponseModel>>(result);
|
||||||
|
var resultDevice = result.Data.First();
|
||||||
|
Assert.Equal("chrome", resultDevice.Name);
|
||||||
|
Assert.Equal(DeviceType.ChromeBrowser, resultDevice.Type);
|
||||||
|
Assert.Equal(Guid.Parse("B3136B10-7818-444F-B05B-4D7A9B8C48BF"), resultDevice.Id);
|
||||||
|
Assert.Equal(Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString(), resultDevice.Identifier);
|
||||||
|
Assert.Equal("PublicKey", resultDevice.EncryptedPublicKey);
|
||||||
|
Assert.Equal("UserKey", resultDevice.EncryptedUserKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -178,4 +178,133 @@ public class AccountsKeyManagementControllerTests
|
|||||||
Assert.NotEmpty(ex.ModelState.Values);
|
Assert.NotEmpty(ex.ModelState.Values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||||
|
{
|
||||||
|
expectedUser.PublicKey = null;
|
||||||
|
expectedUser.PrivateKey = null;
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(IdentityResult.Failed(new IdentityError { Description = "set key connector key error" }));
|
||||||
|
|
||||||
|
var badRequestException =
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||||
|
|
||||||
|
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||||
|
Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedUser.Id, user.Id);
|
||||||
|
Assert.Equal(data.Key, user.Key);
|
||||||
|
Assert.Equal(data.Kdf, user.Kdf);
|
||||||
|
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||||
|
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||||
|
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||||
|
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||||
|
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||||
|
{
|
||||||
|
expectedUser.PublicKey = null;
|
||||||
|
expectedUser.PrivateKey = null;
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(data);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedUser.Id, user.Id);
|
||||||
|
Assert.Equal(data.Key, user.Key);
|
||||||
|
Assert.Equal(data.Kdf, user.Kdf);
|
||||||
|
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||||
|
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||||
|
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||||
|
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||||
|
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
User expectedUser)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>())
|
||||||
|
.Returns(IdentityResult.Failed(new IdentityError { Description = "convert to key connector error" }));
|
||||||
|
|
||||||
|
var badRequestException =
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
|
||||||
|
|
||||||
|
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||||
|
Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
User expectedUser)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>())
|
||||||
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
await sutProvider.Sut.PostConvertToKeyConnectorAsync();
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,6 +287,77 @@ public class InviteOrganizationUserCommandTests
|
|||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenValidInviteCausesOrgToAutoscale_ThenOrganizationOwnersShouldBeNotified(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = null;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.Received(1)
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(organization,
|
||||||
|
inviteOrganization.Seats.Value,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
||||||
@ -349,7 +420,7 @@ public class InviteOrganizationUserCommandTests
|
|||||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPaymentService>()
|
await sutProvider.GetDependency<IPaymentService>()
|
||||||
.AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.SeatsRequiredToAdd);
|
.AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.UpdatedSeatTotal!.Value);
|
||||||
|
|
||||||
await orgRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal));
|
await orgRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal));
|
||||||
|
|
||||||
@ -610,4 +681,237 @@ public class InviteOrganizationUserCommandTests
|
|||||||
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationIsManagedByAProviderAndAutoscaleOccurs_ThenAnEmailShouldBeSentToTheProvider(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.SmSeats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.MaxAutoscaleSmSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = null;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
providerOrganization.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites: [
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)
|
||||||
|
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionUpdate =
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
orgRepository.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)
|
||||||
|
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderOrganizationRepository>()
|
||||||
|
.GetByOrganizationId(organization.Id)
|
||||||
|
.Returns(providerOrganization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed)
|
||||||
|
.Returns(new List<ProviderUserUserDetails>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Email = "provider@email.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(organization, 1,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationAutoscalesButOwnersHaveAlreadyBeenNotified_ThenAnEmailShouldNotBeSent(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||||
|
Arg.Any<int>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationDoesNotAutoScale_ThenAnEmailShouldNotBeSent(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 2;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||||
|
Arg.Any<int>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -599,5 +599,11 @@ public class CollectionRepositoryTests
|
|||||||
Assert.True(actualOrgUser3.Manage);
|
Assert.True(actualOrgUser3.Manage);
|
||||||
Assert.False(actualOrgUser3.HidePasswords);
|
Assert.False(actualOrgUser3.HidePasswords);
|
||||||
Assert.True(actualOrgUser3.ReadOnly);
|
Assert.True(actualOrgUser3.ReadOnly);
|
||||||
|
|
||||||
|
// Clean up data
|
||||||
|
await userRepository.DeleteAsync(user);
|
||||||
|
await organizationRepository.DeleteAsync(organization);
|
||||||
|
await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]);
|
||||||
|
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithGroupsAndUsers]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Name VARCHAR(MAX),
|
||||||
|
@ExternalId NVARCHAR(300),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||||
|
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||||
|
|
||||||
|
-- Groups
|
||||||
|
-- Delete groups that are no longer in source
|
||||||
|
DELETE cg
|
||||||
|
FROM [dbo].[CollectionGroup] cg
|
||||||
|
LEFT JOIN @Groups g ON cg.GroupId = g.Id
|
||||||
|
WHERE cg.CollectionId = @Id
|
||||||
|
AND g.Id IS NULL;
|
||||||
|
|
||||||
|
-- Update existing groups
|
||||||
|
UPDATE cg
|
||||||
|
SET cg.ReadOnly = g.ReadOnly,
|
||||||
|
cg.HidePasswords = g.HidePasswords,
|
||||||
|
cg.Manage = g.Manage
|
||||||
|
FROM [dbo].[CollectionGroup] cg
|
||||||
|
INNER JOIN @Groups g ON cg.GroupId = g.Id
|
||||||
|
WHERE cg.CollectionId = @Id
|
||||||
|
AND (cg.ReadOnly != g.ReadOnly
|
||||||
|
OR cg.HidePasswords != g.HidePasswords
|
||||||
|
OR cg.Manage != g.Manage);
|
||||||
|
|
||||||
|
-- Insert new groups
|
||||||
|
INSERT INTO [dbo].[CollectionGroup]
|
||||||
|
(
|
||||||
|
[CollectionId],
|
||||||
|
[GroupId],
|
||||||
|
[ReadOnly],
|
||||||
|
[HidePasswords],
|
||||||
|
[Manage]
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@Id,
|
||||||
|
g.Id,
|
||||||
|
g.ReadOnly,
|
||||||
|
g.HidePasswords,
|
||||||
|
g.Manage
|
||||||
|
FROM @Groups g
|
||||||
|
INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id
|
||||||
|
LEFT JOIN [dbo].[CollectionGroup] cg
|
||||||
|
ON cg.CollectionId = @Id AND cg.GroupId = g.Id
|
||||||
|
WHERE grp.OrganizationId = @OrganizationId
|
||||||
|
AND cg.CollectionId IS NULL;
|
||||||
|
|
||||||
|
-- Users
|
||||||
|
-- Delete users that are no longer in source
|
||||||
|
DELETE cu
|
||||||
|
FROM [dbo].[CollectionUser] cu
|
||||||
|
LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||||
|
WHERE cu.CollectionId = @Id
|
||||||
|
AND u.Id IS NULL;
|
||||||
|
|
||||||
|
-- Update existing users
|
||||||
|
UPDATE cu
|
||||||
|
SET cu.ReadOnly = u.ReadOnly,
|
||||||
|
cu.HidePasswords = u.HidePasswords,
|
||||||
|
cu.Manage = u.Manage
|
||||||
|
FROM [dbo].[CollectionUser] cu
|
||||||
|
INNER JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||||
|
WHERE cu.CollectionId = @Id
|
||||||
|
AND (cu.ReadOnly != u.ReadOnly
|
||||||
|
OR cu.HidePasswords != u.HidePasswords
|
||||||
|
OR cu.Manage != u.Manage);
|
||||||
|
|
||||||
|
-- Insert new users
|
||||||
|
INSERT INTO [dbo].[CollectionUser]
|
||||||
|
(
|
||||||
|
[CollectionId],
|
||||||
|
[OrganizationUserId],
|
||||||
|
[ReadOnly],
|
||||||
|
[HidePasswords],
|
||||||
|
[Manage]
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@Id,
|
||||||
|
u.Id,
|
||||||
|
u.ReadOnly,
|
||||||
|
u.HidePasswords,
|
||||||
|
u.Manage
|
||||||
|
FROM @Users u
|
||||||
|
INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id
|
||||||
|
LEFT JOIN [dbo].[CollectionUser] cu
|
||||||
|
ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id
|
||||||
|
WHERE ou.OrganizationId = @OrganizationId
|
||||||
|
AND cu.CollectionId IS NULL;
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_CollectionGroup_GroupId')
|
||||||
|
BEGIN
|
||||||
|
CREATE NONCLUSTERED INDEX IX_CollectionGroup_GroupId
|
||||||
|
ON [dbo].[CollectionGroup] (GroupId)
|
||||||
|
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_CollectionUser_OrganizationUserId')
|
||||||
|
BEGIN
|
||||||
|
CREATE NONCLUSTERED INDEX IX_CollectionUser_OrganizationUserId
|
||||||
|
ON [dbo].[CollectionUser] (OrganizationUserId)
|
||||||
|
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user