1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-15 15:30:49 -05:00

Merge branch 'main' into vault/pm-20041/mark-task-complete

This commit is contained in:
Nick Krantz 2025-06-13 15:11:04 -05:00 committed by GitHub
commit d1b43bd044
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
320 changed files with 11779 additions and 1400 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
@ -598,7 +570,7 @@ jobs:
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
with: with:
project: server project: server
pull_request_number: ${{ github.event.number }} pull_request_number: ${{ github.event.number || 0 }}
secrets: inherit secrets: inherit
permissions: read-all permissions: read-all

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

@ -11,7 +11,7 @@ $corsRules = (@{
AllowedMethods = @("Get", "PUT"); AllowedMethods = @("Get", "PUT");
}); });
$containers = "attachments", "sendfiles", "misc"; $containers = "attachments", "sendfiles", "misc";
$queues = "event", "notifications", "reference-events", "mail"; $queues = "event", "notifications", "mail";
$tables = "event", "metadata", "installationdevice"; $tables = "event", "metadata", "installationdevice";
# End configuration # End configuration

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

@ -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;
#if !OSS #if !OSS

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

@ -1,9 +1,7 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
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.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums; using Bit.Core.Vault.Enums;
@ -15,7 +13,6 @@ namespace Bit.Api.Vault.Controllers;
[Route("tasks")] [Route("tasks")]
[Authorize("Application")] [Authorize("Application")]
[RequireFeature(FeatureFlagKeys.SecurityTasks)]
public class SecurityTaskController : Controller public class SecurityTaskController : Controller
{ {
private readonly IUserService _userService; private readonly IUserService _userService;

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

@ -17,6 +17,7 @@ public enum PolicyType : byte
AutomaticAppLogIn = 12, AutomaticAppLogIn = 12,
FreeFamiliesSponsorshipPolicy = 13, FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14, RemoveUnlockWithPin = 14,
RestrictedItemTypesPolicy = 15,
} }
public static class PolicyTypeExtensions public static class PolicyTypeExtensions
@ -43,7 +44,8 @@ public static class PolicyTypeExtensions
PolicyType.ActivateAutofill => "Active auto-fill", PolicyType.ActivateAutofill => "Active auto-fill",
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications", PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship", PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship",
PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN" PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN",
PolicyType.RestrictedItemTypesPolicy => "Restricted item types",
}; };
} }
} }

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

@ -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,13 +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 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.Total
}; };
} }
@ -108,8 +108,6 @@ public class OrganizationBillingService(
? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions())
: null; : null;
var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
return new OrganizationMetadata( return new OrganizationMetadata(
isEligibleForSelfHost, isEligibleForSelfHost,
isManaged, isManaged,
@ -121,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
@ -248,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;
@ -420,7 +429,7 @@ public class OrganizationBillingService(
var setNonUSBusinessUseToReverseCharge = var setNonUSBusinessUseToReverseCharge =
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (setNonUSBusinessUseToReverseCharge) if (setNonUSBusinessUseToReverseCharge && customer.HasBillingLocation())
{ {
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }

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

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class BadRequestException : Exception public class BadRequestException : Exception
{ {
public BadRequestException() : base() public BadRequestException() : base()
@ -41,5 +43,5 @@ public class BadRequestException : Exception
} }
} }
public ModelStateDictionary ModelState { get; set; } public ModelStateDictionary? ModelState { get; set; }
} }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class ConflictException : Exception public class ConflictException : Exception
{ {
public ConflictException() : base("Conflict.") { } public ConflictException() : base("Conflict.") { }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DnsQueryException : Exception public class DnsQueryException : Exception
{ {
public DnsQueryException(string message) public DnsQueryException(string message)

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DomainClaimedException : Exception public class DomainClaimedException : Exception
{ {
public DomainClaimedException() public DomainClaimedException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DomainVerifiedException : Exception public class DomainVerifiedException : Exception
{ {
public DomainVerifiedException() public DomainVerifiedException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DuplicateDomainException : Exception public class DuplicateDomainException : Exception
{ {
public DuplicateDomainException() public DuplicateDomainException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
/// <summary> /// <summary>
/// Exception to throw when a requested feature is not yet enabled/available for the requesting context. /// Exception to throw when a requested feature is not yet enabled/available for the requesting context.
/// </summary> /// </summary>

View File

@ -1,8 +1,10 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class GatewayException : Exception public class GatewayException : Exception
{ {
public GatewayException(string message, Exception innerException = null) public GatewayException(string message, Exception? innerException = null)
: base(message, innerException) : base(message, innerException)
{ } { }
} }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class InvalidEmailException : Exception public class InvalidEmailException : Exception
{ {
public InvalidEmailException() public InvalidEmailException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class InvalidGatewayCustomerIdException : Exception public class InvalidGatewayCustomerIdException : Exception
{ {
public InvalidGatewayCustomerIdException() public InvalidGatewayCustomerIdException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class NotFoundException : Exception public class NotFoundException : Exception
{ {
public NotFoundException() : base() public NotFoundException() : base()

View File

@ -10,9 +10,11 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.HostedServices; namespace Bit.Core.HostedServices;
#nullable enable
public class ApplicationCacheHostedService : IHostedService, IDisposable public class ApplicationCacheHostedService : IHostedService, IDisposable
{ {
private readonly InMemoryServiceBusApplicationCacheService _applicationCacheService; private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
protected readonly ILogger<ApplicationCacheHostedService> _logger; protected readonly ILogger<ApplicationCacheHostedService> _logger;
private readonly ServiceBusClient _serviceBusClient; private readonly ServiceBusClient _serviceBusClient;
@ -20,8 +22,8 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable
private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient; private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient;
private readonly string _subName; private readonly string _subName;
private readonly string _topicName; private readonly string _topicName;
private CancellationTokenSource _cts; private CancellationTokenSource? _cts;
private Task _executingTask; private Task? _executingTask;
public ApplicationCacheHostedService( public ApplicationCacheHostedService(
@ -67,13 +69,17 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable
{ {
await _subscriptionReceiver.CloseAsync(cancellationToken); await _subscriptionReceiver.CloseAsync(cancellationToken);
await _serviceBusClient.DisposeAsync(); await _serviceBusClient.DisposeAsync();
_cts.Cancel(); _cts?.Cancel();
try try
{ {
await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken); await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken);
} }
catch { } catch { }
await _executingTask;
if (_executingTask != null)
{
await _executingTask;
}
} }
public virtual void Dispose() public virtual void Dispose()

View File

@ -3,6 +3,8 @@ using Microsoft.Extensions.Hosting;
namespace Bit.Core.HostedServices; namespace Bit.Core.HostedServices;
#nullable enable
/// <summary> /// <summary>
/// A startup service that will seed the IP rate limiting stores with any values in the /// A startup service that will seed the IP rate limiting stores with any values in the
/// GlobalSettings configuration. /// GlobalSettings configuration.

View File

@ -3,6 +3,8 @@ using Quartz;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public abstract class BaseJob : IJob public abstract class BaseJob : IJob
{ {
protected readonly ILogger _logger; protected readonly ILogger _logger;

View File

@ -8,6 +8,8 @@ using Quartz.Impl.Matchers;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public abstract class BaseJobsHostedService : IHostedService, IDisposable public abstract class BaseJobsHostedService : IHostedService, IDisposable
{ {
private const int MaximumJobRetries = 10; private const int MaximumJobRetries = 10;
@ -16,7 +18,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
private readonly ILogger<JobListener> _listenerLogger; private readonly ILogger<JobListener> _listenerLogger;
protected readonly ILogger _logger; protected readonly ILogger _logger;
private IScheduler _scheduler; private IScheduler? _scheduler;
protected GlobalSettings _globalSettings; protected GlobalSettings _globalSettings;
public BaseJobsHostedService( public BaseJobsHostedService(
@ -31,7 +33,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
public IEnumerable<Tuple<Type, ITrigger>> Jobs { get; protected set; } public IEnumerable<Tuple<Type, ITrigger>>? Jobs { get; protected set; }
public virtual async Task StartAsync(CancellationToken cancellationToken) public virtual async Task StartAsync(CancellationToken cancellationToken)
{ {
@ -61,10 +63,19 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
_scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger), _scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger),
GroupMatcher<JobKey>.AnyGroup()); GroupMatcher<JobKey>.AnyGroup());
await _scheduler.Start(cancellationToken); await _scheduler.Start(cancellationToken);
var jobKeys = new List<JobKey>();
var triggerKeys = new List<TriggerKey>();
if (Jobs != null) if (Jobs != null)
{ {
foreach (var (job, trigger) in Jobs) foreach (var (job, trigger) in Jobs)
{ {
jobKeys.Add(JobBuilder.Create(job)
.WithIdentity(job.FullName!)
.Build().Key);
triggerKeys.Add(trigger.Key);
for (var retry = 0; retry < MaximumJobRetries; retry++) for (var retry = 0; retry < MaximumJobRetries; retry++)
{ {
// There's a race condition when starting multiple containers simultaneously, retry until it succeeds.. // There's a race condition when starting multiple containers simultaneously, retry until it succeeds..
@ -77,7 +88,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
} }
var jobDetail = JobBuilder.Create(job) var jobDetail = JobBuilder.Create(job)
.WithIdentity(job.FullName) .WithIdentity(job.FullName!)
.Build(); .Build();
var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key); var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key);
@ -106,13 +117,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
// Delete old Jobs and Triggers // Delete old Jobs and Triggers
var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup()); var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup());
var jobKeys = Jobs.Select(j =>
{
var job = j.Item1;
return JobBuilder.Create(job)
.WithIdentity(job.FullName)
.Build().Key;
});
foreach (var key in existingJobKeys) foreach (var key in existingJobKeys)
{ {
@ -126,7 +130,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
} }
var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup()); var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup());
var triggerKeys = Jobs.Select(j => j.Item2.Key);
foreach (var key in existingTriggerKeys) foreach (var key in existingTriggerKeys)
{ {
@ -142,7 +145,10 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
public virtual async Task StopAsync(CancellationToken cancellationToken) public virtual async Task StopAsync(CancellationToken cancellationToken)
{ {
await _scheduler?.Shutdown(cancellationToken); if (_scheduler is not null)
{
await _scheduler.Shutdown(cancellationToken);
}
} }
public virtual void Dispose() public virtual void Dispose()

View File

@ -4,6 +4,8 @@ using Quartz.Spi;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public class JobFactory : IJobFactory public class JobFactory : IJobFactory
{ {
private readonly IServiceProvider _container; private readonly IServiceProvider _container;
@ -16,7 +18,7 @@ public class JobFactory : IJobFactory
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{ {
var scope = _container.CreateScope(); var scope = _container.CreateScope();
return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob; return (scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob)!;
} }
public void ReturnJob(IJob job) public void ReturnJob(IJob job)

View File

@ -3,6 +3,8 @@ using Quartz;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public class JobListener : IJobListener public class JobListener : IJobListener
{ {
private readonly ILogger<JobListener> _logger; private readonly ILogger<JobListener> _logger;
@ -28,7 +30,7 @@ public class JobListener : IJobListener
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))
{ {
_logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {1}.", _logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {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

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public interface INotificationHubProxy public interface INotificationHubProxy
{ {
Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression); Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression);

View File

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public interface INotificationHubPool public interface INotificationHubPool
{ {
NotificationHubConnection ConnectionFor(Guid comb); NotificationHubConnection ConnectionFor(Guid comb);

View File

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubClientProxy : INotificationHubProxy public class NotificationHubClientProxy : INotificationHubProxy
{ {
private readonly IEnumerable<INotificationHubClient> _clients; private readonly IEnumerable<INotificationHubClient> _clients;

View File

@ -1,4 +1,5 @@
using System.Security.Cryptography; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Web; using System.Web;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -7,21 +8,23 @@ using Microsoft.Azure.NotificationHubs;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubConnection public class NotificationHubConnection
{ {
public string HubName { get; init; } public string? HubName { get; init; }
public string ConnectionString { get; init; } public string? ConnectionString { get; init; }
private Lazy<NotificationHubConnectionStringBuilder> _parsedConnectionString; private Lazy<NotificationHubConnectionStringBuilder> _parsedConnectionString;
public Uri Endpoint => _parsedConnectionString.Value.Endpoint; public Uri Endpoint => _parsedConnectionString.Value.Endpoint;
private string SasKey => _parsedConnectionString.Value.SharedAccessKey; private string SasKey => _parsedConnectionString.Value.SharedAccessKey;
private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName; private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName;
public bool EnableSendTracing { get; init; } public bool EnableSendTracing { get; init; }
private NotificationHubClient _hubClient; private NotificationHubClient? _hubClient;
/// <summary> /// <summary>
/// Gets the NotificationHubClient for this connection. /// Gets the NotificationHubClient for this connection.
/// ///
/// If the client is null, it will be initialized. /// If the client is null, it will be initialized.
/// ///
/// <throws>Exception</throws> if the connection is invalid. /// <throws>Exception</throws> if the connection is invalid.
/// </summary> /// </summary>
public NotificationHubClient HubClient public NotificationHubClient HubClient
@ -45,13 +48,13 @@ public class NotificationHubConnection
} }
/// <summary> /// <summary>
/// Gets the start date for registration. /// Gets the start date for registration.
/// ///
/// If null, registration is always disabled. /// If null, registration is always disabled.
/// </summary> /// </summary>
public DateTime? RegistrationStartDate { get; init; } public DateTime? RegistrationStartDate { get; init; }
/// <summary> /// <summary>
/// Gets the end date for registration. /// Gets the end date for registration.
/// ///
/// If null, registration has no end date. /// If null, registration has no end date.
/// </summary> /// </summary>
public DateTime? RegistrationEndDate { get; init; } public DateTime? RegistrationEndDate { get; init; }
@ -155,9 +158,10 @@ public class NotificationHubConnection
}; };
} }
[MemberNotNull(nameof(_hubClient))]
private NotificationHubConnection Init() private NotificationHubConnection Init()
{ {
HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); _hubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing);
return this; return this;
} }

View File

@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubPool : INotificationHubPool public class NotificationHubPool : INotificationHubPool
{ {
private List<NotificationHubConnection> _connections { get; } private List<NotificationHubConnection> _connections { get; }

View File

@ -19,6 +19,8 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
/// <summary> /// <summary>
/// Sends mobile push notifications to the Azure Notification Hub. /// Sends mobile push notifications to the Azure Notification Hub.
/// Used by Cloud-Hosted environments. /// Used by Cloud-Hosted environments.

View File

@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubPushRegistrationService : IPushRegistrationService public class NotificationHubPushRegistrationService : IPushRegistrationService
{ {
private static readonly JsonSerializerOptions webPushSerializationOptions = new() private static readonly JsonSerializerOptions webPushSerializationOptions = new()
@ -37,7 +39,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId,
string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId) string? identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)
{ {
var orgIds = organizationIds.ToList(); var orgIds = organizationIds.ToList();
var clientType = DeviceTypes.ToClientType(type); var clientType = DeviceTypes.ToClientType(type);
@ -79,7 +81,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId, private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId,
string identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId) string? identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId)
{ {
if (string.IsNullOrWhiteSpace(installation.PushChannel)) if (string.IsNullOrWhiteSpace(installation.PushChannel))
{ {
@ -137,7 +139,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId, private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId,
string identifier, ClientType clientType, List<string> organizationIds, Guid installationId) string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
{ {
// The Azure SDK is currently lacking support for web push registrations. // The Azure SDK is currently lacking support for web push registrations.
// We need to use the REST API directly. // We need to use the REST API directly.
@ -187,7 +189,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody, private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody,
string userId, string identifier, ClientType clientType, List<string> organizationIds, Guid installationId) string userId, string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
{ {
var fullTemplateId = $"template:{templateId}"; var fullTemplateId = $"template:{templateId}";

View File

@ -1,5 +1,7 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public record struct WebPushRegistrationData public record struct WebPushRegistrationData
{ {
public string Endpoint { get; init; } public string Endpoint { get; init; }
@ -9,9 +11,9 @@ public record struct WebPushRegistrationData
public record class PushRegistrationData public record class PushRegistrationData
{ {
public string Token { get; set; } public string? Token { get; set; }
public WebPushRegistrationData? WebPush { get; set; } public WebPushRegistrationData? WebPush { get; set; }
public PushRegistrationData(string token) public PushRegistrationData(string? token)
{ {
Token = token; Token = token;
} }

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

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

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

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