1
0
mirror of https://github.com/bitwarden/server synced 2026-01-01 16:13:33 +00:00

Merge branch 'main' into auth/pm-22975/client-version-validator

This commit is contained in:
Jared Snider
2025-12-22 12:54:07 -05:00
committed by GitHub
104 changed files with 2983 additions and 282 deletions

View File

@@ -1,25 +0,0 @@
Please review this pull request with a focus on:
- Code quality and best practices
- Potential bugs or issues
- Security implications
- Performance considerations
Note: The PR branch is already checked out in the current working directory.
Provide a comprehensive review including:
- Summary of changes since last review
- Critical issues found (be thorough)
- Suggested improvements (be thorough)
- Good practices observed (be concise - list only the most notable items without elaboration)
- Action items for the author
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability
When reviewing subsequent commits:
- Track status of previously identified issues (fixed/unfixed/reopened)
- Identify NEW problems introduced since last review
- Note if fixes introduced new issues
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.

7
.github/CODEOWNERS vendored
View File

@@ -53,6 +53,11 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
# Dirt (Data Insights & Reporting) team
**/Dirt @bitwarden/team-data-insights-and-reporting-dev
src/Events @bitwarden/team-data-insights-and-reporting-dev
src/EventsProcessor @bitwarden/team-data-insights-and-reporting-dev
test/Events.IntegrationTest @bitwarden/team-data-insights-and-reporting-dev
test/Events.Test @bitwarden/team-data-insights-and-reporting-dev
test/EventsProcessor.Test @bitwarden/team-data-insights-and-reporting-dev
# Vault team
**/Vault @bitwarden/team-vault-dev
@@ -63,8 +68,6 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
bitwarden_license/src/Scim @bitwarden/team-admin-console-dev
bitwarden_license/src/test/Scim.IntegrationTest @bitwarden/team-admin-console-dev
bitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev
src/Events @bitwarden/team-admin-console-dev
src/EventsProcessor @bitwarden/team-admin-console-dev
# Billing team
**/*billing* @bitwarden/team-billing-dev

View File

@@ -38,7 +38,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
persist-credentials: false
@@ -68,7 +68,7 @@ jobs:
if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: true

View File

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -102,7 +102,7 @@ jobs:
echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -289,7 +289,7 @@ jobs:
actions: read
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -416,7 +416,7 @@ jobs:
- win-x64
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

View File

@@ -31,7 +31,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false

View File

@@ -87,7 +87,7 @@ jobs:
datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false

View File

@@ -31,7 +31,7 @@ jobs:
label: "DB-migrations-changed"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
persist-credentials: false

View File

@@ -106,7 +106,7 @@ jobs:
echo "Github Release Option: $RELEASE_OPTION"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false

View File

@@ -39,7 +39,7 @@ jobs:
fi
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false

View File

@@ -91,7 +91,7 @@ jobs:
permission-contents: write
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: main
token: ${{ steps.app-token.outputs.token }}
@@ -215,7 +215,7 @@ jobs:
permission-contents: write
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ inputs.target_ref }}
token: ${{ steps.app-token.outputs.token }}

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Check
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
stale-issue-label: "needs-reply"
stale-pr-label: "needs-changes"

View File

@@ -44,7 +44,7 @@ jobs:
checks: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -269,7 +269,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false

View File

@@ -0,0 +1,91 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.OrganizationLicenses;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Billing.Controllers;
[Route("licenses")]
[Authorize("Licensing")]
[SelfHosted(NotSelfHostedOnly = true)]
public class LicensesController : Controller
{
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
private readonly ICurrentContext _currentContext;
public LicensesController(
IUserRepository userRepository,
IUserService userService,
IOrganizationRepository organizationRepository,
IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
ICurrentContext currentContext)
{
_userRepository = userRepository;
_userService = userService;
_organizationRepository = organizationRepository;
_getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
_currentContext = currentContext;
}
[HttpGet("user/{id}")]
public async Task<UserLicense> GetUser(string id, [FromQuery] string key)
{
var user = await _userRepository.GetByIdAsync(new Guid(id));
if (user == null)
{
return null;
}
else if (!user.LicenseKey.Equals(key))
{
await Task.Delay(2000);
throw new BadRequestException("Invalid license key.");
}
var license = await _userService.GenerateLicenseAsync(user, null);
return license;
}
/// <summary>
/// Used by self-hosted installations to get an updated license file
/// </summary>
[HttpGet("organization/{id}")]
public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)
{
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
if (organization == null)
{
throw new NotFoundException("Organization not found.");
}
if (!organization.LicenseKey.Equals(model.LicenseKey))
{
await Task.Delay(2000);
throw new BadRequestException("Invalid license key.");
}
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))
{
throw new BadRequestException("Invalid Billing Sync Key");
}
var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);
return license;
}
}

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Api.Dirt.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Api.Utilities.DiagnosticTools;
@@ -17,7 +18,7 @@ using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
namespace Bit.Api.Dirt.Controllers;
[Route("events")]
[Authorize("Application")]

View File

@@ -2,7 +2,7 @@
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
namespace Bit.Api.Models.Response;
namespace Bit.Api.Dirt.Models.Response;
public class EventResponseModel : ResponseModel
{

View File

@@ -1,6 +1,5 @@

using System.Net;
using Bit.Api.Models.Public.Request;
using System.Net;
using Bit.Api.Dirt.Public.Models;
using Bit.Api.Models.Public.Response;
using Bit.Api.Utilities.DiagnosticTools;
using Bit.Core.Context;
@@ -12,7 +11,7 @@ using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Public.Controllers;
namespace Bit.Api.Dirt.Public.Controllers;
[Route("public/events")]
[Authorize("Organization")]

View File

@@ -3,7 +3,7 @@
using Bit.Core.Exceptions;
namespace Bit.Api.Models.Public.Request;
namespace Bit.Api.Dirt.Public.Models;
public class EventFilterRequestModel
{

View File

@@ -1,8 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Models.Public.Response;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Api.Models.Public.Response;
namespace Bit.Api.Dirt.Public.Models;
/// <summary>
/// An event log.

View File

@@ -47,6 +47,7 @@ public class AccountsKeyManagementController : Controller
_webauthnKeyValidator;
private readonly IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> _deviceValidator;
private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery;
private readonly ISetKeyConnectorKeyCommand _setKeyConnectorKeyCommand;
public AccountsKeyManagementController(IUserService userService,
IFeatureService featureService,
@@ -62,8 +63,10 @@ public class AccountsKeyManagementController : Controller
emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> deviceValidator)
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
webAuthnKeyValidator,
IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> deviceValidator,
ISetKeyConnectorKeyCommand setKeyConnectorKeyCommand)
{
_userService = userService;
_featureService = featureService;
@@ -79,6 +82,7 @@ public class AccountsKeyManagementController : Controller
_webauthnKeyValidator = webAuthnKeyValidator;
_deviceValidator = deviceValidator;
_keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery;
_setKeyConnectorKeyCommand = setKeyConnectorKeyCommand;
}
[HttpPost("key-management/regenerate-keys")]
@@ -146,18 +150,28 @@ public class AccountsKeyManagementController : Controller
throw new UnauthorizedAccessException();
}
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
if (model.IsV2Request())
{
return;
// V2 account registration
await _setKeyConnectorKeyCommand.SetKeyConnectorKeyForUserAsync(user, model.ToKeyConnectorKeysData());
}
foreach (var error in result.Errors)
else
{
ModelState.AddModelError(string.Empty, error.Description);
}
// V1 account registration
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
throw new BadRequestException(ModelState);
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
}
[HttpPost("convert-to-key-connector")]

View File

@@ -1,36 +1,112 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
public class SetKeyConnectorKeyRequestModel
public class SetKeyConnectorKeyRequestModel : IValidatableObject
{
[Required]
public string Key { get; set; }
[Required]
public KeysRequestModel Keys { get; set; }
[Required]
public KdfType Kdf { get; set; }
[Required]
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
[Required]
public string OrgIdentifier { get; set; }
// TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27328
[Obsolete("Use KeyConnectorKeyWrappedUserKey instead")]
public string? Key { get; set; }
[Obsolete("Use AccountKeys instead")]
public KeysRequestModel? Keys { get; set; }
[Obsolete("Not used anymore")]
public KdfType? Kdf { get; set; }
[Obsolete("Not used anymore")]
public int? KdfIterations { get; set; }
[Obsolete("Not used anymore")]
public int? KdfMemory { get; set; }
[Obsolete("Not used anymore")]
public int? KdfParallelism { get; set; }
[EncryptedString]
public string? KeyConnectorKeyWrappedUserKey { get; set; }
public AccountKeysRequestModel? AccountKeys { get; set; }
[Required]
public required string OrgIdentifier { get; init; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (IsV2Request())
{
// V2 registration
yield break;
}
// V1 registration
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
if (string.IsNullOrEmpty(Key))
{
yield return new ValidationResult("Key must be supplied.");
}
if (Keys == null)
{
yield return new ValidationResult("Keys must be supplied.");
}
if (Kdf == null)
{
yield return new ValidationResult("Kdf must be supplied.");
}
if (KdfIterations == null)
{
yield return new ValidationResult("KdfIterations must be supplied.");
}
if (Kdf == KdfType.Argon2id)
{
if (KdfMemory == null)
{
yield return new ValidationResult("KdfMemory must be supplied when Kdf is Argon2id.");
}
if (KdfParallelism == null)
{
yield return new ValidationResult("KdfParallelism must be supplied when Kdf is Argon2id.");
}
}
}
public bool IsV2Request()
{
return !string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) && AccountKeys != null;
}
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
public User ToUser(User existingUser)
{
existingUser.Kdf = Kdf;
existingUser.KdfIterations = KdfIterations;
existingUser.Kdf = Kdf!.Value;
existingUser.KdfIterations = KdfIterations!.Value;
existingUser.KdfMemory = KdfMemory;
existingUser.KdfParallelism = KdfParallelism;
existingUser.Key = Key;
Keys.ToUser(existingUser);
Keys!.ToUser(existingUser);
return existingUser;
}
public KeyConnectorKeysData ToKeyConnectorKeysData()
{
// TODO remove validation with https://bitwarden.atlassian.net/browse/PM-27328
if (string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) || AccountKeys == null)
{
throw new BadRequestException("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.");
}
return new KeyConnectorKeysData
{
KeyConnectorKeyWrappedUserKey = KeyConnectorKeyWrappedUserKey,
AccountKeys = AccountKeys,
OrgIdentifier = OrgIdentifier
};
}
}

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Api.Dirt.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Core.Exceptions;

View File

@@ -1,4 +1,4 @@
using Bit.Api.Models.Public.Request;
using Bit.Api.Dirt.Public.Models;
using Bit.Api.Models.Public.Response;
using Bit.Core;
using Bit.Core.Services;
@@ -49,7 +49,7 @@ public static class EventDiagnosticLogger
this ILogger logger,
IFeatureService featureService,
Guid organizationId,
IEnumerable<Bit.Api.Models.Response.EventResponseModel> data,
IEnumerable<Dirt.Models.Response.EventResponseModel> data,
string? continuationToken,
DateTime? queryStart = null,
DateTime? queryEnd = null)

View File

@@ -1,12 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
using OneOf.Types;
using Stripe;
@@ -21,14 +22,14 @@ public interface IRestartSubscriptionCommand
}
public class RestartSubscriptionCommand(
ILogger<RestartSubscriptionCommand> logger,
IOrganizationRepository organizationRepository,
IProviderRepository providerRepository,
IPricingClient pricingClient,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
IUserRepository userRepository) : IRestartSubscriptionCommand
ISubscriberService subscriberService) : BaseBillingCommand<RestartSubscriptionCommand>(logger), IRestartSubscriptionCommand
{
public async Task<BillingCommandResult<None>> Run(
ISubscriber subscriber)
public Task<BillingCommandResult<None>> Run(
ISubscriber subscriber) => HandleAsync<None>(async () =>
{
var existingSubscription = await subscriberService.GetSubscription(subscriber);
@@ -37,56 +38,147 @@ public class RestartSubscriptionCommand(
return new BadRequest("Cannot restart a subscription that is not canceled.");
}
await RestartSubscriptionAsync(subscriber, existingSubscription);
return new None();
});
private Task RestartSubscriptionAsync(
ISubscriber subscriber,
Subscription canceledSubscription) => subscriber switch
{
Organization organization => RestartOrganizationSubscriptionAsync(organization, canceledSubscription),
_ => throw new NotSupportedException("Only organization subscriptions can be restarted")
};
private async Task RestartOrganizationSubscriptionAsync(
Organization organization,
Subscription canceledSubscription)
{
var plans = await pricingClient.ListPlans();
var oldPlan = plans.FirstOrDefault(plan => plan.Type == organization.PlanType);
if (oldPlan == null)
{
throw new ConflictException("Could not find plan for organization's plan type");
}
var newPlan = oldPlan.Disabled
? plans.FirstOrDefault(plan =>
plan.ProductTier == oldPlan.ProductTier &&
plan.IsAnnual == oldPlan.IsAnnual &&
!plan.Disabled)
: oldPlan;
if (newPlan == null)
{
throw new ConflictException("Could not find the current, enabled plan for organization's tier and cadence");
}
if (newPlan.Type != oldPlan.Type)
{
organization.PlanType = newPlan.Type;
organization.Plan = newPlan.Name;
organization.SelfHost = newPlan.HasSelfHost;
organization.UsePolicies = newPlan.HasPolicies;
organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents;
organization.UseTotp = newPlan.HasTotp;
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.UseSso = newPlan.HasSso;
organization.UseOrganizationDomains = newPlan.HasOrganizationDomains;
organization.UseKeyConnector = newPlan.HasKeyConnector;
organization.UseScim = newPlan.HasScim;
organization.UseResetPassword = newPlan.HasResetPassword;
organization.UsersGetPremium = newPlan.UsersGetPremium;
organization.UseCustomPermissions = newPlan.HasCustomPermissions;
}
var items = new List<SubscriptionItemOptions>();
// Password Manager
var passwordManagerItem = canceledSubscription.Items.FirstOrDefault(item =>
item.Price.Id == (oldPlan.HasNonSeatBasedPasswordManagerPlan()
? oldPlan.PasswordManager.StripePlanId
: oldPlan.PasswordManager.StripeSeatPlanId));
if (passwordManagerItem == null)
{
throw new ConflictException("Organization's subscription does not have a Password Manager subscription item.");
}
items.Add(new SubscriptionItemOptions
{
Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId,
Quantity = passwordManagerItem.Quantity
});
// Storage
var storageItem = canceledSubscription.Items.FirstOrDefault(
item => item.Price.Id == oldPlan.PasswordManager.StripeStoragePlanId);
if (storageItem != null)
{
items.Add(new SubscriptionItemOptions
{
Price = newPlan.PasswordManager.StripeStoragePlanId,
Quantity = storageItem.Quantity
});
}
// Secrets Manager & Service Accounts
var secretsManagerItem = oldPlan.SecretsManager != null
? canceledSubscription.Items.FirstOrDefault(item =>
item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId)
: null;
var serviceAccountsItem = oldPlan.SecretsManager != null
? canceledSubscription.Items.FirstOrDefault(item =>
item.Price.Id == oldPlan.SecretsManager.StripeServiceAccountPlanId)
: null;
if (newPlan.SecretsManager != null)
{
if (secretsManagerItem != null)
{
items.Add(new SubscriptionItemOptions
{
Price = newPlan.SecretsManager.StripeSeatPlanId,
Quantity = secretsManagerItem.Quantity
});
}
if (serviceAccountsItem != null)
{
items.Add(new SubscriptionItemOptions
{
Price = newPlan.SecretsManager.StripeServiceAccountPlanId,
Quantity = serviceAccountsItem.Quantity
});
}
}
var options = new SubscriptionCreateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
CollectionMethod = CollectionMethod.ChargeAutomatically,
Customer = existingSubscription.CustomerId,
Items = existingSubscription.Items.Select(subscriptionItem => new SubscriptionItemOptions
{
Price = subscriptionItem.Price.Id,
Quantity = subscriptionItem.Quantity
}).ToList(),
Metadata = existingSubscription.Metadata,
Customer = canceledSubscription.CustomerId,
Items = items,
Metadata = canceledSubscription.Metadata,
OffSession = true,
TrialPeriodDays = 0
};
var subscription = await stripeAdapter.CreateSubscriptionAsync(options);
await EnableAsync(subscriber, subscription);
return new None();
}
private async Task EnableAsync(ISubscriber subscriber, Subscription subscription)
{
switch (subscriber)
{
case Organization organization:
{
organization.GatewaySubscriptionId = subscription.Id;
organization.Enabled = true;
organization.ExpirationDate = subscription.GetCurrentPeriodEnd();
organization.RevisionDate = DateTime.UtcNow;
await organizationRepository.ReplaceAsync(organization);
break;
}
case Provider provider:
{
provider.GatewaySubscriptionId = subscription.Id;
provider.Enabled = true;
provider.RevisionDate = DateTime.UtcNow;
await providerRepository.ReplaceAsync(provider);
break;
}
case User user:
{
user.GatewaySubscriptionId = subscription.Id;
user.Premium = true;
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
user.RevisionDate = DateTime.UtcNow;
await userRepository.ReplaceAsync(user);
break;
}
}
organization.GatewaySubscriptionId = subscription.Id;
organization.Enabled = true;
organization.ExpirationDate = subscription.GetCurrentPeriodEnd();
organization.RevisionDate = DateTime.UtcNow;
await organizationRepository.ReplaceAsync(organization);
}
}

View File

@@ -183,6 +183,7 @@ public static class FeatureFlagKeys
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
public const string InlineMenuTotp = "inline-menu-totp";
public const string WindowsDesktopAutotype = "windows-desktop-autotype";
public const string WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga";
/* Billing Team */
public const string TrialPayment = "PM-8163-trial-payment";
@@ -191,7 +192,6 @@ public static class FeatureFlagKeys
public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog";
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page";
public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service";
public const string PM23341_Milestone_2 = "pm-23341-milestone-2";
public const string PM26462_Milestone_3 = "pm-26462-milestone-3";
@@ -212,6 +212,7 @@ public static class FeatureFlagKeys
public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component";
public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit";
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
/* Mobile Team */
public const string AndroidImportLoginsFlow = "import-logins-flow";
@@ -238,6 +239,7 @@ public static class FeatureFlagKeys
public const string UseChromiumImporter = "pm-23982-chromium-importer";
public const string ChromiumImporterWithABE = "pm-25855-chromium-importer-abe";
public const string SendUIRefresh = "pm-28175-send-ui-refresh";
public const string SendEmailOTP = "pm-19051-send-email-verification";
/* Vault Team */
public const string CipherKeyEncryption = "cipher-key-encryption";

View File

@@ -0,0 +1,52 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Core.KeyManagement.Authorization;
public class KeyConnectorAuthorizationHandler : AuthorizationHandler<KeyConnectorOperationsRequirement, User>
{
private readonly ICurrentContext _currentContext;
public KeyConnectorAuthorizationHandler(ICurrentContext currentContext)
{
_currentContext = currentContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
KeyConnectorOperationsRequirement requirement,
User user)
{
var authorized = requirement switch
{
not null when requirement == KeyConnectorOperations.Use => CanUse(user),
_ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement))
};
if (authorized)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool CanUse(User user)
{
// User cannot use Key Connector if they already use it
if (user.UsesKeyConnector)
{
return false;
}
// User cannot use Key Connector if they are an owner or admin of any organization
if (_currentContext.Organizations.Any(u =>
u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin))
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.KeyManagement.Authorization;
public class KeyConnectorOperationsRequirement : OperationAuthorizationRequirement
{
public KeyConnectorOperationsRequirement(string name)
{
Name = name;
}
}
public static class KeyConnectorOperations
{
public static readonly KeyConnectorOperationsRequirement Use = new(nameof(Use));
}

View File

@@ -0,0 +1,13 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
namespace Bit.Core.KeyManagement.Commands.Interfaces;
/// <summary>
/// Creates the user key and account cryptographic state for a new user registering
/// with Key Connector SSO configuration.
/// </summary>
public interface ISetKeyConnectorKeyCommand
{
Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData);
}

View File

@@ -0,0 +1,60 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Authorization;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Core.KeyManagement.Commands;
public class SetKeyConnectorKeyCommand : ISetKeyConnectorKeyCommand
{
private readonly IAuthorizationService _authorizationService;
private readonly ICurrentContext _currentContext;
private readonly IEventService _eventService;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
public SetKeyConnectorKeyCommand(
IAuthorizationService authorizationService,
ICurrentContext currentContext,
IEventService eventService,
IAcceptOrgUserCommand acceptOrgUserCommand,
IUserService userService,
IUserRepository userRepository)
{
_authorizationService = authorizationService;
_currentContext = currentContext;
_eventService = eventService;
_acceptOrgUserCommand = acceptOrgUserCommand;
_userService = userService;
_userRepository = userRepository;
}
public async Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(_currentContext.HttpContext.User, user,
KeyConnectorOperations.Use);
if (!authorizationResult.Succeeded)
{
throw new BadRequestException("Cannot use Key Connector");
}
var setKeyConnectorUserKeyTask =
_userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorKeysData.KeyConnectorKeyWrappedUserKey);
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id,
keyConnectorKeysData.AccountKeys.ToAccountKeysData(), [setKeyConnectorUserKeyTask]);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(keyConnectorKeysData.OrgIdentifier, user,
_userService);
}
}

View File

@@ -1,9 +1,11 @@
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Authorization;
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Kdf.Implementations;
using Bit.Core.KeyManagement.Queries;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.KeyManagement;
@@ -12,15 +14,22 @@ public static class KeyManagementServiceCollectionExtensions
{
public static void AddKeyManagementServices(this IServiceCollection services)
{
services.AddKeyManagementAuthorizationHandlers();
services.AddKeyManagementCommands();
services.AddKeyManagementQueries();
services.AddSendPasswordServices();
}
private static void AddKeyManagementAuthorizationHandlers(this IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, KeyConnectorAuthorizationHandler>();
}
private static void AddKeyManagementCommands(this IServiceCollection services)
{
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
services.AddScoped<ISetKeyConnectorKeyCommand, SetKeyConnectorKeyCommand>();
}
private static void AddKeyManagementQueries(this IServiceCollection services)

View File

@@ -0,0 +1,12 @@
using Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Core.KeyManagement.Models.Data;
public class KeyConnectorKeysData
{
public required string KeyConnectorKeyWrappedUserKey { get; set; }
public required AccountKeysRequestModel AccountKeys { get; set; }
public required string OrgIdentifier { get; init; }
}

View File

@@ -72,6 +72,8 @@ public interface IUserRepository : IRepository<User, Guid>
UserAccountKeysData accountKeysData,
IEnumerable<UpdateUserData>? updateUserDataActions = null);
Task DeleteManyAsync(IEnumerable<User> users);
UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey);
}
public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null,

View File

@@ -33,6 +33,8 @@ public interface IUserService
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,
string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint, string key);
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
[Obsolete("Use ISetKeyConnectorKeyCommand instead. This method will be removed in a future version.")]
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> ConvertToKeyConnectorAsync(User user);
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);

View File

@@ -621,6 +621,7 @@ public class UserService : UserManager<User>, IUserService
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)
{
var identityResult = CheckCanUseKeyConnector(user);

View File

@@ -1029,11 +1029,8 @@ public class CipherService : ICipherService
var existingCipherData = DeserializeCipherData(existingCipher);
var newCipherData = DeserializeCipherData(cipher);
// "hidden password" users may not add cipher key encryption
if (existingCipher.Key == null && cipher.Key != null)
{
throw new BadRequestException("You do not have permission to add cipher key encryption.");
}
// For hidden-password users, never allow Key to change at all.
cipher.Key = existingCipher.Key;
// Keep only non-hidden fileds from the new cipher
var nonHiddenFields = newCipherData.Fields?.Where(f => f.Type != FieldType.Hidden) ?? [];
// Get hidden fields from the existing cipher

View File

@@ -21,17 +21,21 @@ public class CollectController : Controller
private readonly IEventService _eventService;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
public CollectController(
ICurrentContext currentContext,
IEventService eventService,
ICipherRepository cipherRepository,
IOrganizationRepository organizationRepository)
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
_currentContext = currentContext;
_eventService = eventService;
_cipherRepository = cipherRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
}
[HttpPost]
@@ -54,6 +58,24 @@ public class CollectController : Controller
await _eventService.LogUserEventAsync(_currentContext.UserId.Value, eventModel.Type, eventModel.Date);
break;
case EventType.Organization_ItemOrganization_Accepted:
case EventType.Organization_ItemOrganization_Declined:
if (!eventModel.OrganizationId.HasValue || !_currentContext.UserId.HasValue)
{
continue;
}
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(eventModel.OrganizationId.Value, _currentContext.UserId.Value);
if (orgUser == null)
{
continue;
}
await _eventService.LogOrganizationUserEventAsync(orgUser, eventModel.Type, eventModel.Date);
continue;
// Cipher events
case EventType.Cipher_ClientAutofilled:
case EventType.Cipher_ClientCopiedHiddenField:

View File

@@ -703,6 +703,7 @@ public abstract class BaseRequestValidator<T> where T : class
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);

View File

@@ -4,6 +4,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
@@ -153,7 +154,23 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
{
// KeyConnectorUrl is configured in the CLI client, we just need to tell the client to use it
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return Task.CompletedTask;
}
// Key connector data should have already been set in the decryption options
// for backwards compatibility we set them this way too. We can eventually get rid of this once we clean up
// ResetMasterPassword
if (!context.Result.CustomResponse.TryGetValue("UserDecryptionOptions", out var userDecryptionOptionsObj) ||
userDecryptionOptionsObj is not UserDecryptionOptions userDecryptionOptions)
{
return Task.CompletedTask;
}
if (userDecryptionOptions is { KeyConnectorOption: { } })
{
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return Task.CompletedTask;

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Core;
using Bit.Core.Billing.Premium.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
@@ -401,6 +402,32 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
return result.SingleOrDefault();
}
public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)
{
return async (connection, transaction) =>
{
var timestamp = DateTime.UtcNow;
await connection!.ExecuteAsync(
"[dbo].[User_UpdateKeyConnectorUserKey]",
new
{
Id = userId,
Key = keyConnectorWrappedUserKey,
// Key Connector does not use KDF, so we set some defaults
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
UsesKeyConnector = true,
RevisionDate = timestamp,
AccountRevisionDate = timestamp
},
transaction: transaction,
commandType: CommandType.StoredProcedure);
};
}
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
{
if (user == null)

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Bit.Core;
using Bit.Core.Billing.Premium.Models;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
@@ -479,6 +481,35 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
}
}
public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)
{
return async (_, _) =>
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var userEntity = await dbContext.Users.FindAsync(userId);
if (userEntity == null)
{
throw new ArgumentException("User not found", nameof(userId));
}
var timestamp = DateTime.UtcNow;
userEntity.Key = keyConnectorWrappedUserKey;
// Key Connector does not use KDF, so we set some defaults
userEntity.Kdf = KdfType.Argon2id;
userEntity.KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default;
userEntity.KdfMemory = AuthConstants.ARGON2_MEMORY.Default;
userEntity.KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default;
userEntity.UsesKeyConnector = true;
userEntity.RevisionDate = timestamp;
userEntity.AccountRevisionDate = timestamp;
await dbContext.SaveChangesAsync();
};
}
private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable<Guid> userIds)
{
var defaultCollections = (from c in dbContext.Collections

View File

@@ -0,0 +1,28 @@
CREATE PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey]
@Id UNIQUEIDENTIFIER,
@Key VARCHAR(MAX),
@Kdf TINYINT,
@KdfIterations INT,
@KdfMemory INT,
@KdfParallelism INT,
@UsesKeyConnector BIT,
@RevisionDate DATETIME2(7),
@AccountRevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[User]
SET
[Key] = @Key,
[Kdf] = @Kdf,
[KdfIterations] = @KdfIterations,
[KdfMemory] = @KdfMemory,
[KdfParallelism] = @KdfParallelism,
[UsesKeyConnector] = @UsesKeyConnector,
[RevisionDate] = @RevisionDate,
[AccountRevisionDate] = @AccountRevisionDate
WHERE
[Id] = @Id
END

View File

@@ -128,7 +128,8 @@ BEGIN
[UseAdminSponsoredFamilies],
[SyncSeats],
[UseAutomaticUserConfirmation],
[UsePhishingBlocker]
[UsePhishingBlocker],
[MaxStorageGbIncreased]
)
VALUES
(
@@ -193,6 +194,7 @@ BEGIN
@UseAdminSponsoredFamilies,
@SyncSeats,
@UseAutomaticUserConfirmation,
@UsePhishingBlocker
@UsePhishingBlocker,
@MaxStorageGb
);
END

View File

@@ -128,7 +128,8 @@ BEGIN
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,
[SyncSeats] = @SyncSeats,
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation,
[UsePhishingBlocker] = @UsePhishingBlocker
[UsePhishingBlocker] = @UsePhishingBlocker,
[MaxStorageGbIncreased] = @MaxStorageGb
WHERE
[Id] = @Id;
END

View File

@@ -96,7 +96,8 @@ BEGIN
[VerifyDevices],
[SecurityState],
[SecurityVersion],
[SignedPublicKey]
[SignedPublicKey],
[MaxStorageGbIncreased]
)
VALUES
(
@@ -145,6 +146,7 @@ BEGIN
@VerifyDevices,
@SecurityState,
@SecurityVersion,
@SignedPublicKey
@SignedPublicKey,
@MaxStorageGb
)
END

View File

@@ -96,7 +96,8 @@ BEGIN
[VerifyDevices] = @VerifyDevices,
[SecurityState] = @SecurityState,
[SecurityVersion] = @SecurityVersion,
[SignedPublicKey] = @SignedPublicKey
[SignedPublicKey] = @SignedPublicKey,
[MaxStorageGbIncreased] = @MaxStorageGb
WHERE
[Id] = @Id
END

View File

@@ -7,6 +7,7 @@ using Bit.Api.KeyManagement.Models.Responses;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
@@ -19,9 +20,11 @@ using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Vault.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
@@ -31,6 +34,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
private static readonly string _mockEncryptedType7WrappedSigningKey = "7.DRv74Kg1RSlFSam1MNFlGD==";
private readonly HttpClient _client;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
@@ -47,8 +51,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"true");
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
.Returns(true);
});
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_userRepository = _factory.GetService<IUserRepository>();
@@ -78,8 +85,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
{
// Localize factory to inject a false value for the feature flag.
var localFactory = new ApiApplicationFactory();
localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"false");
localFactory.SubstituteService<IFeatureService>(featureService =>
{
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
.Returns(false);
});
var localClient = localFactory.CreateClient();
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var localLoginHelper = new LoginHelper(localFactory, localClient);
@@ -285,21 +295,21 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
SetKeyConnectorKeyRequestModel request)
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier)
{
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(ssoUser);
request.Keys = new KeysRequestModel
var request = new SetKeyConnectorKeyRequestModel
{
PublicKey = ssoUser.PublicKey,
EncryptedPrivateKey = ssoUser.PrivateKey
Key = _mockEncryptedString,
Keys = new KeysRequestModel { PublicKey = ssoUser.PublicKey, EncryptedPrivateKey = ssoUser.PrivateKey },
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = organizationSsoIdentifier
};
request.Key = _mockEncryptedString;
request.OrgIdentifier = organizationSsoIdentifier;
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
response.EnsureSuccessStatusCode();
@@ -310,12 +320,95 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
Assert.True(user.UsesKeyConnector);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
var ssoOrganizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
Assert.NotNull(ssoOrganizationUser);
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
Assert.Null(ssoOrganizationUser.Email);
}
[Fact]
public async Task PostSetKeyConnectorKeyAsync_V2_NotLoggedIn_Unauthorized()
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "publicKey",
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String
},
OrgIdentifier = "test-org"
};
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_Success(string organizationSsoIdentifier)
{
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "publicKey",
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "publicKey",
WrappedPrivateKey = _mockEncryptedType7String,
SignedPublicKey = "signedPublicKey"
},
SignatureKeyPair = new SignatureKeyPairRequestModel
{
SignatureAlgorithm = "ed25519",
WrappedSigningKey = _mockEncryptedType7WrappedSigningKey,
VerifyingKey = "verifyingKey"
},
SecurityState = new SecurityStateModel
{
SecurityVersion = 2,
SecurityState = "v2"
}
},
OrgIdentifier = organizationSsoIdentifier
};
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
response.EnsureSuccessStatusCode();
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);
Assert.True(user.UsesKeyConnector);
Assert.Equal(KdfType.Argon2id, user.Kdf);
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, user.KdfIterations);
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, user.KdfMemory);
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, user.KdfParallelism);
Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, user.SignedPublicKey);
Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, user.SecurityState);
Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, user.SecurityVersion);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
var ssoOrganizationUser =
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
Assert.NotNull(ssoOrganizationUser);
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
Assert.Null(ssoOrganizationUser.Email);
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
Assert.NotNull(signatureKeyPair);
Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
Assert.Equal("verifyingKey", signatureKeyPair.VerifyingKey);
}
[Fact]

View File

@@ -238,10 +238,13 @@ public class AccountsKeyManagementControllerTests
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
public async Task PostSetKeyConnectorKeyAsync_V1_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
@@ -252,10 +255,13 @@ public class AccountsKeyManagementControllerTests
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
@@ -278,17 +284,20 @@ public class AccountsKeyManagementControllerTests
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
@@ -308,11 +317,108 @@ public class AccountsKeyManagementControllerTests
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().DidNotReceive()
.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>());
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_Success(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request);
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
Arg.Do<KeyConnectorKeysData>(data =>
{
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
}));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<ISetKeyConnectorKeyCommand>()
.When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>()))
.Do(_ => throw new BadRequestException("Command failed"));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
Assert.Equal("Command failed", exception.Message);
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
Arg.Do<KeyConnectorKeysData>(data =>
{
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
}));
}
[Theory]
[BitAutoData]
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(

View File

@@ -0,0 +1,333 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Xunit;
namespace Bit.Api.Test.KeyManagement.Models.Request;
public class SetKeyConnectorKeyRequestModelTests
{
private const string _wrappedUserKey = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private const string _publicKey = "public-key";
private const string _privateKey = "private-key";
private const string _userKey = "user-key";
private const string _orgIdentifier = "org-identifier";
[Fact]
public void Validate_V2Registration_Valid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Empty(results);
}
[Fact]
public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "not-encrypted-string",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results,
r => r.ErrorMessage == "KeyConnectorKeyWrappedUserKey is not a valid encrypted string.");
}
[Fact]
public void Validate_V1Registration_Valid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Empty(results);
}
[Fact]
public void Validate_V1Registration_MissingKey_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = null,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Key must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKeys_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = null,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Keys must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKdf_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = null,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Kdf must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKdfIterations_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = null,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfIterations must be supplied.");
}
[Fact]
public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = null,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfMemory must be supplied when Kdf is Argon2id.");
}
[Fact]
public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = null,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfParallelism must be supplied when Kdf is Argon2id.");
}
[Fact]
public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = null,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = null,
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_Valid_Success()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var data = model.ToKeyConnectorKeysData();
// Assert
Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(_orgIdentifier, data.OrgIdentifier);
}
private static List<ValidationResult> Validate(SetKeyConnectorKeyRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
return results;
}
}

View File

@@ -1,4 +1,4 @@
using Bit.Api.Models.Public.Request;
using Bit.Api.Dirt.Public.Models;
using Bit.Api.Models.Public.Response;
using Bit.Api.Utilities.DiagnosticTools;
using Bit.Core;
@@ -155,7 +155,7 @@ public class EventDiagnosticLoggerTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
Bit.Api.Models.Response.EventResponseModel[] emptyEvents = [];
Api.Dirt.Models.Response.EventResponseModel[] emptyEvents = [];
// Act
logger.LogAggregateData(featureService, organizationId, emptyEvents, null, null, null);
@@ -188,7 +188,7 @@ public class EventDiagnosticLoggerTests
var oldestEvent = Substitute.For<IEvent>();
oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-2));
var events = new List<Bit.Api.Models.Response.EventResponseModel>
var events = new List<Api.Dirt.Models.Response.EventResponseModel>
{
new (newestEvent),
new (middleEvent),

View File

@@ -1,11 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Test.Billing.Mocks;
using NSubstitute;
using Stripe;
using Xunit;
@@ -17,20 +20,19 @@ using static StripeConstants;
public class RestartSubscriptionCommandTests
{
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly RestartSubscriptionCommand _command;
public RestartSubscriptionCommandTests()
{
_command = new RestartSubscriptionCommand(
Substitute.For<Microsoft.Extensions.Logging.ILogger<RestartSubscriptionCommand>>(),
_organizationRepository,
_providerRepository,
_pricingClient,
_stripeAdapter,
_subscriberService,
_userRepository);
_subscriberService);
}
[Fact]
@@ -63,11 +65,56 @@ public class RestartSubscriptionCommandTests
}
[Fact]
public async Task Run_Organization_Success_ReturnsNone()
public async Task Run_Provider_ReturnsUnhandledWithNotSupportedException()
{
var provider = new Provider { Id = Guid.NewGuid() };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123"
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
var result = await _command.Run(provider);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<NotSupportedException>(unhandled.Exception);
}
[Fact]
public async Task Run_User_ReturnsUnhandledWithNotSupportedException()
{
var user = new User { Id = Guid.NewGuid() };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123"
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);
var result = await _command.Run(user);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<NotSupportedException>(unhandled.Exception);
}
[Fact]
public async Task Run_Organization_MissingPasswordManagerItem_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId };
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
@@ -77,11 +124,122 @@ public class RestartSubscriptionCommandTests
{
Data =
[
new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 },
new SubscriptionItem { Price = new Price { Id = "price_2" }, Quantity = 2 }
new SubscriptionItem { Price = new Price { Id = "some-other-price-id" }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["key"] = "value" }
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Organization's subscription does not have a Password Manager subscription item.", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_PlanNotFound_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = "some-price-id" }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
// Return a plan list that doesn't contain the organization's plan type
_pricingClient.ListPlans().Returns([MockPlans.Get(PlanType.TeamsAnnually)]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Could not find plan for organization's plan type", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_DisabledPlanWithNoEnabledReplacement_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var oldPlan = new DisabledEnterprisePlan2023(true);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_old",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
// Return only the disabled plan, with no enabled replacement
_pricingClient.ListPlans().Returns([oldPlan]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Could not find the current, enabled plan for organization's tier and cadence", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_WithNonDisabledPlan_PasswordManagerOnly_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
@@ -89,30 +247,26 @@ public class RestartSubscriptionCommandTests
Id = "sub_new",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is((SubscriptionCreateOptions options) =>
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.CollectionMethod == CollectionMethod.ChargeAutomatically &&
options.Customer == "cus_123" &&
options.Items.Count == 2 &&
options.Items[0].Price == "price_1" &&
options.Items[0].Quantity == 1 &&
options.Items[1].Price == "price_2" &&
options.Items[1].Quantity == 2 &&
options.Metadata["key"] == "value" &&
options.Items.Count == 1 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 10 &&
options.Metadata["organizationId"] == organizationId.ToString() &&
options.OffSession == true &&
options.TrialPeriodDays == 0));
@@ -120,96 +274,417 @@ public class RestartSubscriptionCommandTests
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new" &&
org.Enabled == true &&
org.ExpirationDate == currentPeriodEnd));
org.ExpirationDate == currentPeriodEnd &&
org.PlanType == PlanType.EnterpriseAnnually));
}
[Fact]
public async Task Run_Provider_Success_ReturnsNone()
public async Task Run_Organization_WithNonDisabledPlan_WithStorage_Success()
{
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
},
Metadata = new Dictionary<string, string>()
};
var newSubscription = new Subscription
{
Id = "sub_new",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
]
}
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(provider);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _providerRepository.Received(1).ReplaceAsync(Arg.Is<Provider>(prov =>
prov.Id == providerId &&
prov.GatewaySubscriptionId == "sub_new" &&
prov.Enabled == true));
}
[Fact]
public async Task Run_User_Success_ReturnsNone()
{
var userId = Guid.NewGuid();
var user = new User { Id = userId };
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsAnnually
};
var plan = MockPlans.Get(PlanType.TeamsAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
},
Metadata = new Dictionary<string, string>()
};
var newSubscription = new Subscription
{
Id = "sub_new",
CustomerId = "cus_456",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 5 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 3 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_new_2",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(user);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 5 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 3));
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == userId &&
u.GatewaySubscriptionId == "sub_new" &&
u.Premium == true &&
u.PremiumExpirationDate == currentPeriodEnd));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new_2" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithSecretsManager_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseMonthly
};
var plan = MockPlans.Get(PlanType.EnterpriseMonthly);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_789",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 15 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 2 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 10 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId }, Quantity = 100 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_new_3",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 4 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 15 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 2 &&
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[2].Quantity == 10 &&
options.Items[3].Price == plan.SecretsManager.StripeServiceAccountPlanId &&
options.Items[3].Quantity == 100));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new_3" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithDisabledPlan_UpgradesToNewPlan_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var oldPlan = new DisabledEnterprisePlan2023(true);
var newPlan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_old",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 },
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeStoragePlanId }, Quantity = 5 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_upgraded",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([oldPlan, newPlan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == newPlan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 20 &&
options.Items[1].Price == newPlan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 5));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_upgraded" &&
org.Enabled == true &&
org.PlanType == PlanType.EnterpriseAnnually &&
org.Plan == newPlan.Name &&
org.SelfHost == newPlan.HasSelfHost &&
org.UsePolicies == newPlan.HasPolicies &&
org.UseGroups == newPlan.HasGroups &&
org.UseDirectory == newPlan.HasDirectory &&
org.UseEvents == newPlan.HasEvents &&
org.UseTotp == newPlan.HasTotp &&
org.Use2fa == newPlan.Has2fa &&
org.UseApi == newPlan.HasApi &&
org.UseSso == newPlan.HasSso &&
org.UseOrganizationDomains == newPlan.HasOrganizationDomains &&
org.UseKeyConnector == newPlan.HasKeyConnector &&
org.UseScim == newPlan.HasScim &&
org.UseResetPassword == newPlan.HasResetPassword &&
org.UsersGetPremium == newPlan.UsersGetPremium &&
org.UseCustomPermissions == newPlan.HasCustomPermissions));
}
[Fact]
public async Task Run_Organization_WithStorageAndSecretManagerButNoServiceAccounts_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsAnnually
};
var plan = MockPlans.Get(PlanType.TeamsAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_complex",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 12 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 8 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 6 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_complex",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 3 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 12 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 8 &&
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[2].Quantity == 6));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_complex" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithSecretsManagerOnly_NoServiceAccounts_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsMonthly
};
var plan = MockPlans.Get(PlanType.TeamsMonthly);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_sm",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 8 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 5 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_sm",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 8 &&
options.Items[1].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[1].Quantity == 5));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_sm" &&
org.Enabled == true));
}
private record DisabledEnterprisePlan2023 : Bit.Core.Models.StaticStore.Plan
{
public DisabledEnterprisePlan2023(bool isAnnual)
{
Type = PlanType.EnterpriseAnnually2023;
ProductTier = ProductTierType.Enterprise;
Name = "Enterprise (Annually) 2023";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameEnterprise";
DescriptionLocalizationKey = "planDescEnterprise";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasPolicies = true;
HasSelfHost = true;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
HasSso = true;
HasOrganizationDomains = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true;
HasCustomPermissions = true;
UpgradeSortOrder = 4;
DisplaySortOrder = 4;
LegacyYear = 2024;
Disabled = true;
PasswordManager = new PasswordManagerFeatures(isAnnual);
SecretsManager = new SecretsManagerFeatures(isAnnual);
}
private record SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually-2023";
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 12;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly-2023";
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 1;
}
}
}
private record PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2023-enterprise-org-seat-annually-old";
SeatPrice = 72;
}
else
{
StripeSeatPlanId = "2023-enterprise-seat-monthly-old";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 7;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Authorization;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Authorization;
[SutProviderCustomize]
public class KeyConnectorAuthorizationHandlerTests
{
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsOwner_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Owner }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsAdmin_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Admin }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsRegularMember_Success(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.User }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation");
var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));
}
}

View File

@@ -0,0 +1,125 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Commands;
[SutProviderCustomize]
public class SetKeyConnectorKeyCommandTests
{
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Set up valid V2 encryption data
if (data.AccountKeys!.SignatureKeyPair != null)
{
data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519";
}
var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();
// Arrange
user.UsesKeyConnector = false;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Success());
var userRepository = sutProvider.GetDependency<IUserRepository>();
var mockUpdateUserData = Substitute.For<UpdateUserData>();
userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)
.Returns(mockUpdateUserData);
// Act
await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);
// Assert
userRepository
.Received(1)
.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);
await userRepository
.Received(1)
.SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Is<UserAccountKeysData>(data =>
data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&
data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&
data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&
data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&
data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&
data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&
data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&
data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),
Arg.Is<IEnumerable<UpdateUserData>>(actions =>
actions.Count() == 1 && actions.First() == mockUpdateUserData));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.Received(1)
.AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());
}
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Failed());
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));
sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.DidNotReceiveWithAnyArgs()
.AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());
}
}

View File

@@ -1215,12 +1215,12 @@ public class CipherServiceTests
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
SutProvider<CipherService> sutProvider,
string newPassword,
bool viewPassword,
bool editPermission,
bool permission,
string? key = null,
string? totp = null,
CipherLoginFido2CredentialData[]? passkeys = null,
CipherFieldData[]? fields = null
CipherFieldData[]? fields = null,
string? existingKey = "OriginalKey"
)
{
var cipherDetails = new CipherDetails
@@ -1233,13 +1233,22 @@ public class CipherServiceTests
Key = key,
};
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys, Fields = fields };
var newLoginData = new CipherLoginData
{
Username = "user",
Password = newPassword,
Totp = totp,
Fido2Credentials = passkeys,
Fields = fields
};
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
var existingCipher = new Cipher
{
Id = cipherDetails.Id,
Type = CipherType.Login,
Key = existingKey,
Data = JsonSerializer.Serialize(
new CipherLoginData
{
@@ -1261,7 +1270,14 @@ public class CipherServiceTests
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
{
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
{
cipherDetails.Id,
new OrganizationCipherPermission
{
ViewPassword = permission,
Edit = permission
}
}
};
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
@@ -1278,7 +1294,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1294,7 +1310,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1310,7 +1326,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1326,7 +1342,11 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
var deps = GetSaveDetailsAsyncDependencies(
sutProvider,
newPassword: "NewPassword",
permission: true,
key: "NewKey");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1336,27 +1356,40 @@ public class CipherServiceTests
true);
Assert.Equal("NewKey", deps.CipherDetails.Key);
await sutProvider.GetDependency<ICipherRepository>()
.Received()
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "NewKey"));
}
[Theory, BitAutoData]
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
public async Task SaveDetailsAsync_CipherKeyNotChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
var deps = GetSaveDetailsAsyncDependencies(
sutProvider,
newPassword: "NewPassword",
permission: false,
key: "NewKey"
);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
deps.CipherDetails.UserId.Value,
deps.CipherDetails.RevisionDate,
null,
true));
true);
Assert.Contains("do not have permission", exception.Message);
Assert.Equal("OriginalKey", deps.CipherDetails.Key);
await sutProvider.GetDependency<ICipherRepository>()
.Received()
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "OriginalKey"));
}
[Theory, BitAutoData]
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, totp: "NewTotp");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1372,7 +1405,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, totp: "NewTotp");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1397,7 +1430,7 @@ public class CipherServiceTests
}
};
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, passkeys: passkeys);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1422,7 +1455,7 @@ public class CipherServiceTests
}
};
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, passkeys: passkeys);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1439,7 +1472,7 @@ public class CipherServiceTests
[BitAutoData]
public async Task SaveDetailsAsync_HiddenFieldsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: false, fields:
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, fields:
[
new CipherFieldData
{
@@ -1464,7 +1497,7 @@ public class CipherServiceTests
[BitAutoData]
public async Task SaveDetailsAsync_HiddenFieldsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, fields:
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, fields:
[
new CipherFieldData
{

View File

@@ -1,6 +1,7 @@
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -9,6 +10,7 @@ using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Events.Controllers;
using Bit.Events.Models;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
@@ -21,6 +23,7 @@ public class CollectControllerTests
private readonly IEventService _eventService;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
public CollectControllerTests()
{
@@ -28,12 +31,14 @@ public class CollectControllerTests
_eventService = Substitute.For<IEventService>();
_cipherRepository = Substitute.For<ICipherRepository>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_sut = new CollectController(
_currentContext,
_eventService,
_cipherRepository,
_organizationRepository
_organizationRepository,
_organizationUserRepository
);
}
@@ -74,6 +79,32 @@ public class CollectControllerTests
await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, eventDate);
}
[Theory]
[BitAutoData(EventType.Organization_ItemOrganization_Accepted)]
[BitAutoData(EventType.Organization_ItemOrganization_Declined)]
public async Task Post_Organization_ItemOrganization_LogsOrganizationUserEvent(
EventType type, Guid userId, Guid orgId, OrganizationUser orgUser)
{
_currentContext.UserId.Returns(userId);
orgUser.OrganizationId = orgId;
_organizationUserRepository.GetByOrganizationAsync(orgId, userId).Returns(orgUser);
var eventDate = DateTime.UtcNow;
var events = new List<EventModel>
{
new EventModel
{
Type = type,
OrganizationId = orgId,
Date = eventDate
}
};
var result = await _sut.Post(events);
Assert.IsType<OkResult>(result);
await _eventService.Received(1).LogOrganizationUserEventAsync(orgUser, type, eventDate);
}
[Theory]
[AutoData]
public async Task Post_CipherAutofilled_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)

View File

@@ -81,6 +81,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
var root = body.RootElement;
AssertRefreshTokenExists(root);
AssertHelper.AssertJsonProperty(root, "ForcePasswordReset", JsonValueKind.False);
AssertHelper.AssertJsonProperty(root, "ResetMasterPassword", JsonValueKind.False);
var kdf = AssertHelper.AssertJsonProperty(root, "Kdf", JsonValueKind.Number).GetInt32();
Assert.Equal(0, kdf);
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();

Some files were not shown because too many files have changed in this diff Show More