1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-20 02:48:03 -05:00

Merge branch 'km/db-signing-keys' into km/signing-api-changes

This commit is contained in:
Bernd Schoolmann 2025-06-16 09:43:02 +02:00 committed by GitHub
commit e0a6fd7af6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 11811 additions and 1394 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"swashbuckle.aspnetcore.cli": { "swashbuckle.aspnetcore.cli": {
"version": "7.2.0", "version": "7.3.2",
"commands": ["swagger"] "commands": ["swagger"]
}, },
"dotnet-ef": { "dotnet-ef": {

View File

@ -350,14 +350,6 @@ jobs:
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../.. cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../.. cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
- name: Make Docker stub checksums
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
run: |
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
- name: Upload Docker stub US artifact - name: Upload Docker stub US artifact
if: | if: |
github.event_name != 'pull_request' github.event_name != 'pull_request'
@ -378,26 +370,6 @@ jobs:
path: docker-stub-EU.zip path: docker-stub-EU.zip
if-no-files-found: error if-no-files-found: error
- name: Upload Docker stub US checksum artifact
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt
if-no-files-found: error
- name: Upload Docker stub EU checksum artifact
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt
if-no-files-found: error
- name: Build Public API Swagger - name: Build Public API Swagger
run: | run: |
cd ./src/Api cd ./src/Api

View File

@ -17,6 +17,9 @@ on:
env: env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io" _AZ_REGISTRY: "bitwardenprod.azurecr.io"
permissions:
contents: read
jobs: jobs:
setup: setup:
name: Setup name: Setup
@ -65,9 +68,7 @@ jobs:
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }} branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "docker-stub-US.zip, artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip, docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json" swagger.json"
- name: Dry Run - Download latest release Docker stubs - name: Dry Run - Download latest release Docker stubs
@ -78,9 +79,7 @@ jobs:
workflow_conclusion: success workflow_conclusion: success
branch: main branch: main
artifacts: "docker-stub-US.zip, artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip, docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json" swagger.json"
- name: Create release - name: Create release
@ -88,9 +87,7 @@ jobs:
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
with: with:
artifacts: "docker-stub-US.zip, artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip, docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json" swagger.json"
commit: ${{ github.sha }} commit: ${{ github.sha }}
tag: "v${{ needs.setup.outputs.release_version }}" tag: "v${{ needs.setup.outputs.release_version }}"

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.6.0</Version> <Version>2025.6.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -550,6 +550,15 @@ public class ProviderBillingService(
[ [
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
]; ];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
options.TaxIdData.Add(new CustomerTaxIdDataOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{taxInfo.TaxIdNumber}"
});
}
} }
if (!string.IsNullOrEmpty(provider.DiscountId)) if (!string.IsNullOrEmpty(provider.DiscountId))

View File

@ -499,9 +499,9 @@ public class AccountController : Controller
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one // Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
if (orgUser == null && organization.Seats.HasValue) if (orgUser == null && organization.Seats.HasValue)
{ {
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var occupiedSeats = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var initialSeatCount = organization.Seats.Value; var initialSeatCount = organization.Seats.Value;
var availableSeats = initialSeatCount - occupiedSeats; var availableSeats = initialSeatCount - occupiedSeats.Total;
if (availableSeats < 1) if (availableSeats < 1)
{ {
try try

View File

@ -242,10 +242,32 @@ public class OrganizationsController : Controller
Seats = organization.Seats Seats = organization.Seats
}; };
if (model.PlanType.HasValue)
{
var freePlan = await _pricingClient.GetPlanOrThrow(model.PlanType.Value);
var isDowngradingToFree = organization.PlanType != PlanType.Free && model.PlanType.Value == PlanType.Free;
if (isDowngradingToFree)
{
if (model.Seats.HasValue && model.Seats.Value > freePlan.PasswordManager.MaxSeats)
{
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxSeats} seats cannot be downgraded to the Free plan";
return RedirectToAction("Edit", new { id });
}
if (model.MaxCollections > freePlan.PasswordManager.MaxCollections)
{
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxCollections} collections cannot be downgraded to the Free plan. Your organization currently has {organization.MaxCollections} collections.";
return RedirectToAction("Edit", new { id });
}
model.MaxStorageGb = null;
model.ExpirationDate = null;
model.Enabled = true;
}
}
UpdateOrganization(organization, model); UpdateOrganization(organization, model);
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (organization.UseSecretsManager && !plan.SupportsSecretsManager) if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
{ {
TempData["Error"] = "Plan does not support Secrets Manager"; TempData["Error"] = "Plan does not support Secrets Manager";

View File

@ -521,7 +521,9 @@ public class OrganizationUsersController : Controller
.Concat(readonlyCollectionAccess) .Concat(readonlyCollectionAccess)
.ToList(); .ToList();
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId, var existingUserType = organizationUser.Type;
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), existingUserType, userId,
collectionsToSave, groupsToSave); collectionsToSave, groupsToSave);
} }

View File

@ -177,9 +177,10 @@ public class MembersController : Controller
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
var existingUserType = existingUser.Type;
var updatedUser = model.ToOrganizationUser(existingUser); var updatedUser = model.ToOrganizationUser(existingUser);
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(); var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();
await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, null, associations, model.Groups); await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups);
MemberResponseModel response = null; MemberResponseModel response = null;
if (existingUser.UserId.HasValue) if (existingUser.UserId.HasValue)
{ {

View File

@ -34,7 +34,7 @@
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" /> <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" /> <PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" /> <PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,6 +4,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Api.Billing.Queries.Organizations; using Bit.Api.Billing.Queries.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
@ -280,17 +281,36 @@ public class OrganizationBillingController(
} }
var organization = await organizationRepository.GetByIdAsync(organizationId); var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null) if (organization == null)
{ {
return Error.NotFound(); return Error.NotFound();
} }
var existingPlan = organization.PlanType;
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup); var sale = OrganizationSale.From(organization, organizationSignup);
var plan = await pricingClient.GetPlanOrThrow(model.PlanType); var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type; sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name; sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;
if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
{
sale.Organization.UseTotp = plan.HasTotp;
sale.Organization.UseGroups = plan.HasGroups;
sale.Organization.UseDirectory = plan.HasDirectory;
sale.Organization.SelfHost = plan.HasSelfHost;
sale.Organization.UsersGetPremium = plan.UsersGetPremium;
sale.Organization.UseEvents = plan.HasEvents;
sale.Organization.Use2fa = plan.Has2fa;
sale.Organization.UseApi = plan.HasApi;
sale.Organization.UsePolicies = plan.HasPolicies;
sale.Organization.UseSso = plan.HasSso;
sale.Organization.UseResetPassword = plan.HasResetPassword;
sale.Organization.UseKeyConnector = plan.HasKeyConnector;
sale.Organization.UseScim = plan.HasScim;
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
}
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken)) if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
{ {

View File

@ -8,7 +8,7 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers; namespace Bit.Api.Dirt.Controllers;
[Route("hibp")] [Route("hibp")]
[Authorize("Application")] [Authorize("Application")]

View File

@ -1,16 +1,16 @@
using Bit.Api.Tools.Models; using Bit.Api.Dirt.Models;
using Bit.Api.Tools.Models.Response; using Bit.Api.Dirt.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers; namespace Bit.Api.Dirt.Controllers;
[Route("reports")] [Route("reports")]
[Authorize("Application")] [Authorize("Application")]
@ -47,7 +47,7 @@ public class ReportsController : Controller
[HttpGet("member-cipher-details/{orgId}")] [HttpGet("member-cipher-details/{orgId}")]
public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId) public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId)
{ {
// Using the AccessReports permission here until new permissions // Using the AccessReports permission here until new permissions
// are needed for more control over reports // are needed for more control over reports
if (!await _currentContext.AccessReports(orgId)) if (!await _currentContext.AccessReports(orgId))
{ {
@ -84,7 +84,7 @@ public class ReportsController : Controller
} }
/// <summary> /// <summary>
/// Contains the organization member info, the cipher ids associated with the member, /// Contains the organization member info, the cipher ids associated with the member,
/// and details on their collections, groups, and permissions /// and details on their collections, groups, and permissions
/// </summary> /// </summary>
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param> /// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>

View File

@ -1,4 +1,4 @@
namespace Bit.Api.Tools.Models; namespace Bit.Api.Dirt.Models;
public class PasswordHealthReportApplicationModel public class PasswordHealthReportApplicationModel
{ {

View File

@ -1,10 +1,10 @@
using Bit.Core.Tools.Models.Data; using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response; namespace Bit.Api.Dirt.Models.Response;
/// <summary> /// <summary>
/// Contains the collections and group collections a user has access to including /// Contains the collections and group collections a user has access to including
/// the permission level for the collection and group collection. /// the permission level for the collection and group collection.
/// </summary> /// </summary>
public class MemberAccessReportResponseModel public class MemberAccessReportResponseModel
{ {

View File

@ -1,6 +1,6 @@
using Bit.Core.Tools.Models.Data; using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response; namespace Bit.Api.Dirt.Models.Response;
public class MemberCipherDetailsResponseModel public class MemberCipherDetailsResponseModel
{ {

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
namespace Bit.Api.Models.Public.Response; namespace Bit.Api.Models.Public.Response;
@ -20,6 +21,7 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
Id = collection.Id; Id = collection.Id;
ExternalId = collection.ExternalId; ExternalId = collection.ExternalId;
Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c)); Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));
Type = collection.Type;
} }
/// <summary> /// <summary>
@ -38,4 +40,8 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
/// The associated groups that this collection is assigned to. /// The associated groups that this collection is assigned to.
/// </summary> /// </summary>
public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; } public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; }
/// <summary>
/// The type of this collection
/// </summary>
public CollectionType Type { get; set; }
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -18,12 +19,14 @@ public class CollectionResponseModel : ResponseModel
OrganizationId = collection.OrganizationId; OrganizationId = collection.OrganizationId;
Name = collection.Name; Name = collection.Name;
ExternalId = collection.ExternalId; ExternalId = collection.ExternalId;
Type = collection.Type;
} }
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string ExternalId { get; set; } public string ExternalId { get; set; }
public CollectionType Type { get; set; }
} }
/// <summary> /// <summary>

View File

@ -31,8 +31,8 @@ using Bit.Api.Billing;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.SendFeatures;
using Bit.Api.KeyManagement; using Bit.Api.KeyManagement;

View File

@ -42,7 +42,6 @@ public class CiphersController : Controller
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ILogger<CiphersController> _logger; private readonly ILogger<CiphersController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
@ -57,7 +56,6 @@ public class CiphersController : Controller
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<CiphersController> logger, ILogger<CiphersController> logger,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IFeatureService featureService,
IOrganizationCiphersQuery organizationCiphersQuery, IOrganizationCiphersQuery organizationCiphersQuery,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository) ICollectionRepository collectionRepository)
@ -71,7 +69,6 @@ public class CiphersController : Controller
_currentContext = currentContext; _currentContext = currentContext;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_featureService = featureService;
_organizationCiphersQuery = organizationCiphersQuery; _organizationCiphersQuery = organizationCiphersQuery;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
@ -375,11 +372,6 @@ public class CiphersController : Controller
private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds) private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
{
return await CanEditCipherAsAdminAsync(organizationId, cipherIds);
}
var org = _currentContext.GetOrganization(organizationId); var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin" or if we're a provider user we don't need to check the ciphers // If we're not an "admin" or if we're a provider user we don't need to check the ciphers

View File

@ -10,7 +10,7 @@
<ProjectReference Include="..\Core\Core.csproj" /> <ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models; using Bit.Core.Models;
@ -9,23 +10,75 @@ using Bit.Core.Utilities;
namespace Bit.Core.Entities; namespace Bit.Core.Entities;
/// <summary>
/// An association table between one <see cref="User"/> and one <see cref="Organization"/>, representing that user's
/// membership in the organization. "Member" refers to the OrganizationUser object.
/// </summary>
public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser
{ {
/// <summary>
/// A unique random identifier.
/// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// The ID of the Organization that the user is a member of.
/// </summary>
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
/// <summary>
/// The ID of the User that is the member. This is NULL if the Status is Invited (or Invited and then Revoked), because
/// it is not linked to a specific User yet.
/// </summary>
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
/// <summary>
/// The email address of the user invited to the organization. This is NULL if the Status is not Invited (or
/// Invited and then Revoked), because in that case the OrganizationUser is linked to a User
/// and the email is stored on the User object.
/// </summary>
[MaxLength(256)] [MaxLength(256)]
public string? Email { get; set; } public string? Email { get; set; }
/// <summary>
/// The Organization symmetric key encrypted with the User's public key. NULL if the user is not in a Confirmed
/// (or Confirmed and then Revoked) status.
/// </summary>
public string? Key { get; set; } public string? Key { get; set; }
/// <summary>
/// The User's symmetric key encrypted with the Organization's public key. NULL if the OrganizationUser
/// is not enrolled in account recovery.
/// </summary>
public string? ResetPasswordKey { get; set; } public string? ResetPasswordKey { get; set; }
/// <inheritdoc cref="OrganizationUserStatusType"/>
public OrganizationUserStatusType Status { get; set; } public OrganizationUserStatusType Status { get; set; }
/// <summary>
/// The User's role in the Organization.
/// </summary>
public OrganizationUserType Type { get; set; } public OrganizationUserType Type { get; set; }
/// <summary>
/// An ID used to identify the OrganizationUser with an external directory service. Used by Directory Connector
/// and SCIM.
/// </summary>
[MaxLength(300)] [MaxLength(300)]
public string? ExternalId { get; set; } public string? ExternalId { get; set; }
/// <summary>
/// The date the OrganizationUser was created, i.e. when the User was first invited to the Organization.
/// </summary>
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
/// <summary>
/// The last date the OrganizationUser entry was updated.
/// </summary>
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
/// <summary>
/// A json blob representing the <see cref="Bit.Core.Models.Data.Permissions"/> of the OrganizationUser if they
/// are a Custom user role (i.e. the <see cref="OrganizationUserType"/> is Custom). MAY be NULL if they are not
/// a custom user, but this is not guaranteed; do not use this to determine their role.
/// </summary>
/// <remarks>
/// Avoid using this property directly - instead use the <see cref="GetPermissions"/> and <see cref="SetPermissions"/>
/// helper methods.
/// </remarks>
public string? Permissions { get; set; } public string? Permissions { get; set; }
/// <summary>
/// True if the User has access to Secrets Manager for this Organization, false otherwise.
/// </summary>
public bool AccessSecretsManager { get; set; } public bool AccessSecretsManager { get; set; }
public void SetNewId() public void SetNewId()

View File

@ -1,9 +1,34 @@
namespace Bit.Core.Enums; using Bit.Core.Entities;
namespace Bit.Core.Enums;
/// <summary>
/// Represents the different stages of a member's lifecycle in an organization.
/// The <see cref="OrganizationUser"/> object is populated differently depending on their Status.
/// </summary>
public enum OrganizationUserStatusType : short public enum OrganizationUserStatusType : short
{ {
/// <summary>
/// The OrganizationUser entry only represents an invitation to join the organization. It is not linked to a
/// specific User yet.
/// </summary>
Invited = 0, Invited = 0,
/// <summary>
/// The User has accepted the invitation and linked their User account to the OrganizationUser entry.
/// </summary>
Accepted = 1, Accepted = 1,
/// <summary>
/// An administrator has granted the User access to the organization. This is the final step in the User becoming
/// a "full" member of the organization, including a key exchange so that they can decrypt organization data.
/// </summary>
Confirmed = 2, Confirmed = 2,
/// <summary>
/// The OrganizationUser has been revoked from the organization and cannot access organization data while in this state.
/// </summary>
/// <remarks>
/// An OrganizationUser may move into this status from any other status, and will move back to their original status
/// if restored. This allows an administrator to easily suspend and restore access without going through the
/// Invite flow again.
/// </remarks>
Revoked = -1, Revoked = -1,
} }

View File

@ -1,11 +1,12 @@
#nullable enable #nullable enable
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IUpdateOrganizationUserCommand public interface IUpdateOrganizationUserCommand
{ {
Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType, Guid? savingUserId,
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess); List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess);
} }

View File

@ -87,7 +87,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
InviteOrganization = request.InviteOrganization, InviteOrganization = request.InviteOrganization,
PerformedBy = request.PerformedBy, PerformedBy = request.PerformedBy,
PerformedAt = request.PerformedAt, PerformedAt = request.PerformedAt,
OccupiedPmSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId), OccupiedPmSeats = (await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)).Total,
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId) OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
}); });

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.AdminConsole.Utilities.Validation;
@ -83,14 +84,9 @@ public class InviteUsersPasswordManagerValidator(
return invalidEnvironment.Map(request); return invalidEnvironment.Map(request);
} }
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization); // Organizations managed by a provider need to be scaled by the provider. This needs to be checked in the event seats are increasing.
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
{
return organizationValidation.Map(request);
}
var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId); var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
if (provider is not null) if (provider is not null)
{ {
var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider)); var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider));
@ -101,6 +97,13 @@ public class InviteUsersPasswordManagerValidator(
} }
} }
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
{
return organizationValidation.Map(request);
}
var paymentSubscription = await paymentService.GetSubscriptionAsync( var paymentSubscription = await paymentService.GetSubscriptionAsync(
await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId)); await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));

View File

@ -1,10 +1,9 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
public static class InviteUserPaymentValidation public static class InviteUserPaymentValidation
{ {

View File

@ -70,8 +70,8 @@ public class RestoreOrganizationUserCommand(
} }
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId); var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats; var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
if (availableSeats < 1) if (availableSeats < 1)
{ {
@ -163,8 +163,8 @@ public class RestoreOrganizationUserCommand(
} }
var organization = await organizationRepository.GetByIdAsync(organizationId); var organization = await organizationRepository.GetByIdAsync(organizationId);
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats; var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
var newSeatsRequired = organizationUserIds.Count() - availableSeats; var newSeatsRequired = organizationUserIds.Count() - availableSeats;
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired); await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);

View File

@ -55,11 +55,13 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
/// Update an organization user. /// Update an organization user.
/// </summary> /// </summary>
/// <param name="organizationUser">The modified organization user to save.</param> /// <param name="organizationUser">The modified organization user to save.</param>
/// <param name="existingUserType">The current type (member role) of the user.</param>
/// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param> /// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param>
/// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param> /// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param>
/// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param> /// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param>
/// <exception cref="BadRequestException"></exception> /// <exception cref="BadRequestException"></exception>
public async Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, public async Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType,
Guid? savingUserId,
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess) List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)
{ {
// Avoid multiple enumeration // Avoid multiple enumeration
@ -83,15 +85,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (organizationUser.UserId.HasValue && organization.PlanType == PlanType.Free && organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner) await EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(organizationUser, existingUserType, organization);
{
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(organizationUser.UserId.Value);
if (adminCount > 0)
{
throw new BadRequestException("User can only be an admin of one free organization.");
}
}
if (collectionAccessList.Count != 0) if (collectionAccessList.Count != 0)
{ {
@ -151,6 +145,40 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated);
} }
private async Task EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(OrganizationUser updatedOrgUser, OrganizationUserType existingUserType, Entities.Organization organization)
{
if (organization.PlanType != PlanType.Free)
{
return;
}
if (!updatedOrgUser.UserId.HasValue)
{
return;
}
if (updatedOrgUser.Type is not (OrganizationUserType.Admin or OrganizationUserType.Owner))
{
return;
}
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(updatedOrgUser.UserId!.Value);
var isCurrentAdminOrOwner = existingUserType is OrganizationUserType.Admin or OrganizationUserType.Owner;
if (isCurrentAdminOrOwner && adminCount <= 1)
{
return;
}
if (!isCurrentAdminOrOwner && adminCount == 0)
{
return;
}
throw new BadRequestException("User can only be an admin of one free organization.");
}
private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser, private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser,
ICollection<CollectionAccessSelection> collectionAccess) ICollection<CollectionAccessSelection> collectionAccess)
{ {

View File

@ -104,8 +104,8 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages)); throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
} }
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => await Task.WhenAll(nonCompliantUsers.Select(nonCompliantUser =>
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), nonCompliantUser.user.Email)));
} }
private static bool MembersWithNoMasterPasswordWillLoseAccess( private static bool MembersWithNoMasterPasswordWillLoseAccess(

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
#nullable enable #nullable enable
@ -25,4 +26,14 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId); Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType); Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids); Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
/// <summary>
/// Returns the number of occupied seats for an organization.
/// OrganizationUsers occupy a seat, unless they are revoked.
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
/// <returns>The number of occupied seats for the organization.</returns>
Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
} }

View File

@ -18,16 +18,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId); Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type); Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers); Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
/// <summary>
/// Returns the number of occupied seats for an organization.
/// Occupied seats are OrganizationUsers that have at least been invited.
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
/// <returns>The number of occupied seats for the organization.</returns>
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers); Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId); Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id); Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);

View File

@ -294,11 +294,20 @@ public class OrganizationService : IOrganizationService
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal) if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
{ {
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > newSeatTotal)
if (seatCounts.Total > newSeatTotal)
{ {
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " + if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
$"Your new plan only has ({newSeatTotal}) seats. Remove some users."); {
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
}
else
{
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
}
} }
} }
@ -726,8 +735,8 @@ public class OrganizationService : IOrganizationService
var newSeatsRequired = 0; var newSeatsRequired = 0;
if (organization.Seats.HasValue) if (organization.Seats.HasValue)
{ {
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.Value - occupiedSeats; var availableSeats = organization.Seats.Value - seatCounts.Total;
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats; newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
} }
@ -1177,8 +1186,8 @@ public class OrganizationService : IOrganizationService
var enoughSeatsAvailable = true; var enoughSeatsAvailable = true;
if (organization.Seats.HasValue) if (organization.Seats.HasValue)
{ {
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
seatsAvailable = organization.Seats.Value - occupiedSeats; seatsAvailable = organization.Seats.Value - seatCounts.Total;
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count; enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
} }

View File

@ -96,6 +96,12 @@ public static class StripeConstants
public const string Reverse = "reverse"; public const string Reverse = "reverse";
} }
public static class TaxIdType
{
public const string EUVAT = "eu_vat";
public const string SpanishNIF = "es_cif";
}
public static class ValidateTaxLocationTiming public static class ValidateTaxLocationTiming
{ {
public const string Deferred = "deferred"; public const string Deferred = "deferred";

View File

@ -31,6 +31,7 @@ public record PlanAdapter : Plan
HasScim = HasFeature("scim"); HasScim = HasFeature("scim");
HasResetPassword = HasFeature("resetPassword"); HasResetPassword = HasFeature("resetPassword");
UsersGetPremium = HasFeature("usersGetPremium"); UsersGetPremium = HasFeature("usersGetPremium");
HasCustomPermissions = HasFeature("customPermissions");
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder) UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
? int.Parse(upgradeSortOrder) ? int.Parse(upgradeSortOrder)
: 0; : 0;
@ -141,6 +142,7 @@ public record PlanAdapter : Plan
var stripeSeatPlanId = GetStripeSeatPlanId(seats); var stripeSeatPlanId = GetStripeSeatPlanId(seats);
var hasAdditionalSeatsOption = seats.IsScalable; var hasAdditionalSeatsOption = seats.IsScalable;
var seatPrice = GetSeatPrice(seats); var seatPrice = GetSeatPrice(seats);
var baseSeats = GetBaseSeats(seats);
var maxSeats = GetMaxSeats(seats); var maxSeats = GetMaxSeats(seats);
var allowSeatAutoscale = seats.IsScalable; var allowSeatAutoscale = seats.IsScalable;
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0; var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
@ -156,6 +158,7 @@ public record PlanAdapter : Plan
StripeSeatPlanId = stripeSeatPlanId, StripeSeatPlanId = stripeSeatPlanId,
HasAdditionalSeatsOption = hasAdditionalSeatsOption, HasAdditionalSeatsOption = hasAdditionalSeatsOption,
SeatPrice = seatPrice, SeatPrice = seatPrice,
BaseSeats = baseSeats,
MaxSeats = maxSeats, MaxSeats = maxSeats,
AllowSeatAutoscale = allowSeatAutoscale, AllowSeatAutoscale = allowSeatAutoscale,
MaxProjects = maxProjects MaxProjects = maxProjects
@ -168,8 +171,16 @@ public record PlanAdapter : Plan
private static decimal GetBasePrice(PurchasableDTO purchasable) private static decimal GetBasePrice(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.Price); => purchasable.FromPackaged(x => x.Price);
private static int GetBaseSeats(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.Match(
free => free.Quantity,
scalable => scalable.Provided);
private static int GetBaseSeats(PurchasableDTO purchasable) private static int GetBaseSeats(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.Quantity); => purchasable.Match(
free => free.Quantity,
packaged => packaged.Quantity,
scalable => scalable.Provided);
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable) private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.Match( => freeOrScalable.Match(

View File

@ -31,7 +31,6 @@ public class OrganizationBillingService(
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger, ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPricingClient pricingClient, IPricingClient pricingClient,
ISetupIntentCache setupIntentCache, ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
@ -78,14 +77,14 @@ public class OrganizationBillingService(
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
var isManaged = organization.Status == OrganizationStatusType.Managed; var isManaged = organization.Status == OrganizationStatusType.Managed;
var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
return OrganizationMetadata.Default with return OrganizationMetadata.Default with
{ {
IsEligibleForSelfHost = isEligibleForSelfHost, IsEligibleForSelfHost = isEligibleForSelfHost,
IsManaged = isManaged, IsManaged = isManaged,
OrganizationOccupiedSeats = orgOccupiedSeats OrganizationOccupiedSeats = orgOccupiedSeats.Total
}; };
} }
@ -120,7 +119,7 @@ public class OrganizationBillingService(
invoice?.DueDate, invoice?.DueDate,
invoice?.Created, invoice?.Created,
subscription.CurrentPeriodEnd, subscription.CurrentPeriodEnd,
orgOccupiedSeats); orgOccupiedSeats.Total);
} }
public async Task public async Task
@ -247,12 +246,23 @@ public class OrganizationBillingService(
organization.Id, organization.Id,
customerSetup.TaxInformation.Country, customerSetup.TaxInformation.Country,
customerSetup.TaxInformation.TaxId); customerSetup.TaxInformation.TaxId);
throw new BadRequestException("billingTaxIdTypeInferenceError");
} }
customerCreateOptions.TaxIdData = customerCreateOptions.TaxIdData =
[ [
new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId }
]; ];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
customerCreateOptions.TaxIdData.Add(new CustomerTaxIdDataOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{customerSetup.TaxInformation.TaxId}"
});
}
} }
var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource;

View File

@ -648,6 +648,12 @@ public class SubscriberService(
{ {
await stripeAdapter.TaxIdCreateAsync(customer.Id, await stripeAdapter.TaxIdCreateAsync(customer.Id,
new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId }); new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId });
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
await stripeAdapter.TaxIdCreateAsync(customer.Id,
new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $"ES{taxInformation.TaxId}" });
}
} }
catch (StripeException e) catch (StripeException e)
{ {

View File

@ -80,6 +80,15 @@ public class PreviewTaxAmountCommand(
Value = taxInformation.TaxId Value = taxInformation.TaxId
} }
]; ];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{parameters.TaxInformation.TaxId}"
});
}
} }
if (planType.GetProductTier() == ProductTierType.Families) if (planType.GetProductTier() == ProductTierType.Families)

View File

@ -181,6 +181,8 @@ public static class FeatureFlagKeys
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder"; public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
public const string MobileErrorReporting = "mobile-error-reporting"; public const string MobileErrorReporting = "mobile-error-reporting";
public const string AndroidChromeAutofill = "android-chrome-autofill"; public const string AndroidChromeAutofill = "android-chrome-autofill";
public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings";
public const string AppIntents = "app-intents";
/* Platform Team */ /* Platform Team */
public const string PersistPopupView = "persist-popup-view"; public const string PersistPopupView = "persist-popup-view";

View File

@ -1,9 +1,9 @@
using Bit.Core.Entities; #nullable enable
using Bit.Core.Entities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
#nullable enable namespace Bit.Core.Dirt.Reports.Entities;
namespace Bit.Core.Tools.Entities;
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
{ {

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Tools.Models.Data; namespace Bit.Core.Dirt.Reports.Models.Data;
public class MemberAccessDetails public class MemberAccessDetails
{ {
@ -30,13 +30,13 @@ public class MemberAccessCipherDetails
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }
/// <summary> /// <summary>
/// The details for the member's collection access depending /// The details for the member's collection access depending
/// on the collections and groups they are assigned to /// on the collections and groups they are assigned to
/// </summary> /// </summary>
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; } public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
/// <summary> /// <summary>
/// A distinct list of the cipher ids associated with /// A distinct list of the cipher ids associated with
/// the organization member /// the organization member
/// </summary> /// </summary>
public IEnumerable<string> CipherIds { get; set; } public IEnumerable<string> CipherIds { get; set; }

View File

@ -1,11 +1,11 @@
using Bit.Core.Exceptions; using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Repositories;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand
{ {

View File

@ -1,9 +1,9 @@
using Bit.Core.Exceptions; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Tools.Repositories; using Bit.Core.Exceptions;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand
{ {

View File

@ -1,9 +1,9 @@
using Bit.Core.Exceptions; using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Tools.Entities; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Tools.Repositories; using Bit.Core.Exceptions;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery
{ {

View File

@ -1,7 +1,7 @@
using Bit.Core.Tools.Entities; using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.Interfaces; namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IAddPasswordHealthReportApplicationCommand public interface IAddPasswordHealthReportApplicationCommand
{ {

View File

@ -1,6 +1,6 @@
using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.Interfaces; namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IDropPasswordHealthReportApplicationCommand public interface IDropPasswordHealthReportApplicationCommand
{ {

View File

@ -1,6 +1,6 @@
using Bit.Core.Tools.Entities; using Bit.Core.Dirt.Reports.Entities;
namespace Bit.Core.Tools.ReportFeatures.Interfaces; namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
public interface IGetPasswordHealthReportApplicationQuery public interface IGetPasswordHealthReportApplicationQuery
{ {

View File

@ -2,21 +2,21 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries; using Bit.Core.Vault.Queries;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Dirt.Reports.ReportFeatures;
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
{ {

View File

@ -1,7 +1,7 @@
using Bit.Core.Tools.Models.Data; using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
public interface IMemberAccessCipherDetailsQuery public interface IMemberAccessCipherDetailsQuery
{ {

View File

@ -1,8 +1,8 @@
using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Tools.ReportFeatures; namespace Bit.Core.Dirt.Reports.ReportFeatures;
public static class ReportingServiceCollectionExtensions public static class ReportingServiceCollectionExtensions
{ {

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Tools.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class AddPasswordHealthReportApplicationRequest public class AddPasswordHealthReportApplicationRequest
{ {

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Tools.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class DropPasswordHealthReportApplicationRequest public class DropPasswordHealthReportApplicationRequest
{ {

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Tools.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class MemberAccessCipherDetailsRequest public class MemberAccessCipherDetailsRequest
{ {

View File

@ -1,7 +1,7 @@
using Bit.Core.Repositories; using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Tools.Entities; using Bit.Core.Repositories;
namespace Bit.Core.Tools.Repositories; namespace Bit.Core.Dirt.Reports.Repositories;
public interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid> public interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid>
{ {

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
#nullable enable #nullable enable
@ -14,6 +15,8 @@ public class Collection : ITableObject<Guid>
public string? ExternalId { get; set; } public string? ExternalId { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow; public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow; public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public CollectionType Type { get; set; } = CollectionType.SharedCollection;
public string? DefaultUserCollectionEmail { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums;
public enum CollectionType
{
SharedCollection = 0,
DefaultUserCollection = 1,
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
public class OrganizationSeatCounts
{
public int Users { get; set; }
public int Sponsored { get; set; }
public int Total => Users + Sponsored;
}

View File

@ -16,7 +16,7 @@ public class CreateSponsorshipCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService, IUserService userService,
IOrganizationService organizationService, IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand IOrganizationRepository organizationRepository) : ICreateSponsorshipCommand
{ {
public async Task<OrganizationSponsorship> CreateSponsorshipAsync( public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
Organization sponsoringOrganization, Organization sponsoringOrganization,
@ -89,8 +89,8 @@ public class CreateSponsorshipCommand(
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue) if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
{ {
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id); var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats; var availableSeats = sponsoringOrganization.Seats.Value - seatCounts.Total;
if (availableSeats <= 0) if (availableSeats <= 0)
{ {

View File

@ -107,12 +107,20 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0)); (newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats) if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
{ {
var occupiedSeats = var seatCounts =
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > updatedPasswordManagerSeats) if (seatCounts.Total > updatedPasswordManagerSeats)
{ {
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " + if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
{
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
}
else
{
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users."); $"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
}
} }
} }

View File

@ -842,7 +842,13 @@ public class StripePaymentService : IPaymentService
try try
{ {
await _stripeAdapter.TaxIdCreateAsync(customer.Id, await _stripeAdapter.TaxIdCreateAsync(customer.Id,
new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }); new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber });
if (taxInfo.TaxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $"ES{taxInfo.TaxIdNumber}" });
}
} }
catch (StripeException e) catch (StripeException e)
{ {
@ -1000,6 +1006,15 @@ public class StripePaymentService : IPaymentService
Value = parameters.TaxInformation.TaxId Value = parameters.TaxInformation.TaxId
} }
]; ];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{parameters.TaxInformation.TaxId}"
});
}
} }
if (!string.IsNullOrWhiteSpace(gatewayCustomerId)) if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
@ -1154,6 +1169,15 @@ public class StripePaymentService : IPaymentService
Value = parameters.TaxInformation.TaxId Value = parameters.TaxInformation.TaxId
} }
]; ];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{parameters.TaxInformation.TaxId}"
});
}
} }
Customer gatewayCustomer = null; Customer gatewayCustomer = null;

View File

@ -12,7 +12,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
@ -29,12 +28,9 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Fido2NetLib; using Fido2NetLib;
using Fido2NetLib.Objects; using Fido2NetLib.Objects;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -44,12 +40,11 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Bit.Core.Services; namespace Bit.Core.Services;
public class UserService : UserManager<User>, IUserService, IDisposable public class UserService : UserManager<User>, IUserService
{ {
private const string PremiumPlanId = "premium-annually"; private const string PremiumPlanId = "premium-annually";
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IOrganizationDomainRepository _organizationDomainRepository;
@ -65,17 +60,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly IFido2 _fido2; private readonly IFido2 _fido2;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IStripeSyncService _stripeSyncService; private readonly IStripeSyncService _stripeSyncService;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IPremiumUserBillingService _premiumUserBillingService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache; private readonly IDistributedCache _distributedCache;
@ -83,7 +75,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public UserService( public UserService(
IUserRepository userRepository, IUserRepository userRepository,
ICipherRepository cipherRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository, IOrganizationDomainRepository organizationDomainRepository,
@ -101,7 +92,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
ILicensingService licenseService, ILicensingService licenseService,
IEventService eventService, IEventService eventService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IDataProtectionProvider dataProtectionProvider,
IPaymentService paymentService, IPaymentService paymentService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
@ -111,10 +101,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IAcceptOrgUserCommand acceptOrgUserCommand, IAcceptOrgUserCommand acceptOrgUserCommand,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IStripeSyncService stripeSyncService, IStripeSyncService stripeSyncService,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
IPremiumUserBillingService premiumUserBillingService, IPremiumUserBillingService premiumUserBillingService,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache, IDistributedCache distributedCache,
@ -131,7 +119,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
logger) logger)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationDomainRepository = organizationDomainRepository; _organizationDomainRepository = organizationDomainRepository;
@ -147,18 +134,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_paymentService = paymentService; _paymentService = paymentService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");
_fido2 = fido2; _fido2 = fido2;
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_acceptOrgUserCommand = acceptOrgUserCommand; _acceptOrgUserCommand = acceptOrgUserCommand;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_stripeSyncService = stripeSyncService; _stripeSyncService = stripeSyncService;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService; _featureService = featureService;
_premiumUserBillingService = premiumUserBillingService; _premiumUserBillingService = premiumUserBillingService;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache; _distributedCache = distributedCache;

View File

@ -821,11 +821,6 @@ public class CipherService : ICipherService
private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId) private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
{
return await UserCanEditAsync(cipher, userId);
}
var user = await _userService.GetUserByIdAsync(userId); var user = await _userService.GetUserByIdAsync(userId);
var organizationAbility = cipher.OrganizationId.HasValue ? var organizationAbility = cipher.OrganizationId.HasValue ?
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null; await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
@ -835,11 +830,6 @@ public class CipherService : ICipherService
private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId) private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
{
return await UserCanEditAsync(cipher, userId);
}
var user = await _userService.GetUserByIdAsync(userId); var user = await _userService.GetUserByIdAsync(userId);
var organizationAbility = cipher.OrganizationId.HasValue ? var organizationAbility = cipher.OrganizationId.HasValue ?
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null; await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
@ -1059,17 +1049,11 @@ public class CipherService : ICipherService
} }
// This method is used to filter ciphers based on the user's permissions to delete them. // This method is used to filter ciphers based on the user's permissions to delete them.
// It supports both the old and new logic depending on the feature flag.
private async Task<List<T>> FilterCiphersByDeletePermission<T>( private async Task<List<T>> FilterCiphersByDeletePermission<T>(
IEnumerable<T> ciphers, IEnumerable<T> ciphers,
HashSet<Guid> cipherIdsSet, HashSet<Guid> cipherIdsSet,
Guid userId) where T : CipherDetails Guid userId) where T : CipherDetails
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
{
return ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).ToList();
}
var user = await _userService.GetUserByIdAsync(userId); var user = await _userService.GetUserByIdAsync(userId);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();

View File

@ -8,6 +8,16 @@ namespace Bit.Icons.Controllers;
[Route("")] [Route("")]
public class IconsController : Controller public class IconsController : Controller
{ {
// Basic bwi-globe icon
private static readonly byte[] _notFoundImage = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUg" +
"AAABMAAAATCAQAAADYWf5HAAABu0lEQVR42nXSvWuTURTH8R+t0heI9Y04aJycdBLNJNrBFBU7OFgUER3q21I0bXK+JwZ" +
"pXISm/QdcRB3EgqBBsNihsUbbgODQQSKCuKSDOApJuuhj8tCYQj/jvYfD795z1MZ+nBKrNKhSwrMxbZTrtRnqlEjZkB/x" +
"C/xmhZrlc71qS0Up8yVzTCGucFNKD1JhORVd70SZNU4okNx5d4+U2UXRIpJFWLClsR79YzN88wQvLWNzzPKEeS/wkQGpW" +
"VhhqhW8TtDJD3Mm1x/23zLSrZCdpBY8BueTNjHSbc+8wC9HlHgU5Aj5AW5zPdcVdpq0UcknWBSr/pjixO4gfp899Kd23p" +
"M2qQCH7LkCnqAqGh73OK/8NPOcaibr90LrW/yWAnaUhqjaOSl9nFR2r5rsqo22ypn1B5IN8VOUMHVgOnNQIX+d62plcz6" +
"rg1/jskK8CMb4we4pG6OWHtR/LBJkC2E4a7ZPkuX5ntumAOM2xxveclEhLvGH6XCmLPs735Eetrw63NnOgr9P9q1viC3x" +
"lRUGOjImqFDuOBvrYYoaZU9z1uPpYae5NfdvbNVG2ZjDIlXq/oMi46lo++4vjjPBl2Dlg00AAAAASUVORK5CYII=");
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly IDomainMappingService _domainMappingService; private readonly IDomainMappingService _domainMappingService;
private readonly IIconFetchingService _iconFetchingService; private readonly IIconFetchingService _iconFetchingService;
@ -89,7 +99,7 @@ public class IconsController : Controller
if (icon == null) if (icon == null)
{ {
return new NotFoundResult(); return new FileContentResult(_notFoundImage, "image/png");
} }
return new FileContentResult(icon.Image, icon.Format); return new FileContentResult(icon.Image, icon.Format);

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Dapper; using Dapper;
@ -200,11 +201,23 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids) public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{ {
await using var connection = new SqlConnection(ConnectionString); await using var connection = new SqlConnection(ConnectionString);
return (await connection.QueryAsync<Organization>( return (await connection.QueryAsync<Organization>(
$"[{Schema}].[{Table}_ReadManyByIds]", $"[{Schema}].[{Table}_ReadManyByIds]",
new { OrganizationIds = ids.ToGuidIdArrayTVP() }, new { OrganizationIds = ids.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure)) commandType: CommandType.StoredProcedure))
.ToList(); .ToList();
} }
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var result = await connection.QueryAsync<OrganizationSeatCounts>(
"[dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return result.SingleOrDefault() ?? new OrganizationSeatCounts();
}
}
} }

View File

@ -88,19 +88,6 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
} }
} }
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var result = await connection.ExecuteScalarAsync<int>(
"[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return result;
}
}
public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId) public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)
{ {
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))

View File

@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.KeyManagement.Repositories; using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories; using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Installations; using Bit.Core.Platform.Installations;
@ -12,6 +13,7 @@ using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories;
using Bit.Infrastructure.Dapper.Billing.Repositories; using Bit.Infrastructure.Dapper.Billing.Repositories;
using Bit.Infrastructure.Dapper.Dirt;
using Bit.Infrastructure.Dapper.KeyManagement.Repositories; using Bit.Infrastructure.Dapper.KeyManagement.Repositories;
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories; using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
using Bit.Infrastructure.Dapper.Platform; using Bit.Infrastructure.Dapper.Platform;

View File

@ -1,14 +1,14 @@
using System.Data; using System.Data;
using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Repositories;
using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.Repositories;
using Dapper; using Dapper;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using ToolsEntities = Bit.Core.Tools.Entities;
namespace Bit.Infrastructure.Dapper.Tools.Repositories; namespace Bit.Infrastructure.Dapper.Dirt;
public class PasswordHealthReportApplicationRepository : Repository<ToolsEntities.PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository public class PasswordHealthReportApplicationRepository : Repository<PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository
{ {
public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings) public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
@ -18,11 +18,11 @@ public class PasswordHealthReportApplicationRepository : Repository<ToolsEntitie
: base(connectionString, readOnlyConnectionString) : base(connectionString, readOnlyConnectionString)
{ } { }
public async Task<ICollection<ToolsEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId) public async Task<ICollection<PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
{ {
using (var connection = new SqlConnection(ReadOnlyConnectionString)) using (var connection = new SqlConnection(ReadOnlyConnectionString))
{ {
var results = await connection.QueryAsync<ToolsEntities.PasswordHealthReportApplication>( var results = await connection.QueryAsync<PasswordHealthReportApplication>(
$"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]", $"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]",
new { OrganizationId = organizationId }, new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);

View File

@ -5,6 +5,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using LinqToDB.Tools; using LinqToDB.Tools;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -375,4 +376,28 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
{ {
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework."); throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
} }
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var users = await dbContext.OrganizationUsers
.Where(ou => ou.OrganizationId == organizationId && ou.Status >= 0)
.CountAsync();
var sponsored = await dbContext.OrganizationSponsorships
.Where(os => os.SponsoringOrganizationId == organizationId &&
os.IsAdminInitiated &&
(os.ToDelete == false || (os.ToDelete == true && os.ValidUntil != null && os.ValidUntil > DateTime.UtcNow)) &&
(os.SponsoredOrganizationId == null || (os.SponsoredOrganizationId != null && (os.ValidUntil == null || os.ValidUntil > DateTime.UtcNow))))
.CountAsync();
return new OrganizationSeatCounts
{
Users = users,
Sponsored = sponsored
};
}
}
} }

View File

@ -228,12 +228,6 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
return await GetCountFromQuery(query); return await GetCountFromQuery(query);
} }
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
{
var query = new OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(organizationId);
return await GetCountFromQuery(query);
}
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId) public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
{ {
var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId); var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);

View File

@ -1,48 +0,0 @@
using Bit.Core.Enums;
using Bit.Infrastructure.EntityFramework.Models;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery<OrganizationUser>
{
private readonly Guid _organizationId;
public OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(Guid organizationId)
{
_organizationId = organizationId;
}
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
{
var orgUsersQuery = from ou in dbContext.OrganizationUsers
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
where os.SponsoringOrganizationId == _organizationId &&
os.IsAdminInitiated &&
(
// Not marked for deletion - always count
(!os.ToDelete) ||
// Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow)
) &&
(
// SENT status: When SponsoredOrganizationId is null
os.SponsoredOrganizationId == null ||
// ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(os.SponsoredOrganizationId != null &&
(!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow))
)
select new OrganizationUser
{
Id = os.Id,
OrganizationId = _organizationId,
Status = OrganizationUserStatusType.Invited
};
return orgUsersQuery.Concat(sponsorshipsQuery);
}
}

View File

@ -1,8 +1,8 @@
using Bit.Infrastructure.EntityFramework.Tools.Models; using Bit.Infrastructure.EntityFramework.Dirt.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Tools.Configurations; namespace Bit.Infrastructure.EntityFramework.Dirt.Configurations;
public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication> public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication>
{ {

View File

@ -1,9 +1,9 @@
using AutoMapper; using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
namespace Bit.Infrastructure.EntityFramework.Tools.Models; namespace Bit.Infrastructure.EntityFramework.Dirt.Models;
public class PasswordHealthReportApplication : Core.Tools.Entities.PasswordHealthReportApplication public class PasswordHealthReportApplication : Core.Dirt.Reports.Entities.PasswordHealthReportApplication
{ {
public virtual Organization Organization { get; set; } public virtual Organization Organization { get; set; }
} }
@ -12,7 +12,7 @@ public class PasswordHealthReportApplicationProfile : Profile
{ {
public PasswordHealthReportApplicationProfile() public PasswordHealthReportApplicationProfile()
{ {
CreateMap<Core.Tools.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>() CreateMap<Core.Dirt.Reports.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>()
.ReverseMap(); .ReverseMap();
} }
} }

View File

@ -1,22 +1,21 @@
using AutoMapper; using AutoMapper;
using Bit.Core.Tools.Repositories; using Bit.Core.Dirt.Reports.Repositories;
using Bit.Infrastructure.EntityFramework.Dirt.Models;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Tools.Models;
using LinqToDB; using LinqToDB;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using AdminConsoleEntities = Bit.Core.Tools.Entities;
namespace Bit.Infrastructure.EntityFramework.Tools.Repositories; namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;
public class PasswordHealthReportApplicationRepository : public class PasswordHealthReportApplicationRepository :
Repository<AdminConsoleEntities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>, Repository<Core.Dirt.Reports.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>,
IPasswordHealthReportApplicationRepository IPasswordHealthReportApplicationRepository
{ {
public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory, public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory,
IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications) IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications)
{ } { }
public async Task<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId) public async Task<ICollection<Core.Dirt.Reports.Entities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
@ -24,7 +23,7 @@ public class PasswordHealthReportApplicationRepository :
var results = await dbContext.PasswordHealthReportApplications var results = await dbContext.PasswordHealthReportApplications
.Where(p => p.OrganizationId == organizationId) .Where(p => p.OrganizationId == organizationId)
.ToListAsync(); .ToListAsync();
return Mapper.Map<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>>(results); return Mapper.Map<ICollection<Core.Dirt.Reports.Entities.PasswordHealthReportApplication>>(results);
} }
} }
} }

View File

@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.KeyManagement.Repositories; using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories; using Bit.Core.NotificationCenter.Repositories;
@ -13,6 +14,7 @@ using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
using Bit.Infrastructure.EntityFramework.Platform; using Bit.Infrastructure.EntityFramework.Platform;

View File

@ -17,7 +17,7 @@ public class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFac
{ {
await using var scope = ServiceScopeFactory.CreateAsyncScope(); await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var signingKeys = await dbContext.UserSignatureKeyPairs.FindAsync(userId); var signingKeys = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == userId);
if (signingKeys == null) if (signingKeys == null)
{ {
return null; return null;

View File

@ -4,11 +4,11 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Models;
using Bit.Infrastructure.EntityFramework.Billing.Models; using Bit.Infrastructure.EntityFramework.Billing.Models;
using Bit.Infrastructure.EntityFramework.Converters; using Bit.Infrastructure.EntityFramework.Converters;
using Bit.Infrastructure.EntityFramework.Dirt.Models;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models; using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
using Bit.Infrastructure.EntityFramework.Platform; using Bit.Infrastructure.EntityFramework.Platform;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Bit.Infrastructure.EntityFramework.Tools.Models;
using Bit.Infrastructure.EntityFramework.Vault.Models; using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;

View File

@ -7,7 +7,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -23,6 +23,7 @@ using Bit.Core.Auth.UserFeatures;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.TrialInitiation; using Bit.Core.Billing.TrialInitiation;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.HostedServices; using Bit.Core.HostedServices;
@ -43,7 +44,6 @@ using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens; using Bit.Core.Tokens;
using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.SendFeatures;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;

View File

@ -4,7 +4,9 @@
@Name VARCHAR(MAX), @Name VARCHAR(MAX),
@ExternalId NVARCHAR(300), @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7),
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
@Type TINYINT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -16,7 +18,9 @@ BEGIN
[Name], [Name],
[ExternalId], [ExternalId],
[CreationDate], [CreationDate],
[RevisionDate] [RevisionDate],
[DefaultUserCollectionEmail],
[Type]
) )
VALUES VALUES
( (
@ -25,7 +29,9 @@ BEGIN
@Name, @Name,
@ExternalId, @ExternalId,
@CreationDate, @CreationDate,
@RevisionDate @RevisionDate,
@DefaultUserCollectionEmail,
@Type
) )
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId

View File

@ -6,12 +6,14 @@ CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers]
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY, @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
@Users AS [dbo].[CollectionAccessSelectionType] READONLY @Users AS [dbo].[CollectionAccessSelectionType] READONLY,
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
@Type TINYINT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
-- Groups -- Groups
;WITH [AvailableGroupsCTE] AS( ;WITH [AvailableGroupsCTE] AS(

View File

@ -13,7 +13,9 @@ BEGIN
ExternalId, ExternalId,
MIN([ReadOnly]) AS [ReadOnly], MIN([ReadOnly]) AS [ReadOnly],
MIN([HidePasswords]) AS [HidePasswords], MIN([HidePasswords]) AS [HidePasswords],
MAX([Manage]) AS [Manage] MAX([Manage]) AS [Manage],
[DefaultUserCollectionEmail],
[Type]
FROM FROM
[dbo].[UserCollectionDetails](@UserId) [dbo].[UserCollectionDetails](@UserId)
GROUP BY GROUP BY
@ -22,5 +24,7 @@ BEGIN
[Name], [Name],
CreationDate, CreationDate,
RevisionDate, RevisionDate,
ExternalId ExternalId,
[DefaultUserCollectionEmail],
[Type]
END END

View File

@ -4,7 +4,9 @@
@Name VARCHAR(MAX), @Name VARCHAR(MAX),
@ExternalId NVARCHAR(300), @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7),
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
@Type TINYINT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -16,9 +18,11 @@ BEGIN
[Name] = @Name, [Name] = @Name,
[ExternalId] = @ExternalId, [ExternalId] = @ExternalId,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate [RevisionDate] = @RevisionDate,
[DefaultUserCollectionEmail] = @DefaultUserCollectionEmail,
[Type] = @Type
WHERE WHERE
[Id] = @Id [Id] = @Id
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
END END

View File

@ -6,12 +6,14 @@
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY, @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
@Users AS [dbo].[CollectionAccessSelectionType] READONLY @Users AS [dbo].[CollectionAccessSelectionType] READONLY,
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
@Type TINYINT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
-- Groups -- Groups
-- Delete groups that are no longer in source -- Delete groups that are no longer in source

View File

@ -0,0 +1,38 @@
CREATE PROCEDURE [dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
(
-- Count organization users
SELECT COUNT(1)
FROM [dbo].[OrganizationUserView]
WHERE OrganizationId = @OrganizationId
AND Status >= 0 --Invited
) as Users,
(
-- Count admin-initiated sponsorships towards the seat count
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
SELECT COUNT(1)
FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1
AND (
-- Not marked for deletion - always count
(ToDelete = 0)
OR
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
)
AND (
-- SENT status: When SponsoredOrganizationId is null
SponsoredOrganizationId IS NULL
OR
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
)
) as Sponsored
END
GO

View File

@ -1,10 +1,12 @@
CREATE TABLE [dbo].[Collection] ( CREATE TABLE [dbo].[Collection] (
[Id] UNIQUEIDENTIFIER NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL, [OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[Name] VARCHAR (MAX) NOT NULL, [Name] VARCHAR (MAX) NOT NULL,
[ExternalId] NVARCHAR (300) NULL, [ExternalId] NVARCHAR (300) NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL,
[DefaultUserCollectionEmail] NVARCHAR(256) NULL,
[Type] TINYINT NOT NULL DEFAULT(0),
CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
); );

View File

@ -73,7 +73,9 @@ BEGIN
C.[Name], C.[Name],
C.[CreationDate], C.[CreationDate],
C.[RevisionDate], C.[RevisionDate],
C.[ExternalId] C.[ExternalId],
C.[DefaultUserCollectionEmail],
C.[Type]
IF (@IncludeAccessRelationships = 1) IF (@IncludeAccessRelationships = 1)
BEGIN BEGIN

View File

@ -73,7 +73,9 @@ BEGIN
C.[Name], C.[Name],
C.[CreationDate], C.[CreationDate],
C.[RevisionDate], C.[RevisionDate],
C.[ExternalId] C.[ExternalId],
C.[DefaultUserCollectionEmail],
C.[Type]
IF (@IncludeAccessRelationships = 1) IF (@IncludeAccessRelationships = 1)
BEGIN BEGIN

View File

@ -30,6 +30,7 @@ public class OrganizationUserControllerPutTests
OrganizationUser organizationUser, OrganizationAbility organizationAbility, OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId) SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{ {
// Arrange
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);
// Authorize all changes for basic happy path test // Authorize all changes for basic happy path test
@ -41,15 +42,18 @@ public class OrganizationUserControllerPutTests
// Save these for later - organizationUser object will be mutated // Save these for later - organizationUser object will be mutated
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
var existingUserType = organizationUser.Type;
// Act
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
// Assert
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
ou.AccessSecretsManager == model.AccessSecretsManager && ou.AccessSecretsManager == model.AccessSecretsManager &&
ou.Id == orgUserId && ou.Id == orgUserId &&
ou.Email == orgUserEmail), ou.Email == orgUserEmail), existingUserType,
savingUserId, savingUserId,
Arg.Is<List<CollectionAccessSelection>>(cas => Arg.Is<List<CollectionAccessSelection>>(cas =>
cas.All(c => model.Collections.Any(m => m.Id == c.Id))), cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
@ -77,6 +81,7 @@ public class OrganizationUserControllerPutTests
OrganizationUser organizationUser, OrganizationAbility organizationAbility, OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId) SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{ {
// Arrange
// Updating self // Updating self
organizationUser.UserId = savingUserId; organizationUser.UserId = savingUserId;
organizationAbility.AllowAdminAccessToAllCollectionItems = false; organizationAbility.AllowAdminAccessToAllCollectionItems = false;
@ -88,15 +93,18 @@ public class OrganizationUserControllerPutTests
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
var existingUserType = organizationUser.Type;
// Act
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
// Assert
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
ou.AccessSecretsManager == model.AccessSecretsManager && ou.AccessSecretsManager == model.AccessSecretsManager &&
ou.Id == orgUserId && ou.Id == orgUserId &&
ou.Email == orgUserEmail), ou.Email == orgUserEmail), existingUserType,
savingUserId, savingUserId,
Arg.Is<List<CollectionAccessSelection>>(cas => Arg.Is<List<CollectionAccessSelection>>(cas =>
cas.All(c => model.Collections.Any(m => m.Id == c.Id))), cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
@ -110,6 +118,7 @@ public class OrganizationUserControllerPutTests
OrganizationUser organizationUser, OrganizationAbility organizationAbility, OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId) SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{ {
// Arrange
// Updating self // Updating self
organizationUser.UserId = savingUserId; organizationUser.UserId = savingUserId;
organizationAbility.AllowAdminAccessToAllCollectionItems = true; organizationAbility.AllowAdminAccessToAllCollectionItems = true;
@ -121,15 +130,18 @@ public class OrganizationUserControllerPutTests
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
var existingUserType = organizationUser.Type;
// Act
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
// Assert
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
ou.AccessSecretsManager == model.AccessSecretsManager && ou.AccessSecretsManager == model.AccessSecretsManager &&
ou.Id == orgUserId && ou.Id == orgUserId &&
ou.Email == orgUserEmail), ou.Email == orgUserEmail), existingUserType,
savingUserId, savingUserId,
Arg.Is<List<CollectionAccessSelection>>(cas => Arg.Is<List<CollectionAccessSelection>>(cas =>
cas.All(c => model.Collections.Any(m => m.Id == c.Id))), cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
@ -142,6 +154,7 @@ public class OrganizationUserControllerPutTests
OrganizationUser organizationUser, OrganizationAbility organizationAbility, OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId) SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{ {
// Arrange
var editedCollectionId = CoreHelpers.GenerateComb(); var editedCollectionId = CoreHelpers.GenerateComb();
var readonlyCollectionId1 = CoreHelpers.GenerateComb(); var readonlyCollectionId1 = CoreHelpers.GenerateComb();
var readonlyCollectionId2 = CoreHelpers.GenerateComb(); var readonlyCollectionId2 = CoreHelpers.GenerateComb();
@ -194,16 +207,19 @@ public class OrganizationUserControllerPutTests
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))
.Returns(AuthorizationResult.Failed()); .Returns(AuthorizationResult.Failed());
var existingUserType = organizationUser.Type;
// Act
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
// Assert
// Expect all collection access (modified and unmodified) to be saved // Expect all collection access (modified and unmodified) to be saved
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
ou.AccessSecretsManager == model.AccessSecretsManager && ou.AccessSecretsManager == model.AccessSecretsManager &&
ou.Id == orgUserId && ou.Id == orgUserId &&
ou.Email == orgUserEmail), ou.Email == orgUserEmail), existingUserType,
savingUserId, savingUserId,
Arg.Is<List<CollectionAccessSelection>>(cas => Arg.Is<List<CollectionAccessSelection>>(cas =>
cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) && cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&

View File

@ -1,15 +1,16 @@
using AutoFixture; using AutoFixture;
using Bit.Api.Tools.Controllers; using Bit.Api.Dirt.Controllers;
using Bit.Api.Dirt.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Api.Test.Tools.Controllers; namespace Bit.Api.Test.Dirt;
[ControllerCustomize(typeof(ReportsController))] [ControllerCustomize(typeof(ReportsController))]
@ -54,7 +55,7 @@ public class ReportsControllerTests
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true); sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act // Act
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel var request = new PasswordHealthReportApplicationModel
{ {
OrganizationId = Guid.NewGuid(), OrganizationId = Guid.NewGuid(),
Url = "https://example.com", Url = "https://example.com",
@ -77,7 +78,7 @@ public class ReportsControllerTests
// Act // Act
var fixture = new Fixture(); var fixture = new Fixture();
var request = fixture.CreateMany<Api.Tools.Models.PasswordHealthReportApplicationModel>(2); var request = fixture.CreateMany<PasswordHealthReportApplicationModel>(2);
await sutProvider.Sut.AddPasswordHealthReportApplications(request); await sutProvider.Sut.AddPasswordHealthReportApplications(request);
// Assert // Assert
@ -93,7 +94,7 @@ public class ReportsControllerTests
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act // Act
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel var request = new PasswordHealthReportApplicationModel
{ {
OrganizationId = Guid.NewGuid(), OrganizationId = Guid.NewGuid(),
Url = "https://example.com", Url = "https://example.com",
@ -114,7 +115,7 @@ public class ReportsControllerTests
// Act // Act
var fixture = new Fixture(); var fixture = new Fixture();
var request = fixture.Create<Api.Tools.Models.PasswordHealthReportApplicationModel>(); var request = fixture.Create<PasswordHealthReportApplicationModel>();
await Assert.ThrowsAsync<NotFoundException>(async () => await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddPasswordHealthReportApplication(request)); await sutProvider.Sut.AddPasswordHealthReportApplication(request));

View File

@ -4,7 +4,6 @@ using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models; using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Models.Response; using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -169,6 +168,7 @@ public class CiphersControllerTests
} }
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
@ -197,65 +197,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithOwnerOrAdmin_WithEditPermission_DeletesCipher( public async Task DeleteAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
cipherDetails.Manage = false;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = false;
cipherDetails.Manage = false;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id));
await sutProvider.GetDependency<ICipherService>().DidNotReceive().DeleteAsync(Arg.Any<CipherDetails>(), Arg.Any<Guid>(), Arg.Any<bool>());
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -266,7 +208,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -293,7 +234,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -304,7 +245,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -339,11 +279,22 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id } }); .Returns(new List<CipherOrganizationDetails>
{
new() { Id = cipherDetails.Id, OrganizationId = cipherDetails.OrganizationId }
});
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id); await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
@ -426,10 +377,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id));
} }
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithEditPermission_DeletesCiphers( public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -437,74 +392,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(ciphers.Select(c => new CipherDetails
{
Id = c.Id,
OrganizationId = organization.Id,
Edit = true
}).ToList());
await sutProvider.Sut.DeleteManyAdmin(model);
await sutProvider.GetDependency<ICipherService>()
.Received(1)
.DeleteManyAsync(
Arg.Is<IEnumerable<Guid>>(ids =>
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
userId, organization.Id, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id.ToString();
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>()
.GetProperUserId(default)
.ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>()
.GetOrganization(new Guid(model.OrganizationId))
.Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByOrganizationIdAsync(new Guid(model.OrganizationId))
.Returns(ciphers);
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(new Guid(model.OrganizationId))
.Returns(new OrganizationAbility
{
Id = new Guid(model.OrganizationId),
AllowAdminAccessToAllCollectionItems = false,
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_DeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id.ToString();
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -540,7 +427,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -548,7 +435,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -586,10 +472,18 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList()); .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
await sutProvider.Sut.DeleteManyAdmin(model); await sutProvider.Sut.DeleteManyAdmin(model);
@ -688,67 +582,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));
} }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
}
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException( public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = false;
cipherDetails.Manage = false;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));
await sutProvider.GetDependency<ICipherService>().DidNotReceive().SoftDeleteAsync(Arg.Any<CipherDetails>(), Arg.Any<Guid>(), Arg.Any<bool>());
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -759,7 +600,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -786,7 +626,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -797,7 +637,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -833,12 +672,20 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id } }); .Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
@ -856,6 +703,7 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails }); sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
@ -890,6 +738,70 @@ public class CiphersControllerTests
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true); await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
} }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionFalse_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = false
});
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionTrue_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(
@ -922,10 +834,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id));
} }
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithEditPermission_SoftDeletesCiphers( public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -933,65 +849,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(ciphers.Select(c => new CipherDetails
{
Id = c.Id,
OrganizationId = organization.Id,
Edit = true
}).ToList());
await sutProvider.Sut.PutDeleteManyAdmin(model);
await sutProvider.GetDependency<ICipherService>()
.Received(1)
.SoftDeleteManyAsync(
Arg.Is<IEnumerable<Guid>>(ids =>
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
userId, organization.Id, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id.ToString();
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(ciphers.Select(c => new CipherDetails
{
Id = c.Id,
OrganizationId = organization.Id,
Edit = false
}).ToList());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id.ToString();
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1027,7 +884,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -1035,7 +892,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1073,10 +929,18 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList()); .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
await sutProvider.Sut.PutDeleteManyAdmin(model); await sutProvider.Sut.PutDeleteManyAdmin(model);
@ -1099,7 +963,14 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
// Set organization ID on ciphers to avoid "Cipher needs to belong to a user or an organization" error
foreach (var cipher in ciphers)
{
cipher.OrganizationId = organization.Id;
}
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
@ -1130,7 +1001,14 @@ public class CiphersControllerTests
organization.Type = OrganizationUserType.Custom; organization.Type = OrganizationUserType.Custom;
organization.Permissions.EditAnyCollection = true; organization.Permissions.EditAnyCollection = true;
// Set organization ID on ciphers to avoid "Cipher needs to belong to a user or an organization" error
foreach (var cipher in ciphers)
{
cipher.OrganizationId = organization.Id;
}
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);
@ -1175,68 +1053,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));
} }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherDetails.Edit = true;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
}
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException( public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = false;
cipherDetails.Manage = false;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -1249,7 +1073,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1277,7 +1100,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -1288,7 +1111,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1323,11 +1145,19 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id } }); .Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
@ -1386,6 +1216,75 @@ public class CiphersControllerTests
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true); await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
} }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionFalse_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = false // Permissive mode - Edit permission should work
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionTrue_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true // Restrictive mode - Edit permission should NOT work
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(
@ -1420,10 +1319,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
} }
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithEditPermission_RestoresCiphers( public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCiphers(
OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -1431,77 +1334,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(ciphers.Select(c => new CipherDetails
{
Id = c.Id,
OrganizationId = organization.Id,
Edit = true
}).ToList());
var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails
{
Id = c.Id,
OrganizationId = organization.Id
}).ToList();
sutProvider.GetDependency<ICipherService>()
.RestoreManyAsync(Arg.Is<HashSet<Guid>>(ids =>
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),
userId, organization.Id, true)
.Returns(cipherOrgDetails);
var result = await sutProvider.Sut.PutRestoreManyAdmin(model);
await sutProvider.GetDependency<ICipherService>().Received(1)
.RestoreManyAsync(
Arg.Is<HashSet<Guid>>(ids =>
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),
userId, organization.Id, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id;
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(ciphers.Select(c => new CipherDetails
{
Id = c.Id,
OrganizationId = organization.Id,
Edit = false,
Type = CipherType.Login,
Data = JsonSerializer.Serialize(new CipherLoginData())
}).ToList());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreManyAdmin(model));
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_RestoresCiphers(
OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
model.OrganizationId = organization.Id;
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1553,7 +1385,7 @@ public class CiphersControllerTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers, OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider) CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{ {
@ -1561,7 +1393,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
@ -1599,6 +1430,7 @@ public class CiphersControllerTests
organization.Type = organizationUserType; organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails
@ -1614,9 +1446,16 @@ public class CiphersControllerTests
.Returns(cipherOrgDetails); .Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherService>() sutProvider.GetDependency<ICipherService>()
.RestoreManyAsync(Arg.Is<HashSet<Guid>>(ids => .RestoreManyAsync(Arg.Is<HashSet<Guid>>(ids =>
ids.All(id => model.Ids.Contains(id.ToString()) && ids.Count == model.Ids.Count())), ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
userId, organization.Id, true) userId, organization.Id, true)
.Returns(cipherOrgDetails); .Returns(cipherOrgDetails);
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
{
Id = organization.Id,
LimitItemDeletion = true
});
var result = await sutProvider.Sut.PutRestoreManyAdmin(model); var result = await sutProvider.Sut.PutRestoreManyAdmin(model);

View File

@ -4,8 +4,19 @@ using AutoFixture.Kernel;
namespace Bit.Test.Common.AutoFixture; namespace Bit.Test.Common.AutoFixture;
/// <summary>
/// A utility class that encapsulates a system under test (sut) and its dependencies.
/// By default, all dependencies are initialized as mocks using the NSubstitute library.
/// SutProvider provides an interface for accessing these dependencies in the arrange and assert stages of your tests.
/// </summary>
/// <typeparam name="TSut">The concrete implementation of the class being tested.</typeparam>
public class SutProvider<TSut> : ISutProvider public class SutProvider<TSut> : ISutProvider
{ {
/// <summary>
/// A record of the configured dependencies (constructor parameters). The outer Dictionary is keyed by the dependency's
/// type, and the inner dictionary is keyed by the parameter name (optionally used to disambiguate parameters with the same type).
/// The inner dictionary value is the dependency.
/// </summary>
private Dictionary<Type, Dictionary<string, object>> _dependencies; private Dictionary<Type, Dictionary<string, object>> _dependencies;
private readonly IFixture _fixture; private readonly IFixture _fixture;
private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay; private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay;
@ -23,9 +34,21 @@ public class SutProvider<TSut> : ISutProvider
_fixture.Customizations.Add(_constructorParameterRelay); _fixture.Customizations.Add(_constructorParameterRelay);
} }
/// <summary>
/// Registers a dependency to be injected when the sut is created. You must call <see cref="Create"/> after
/// this method to (re)create the sut with the dependency.
/// </summary>
/// <param name="dependency">The dependency to register.</param>
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
/// <typeparam name="T">The type to register the dependency under - usually an interface. This should match the type expected by the sut's constructor.</typeparam>
/// <returns></returns>
public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "") public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "")
=> SetDependency(typeof(T), dependency, parameterName); => SetDependency(typeof(T), dependency, parameterName);
public SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
/// <summary>
/// An overload for <see cref="SetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
/// </summary>
private SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
{ {
if (_dependencies.TryGetValue(dependencyType, out var dependencyForType)) if (_dependencies.TryGetValue(dependencyType, out var dependencyForType))
{ {
@ -39,45 +62,69 @@ public class SutProvider<TSut> : ISutProvider
return this; return this;
} }
/// <summary>
/// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with
/// <see cref="SetDependency{T}"/> or automatically with <see cref="Create"/>.
/// As dependencies are initialized with NSubstitute mocks by default, this is often used to retrieve those mocks in order to
/// configure them during the arrange stage, or check received calls in the assert stage.
/// </summary>
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
/// <typeparam name="T">The type of the dependency you want to get - usually an interface.</typeparam>
/// <returns>The dependency.</returns>
public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName); public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName);
public object GetDependency(Type dependencyType, string parameterName = "")
/// <summary>
/// An overload for <see cref="GetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
/// </summary>
private object GetDependency(Type dependencyType, string parameterName = "")
{ {
if (DependencyIsSet(dependencyType, parameterName)) if (DependencyIsSet(dependencyType, parameterName))
{ {
return _dependencies[dependencyType][parameterName]; return _dependencies[dependencyType][parameterName];
} }
else if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
{ {
if (knownDependencies.Values.Count == 1) if (knownDependencies.Values.Count == 1)
{ {
return knownDependencies.Values.Single(); return knownDependencies.Values.Single();
} }
else
{ throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ",
throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ", $"{parameterName} does not exist. Available dependency names are: ",
$"{parameterName} does not exist. Available dependency names are: ", string.Join(", ", knownDependencies.Keys)));
string.Join(", ", knownDependencies.Keys)));
}
}
else
{
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
} }
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
} }
/// <summary>
/// Clear all the dependencies and the sut. This reverts the SutProvider back to a fully uninitialized state.
/// </summary>
public void Reset() public void Reset()
{ {
_dependencies = new Dictionary<Type, Dictionary<string, object>>(); _dependencies = new Dictionary<Type, Dictionary<string, object>>();
Sut = default; Sut = default;
} }
/// <summary>
/// Recreate a new sut with all new dependencies. This will reset all dependencies, including mocked return values
/// and any dependencies set with <see cref="SetDependency{T}"/>.
/// </summary>
public void Recreate() public void Recreate()
{ {
_dependencies = new Dictionary<Type, Dictionary<string, object>>(); _dependencies = new Dictionary<Type, Dictionary<string, object>>();
Sut = _fixture.Create<TSut>(); Sut = _fixture.Create<TSut>();
} }
/// <inheritdoc cref="Create()"/>>
ISutProvider ISutProvider.Create() => Create(); ISutProvider ISutProvider.Create() => Create();
/// <summary>
/// Creates the sut, injecting any dependencies configured via <see cref="SetDependency{T}"/> and falling back to
/// NSubstitute mocks for any dependencies that have not been explicitly configured.
/// </summary>
/// <returns></returns>
public SutProvider<TSut> Create() public SutProvider<TSut> Create()
{ {
Sut = _fixture.Create<TSut>(); Sut = _fixture.Create<TSut>();
@ -89,6 +136,19 @@ public class SutProvider<TSut> : ISutProvider
private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null; private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
/// <summary>
/// A specimen builder which tells Autofixture to use the dependency registered in <see cref="SutProvider{T}"/>
/// when creating test data. If no matching dependency exists in <see cref="SutProvider{TSut}"/>, it creates
/// an NSubstitute mock and registers it using <see cref="SutProvider{TSut}.SetDependency{T}"/>
/// so it can be retrieved later.
/// This is the link between <see cref="SutProvider{T}"/> and Autofixture.
/// </summary>
/// <remarks>
/// Autofixture knows how to create sample data of simple types (such as an int or string) but not more complex classes.
/// We create our own <see cref="ISpecimenBuilder"/> and register it with the <see cref="Fixture"/> in
/// <see cref="SutProvider{TSut}"/> to provide that instruction.
/// </remarks>
/// <typeparam name="T">The type of the sut.</typeparam>
private class ConstructorParameterRelay<T> : ISpecimenBuilder private class ConstructorParameterRelay<T> : ISpecimenBuilder
{ {
private readonly SutProvider<T> _sutProvider; private readonly SutProvider<T> _sutProvider;
@ -102,6 +162,7 @@ public class SutProvider<TSut> : ISutProvider
public object Create(object request, ISpecimenContext context) public object Create(object request, ISpecimenContext context)
{ {
// Basic checks to filter out irrelevant requests from Autofixture
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
@ -116,16 +177,22 @@ public class SutProvider<TSut> : ISutProvider
return new NoSpecimen(); return new NoSpecimen();
} }
// Use the dependency set under this parameter name, if any
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name)) if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name))
{ {
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name); return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);
} }
// Return default type if set
else if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, "")) // Use the default dependency set for this type, if any (i.e. no parameter name has been specified)
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, ""))
{ {
return _sutProvider.GetDependency(parameterInfo.ParameterType, ""); return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
} }
// Fallback: pass the request down the chain. This lets another fixture customization populate the value.
// If you haven't added any customizations, this should be an NSubstitute mock.
// It is registered with SetDependency so you can retrieve it later.
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for // This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
// Create(Type type) exists. // Create(Type type) exists.
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType, var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.5.1" /> <PackageReference Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" /> <PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />

View File

@ -137,6 +137,14 @@ public class InviteOrganizationUserCommandTests
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>()) .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization))); .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -202,6 +210,14 @@ public class InviteOrganizationUserCommandTests
.Returns(new Invalid<InviteOrganizationUsersValidationRequest>( .Returns(new Invalid<InviteOrganizationUsersValidationRequest>(
new Error<InviteOrganizationUsersValidationRequest>(errorMessage, validationRequest))); new Error<InviteOrganizationUsersValidationRequest>(errorMessage, validationRequest)));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -272,6 +288,14 @@ public class InviteOrganizationUserCommandTests
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization) .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1)))); .WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -343,6 +367,14 @@ public class InviteOrganizationUserCommandTests
.WithPasswordManagerUpdate( .WithPasswordManagerUpdate(
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1)))); new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -413,6 +445,14 @@ public class InviteOrganizationUserCommandTests
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization) .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(passwordManagerUpdate))); .WithPasswordManagerUpdate(passwordManagerUpdate)));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -469,6 +509,7 @@ public class InviteOrganizationUserCommandTests
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager)); .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
orgUserRepository orgUserRepository
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false) .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
@ -476,11 +517,13 @@ public class InviteOrganizationUserCommandTests
orgUserRepository orgUserRepository
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner) .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
.Returns([ownerDetails]); .Returns([ownerDetails]);
orgUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(1); orgRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1); orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
orgRepository.GetByIdAsync(organization.Id) orgRepository.GetByIdAsync(organization.Id)
.Returns(organization); .Returns(organization);
@ -566,6 +609,14 @@ public class InviteOrganizationUserCommandTests
.SendInvitesAsync(Arg.Any<SendInvitesRequest>()) .SendInvitesAsync(Arg.Any<SendInvitesRequest>())
.Throws(new Exception("Something went wrong")); .Throws(new Exception("Something went wrong"));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -671,6 +722,14 @@ public class InviteOrganizationUserCommandTests
} }
}); });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -762,6 +821,14 @@ public class InviteOrganizationUserCommandTests
} }
}); });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -829,6 +896,14 @@ public class InviteOrganizationUserCommandTests
.WithPasswordManagerUpdate( .WithPasswordManagerUpdate(
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1)))); new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -900,6 +975,14 @@ public class InviteOrganizationUserCommandTests
.WithPasswordManagerUpdate( .WithPasswordManagerUpdate(
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1)))); new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(0);
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);

View File

@ -1,6 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.AdminConsole.Utilities.Validation;

View File

@ -31,7 +31,12 @@ public class RestoreOrganizationUserCommandTests
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
{ {
RestoreUser_Setup(organization, owner, organizationUser, sutProvider); RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>() await sutProvider.GetDependency<IOrganizationUserRepository>()
@ -49,7 +54,12 @@ public class RestoreOrganizationUserCommandTests
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider) public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
{ {
RestoreUser_Setup(organization, null, organizationUser, sutProvider); RestoreUser_Setup(organization, null, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser); await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
await sutProvider.GetDependency<IOrganizationUserRepository>() await sutProvider.GetDependency<IOrganizationUserRepository>()
@ -151,7 +161,12 @@ public class RestoreOrganizationUserCommandTests
sutProvider.GetDependency<IPolicyService>() sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>()) .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var user = new User(); var user = new User();
user.Email = "test@bitwarden.com"; user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user); sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
@ -184,7 +199,12 @@ public class RestoreOrganizationUserCommandTests
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>() sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value))) .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
RestoreUser_Setup(organization, owner, organizationUser, sutProvider); RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IPolicyService>() sutProvider.GetDependency<IPolicyService>()
@ -219,7 +239,12 @@ public class RestoreOrganizationUserCommandTests
SutProvider<RestoreOrganizationUserCommand> sutProvider) SutProvider<RestoreOrganizationUserCommand> sutProvider)
{ {
organizationUser.Email = null; organizationUser.Email = null;
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements) .IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true); .Returns(true);
@ -278,7 +303,12 @@ public class RestoreOrganizationUserCommandTests
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>() sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value))) .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>() await sutProvider.GetDependency<IOrganizationUserRepository>()
@ -344,6 +374,15 @@ public class RestoreOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(organizationUser.UserId.Value) .GetManyByUserAsync(organizationUser.UserId.Value)
.Returns(new[] { organizationUser, secondOrganizationUser }); .Returns(new[] { organizationUser, secondOrganizationUser });
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IPolicyService>() sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>()) .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(new[] .Returns(new[]
@ -392,7 +431,12 @@ public class RestoreOrganizationUserCommandTests
{ {
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked } new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
}); });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IPolicyService>() sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>()) .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns([ .Returns([
@ -455,7 +499,12 @@ public class RestoreOrganizationUserCommandTests
PolicyType = PolicyType.TwoFactorAuthentication PolicyType = PolicyType.TwoFactorAuthentication
} }
])); ]));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var user = new User { Email = "test@bitwarden.com" }; var user = new User { Email = "test@bitwarden.com" };
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user); sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
@ -475,6 +524,40 @@ public class RestoreOrganizationUserCommandTests
.PushSyncOrgKeysAsync(Arg.Any<Guid>()); .PushSyncOrgKeysAsync(Arg.Any<Guid>());
} }
[Theory, BitAutoData]
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails( public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(
Organization organization, Organization organization,
@ -492,7 +575,12 @@ public class RestoreOrganizationUserCommandTests
otherOrganization.PlanType = PlanType.Free; otherOrganization.PlanType = PlanType.Free;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider); RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(organizationUser.UserId.Value) .GetManyByUserAsync(organizationUser.UserId.Value)
.Returns([orgUserOwnerFromDifferentOrg]); .Returns([orgUserOwnerFromDifferentOrg]);
@ -533,7 +621,12 @@ public class RestoreOrganizationUserCommandTests
otherOrganization.PlanType = PlanType.Free; otherOrganization.PlanType = PlanType.Free;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider); RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository organizationUserRepository
.GetManyByUserAsync(organizationUser.UserId.Value) .GetManyByUserAsync(organizationUser.UserId.Value)
@ -584,7 +677,12 @@ public class RestoreOrganizationUserCommandTests
otherOrganization.PlanType = PlanType.Free; otherOrganization.PlanType = PlanType.Free;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider); RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository organizationUserRepository
.GetManyByUserAsync(organizationUser.UserId.Value) .GetManyByUserAsync(organizationUser.UserId.Value)
@ -636,7 +734,12 @@ public class RestoreOrganizationUserCommandTests
organizationUserRepository organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id))) .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
.Returns([orgUser1, orgUser2]); .Returns([orgUser1, orgUser2]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
twoFactorIsEnabledQuery twoFactorIsEnabledQuery
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value))) .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
@ -685,7 +788,12 @@ public class RestoreOrganizationUserCommandTests
organizationUserRepository organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id))) .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
.Returns(new[] { orgUser1, orgUser2, orgUser3 }); .Returns(new[] { orgUser1, orgUser2, orgUser3 });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" }); userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
// Setup 2FA policy // Setup 2FA policy
@ -820,7 +928,12 @@ public class RestoreOrganizationUserCommandTests
organizationUserRepository organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id))) .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
.Returns([orgUser1, orgUser2, orgUser3]); .Returns([orgUser1, orgUser2, orgUser3]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" }); userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
@ -882,7 +995,12 @@ public class RestoreOrganizationUserCommandTests
organizationUserRepository organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id))) .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))
.Returns([orgUser1]); .Returns([orgUser1]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
organizationUserRepository organizationUserRepository
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>()) .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([orgUserFromOtherOrg]); .Returns([orgUserFromOtherOrg]);
@ -942,7 +1060,12 @@ public class RestoreOrganizationUserCommandTests
organizationUserRepository organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id))) .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))
.Returns([orgUser1]); .Returns([orgUser1]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
organizationUserRepository organizationUserRepository
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>()) .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([orgUserFromOtherOrg]); .Returns([orgUserFromOtherOrg]);
@ -972,7 +1095,14 @@ public class RestoreOrganizationUserCommandTests
} }
targetOrganizationUser.OrganizationId = organization.Id; targetOrganizationUser.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)); sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin));
} }

View File

@ -27,8 +27,10 @@ public class UpdateOrganizationUserCommandTests
List<CollectionAccessSelection> collections, List<Guid> groups, SutProvider<UpdateOrganizationUserCommand> sutProvider) List<CollectionAccessSelection> collections, List<Guid> groups, SutProvider<UpdateOrganizationUserCommand> sutProvider)
{ {
user.Id = default(Guid); user.Id = default(Guid);
var existingUserType = OrganizationUserType.User;
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collections, groups)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collections, groups));
Assert.Contains("invite the user first", exception.Message.ToLowerInvariant()); Assert.Contains("invite the user first", exception.Message.ToLowerInvariant());
} }
@ -37,9 +39,10 @@ public class UpdateOrganizationUserCommandTests
Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider) Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider)
{ {
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(user.Id).Returns(originalUser); sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(user.Id).Returns(originalUser);
var existingUserType = OrganizationUserType.User;
await Assert.ThrowsAsync<NotFoundException>( await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, null)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, null));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -55,8 +58,10 @@ public class UpdateOrganizationUserCommandTests
.Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>() .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()
.Select(guid => new Collection { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList()); .Select(guid => new Collection { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());
var existingUserType = OrganizationUserType.User;
await Assert.ThrowsAsync<NotFoundException>( await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collectionAccess, null)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -76,9 +81,9 @@ public class UpdateOrganizationUserCommandTests
result.RemoveAt(0); result.RemoveAt(0);
return result; return result;
}); });
var existingUserType = OrganizationUserType.User;
await Assert.ThrowsAsync<NotFoundException>( await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collectionAccess, null)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -94,8 +99,10 @@ public class UpdateOrganizationUserCommandTests
.Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>() .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()
.Select(guid => new Group { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList()); .Select(guid => new Group { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());
var existingUserType = OrganizationUserType.User;
await Assert.ThrowsAsync<NotFoundException>( await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, groupAccess)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -115,9 +122,9 @@ public class UpdateOrganizationUserCommandTests
result.RemoveAt(0); result.RemoveAt(0);
return result; return result;
}); });
var existingUserType = OrganizationUserType.User;
await Assert.ThrowsAsync<NotFoundException>( await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, groupAccess)); () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -165,7 +172,9 @@ public class UpdateOrganizationUserCommandTests
.GetCountByFreeOrganizationAdminUserAsync(newUserData.Id) .GetCountByFreeOrganizationAdminUserAsync(newUserData.Id)
.Returns(0); .Returns(0);
await sutProvider.Sut.UpdateUserAsync(newUserData, savingUser.UserId, collections, groups); var existingUserType = OrganizationUserType.User;
await sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, savingUser.UserId, collections, groups);
var organizationService = sutProvider.GetDependency<IOrganizationService>(); var organizationService = sutProvider.GetDependency<IOrganizationService>();
await organizationService.Received(1).ValidateOrganizationUserUpdatePermissions( await organizationService.Received(1).ValidateOrganizationUserUpdatePermissions(
@ -184,7 +193,7 @@ public class UpdateOrganizationUserCommandTests
[Theory] [Theory]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws( public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsNotAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws(
OrganizationUserType userType, OrganizationUserType userType,
OrganizationUser oldUserData, OrganizationUser oldUserData,
OrganizationUser newUserData, OrganizationUser newUserData,
@ -199,10 +208,39 @@ public class UpdateOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value) .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value)
.Returns(1); .Returns(1);
var existingUserType = OrganizationUserType.User;
// Assert // Assert
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateUserAsync(newUserData, null, null, null)); () => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null));
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
}
[Theory]
[BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Owner)]
public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws(
OrganizationUserType newUserType,
OrganizationUserType existingUserType,
OrganizationUser oldUserData,
OrganizationUser newUserData,
Organization organization,
SutProvider<UpdateOrganizationUserCommand> sutProvider)
{
organization.PlanType = PlanType.Free;
newUserData.Type = newUserType;
Setup(sutProvider, organization, newUserData, oldUserData);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value)
.Returns(2);
// Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null));
Assert.Contains("User can only be an admin of one free organization.", exception.Message); Assert.Contains("User can only be an admin of one free organization.", exception.Message);
} }

View File

@ -60,16 +60,19 @@ public class TwoFactorAuthenticationPolicyValidatorTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers( public async Task OnSaveSideEffectsAsync_RevokesOnlyNonCompliantUsers(
Organization organization, Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider) SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{ {
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; // Arrange
policy.OrganizationId = policyUpdate.OrganizationId;
organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails var nonCompliantUser = new OrganizationUserUserDetails
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed, Status = OrganizationUserStatusType.Confirmed,
@ -80,30 +83,57 @@ public class TwoFactorAuthenticationPolicyValidatorTests
HasMasterPassword = true HasMasterPassword = true
}; };
var compliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user4@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUserDetailUserWithout2Fa]); .Returns([nonCompliantUser, compliantUser]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>() sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>()) .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{ {
(orgUserDetailUserWithout2Fa, false) (nonCompliantUser, false),
(compliantUser, true)
}); });
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>() sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>()) .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult()); .Returns(new CommandResult());
// Act
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
// Assert
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>() await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1) .Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>()); .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>
req.OrganizationId == policyUpdate.OrganizationId &&
req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })
));
await sutProvider.GetDependency<IMailService>() await sutProvider.GetDependency<IMailService>()
.Received(1) .Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
"user3@test.com"); nonCompliantUser.Email);
// Did not send out an email for compliantUser
await sutProvider.GetDependency<IMailService>()
.Received(0)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
compliantUser.Email);
} }
} }

View File

@ -288,7 +288,7 @@ public class SavePolicyCommandTests
{ {
return new SutProvider<SavePolicyCommand>() return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider() .WithFakeTimeProvider()
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? []) .SetDependency(policyValidators ?? [])
.Create(); .Create();
} }

View File

@ -60,7 +60,12 @@ public class OrganizationServiceTests
existingUsers.First().Type = OrganizationUserType.Owner; existingUsers.First().Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
@ -117,7 +122,12 @@ public class OrganizationServiceTests
ExternalId = reInvitedUser.Email, ExternalId = reInvitedUser.Email,
}); });
var expectedNewUsersCount = newUsers.Count - 1; var expectedNewUsersCount = newUsers.Count - 1;
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id) sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id)
.Returns(existingUsers); .Returns(existingUsers);
@ -190,7 +200,12 @@ public class OrganizationServiceTests
sutProvider.Create(); sutProvider.Create();
invite.Emails = invite.Emails.Append(invite.Emails.First()); invite.Emails = invite.Emails.Append(invite.Emails.First());
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true); sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
@ -221,6 +236,12 @@ public class OrganizationServiceTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true); sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
@ -314,6 +335,12 @@ public class OrganizationServiceTests
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>() sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>()) .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
@ -340,6 +367,13 @@ public class OrganizationServiceTests
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>(); var currentContext = sutProvider.GetDependency<ICurrentContext>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
organizationRepository.GetByIdAsync(organization.Id).Returns(organization); organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>() sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>()) .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
@ -397,7 +431,12 @@ public class OrganizationServiceTests
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>(); var currentContext = sutProvider.GetDependency<ICurrentContext>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
organizationRepository.GetByIdAsync(organization.Id).Returns(organization); organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
currentContext.OrganizationCustom(organization.Id).Returns(true); currentContext.OrganizationCustom(organization.Id).Returns(true);
currentContext.ManageUsers(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true);
@ -425,7 +464,12 @@ public class OrganizationServiceTests
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>() sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>()) .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
currentContext.OrganizationOwner(organization.Id).Returns(true); currentContext.OrganizationOwner(organization.Id).Returns(true);
@ -473,7 +517,12 @@ public class OrganizationServiceTests
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId); await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId);
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1) await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
@ -538,7 +587,12 @@ public class OrganizationServiceTests
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut
.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId)); .InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));
Assert.Contains("This user has already been invited", exception.Message); Assert.Contains("This user has already been invited", exception.Message);
@ -595,7 +649,12 @@ public class OrganizationServiceTests
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization); organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>() sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>()) .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true); .Returns(true);
@ -631,7 +690,12 @@ public class OrganizationServiceTests
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}); });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>(); var currentContext = sutProvider.GetDependency<ICurrentContext>();
@ -664,6 +728,13 @@ public class OrganizationServiceTests
organization.PlanType = PlanType.EnterpriseAnnually; organization.PlanType = PlanType.EnterpriseAnnually;
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider); InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
// Set up some invites to grant access to SM // Set up some invites to grant access to SM
invites.First().invite.AccessSecretsManager = true; invites.First().invite.AccessSecretsManager = true;
var invitedSmUsers = invites.First().invite.Emails.Count(); var invitedSmUsers = invites.First().invite.Emails.Count();
@ -708,6 +779,13 @@ public class OrganizationServiceTests
invite.AccessSecretsManager = false; invite.AccessSecretsManager = false;
} }
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
// Assume we need to add seats for all invited SM users // Assume we need to add seats for all invited SM users
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>() sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
.CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers); .CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);
@ -813,7 +891,12 @@ public class OrganizationServiceTests
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType)); .Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null)); var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null));

View File

@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@ -25,30 +26,32 @@ public class OrganizationBillingServiceTests
SutProvider<OrganizationBillingService> sutProvider) SutProvider<OrganizationBillingService> sutProvider)
{ {
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList()); sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList());
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType)); .Returns(StaticStore.GetPlan(organization.PlanType));
var subscriberService = sutProvider.GetDependency<ISubscriberService>(); var subscriberService = sutProvider.GetDependency<ISubscriberService>();
var organizationSeatCount = new OrganizationSeatCounts { Users = 1, Sponsored = 0 };
subscriberService var customer = new Customer
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options => options.Expand.FirstOrDefault() == "discount.coupon.applies_to")) {
.Returns(new Customer Discount = new Discount
{ {
Discount = new Discount Coupon = new Coupon
{ {
Coupon = new Coupon Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
AppliesTo = new CouponAppliesTo
{ {
Id = StripeConstants.CouponIDs.SecretsManagerStandalone, Products = ["product_id"]
AppliesTo = new CouponAppliesTo
{
Products = ["product_id"]
}
} }
} }
}); }
};
subscriberService
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("discount.coupon.applies_to")))
.Returns(customer);
subscriberService.GetSubscription(organization).Returns(new Subscription subscriberService.GetSubscription(organization).Returns(new Subscription
{ {
@ -67,6 +70,10 @@ public class OrganizationBillingServiceTests
} }
}); });
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 });
var metadata = await sutProvider.Sut.GetMetadata(organizationId); var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.True(metadata!.IsOnSecretsManagerStandalone); Assert.True(metadata!.IsOnSecretsManagerStandalone);

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