mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00
Merge branch 'main' into PM-19147_2
This commit is contained in:
commit
9cabf31cbe
3
.github/workflows/test-database.yml
vendored
3
.github/workflows/test-database.yml
vendored
@ -32,7 +32,6 @@ on:
|
||||
- "src/**/Entities/**/*.cs" # Database entity definitions
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
@ -148,7 +147,7 @@ jobs:
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@ -13,7 +13,6 @@ env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
@ -50,7 +49,7 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
|
@ -630,6 +630,19 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
Provider provider,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
TaxInformation taxInformation)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
|
||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
{
|
||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -42,6 +43,7 @@ public class ProvidersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
@ -60,7 +62,8 @@ public class ProvidersController : Controller
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -75,6 +78,7 @@ public class ProvidersController : Controller
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
@ -415,7 +419,9 @@ public class ProvidersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return View(new OrganizationEditModel(provider));
|
||||
var plans = await _pricingClient.ListPlans();
|
||||
|
||||
return View(new OrganizationEditModel(provider, plans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -22,13 +22,14 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
public OrganizationEditModel(Provider provider, List<Plan> plans)
|
||||
{
|
||||
Provider = provider;
|
||||
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
|
||||
PlanType = Core.Billing.Enums.PlanType.TeamsMonthly;
|
||||
Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
|
||||
LicenseKey = RandomLicenseKey;
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
public OrganizationEditModel(
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ProviderBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IPricingClient pricingClient,
|
||||
IProviderBillingService providerBillingService,
|
||||
@ -71,6 +73,65 @@ public class ProviderBillingController(
|
||||
"text/csv");
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
|
||||
|
||||
if (!allowProviderPaymentMethod)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||
|
||||
await providerBillingService.UpdatePaymentMethod(
|
||||
provider,
|
||||
tokenizedPaymentSource,
|
||||
taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
|
||||
|
||||
if (!allowProviderPaymentMethod)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
||||
{
|
||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpGet("subscription")]
|
||||
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
@ -102,12 +163,32 @@ public class ProviderBillingController(
|
||||
|
||||
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
|
||||
|
||||
var paymentSource = await subscriberService.GetPaymentSource(provider);
|
||||
|
||||
var response = ProviderSubscriptionResponse.From(
|
||||
subscription,
|
||||
configuredProviderPlans,
|
||||
taxInformation,
|
||||
subscriptionSuspension,
|
||||
provider);
|
||||
provider,
|
||||
paymentSource);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var taxInformation = await subscriberService.GetTaxInformation(provider);
|
||||
|
||||
var response = TaxInformationResponse.From(taxInformation);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ public record ProviderSubscriptionResponse(
|
||||
TaxInformation TaxInformation,
|
||||
DateTime? CancelAt,
|
||||
SubscriptionSuspension Suspension,
|
||||
ProviderType ProviderType)
|
||||
ProviderType ProviderType,
|
||||
PaymentSource PaymentSource)
|
||||
{
|
||||
private const string _annualCadence = "Annual";
|
||||
private const string _monthlyCadence = "Monthly";
|
||||
@ -26,7 +27,8 @@ public record ProviderSubscriptionResponse(
|
||||
ICollection<ConfiguredProviderPlan> providerPlans,
|
||||
TaxInformation taxInformation,
|
||||
SubscriptionSuspension subscriptionSuspension,
|
||||
Provider provider)
|
||||
Provider provider,
|
||||
PaymentSource paymentSource)
|
||||
{
|
||||
var providerPlanResponses = providerPlans
|
||||
.Select(providerPlan =>
|
||||
@ -57,7 +59,8 @@ public record ProviderSubscriptionResponse(
|
||||
taxInformation,
|
||||
subscription.CancelAt,
|
||||
subscriptionSuspension,
|
||||
provider.Type);
|
||||
provider.Type,
|
||||
paymentSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ public static class CommandResultExtensions
|
||||
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
||||
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||
Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||
Success<T> success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK },
|
||||
Success<T> success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK },
|
||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
||||
};
|
||||
}
|
||||
|
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record Error<T>(string Message, T ErroredValue);
|
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Insufficient Permissions";
|
||||
|
||||
public InsufficientPermissionsError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Record Not Found";
|
||||
|
||||
public RecordNotFoundError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public interface IValidator<T>
|
||||
{
|
||||
public Task<ValidationResult<T>> ValidateAsync(T value);
|
||||
}
|
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public abstract record ValidationResult<T>;
|
||||
|
||||
public record Valid<T> : ValidationResult<T>
|
||||
{
|
||||
public T Value { get; init; }
|
||||
}
|
||||
|
||||
public record Invalid<T> : ValidationResult<T>
|
||||
{
|
||||
public IEnumerable<Error<T>> Errors { get; init; }
|
||||
}
|
@ -95,5 +95,16 @@ public interface IProviderBillingService
|
||||
Task<Subscription> SetupSubscription(
|
||||
Provider provider);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <paramref name="provider"/>'s payment source and tax information and then sets their subscription's collection_method to be "charge_automatically".
|
||||
/// </summary>
|
||||
/// <param name="provider">The <paramref name="provider"/> to update the payment source and tax information for.</param>
|
||||
/// <param name="tokenizedPaymentSource">The tokenized payment source (ex. Credit Card) to attach to the <paramref name="provider"/>.</param>
|
||||
/// <param name="taxInformation">The <paramref name="provider"/>'s updated tax information.</param>
|
||||
Task UpdatePaymentMethod(
|
||||
Provider provider,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
TaxInformation taxInformation);
|
||||
|
||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||
}
|
||||
|
@ -115,6 +115,15 @@ public static class FeatureFlagKeys
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
@ -122,9 +131,7 @@ public static class FeatureFlagKeys
|
||||
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 RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
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";
|
||||
@ -149,11 +156,7 @@ public static class FeatureFlagKeys
|
||||
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 NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
@ -172,6 +175,8 @@ public static class FeatureFlagKeys
|
||||
public const string WebPush = "web-push";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
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 static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -36,7 +36,7 @@
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.10.0" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.1" />
|
||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||
|
@ -7,7 +7,7 @@
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
To leave an organization, first log into the <a href="https://vault.bitwarden.com/#/login">web app</a>, select the three dot menu next to the organization name, and select Leave.
|
||||
To leave an organization, first log into the <a href="{{{WebVaultUrl}}}/login">web app</a>, select the three dot menu next to the organization name, and select Leave.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can rejoin {{OrganizationName}}, you must first leave all other organizations.
|
||||
|
||||
To leave an organization, first log in the web app (https://vault.bitwarden.com/#/login), select the three dot menu next to the organization name, and select Leave.
|
||||
To leave an organization, first log in the web app ({{{WebVaultUrl}}}/login), select the three dot menu next to the organization name, and select Leave.
|
||||
{{/BasicTextLayout}}
|
||||
|
@ -26,7 +26,7 @@
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
<a href="{{{VaultSubscriptionUrl}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
Manage subscription
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
|
@ -24,7 +24,7 @@
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
<a href="{{{VaultSubscriptionUrl}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
Manage subscription
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
|
@ -24,7 +24,7 @@
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
<a href="{{{VaultSubscriptionUrl}}}" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
Manage subscription
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
|
@ -24,7 +24,7 @@
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
<a href="{{{VaultSubscriptionUrl}}}" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; 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;">
|
||||
Manage subscription
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
|
@ -1,5 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.Models.Commands;
|
||||
|
||||
public class CommandResult(IEnumerable<string> errors)
|
||||
@ -9,7 +11,6 @@ public class CommandResult(IEnumerable<string> errors)
|
||||
public bool Success => ErrorMessages.Count == 0;
|
||||
public bool HasErrors => ErrorMessages.Count > 0;
|
||||
public List<string> ErrorMessages { get; } = errors.ToList();
|
||||
|
||||
public CommandResult() : this(Array.Empty<string>()) { }
|
||||
}
|
||||
|
||||
@ -29,22 +30,30 @@ public class Success : CommandResult
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CommandResult<T>
|
||||
{
|
||||
public abstract class CommandResult<T>;
|
||||
|
||||
public class Success<T>(T value) : CommandResult<T>
|
||||
{
|
||||
public T Value { get; } = value;
|
||||
}
|
||||
|
||||
public class Success<T>(T data) : CommandResult<T>
|
||||
public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
||||
{
|
||||
public T? Data { get; init; } = data;
|
||||
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
||||
|
||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
||||
|
||||
public Failure(string error) : this([error]) { }
|
||||
}
|
||||
|
||||
public class Failure<T>(IEnumerable<string> errorMessage) : CommandResult<T>
|
||||
public class Partial<T> : CommandResult<T>
|
||||
{
|
||||
public IEnumerable<string> ErrorMessages { get; init; } = errorMessage;
|
||||
public T[] Successes { get; set; } = [];
|
||||
public Error<T>[] Failures { get; set; } = [];
|
||||
|
||||
public Failure(string errorMessage) : this(new[] { errorMessage })
|
||||
public Partial(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
|
||||
{
|
||||
Successes = successfulItems.ToArray();
|
||||
Failures = failedItems.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
public class OrganizationSeatsAutoscaledViewModel : BaseMailModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public int InitialSeatCount { get; set; }
|
||||
public int CurrentSeatCount { get; set; }
|
||||
public string VaultSubscriptionUrl { get; set; }
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
public class OrganizationSeatsMaxReachedViewModel : BaseMailModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public int MaxSeatCount { get; set; }
|
||||
public string VaultSubscriptionUrl { get; set; }
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
public class OrganizationServiceAccountsMaxReachedViewModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public int MaxServiceAccountsCount { get; set; }
|
||||
public string VaultSubscriptionUrl { get; set; }
|
||||
}
|
||||
|
@ -214,9 +214,9 @@ public class HandlebarsMailService : IMailService
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails);
|
||||
var model = new OrganizationSeatsAutoscaledViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
InitialSeatCount = initialSeatCount,
|
||||
CurrentSeatCount = organization.Seats.Value,
|
||||
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
|
||||
};
|
||||
|
||||
await AddMessageContentAsync(message, "OrganizationSeatsAutoscaled", model);
|
||||
@ -229,8 +229,8 @@ public class HandlebarsMailService : IMailService
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails);
|
||||
var model = new OrganizationSeatsMaxReachedViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
MaxSeatCount = maxSeatCount,
|
||||
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
|
||||
};
|
||||
|
||||
await AddMessageContentAsync(message, "OrganizationSeatsMaxReached", model);
|
||||
@ -1103,8 +1103,8 @@ public class HandlebarsMailService : IMailService
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Seat Limit Reached", ownerEmails);
|
||||
var model = new OrganizationSeatsMaxReachedViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
MaxSeatCount = maxSeatCount,
|
||||
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
|
||||
};
|
||||
|
||||
await AddMessageContentAsync(message, "OrganizationSmSeatsMaxReached", model);
|
||||
@ -1118,8 +1118,8 @@ public class HandlebarsMailService : IMailService
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
|
||||
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
MaxServiceAccountsCount = maxSeatCount,
|
||||
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
|
||||
};
|
||||
|
||||
await AddMessageContentAsync(message, "OrganizationSmServiceAccountsMaxReached", model);
|
||||
@ -1223,4 +1223,11 @@ public class HandlebarsMailService : IMailService
|
||||
{
|
||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||
}
|
||||
|
||||
private string GetCloudVaultSubscriptionUrl(Guid organizationId)
|
||||
=> _globalSettings.BaseServiceUri.CloudRegion?.ToLower() switch
|
||||
{
|
||||
"eu" => $"https://vault.bitwarden.eu/#/organizations/{organizationId}/billing/subscription",
|
||||
_ => $"https://vault.bitwarden.com/#/organizations/{organizationId}/billing/subscription"
|
||||
};
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.2" />
|
||||
<PackageReference Include="AngleSharp" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -250,21 +250,26 @@ public class DeviceValidator(
|
||||
var customResponse = new Dictionary<string, object>();
|
||||
switch (errorType)
|
||||
{
|
||||
/*
|
||||
* The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well.
|
||||
* There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards
|
||||
* compatible.
|
||||
*/
|
||||
case DeviceValidationResultType.InvalidUser:
|
||||
result.ErrorDescription = "Invalid user";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid user."));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user"));
|
||||
break;
|
||||
case DeviceValidationResultType.InvalidNewDeviceOtp:
|
||||
result.ErrorDescription = "Invalid New Device OTP";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid new device OTP. Try again."));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("invalid new device otp"));
|
||||
break;
|
||||
case DeviceValidationResultType.NewDeviceVerificationRequired:
|
||||
result.ErrorDescription = "New device verification required";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("New device verification required."));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("new device verification required"));
|
||||
break;
|
||||
case DeviceValidationResultType.NoDeviceInformationProvided:
|
||||
result.ErrorDescription = "No device information provided";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("No device information provided."));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided"));
|
||||
break;
|
||||
}
|
||||
return (result, customResponse);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
@ -155,13 +154,10 @@ public class TwoFactorAuthenticationValidator(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin))
|
||||
{
|
||||
if (type is TwoFactorProviderType.RecoveryCode)
|
||||
{
|
||||
return await _userService.RecoverTwoFactorAsync(user, token);
|
||||
}
|
||||
}
|
||||
|
||||
// These cases we want to always return false, U2f is deprecated and OrganizationDuo
|
||||
// uses a different flow than the other two factor providers, it follows the same
|
||||
|
@ -563,8 +563,8 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
"[dbo].[OrganizationUser_SetStatusForUsersById]",
|
||||
new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked },
|
||||
"[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
|
||||
new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(), Status = OrganizationUserStatusType.Revoked },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]
|
||||
@OrganizationUserIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@Status SMALLINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE OU
|
||||
SET OU.[Status] = @Status
|
||||
FROM [dbo].[OrganizationUser] OU
|
||||
INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds
|
||||
END
|
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Shared;
|
||||
|
||||
public class IValidatorTests
|
||||
{
|
||||
public class TestClass
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)
|
||||
{
|
||||
public const string Code = "InvalidRequest";
|
||||
}
|
||||
|
||||
public class TestClassValidator : IValidator<TestClass>
|
||||
{
|
||||
public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value.Name))
|
||||
{
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Invalid<TestClass>
|
||||
{
|
||||
Errors = [new InvalidRequestError<TestClass>(value)]
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass> { Value = value });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()
|
||||
{
|
||||
var example = new TestClass();
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Invalid<TestClass>>(result);
|
||||
var invalidResult = result as Invalid<TestClass>;
|
||||
Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult.Errors.First().Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenIsValid_ReturnsValid()
|
||||
{
|
||||
var example = new TestClass { Name = "Valid" };
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Valid<TestClass>>(result);
|
||||
var validResult = result as Valid<TestClass>;
|
||||
Assert.Equal(example.Name, validResult.Value.Name);
|
||||
}
|
||||
}
|
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Commands;
|
||||
|
||||
public class CommandResultTests
|
||||
{
|
||||
public class TestItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)
|
||||
{
|
||||
var itemList = items.ToList();
|
||||
var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray();
|
||||
|
||||
var failedItems = itemList.Except(successfulItems).ToArray();
|
||||
|
||||
var notFound = failedItems.First(x => x.Value == "Failed due to not found");
|
||||
var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions");
|
||||
|
||||
var notFoundError = new RecordNotFoundError<TestItem>(notFound);
|
||||
var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);
|
||||
|
||||
return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)
|
||||
{
|
||||
var listOfRecords = new List<TestItem>
|
||||
{
|
||||
new TestItem() { Id = successId1, Value = "SuccessfulRequest" },
|
||||
new TestItem() { Id = failureId1, Value = "Failed due to not found" },
|
||||
new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" }
|
||||
};
|
||||
|
||||
var result = BulkAction(listOfRecords);
|
||||
|
||||
Assert.IsType<Partial<TestItem>>(result);
|
||||
|
||||
var failures = (result as Partial<TestItem>).Failures.ToArray();
|
||||
var success = (result as Partial<TestItem>).Successes.First();
|
||||
|
||||
Assert.Equal(listOfRecords.First(), success);
|
||||
Assert.Equal(2, failures.Length);
|
||||
}
|
||||
}
|
@ -172,7 +172,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorModel = new ErrorResponseModel("No device information provided.");
|
||||
var expectedErrorModel = new ErrorResponseModel("no device information provided");
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorModel.Message, actualResponse.Message);
|
||||
}
|
||||
@ -418,7 +418,7 @@ public class DeviceValidatorTests
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
// PM-13340: The error message should be "invalid user" instead of "no device information provided"
|
||||
var expectedErrorMessage = "No device information provided.";
|
||||
var expectedErrorMessage = "no device information provided";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
@ -552,7 +552,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "Invalid new device OTP. Try again.";
|
||||
var expectedErrorMessage = "invalid new device otp";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
@ -604,7 +604,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "New device verification required.";
|
||||
var expectedErrorMessage = "new device verification required";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -464,7 +463,6 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
user.TwoFactorRecoveryCode = token;
|
||||
|
||||
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactorAsync(
|
||||
@ -486,7 +484,6 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
user.TwoFactorRecoveryCode = token;
|
||||
|
||||
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactorAsync(
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM bitwarden/server:latest
|
||||
FROM ghcr.io/bitwarden/server
|
||||
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]
|
||||
@OrganizationUserIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@Status SMALLINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE OU
|
||||
SET OU.[Status] = @Status
|
||||
FROM [dbo].[OrganizationUser] OU
|
||||
INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds
|
||||
END
|
||||
GO
|
Loading…
x
Reference in New Issue
Block a user