diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index f42f226153..d7814849c6 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
- "version": "7.2.0",
+ "version": "7.3.2",
"commands": ["swagger"]
},
"dotnet-ef": {
diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs
index 8c90d778bc..2b337fb4bb 100644
--- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs
+++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs
@@ -550,6 +550,15 @@ public class ProviderBillingService(
[
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))
diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
index 6d38a77d8b..ecdd372df4 100644
--- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
+++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
@@ -242,10 +242,32 @@ public class OrganizationsController : Controller
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);
-
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
-
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
{
TempData["Error"] = "Plan does not support Secrets Manager";
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index c490e90150..11af4d5e0a 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs
index 071aae5060..f1ab1be6bd 100644
--- a/src/Api/Billing/Controllers/OrganizationBillingController.cs
+++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs
@@ -4,6 +4,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Api.Billing.Queries.Organizations;
+using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
@@ -280,17 +281,36 @@ public class OrganizationBillingController(
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
-
if (organization == null)
{
return Error.NotFound();
}
+ var existingPlan = organization.PlanType;
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
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))
{
diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs
index 7b302f3724..5991d0babb 100644
--- a/src/Api/Vault/Controllers/CiphersController.cs
+++ b/src/Api/Vault/Controllers/CiphersController.cs
@@ -42,7 +42,6 @@ public class CiphersController : Controller
private readonly ICurrentContext _currentContext;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
- private readonly IFeatureService _featureService;
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository;
@@ -57,7 +56,6 @@ public class CiphersController : Controller
ICurrentContext currentContext,
ILogger logger,
GlobalSettings globalSettings,
- IFeatureService featureService,
IOrganizationCiphersQuery organizationCiphersQuery,
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository)
@@ -71,7 +69,6 @@ public class CiphersController : Controller
_currentContext = currentContext;
_logger = logger;
_globalSettings = globalSettings;
- _featureService = featureService;
_organizationCiphersQuery = organizationCiphersQuery;
_applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository;
@@ -375,11 +372,6 @@ public class CiphersController : Controller
private async Task CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable cipherIds)
{
- if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
- {
- return await CanEditCipherAsAdminAsync(organizationId, cipherIds);
- }
-
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
diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj
index 01a8bbdd9b..116efdb68c 100644
--- a/src/Billing/Billing.csproj
+++ b/src/Billing/Billing.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs
index 28f4dea4b2..0cffad72d3 100644
--- a/src/Core/Billing/Constants/StripeConstants.cs
+++ b/src/Core/Billing/Constants/StripeConstants.cs
@@ -96,6 +96,12 @@ public static class StripeConstants
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 const string Deferred = "deferred";
diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs
index 0e5cfeb8d9..70cb8f0cf6 100644
--- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs
+++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs
@@ -247,12 +247,23 @@ public class OrganizationBillingService(
organization.Id,
customerSetup.TaxInformation.Country,
customerSetup.TaxInformation.TaxId);
+
+ throw new BadRequestException("billingTaxIdTypeInferenceError");
}
customerCreateOptions.TaxIdData =
[
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;
diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs
index 75a1bf76ec..796f700e9f 100644
--- a/src/Core/Billing/Services/Implementations/SubscriberService.cs
+++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs
@@ -648,6 +648,12 @@ public class SubscriberService(
{
await stripeAdapter.TaxIdCreateAsync(customer.Id,
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)
{
diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
index 304abbaae0..c777d0c0d1 100644
--- a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
+++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
@@ -80,6 +80,15 @@ public class PreviewTaxAmountCommand(
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)
diff --git a/src/Core/Entities/Collection.cs b/src/Core/Entities/Collection.cs
index 8babe10e4c..275cd80d2f 100644
--- a/src/Core/Entities/Collection.cs
+++ b/src/Core/Entities/Collection.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Bit.Core.Enums;
using Bit.Core.Utilities;
#nullable enable
@@ -14,6 +15,8 @@ public class Collection : ITableObject
public string? ExternalId { get; set; }
public DateTime CreationDate { 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()
{
diff --git a/src/Core/Enums/CollectionType.cs b/src/Core/Enums/CollectionType.cs
new file mode 100644
index 0000000000..9bc4fcc9c2
--- /dev/null
+++ b/src/Core/Enums/CollectionType.cs
@@ -0,0 +1,7 @@
+namespace Bit.Core.Enums;
+
+public enum CollectionType
+{
+ SharedCollection = 0,
+ DefaultUserCollection = 1,
+}
diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs
index 23d06bed2b..bdd558df52 100644
--- a/src/Core/Services/Implementations/StripePaymentService.cs
+++ b/src/Core/Services/Implementations/StripePaymentService.cs
@@ -842,7 +842,13 @@ public class StripePaymentService : IPaymentService
try
{
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)
{
@@ -1000,6 +1006,15 @@ public class StripePaymentService : IPaymentService
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))
@@ -1154,6 +1169,15 @@ public class StripePaymentService : IPaymentService
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;
diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs
index 413aee3e0d..5d17441024 100644
--- a/src/Core/Vault/Services/Implementations/CipherService.cs
+++ b/src/Core/Vault/Services/Implementations/CipherService.cs
@@ -821,11 +821,6 @@ public class CipherService : ICipherService
private async Task UserCanDeleteAsync(CipherDetails cipher, Guid userId)
{
- if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
- {
- return await UserCanEditAsync(cipher, userId);
- }
-
var user = await _userService.GetUserByIdAsync(userId);
var organizationAbility = cipher.OrganizationId.HasValue ?
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
@@ -835,11 +830,6 @@ public class CipherService : ICipherService
private async Task UserCanRestoreAsync(CipherDetails cipher, Guid userId)
{
- if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
- {
- return await UserCanEditAsync(cipher, userId);
- }
-
var user = await _userService.GetUserByIdAsync(userId);
var organizationAbility = cipher.OrganizationId.HasValue ?
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.
- // It supports both the old and new logic depending on the feature flag.
private async Task> FilterCiphersByDeletePermission(
IEnumerable ciphers,
HashSet cipherIdsSet,
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 organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj
index 6df65b2310..1951e4d509 100644
--- a/src/SharedWeb/SharedWeb.csproj
+++ b/src/SharedWeb/SharedWeb.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/Sql/dbo/Stored Procedures/Collection_Create.sql b/src/Sql/dbo/Stored Procedures/Collection_Create.sql
index 2e442c6a28..2b3b14fd6b 100644
--- a/src/Sql/dbo/Stored Procedures/Collection_Create.sql
+++ b/src/Sql/dbo/Stored Procedures/Collection_Create.sql
@@ -4,7 +4,9 @@
@Name VARCHAR(MAX),
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
- @RevisionDate DATETIME2(7)
+ @RevisionDate DATETIME2(7),
+ @DefaultUserCollectionEmail NVARCHAR(256) = NULL,
+ @Type TINYINT = 0
AS
BEGIN
SET NOCOUNT ON
@@ -16,7 +18,9 @@ BEGIN
[Name],
[ExternalId],
[CreationDate],
- [RevisionDate]
+ [RevisionDate],
+ [DefaultUserCollectionEmail],
+ [Type]
)
VALUES
(
@@ -25,7 +29,9 @@ BEGIN
@Name,
@ExternalId,
@CreationDate,
- @RevisionDate
+ @RevisionDate,
+ @DefaultUserCollectionEmail,
+ @Type
)
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
diff --git a/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql b/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql
index 87bac3b385..92ffd366e6 100644
--- a/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql
+++ b/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql
@@ -6,12 +6,14 @@ CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers]
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
- @Users AS [dbo].[CollectionAccessSelectionType] READONLY
+ @Users AS [dbo].[CollectionAccessSelectionType] READONLY,
+ @DefaultUserCollectionEmail NVARCHAR(256) = NULL,
+ @Type TINYINT = 0
AS
BEGIN
SET NOCOUNT ON
- EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
+ EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
-- Groups
;WITH [AvailableGroupsCTE] AS(
diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql
index f0eab509ac..4180dc6909 100644
--- a/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql
+++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql
@@ -13,7 +13,9 @@ BEGIN
ExternalId,
MIN([ReadOnly]) AS [ReadOnly],
MIN([HidePasswords]) AS [HidePasswords],
- MAX([Manage]) AS [Manage]
+ MAX([Manage]) AS [Manage],
+ [DefaultUserCollectionEmail],
+ [Type]
FROM
[dbo].[UserCollectionDetails](@UserId)
GROUP BY
@@ -22,5 +24,7 @@ BEGIN
[Name],
CreationDate,
RevisionDate,
- ExternalId
+ ExternalId,
+ [DefaultUserCollectionEmail],
+ [Type]
END
diff --git a/src/Sql/dbo/Stored Procedures/Collection_Update.sql b/src/Sql/dbo/Stored Procedures/Collection_Update.sql
index e75f088d7d..69a009e27a 100644
--- a/src/Sql/dbo/Stored Procedures/Collection_Update.sql
+++ b/src/Sql/dbo/Stored Procedures/Collection_Update.sql
@@ -4,7 +4,9 @@
@Name VARCHAR(MAX),
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
- @RevisionDate DATETIME2(7)
+ @RevisionDate DATETIME2(7),
+ @DefaultUserCollectionEmail NVARCHAR(256) = NULL,
+ @Type TINYINT = 0
AS
BEGIN
SET NOCOUNT ON
@@ -16,9 +18,11 @@ BEGIN
[Name] = @Name,
[ExternalId] = @ExternalId,
[CreationDate] = @CreationDate,
- [RevisionDate] = @RevisionDate
+ [RevisionDate] = @RevisionDate,
+ [DefaultUserCollectionEmail] = @DefaultUserCollectionEmail,
+ [Type] = @Type
WHERE
[Id] = @Id
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
-END
\ No newline at end of file
+END
diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql
index 4a66b20d86..29894f984b 100644
--- a/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql
+++ b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql
@@ -6,12 +6,14 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
- @Users AS [dbo].[CollectionAccessSelectionType] READONLY
+ @Users AS [dbo].[CollectionAccessSelectionType] READONLY,
+ @DefaultUserCollectionEmail NVARCHAR(256) = NULL,
+ @Type TINYINT = 0
AS
BEGIN
SET NOCOUNT ON
- EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
+ EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
-- Groups
-- Delete groups that are no longer in source
diff --git a/src/Sql/dbo/Tables/Collection.sql b/src/Sql/dbo/Tables/Collection.sql
index 0106f341df..03064fd978 100644
--- a/src/Sql/dbo/Tables/Collection.sql
+++ b/src/Sql/dbo/Tables/Collection.sql
@@ -1,10 +1,12 @@
CREATE TABLE [dbo].[Collection] (
- [Id] UNIQUEIDENTIFIER NOT NULL,
- [OrganizationId] UNIQUEIDENTIFIER NOT NULL,
- [Name] VARCHAR (MAX) NOT NULL,
- [ExternalId] NVARCHAR (300) NULL,
- [CreationDate] DATETIME2 (7) NOT NULL,
- [RevisionDate] DATETIME2 (7) NOT NULL,
+ [Id] UNIQUEIDENTIFIER NOT NULL,
+ [OrganizationId] UNIQUEIDENTIFIER NOT NULL,
+ [Name] VARCHAR (MAX) NOT NULL,
+ [ExternalId] NVARCHAR (300) NULL,
+ [CreationDate] DATETIME2 (7) NOT NULL,
+ [RevisionDate] DATETIME2 (7) NOT NULL,
+ [DefaultUserCollectionEmail] NVARCHAR(256) NULL,
+ [Type] TINYINT NOT NULL DEFAULT(0),
CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
);
diff --git a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql
index 3bb50a51cf..9f2caeb87f 100644
--- a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql
+++ b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql
@@ -73,7 +73,9 @@ BEGIN
C.[Name],
C.[CreationDate],
C.[RevisionDate],
- C.[ExternalId]
+ C.[ExternalId],
+ C.[DefaultUserCollectionEmail],
+ C.[Type]
IF (@IncludeAccessRelationships = 1)
BEGIN
diff --git a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql
index 2c99282eef..267024f56c 100644
--- a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql
+++ b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql
@@ -73,7 +73,9 @@ BEGIN
C.[Name],
C.[CreationDate],
C.[RevisionDate],
- C.[ExternalId]
+ C.[ExternalId],
+ C.[DefaultUserCollectionEmail],
+ C.[Type]
IF (@IncludeAccessRelationships = 1)
BEGIN
diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs
index bca6bbc048..d1f5a212c9 100644
--- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs
+++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs
@@ -4,7 +4,6 @@ using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Models.Response;
-using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -169,6 +168,7 @@ public class CiphersControllerTests
}
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
@@ -197,65 +197,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteAdmin_WithOwnerOrAdmin_WithEditPermission_DeletesCipher(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Edit = true;
- cipherDetails.Manage = false;
-
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
-
- await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
-
- await sutProvider.GetDependency().Received(1).DeleteAsync(cipherDetails, userId, true);
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Edit = false;
- cipherDetails.Manage = false;
-
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
-
- await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id));
-
- await sutProvider.GetDependency().DidNotReceive().DeleteAsync(Arg.Any(), Arg.Any(), Arg.Any());
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(
+ public async Task DeleteAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -266,7 +208,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -293,7 +234,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
+ public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -304,7 +245,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -339,11 +279,22 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
- .Returns(new List { new() { Id = cipherDetails.Id } });
+ .Returns(new List
+ {
+ new() { Id = cipherDetails.Id, OrganizationId = cipherDetails.OrganizationId }
+ });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
@@ -426,10 +377,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipher.Id));
}
+
+
+
+
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithEditPermission_DeletesCiphers(
+ public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -437,74 +392,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(ciphers.Select(c => new CipherDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id,
- Edit = true
- }).ToList());
-
- await sutProvider.Sut.DeleteManyAdmin(model);
-
- await sutProvider.GetDependency()
- .Received(1)
- .DeleteManyAsync(
- Arg.Is>(ids =>
- ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
- userId, organization.Id, true);
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- model.OrganizationId = organization.Id.ToString();
- model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
-
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency()
- .GetProperUserId(default)
- .ReturnsForAnyArgs(userId);
-
- sutProvider.GetDependency()
- .GetOrganization(new Guid(model.OrganizationId))
- .Returns(organization);
-
- sutProvider.GetDependency()
- .GetManyByOrganizationIdAsync(new Guid(model.OrganizationId))
- .Returns(ciphers);
-
- sutProvider.GetDependency()
- .GetOrganizationAbilityAsync(new Guid(model.OrganizationId))
- .Returns(new OrganizationAbility
- {
- Id = new Guid(model.OrganizationId),
- AllowAdminAccessToAllCollectionItems = false,
- });
-
- await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAdmin(model));
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_DeletesCiphers(
- OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- model.OrganizationId = organization.Id.ToString();
- model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -540,7 +427,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task DeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
+ public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -548,7 +435,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -586,10 +472,18 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
- .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList());
+ .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
await sutProvider.Sut.DeleteManyAdmin(model);
@@ -688,67 +582,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAdmin(model));
}
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_SoftDeletesCipher(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Edit = true;
- organization.Type = organizationUserType;
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
- await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
-
- await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
- }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Edit = false;
- cipherDetails.Manage = false;
-
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
-
- await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));
-
- await sutProvider.GetDependency().DidNotReceive().SoftDeleteAsync(Arg.Any(), Arg.Any(), Arg.Any());
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(
+ public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -759,7 +600,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -786,7 +626,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
+ public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -797,7 +637,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -833,12 +672,20 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
- .Returns(new List { new() { Id = cipherDetails.Id } });
+ .Returns(new List { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
@@ -856,6 +703,7 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails });
@@ -890,6 +738,70 @@ public class CiphersControllerTests
await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
}
+ [Theory]
+ [BitAutoData(OrganizationUserType.Owner)]
+ [BitAutoData(OrganizationUserType.Admin)]
+ public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionFalse_SoftDeletesCipher(
+ OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
+ CurrentContextOrganization organization, SutProvider sutProvider)
+ {
+ cipherDetails.UserId = null;
+ cipherDetails.OrganizationId = organization.Id;
+ cipherDetails.Edit = true;
+ cipherDetails.Manage = false; // Only Edit permission, not Manage
+ organization.Type = organizationUserType;
+
+ sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
+ sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
+ sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(userId)
+ .Returns(new List { cipherDetails });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = false
+ });
+
+ await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
+
+ await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
+ }
+
+ [Theory]
+ [BitAutoData(OrganizationUserType.Owner)]
+ [BitAutoData(OrganizationUserType.Admin)]
+ public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionTrue_ThrowsNotFoundException(
+ OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
+ CurrentContextOrganization organization, SutProvider sutProvider)
+ {
+ cipherDetails.UserId = null;
+ cipherDetails.OrganizationId = organization.Id;
+ cipherDetails.Edit = true;
+ cipherDetails.Manage = false; // Only Edit permission, not Manage
+ organization.Type = organizationUserType;
+
+ sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
+ sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
+ sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(userId)
+ .Returns(new List { cipherDetails });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
+
+ await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));
+ }
+
[Theory]
[BitAutoData]
public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(
@@ -922,10 +834,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id));
}
+
+
+
+
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithEditPermission_SoftDeletesCiphers(
+ public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCiphers(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -933,65 +849,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(ciphers.Select(c => new CipherDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id,
- Edit = true
- }).ToList());
-
- await sutProvider.Sut.PutDeleteManyAdmin(model);
-
- await sutProvider.GetDependency()
- .Received(1)
- .SoftDeleteManyAsync(
- Arg.Is>(ids =>
- ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
- userId, organization.Id, true);
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- model.OrganizationId = organization.Id.ToString();
- model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
-
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(ciphers.Select(c => new CipherDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id,
- Edit = false
- }).ToList());
-
- await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteManyAdmin(model));
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCiphers(
- OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- model.OrganizationId = organization.Id.ToString();
- model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -1027,7 +884,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutDeleteManyAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
+ public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -1035,7 +892,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -1073,10 +929,18 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
- .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList());
+ .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
await sutProvider.Sut.PutDeleteManyAdmin(model);
@@ -1099,7 +963,14 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
+ // Set organization ID on ciphers to avoid "Cipher needs to belong to a user or an organization" error
+ foreach (var cipher in ciphers)
+ {
+ cipher.OrganizationId = organization.Id;
+ }
+
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);
sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
@@ -1130,7 +1001,14 @@ public class CiphersControllerTests
organization.Type = OrganizationUserType.Custom;
organization.Permissions.EditAnyCollection = true;
+ // Set organization ID on ciphers to avoid "Cipher needs to belong to a user or an organization" error
+ foreach (var cipher in ciphers)
+ {
+ cipher.OrganizationId = organization.Id;
+ }
+
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);
@@ -1175,68 +1053,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteManyAdmin(model));
}
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_RestoresCipher(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Type = CipherType.Login;
- cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
- cipherDetails.Edit = true;
- organization.Type = organizationUserType;
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
- var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
-
- Assert.IsType(result);
- await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true);
- }
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- cipherDetails.UserId = null;
- cipherDetails.OrganizationId = organization.Id;
- cipherDetails.Edit = false;
- cipherDetails.Manage = false;
-
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(new List
- {
- cipherDetails
- });
-
- await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(
+ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -1249,7 +1073,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -1277,7 +1100,7 @@ public class CiphersControllerTests
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreAdmin_WithLimitItemDeletionEnabled_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
+ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -1288,7 +1111,6 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitItemDeletion).Returns(true);
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
@@ -1323,11 +1145,19 @@ public class CiphersControllerTests
organization.Type = organizationUserType;
sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
- .Returns(new List { new() { Id = cipherDetails.Id } });
+ .Returns(new List { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true
+ });
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
@@ -1386,6 +1216,75 @@ public class CiphersControllerTests
await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true);
}
+ [Theory]
+ [BitAutoData(OrganizationUserType.Owner)]
+ [BitAutoData(OrganizationUserType.Admin)]
+ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionFalse_RestoresCipher(
+ OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
+ CurrentContextOrganization organization, SutProvider sutProvider)
+ {
+ cipherDetails.UserId = null;
+ cipherDetails.OrganizationId = organization.Id;
+ cipherDetails.Type = CipherType.Login;
+ cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
+ cipherDetails.Edit = true;
+ cipherDetails.Manage = false; // Only Edit permission, not Manage
+ organization.Type = organizationUserType;
+
+ sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
+ sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
+ sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(userId)
+ .Returns(new List { cipherDetails });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = false // Permissive mode - Edit permission should work
+ });
+
+ var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
+
+ Assert.IsType(result);
+ await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true);
+ }
+
+ [Theory]
+ [BitAutoData(OrganizationUserType.Owner)]
+ [BitAutoData(OrganizationUserType.Admin)]
+ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionTrue_ThrowsNotFoundException(
+ OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
+ CurrentContextOrganization organization, SutProvider sutProvider)
+ {
+ cipherDetails.UserId = null;
+ cipherDetails.OrganizationId = organization.Id;
+ cipherDetails.Type = CipherType.Login;
+ cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
+ cipherDetails.Edit = true;
+ cipherDetails.Manage = false; // Only Edit permission, not Manage
+ organization.Type = organizationUserType;
+
+ sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
+ sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
+ sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
+ sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(userId)
+ .Returns(new List { cipherDetails });
+ sutProvider.GetDependency()
+ .GetOrganizationAbilityAsync(organization.Id)
+ .Returns(new OrganizationAbility
+ {
+ Id = organization.Id,
+ LimitItemDeletion = true // Restrictive mode - Edit permission should NOT work
+ });
+
+ await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
+ }
+
[Theory]
[BitAutoData]
public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(
@@ -1420,10 +1319,14 @@ public class CiphersControllerTests
await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
}
+
+
+
+
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithEditPermission_RestoresCiphers(
+ public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCiphers(
OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List ciphers,
CurrentContextOrganization organization, SutProvider sutProvider)
{
@@ -1431,77 +1334,6 @@ public class CiphersControllerTests
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
organization.Type = organizationUserType;
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(ciphers.Select(c => new CipherDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id,
- Edit = true
- }).ToList());
-
- var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id
- }).ToList();
-
- sutProvider.GetDependency()
- .RestoreManyAsync(Arg.Is>(ids =>
- ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),
- userId, organization.Id, true)
- .Returns(cipherOrgDetails);
-
- var result = await sutProvider.Sut.PutRestoreManyAdmin(model);
-
- await sutProvider.GetDependency().Received(1)
- .RestoreManyAsync(
- Arg.Is>(ids =>
- ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),
- userId, organization.Id, true);
- }
-
- [Theory]
- [BitAutoData(OrganizationUserType.Owner)]
- [BitAutoData(OrganizationUserType.Admin)]
- public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithoutEditPermission_ThrowsNotFoundException(
- OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List ciphers,
- CurrentContextOrganization organization, SutProvider sutProvider)
- {
- model.OrganizationId = organization.Id;
- model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
- organization.Type = organizationUserType;
-
- sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);
- sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization);
- sutProvider.GetDependency()
- .GetManyByUserIdAsync(userId)
- .Returns(ciphers.Select(c => new CipherDetails
- {
- Id = c.Id,
- OrganizationId = organization.Id,
- Edit = false,
- Type = CipherType.Login,
- Data = JsonSerializer.Serialize(new CipherLoginData())
- }).ToList());
-
- await Assert.ThrowsAsync