1
0
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:
Jonas Hendrickx 2025-03-19 18:16:27 +01:00 committed by GitHub
commit 9cabf31cbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 381 additions and 63 deletions

View File

@ -32,7 +32,6 @@ on:
- "src/**/Entities/**/*.cs" # Database entity definitions - "src/**/Entities/**/*.cs" # Database entity definitions
jobs: jobs:
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -148,7 +147,7 @@ jobs:
run: 'docker logs $(docker ps --quiet --filter "name=mssql")' run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
- name: Report test results - 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() }} if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with: with:
name: Test Results name: Test Results

View File

@ -13,7 +13,6 @@ env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io" _AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs: jobs:
testing: testing:
name: Run tests name: Run tests
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }} 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" 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 - 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() }} if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with: with:
name: Test Results name: Test Results

View File

@ -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) public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
{ {
if (command.Configuration.Any(x => x.SeatsMinimum < 0)) if (command.Configuration.Any(x => x.SeatsMinimum < 0))

View File

@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Contracts;
@ -42,6 +43,7 @@ public class ProvidersController : Controller
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IProviderPlanRepository _providerPlanRepository; private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
private readonly string _stripeUrl; private readonly string _stripeUrl;
private readonly string _braintreeMerchantUrl; private readonly string _braintreeMerchantUrl;
private readonly string _braintreeMerchantId; private readonly string _braintreeMerchantId;
@ -60,7 +62,8 @@ public class ProvidersController : Controller
IFeatureService featureService, IFeatureService featureService,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IWebHostEnvironment webHostEnvironment) IWebHostEnvironment webHostEnvironment,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService; _organizationService = organizationService;
@ -75,6 +78,7 @@ public class ProvidersController : Controller
_featureService = featureService; _featureService = featureService;
_providerPlanRepository = providerPlanRepository; _providerPlanRepository = providerPlanRepository;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_pricingClient = pricingClient;
_stripeUrl = webHostEnvironment.GetStripeUrl(); _stripeUrl = webHostEnvironment.GetStripeUrl();
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
_braintreeMerchantId = globalSettings.Braintree.MerchantId; _braintreeMerchantId = globalSettings.Braintree.MerchantId;
@ -415,7 +419,9 @@ public class ProvidersController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
return View(new OrganizationEditModel(provider)); var plans = await _pricingClient.ListPlans();
return View(new OrganizationEditModel(provider, plans));
} }
[HttpPost] [HttpPost]

View File

@ -22,13 +22,14 @@ public class OrganizationEditModel : OrganizationViewModel
public OrganizationEditModel() { } public OrganizationEditModel() { }
public OrganizationEditModel(Provider provider) public OrganizationEditModel(Provider provider, List<Plan> plans)
{ {
Provider = provider; Provider = provider;
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty; BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
PlanType = Core.Billing.Enums.PlanType.TeamsMonthly; PlanType = Core.Billing.Enums.PlanType.TeamsMonthly;
Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName(); Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
LicenseKey = RandomLicenseKey; LicenseKey = RandomLicenseKey;
_plans = plans;
} }
public OrganizationEditModel( public OrganizationEditModel(

View File

@ -1,5 +1,6 @@
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class ProviderBillingController( public class ProviderBillingController(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService,
ILogger<BaseProviderController> logger, ILogger<BaseProviderController> logger,
IPricingClient pricingClient, IPricingClient pricingClient,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
@ -71,6 +73,65 @@ public class ProviderBillingController(
"text/csv"); "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")] [HttpGet("subscription")]
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId) public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
{ {
@ -102,12 +163,32 @@ public class ProviderBillingController(
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription); var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
var paymentSource = await subscriberService.GetPaymentSource(provider);
var response = ProviderSubscriptionResponse.From( var response = ProviderSubscriptionResponse.From(
subscription, subscription,
configuredProviderPlans, configuredProviderPlans,
taxInformation, taxInformation,
subscriptionSuspension, 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); return TypedResults.Ok(response);
} }

View File

@ -16,7 +16,8 @@ public record ProviderSubscriptionResponse(
TaxInformation TaxInformation, TaxInformation TaxInformation,
DateTime? CancelAt, DateTime? CancelAt,
SubscriptionSuspension Suspension, SubscriptionSuspension Suspension,
ProviderType ProviderType) ProviderType ProviderType,
PaymentSource PaymentSource)
{ {
private const string _annualCadence = "Annual"; private const string _annualCadence = "Annual";
private const string _monthlyCadence = "Monthly"; private const string _monthlyCadence = "Monthly";
@ -26,7 +27,8 @@ public record ProviderSubscriptionResponse(
ICollection<ConfiguredProviderPlan> providerPlans, ICollection<ConfiguredProviderPlan> providerPlans,
TaxInformation taxInformation, TaxInformation taxInformation,
SubscriptionSuspension subscriptionSuspension, SubscriptionSuspension subscriptionSuspension,
Provider provider) Provider provider,
PaymentSource paymentSource)
{ {
var providerPlanResponses = providerPlans var providerPlanResponses = providerPlans
.Select(providerPlan => .Select(providerPlan =>
@ -57,7 +59,8 @@ public record ProviderSubscriptionResponse(
taxInformation, taxInformation,
subscription.CancelAt, subscription.CancelAt,
subscriptionSuspension, subscriptionSuspension,
provider.Type); provider.Type,
paymentSource);
} }
} }

View File

@ -12,7 +12,7 @@ public static class CommandResultExtensions
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
Failure<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}") _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
}; };
} }

View File

@ -0,0 +1,3 @@
namespace Bit.Core.AdminConsole.Errors;
public record Error<T>(string Message, T ErroredValue);

View 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)
{
}
}

View 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)
{
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.AdminConsole.Shared.Validation;
public interface IValidator<T>
{
public Task<ValidationResult<T>> ValidateAsync(T value);
}

View 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; }
}

View File

@ -95,5 +95,16 @@ public interface IProviderBillingService
Task<Subscription> SetupSubscription( Task<Subscription> SetupSubscription(
Provider provider); 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); Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
} }

View File

@ -115,6 +115,15 @@ public static class FeatureFlagKeys
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; 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 ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; 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 AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string EmailVerification = "email-verification"; public const string EmailVerification = "email-verification";
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; 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 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 InlineMenuFieldQualification = "inline-menu-field-qualification";
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; 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 RemoveServerVersionHeader = "remove-server-version-header";
public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification"; 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 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 InlineMenuTotp = "inline-menu-totp";
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
public const string AppReviewPrompt = "app-review-prompt"; public const string AppReviewPrompt = "app-review-prompt";
@ -172,6 +175,8 @@ public static class FeatureFlagKeys
public const string WebPush = "web-push"; public const string WebPush = "web-push";
public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string AndroidImportLoginsFlow = "import-logins-flow";
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; 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() public static List<string> GetAllKeys()
{ {

View File

@ -36,7 +36,7 @@
<PackageReference Include="DnsClient" Version="1.8.0" /> <PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" /> <PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" /> <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.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.1" /> <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.1" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" /> <PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />

View File

@ -7,7 +7,7 @@
</tr> </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;"> <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"> <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> </td>
</tr> </tr>
</table> </table>

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}} {{#>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. 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}} {{/BasicTextLayout}}

View File

@ -26,7 +26,7 @@
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <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"> <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 Manage subscription
</a> </a>
<br class="line-break" /> <br class="line-break" />

View File

@ -24,7 +24,7 @@
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <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"> <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 Manage subscription
</a> </a>
<br class="line-break" /> <br class="line-break" />

View File

@ -24,7 +24,7 @@
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <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"> <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 Manage subscription
</a> </a>
<br class="line-break" /> <br class="line-break" />

View File

@ -24,7 +24,7 @@
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <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"> <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 Manage subscription
</a> </a>
<br class="line-break" /> <br class="line-break" />

View File

@ -1,5 +1,7 @@
#nullable enable #nullable enable
using Bit.Core.AdminConsole.Errors;
namespace Bit.Core.Models.Commands; namespace Bit.Core.Models.Commands;
public class CommandResult(IEnumerable<string> errors) public class CommandResult(IEnumerable<string> errors)
@ -9,7 +11,6 @@ public class CommandResult(IEnumerable<string> errors)
public bool Success => ErrorMessages.Count == 0; public bool Success => ErrorMessages.Count == 0;
public bool HasErrors => ErrorMessages.Count > 0; public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList(); public List<string> ErrorMessages { get; } = errors.ToList();
public CommandResult() : this(Array.Empty<string>()) { } 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();
} }
} }

View File

@ -2,7 +2,7 @@
public class OrganizationSeatsAutoscaledViewModel : BaseMailModel public class OrganizationSeatsAutoscaledViewModel : BaseMailModel
{ {
public Guid OrganizationId { get; set; }
public int InitialSeatCount { get; set; } public int InitialSeatCount { get; set; }
public int CurrentSeatCount { get; set; } public int CurrentSeatCount { get; set; }
public string VaultSubscriptionUrl { get; set; }
} }

View File

@ -2,6 +2,6 @@
public class OrganizationSeatsMaxReachedViewModel : BaseMailModel public class OrganizationSeatsMaxReachedViewModel : BaseMailModel
{ {
public Guid OrganizationId { get; set; }
public int MaxSeatCount { get; set; } public int MaxSeatCount { get; set; }
public string VaultSubscriptionUrl { get; set; }
} }

View File

@ -2,6 +2,6 @@
public class OrganizationServiceAccountsMaxReachedViewModel public class OrganizationServiceAccountsMaxReachedViewModel
{ {
public Guid OrganizationId { get; set; }
public int MaxServiceAccountsCount { get; set; } public int MaxServiceAccountsCount { get; set; }
public string VaultSubscriptionUrl { get; set; }
} }

View File

@ -214,9 +214,9 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails);
var model = new OrganizationSeatsAutoscaledViewModel var model = new OrganizationSeatsAutoscaledViewModel
{ {
OrganizationId = organization.Id,
InitialSeatCount = initialSeatCount, InitialSeatCount = initialSeatCount,
CurrentSeatCount = organization.Seats.Value, CurrentSeatCount = organization.Seats.Value,
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
}; };
await AddMessageContentAsync(message, "OrganizationSeatsAutoscaled", model); await AddMessageContentAsync(message, "OrganizationSeatsAutoscaled", model);
@ -229,8 +229,8 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel var model = new OrganizationSeatsMaxReachedViewModel
{ {
OrganizationId = organization.Id,
MaxSeatCount = maxSeatCount, MaxSeatCount = maxSeatCount,
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
}; };
await AddMessageContentAsync(message, "OrganizationSeatsMaxReached", model); 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 message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel var model = new OrganizationSeatsMaxReachedViewModel
{ {
OrganizationId = organization.Id,
MaxSeatCount = maxSeatCount, MaxSeatCount = maxSeatCount,
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
}; };
await AddMessageContentAsync(message, "OrganizationSmSeatsMaxReached", model); 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 message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
var model = new OrganizationServiceAccountsMaxReachedViewModel var model = new OrganizationServiceAccountsMaxReachedViewModel
{ {
OrganizationId = organization.Id,
MaxServiceAccountsCount = maxSeatCount, MaxServiceAccountsCount = maxSeatCount,
VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
}; };
await AddMessageContentAsync(message, "OrganizationSmServiceAccountsMaxReached", model); await AddMessageContentAsync(message, "OrganizationSmServiceAccountsMaxReached", model);
@ -1223,4 +1223,11 @@ public class HandlebarsMailService : IMailService
{ {
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false); 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"
};
} }

View File

@ -7,7 +7,7 @@
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " />
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2" /> <PackageReference Include="AngleSharp" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -250,21 +250,26 @@ public class DeviceValidator(
var customResponse = new Dictionary<string, object>(); var customResponse = new Dictionary<string, object>();
switch (errorType) 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: case DeviceValidationResultType.InvalidUser:
result.ErrorDescription = "Invalid user"; result.ErrorDescription = "Invalid user";
customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid user.")); customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user"));
break; break;
case DeviceValidationResultType.InvalidNewDeviceOtp: case DeviceValidationResultType.InvalidNewDeviceOtp:
result.ErrorDescription = "Invalid New Device OTP"; 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; break;
case DeviceValidationResultType.NewDeviceVerificationRequired: case DeviceValidationResultType.NewDeviceVerificationRequired:
result.ErrorDescription = "New device verification required"; 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; break;
case DeviceValidationResultType.NoDeviceInformationProvided: case DeviceValidationResultType.NoDeviceInformationProvided:
result.ErrorDescription = "No device information provided"; 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; break;
} }
return (result, customResponse); return (result, customResponse);

View File

@ -1,5 +1,4 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
@ -155,12 +154,9 @@ public class TwoFactorAuthenticationValidator(
return false; return false;
} }
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin)) if (type is TwoFactorProviderType.RecoveryCode)
{ {
if (type is TwoFactorProviderType.RecoveryCode) return await _userService.RecoverTwoFactorAsync(user, token);
{
return await _userService.RecoverTwoFactorAsync(user, token);
}
} }
// These cases we want to always return false, U2f is deprecated and OrganizationDuo // These cases we want to always return false, U2f is deprecated and OrganizationDuo

View File

@ -563,8 +563,8 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
await using var connection = new SqlConnection(ConnectionString); await using var connection = new SqlConnection(ConnectionString);
await connection.ExecuteAsync( await connection.ExecuteAsync(
"[dbo].[OrganizationUser_SetStatusForUsersById]", "[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked }, new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(), Status = OrganizationUserStatusType.Revoked },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }

View File

@ -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

View 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);
}
}

View 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);
}
}

View File

@ -172,7 +172,7 @@ public class DeviceValidatorTests
Assert.False(result); Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]); 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"]; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorModel.Message, actualResponse.Message); Assert.Equal(expectedErrorModel.Message, actualResponse.Message);
} }
@ -418,7 +418,7 @@ public class DeviceValidatorTests
Assert.False(result); Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]); Assert.NotNull(context.CustomResponse["ErrorModel"]);
// PM-13340: The error message should be "invalid user" instead of "no device information provided" // 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"]; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message); Assert.Equal(expectedErrorMessage, actualResponse.Message);
} }
@ -552,7 +552,7 @@ public class DeviceValidatorTests
Assert.False(result); Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]); 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"]; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message); Assert.Equal(expectedErrorMessage, actualResponse.Message);
} }
@ -604,7 +604,7 @@ public class DeviceValidatorTests
Assert.False(result); Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]); Assert.NotNull(context.CustomResponse["ErrorModel"]);
var expectedErrorMessage = "New device verification required."; var expectedErrorMessage = "new device verification required";
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message); Assert.Equal(expectedErrorMessage, actualResponse.Message);
} }

View File

@ -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.Enums;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
@ -464,7 +463,6 @@ public class TwoFactorAuthenticationValidatorTests
user.TwoFactorRecoveryCode = token; user.TwoFactorRecoveryCode = token;
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true); _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true);
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
// Act // Act
var result = await _sut.VerifyTwoFactorAsync( var result = await _sut.VerifyTwoFactorAsync(
@ -486,7 +484,6 @@ public class TwoFactorAuthenticationValidatorTests
user.TwoFactorRecoveryCode = token; user.TwoFactorRecoveryCode = token;
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false); _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false);
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
// Act // Act
var result = await _sut.VerifyTwoFactorAsync( var result = await _sut.VerifyTwoFactorAsync(

View File

@ -1,4 +1,4 @@
FROM bitwarden/server:latest FROM ghcr.io/bitwarden/server
LABEL com.bitwarden.product="bitwarden" LABEL com.bitwarden.product="bitwarden"

View File

@ -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