2023-06-30 18:41:11 -04:00
|
|
|
|
using System.Text.Json;
|
2023-11-29 09:18:08 +10:00
|
|
|
|
using Bit.Core.AdminConsole.Entities;
|
2025-01-09 12:14:24 -08:00
|
|
|
|
using Bit.Core.AdminConsole.Repositories;
|
2025-04-23 13:16:29 -05:00
|
|
|
|
using Bit.Core.Billing.Enums;
|
2023-06-30 18:41:11 -04:00
|
|
|
|
using Bit.Core.Entities;
|
2022-12-02 14:24:30 -05:00
|
|
|
|
using Bit.Core.Enums;
|
2023-05-30 13:25:55 -04:00
|
|
|
|
using Bit.Core.Models.Data;
|
2025-06-27 11:17:47 -05:00
|
|
|
|
using Bit.Core.NotificationCenter.Entities;
|
|
|
|
|
|
using Bit.Core.NotificationCenter.Repositories;
|
2022-12-02 14:24:30 -05:00
|
|
|
|
using Bit.Core.Repositories;
|
[PM-24233] Use BulkResourceCreationService in CipherRepository (#6201)
* Add constant for CipherRepositoryBulkResourceCreation in FeatureFlagKeys
* Add bulk creation methods for Ciphers, Folders, and CollectionCiphers in BulkResourceCreationService
- Implemented CreateCiphersAsync, CreateFoldersAsync, CreateCollectionCiphersAsync, and CreateTempCiphersAsync methods for bulk insertion.
- Added helper methods to build DataTables for Ciphers, Folders, and CollectionCiphers.
- Enhanced error handling for empty collections during bulk operations.
* Refactor CipherRepository to utilize BulkResourceCreationService
- Introduced IFeatureService to manage feature flag checks for bulk operations.
- Updated methods to conditionally use BulkResourceCreationService for creating Ciphers, Folders, and CollectionCiphers based on feature flag status.
- Enhanced existing bulk copy logic to maintain functionality while integrating feature flag checks.
* Add InlineFeatureService to DatabaseDataAttribute for feature flag management
- Introduced EnabledFeatureFlags property to DatabaseDataAttribute for configuring feature flags.
- Integrated InlineFeatureService to provide feature flag checks within the service collection.
- Enhanced GetData method to utilize feature flags for conditional service registration.
* Add tests for bulk creation of Ciphers in CipherRepositoryTests
- Implemented tests for bulk creation of Ciphers, Folders, and Collections with feature flag checks.
- Added test cases for updating multiple Ciphers to validate bulk update functionality.
- Enhanced existing test structure to ensure comprehensive coverage of bulk operations in the CipherRepository.
* Refactor BulkResourceCreationService to use dynamic types for DataColumns
- Updated DataColumn definitions in BulkResourceCreationService to utilize the actual types of properties from the cipher object instead of hardcoded types.
- Simplified the assignment of nullable properties to directly use their values, improving code readability and maintainability.
* Update BulkResourceCreationService to use specific types for DataColumns
- Changed DataColumn definitions to use specific types (short and string) instead of dynamic types based on cipher properties.
- Improved handling of nullable properties when assigning values to DataTable rows, ensuring proper handling of DBNull for null values.
* Refactor CipherRepositoryTests for improved clarity and consistency
- Renamed test methods to better reflect their purpose and improve readability.
- Updated test data to use more descriptive names for users, folders, and collections.
- Enhanced test structure with clear Arrange, Act, and Assert sections for better understanding of test flow.
- Ensured all tests validate the expected outcomes for bulk operations with feature flag checks.
* Update CipherRepositoryBulkResourceCreation feature flag key
* Refactor DatabaseDataAttribute usage in CipherRepositoryTests to use array syntax for EnabledFeatureFlags
* Update CipherRepositoryTests to use GenerateComb for generating unique IDs
* Refactor CipherRepository methods to accept a boolean parameter for enabling bulk resource creation based on feature flags. Update tests to verify functionality with and without the feature flag enabled.
* Refactor CipherRepository and related services to support new methods for bulk resource creation without boolean parameters.
2025-09-03 14:57:53 +01:00
|
|
|
|
using Bit.Core.Utilities;
|
2023-03-02 13:23:38 -05:00
|
|
|
|
using Bit.Core.Vault.Entities;
|
|
|
|
|
|
using Bit.Core.Vault.Enums;
|
|
|
|
|
|
using Bit.Core.Vault.Models.Data;
|
|
|
|
|
|
using Bit.Core.Vault.Repositories;
|
2022-12-02 14:24:30 -05:00
|
|
|
|
using Xunit;
|
[PM-24233] Use BulkResourceCreationService in CipherRepository (#6201)
* Add constant for CipherRepositoryBulkResourceCreation in FeatureFlagKeys
* Add bulk creation methods for Ciphers, Folders, and CollectionCiphers in BulkResourceCreationService
- Implemented CreateCiphersAsync, CreateFoldersAsync, CreateCollectionCiphersAsync, and CreateTempCiphersAsync methods for bulk insertion.
- Added helper methods to build DataTables for Ciphers, Folders, and CollectionCiphers.
- Enhanced error handling for empty collections during bulk operations.
* Refactor CipherRepository to utilize BulkResourceCreationService
- Introduced IFeatureService to manage feature flag checks for bulk operations.
- Updated methods to conditionally use BulkResourceCreationService for creating Ciphers, Folders, and CollectionCiphers based on feature flag status.
- Enhanced existing bulk copy logic to maintain functionality while integrating feature flag checks.
* Add InlineFeatureService to DatabaseDataAttribute for feature flag management
- Introduced EnabledFeatureFlags property to DatabaseDataAttribute for configuring feature flags.
- Integrated InlineFeatureService to provide feature flag checks within the service collection.
- Enhanced GetData method to utilize feature flags for conditional service registration.
* Add tests for bulk creation of Ciphers in CipherRepositoryTests
- Implemented tests for bulk creation of Ciphers, Folders, and Collections with feature flag checks.
- Added test cases for updating multiple Ciphers to validate bulk update functionality.
- Enhanced existing test structure to ensure comprehensive coverage of bulk operations in the CipherRepository.
* Refactor BulkResourceCreationService to use dynamic types for DataColumns
- Updated DataColumn definitions in BulkResourceCreationService to utilize the actual types of properties from the cipher object instead of hardcoded types.
- Simplified the assignment of nullable properties to directly use their values, improving code readability and maintainability.
* Update BulkResourceCreationService to use specific types for DataColumns
- Changed DataColumn definitions to use specific types (short and string) instead of dynamic types based on cipher properties.
- Improved handling of nullable properties when assigning values to DataTable rows, ensuring proper handling of DBNull for null values.
* Refactor CipherRepositoryTests for improved clarity and consistency
- Renamed test methods to better reflect their purpose and improve readability.
- Updated test data to use more descriptive names for users, folders, and collections.
- Enhanced test structure with clear Arrange, Act, and Assert sections for better understanding of test flow.
- Ensured all tests validate the expected outcomes for bulk operations with feature flag checks.
* Update CipherRepositoryBulkResourceCreation feature flag key
* Refactor DatabaseDataAttribute usage in CipherRepositoryTests to use array syntax for EnabledFeatureFlags
* Update CipherRepositoryTests to use GenerateComb for generating unique IDs
* Refactor CipherRepository methods to accept a boolean parameter for enabling bulk resource creation based on feature flags. Update tests to verify functionality with and without the feature flag enabled.
* Refactor CipherRepository and related services to support new methods for bulk resource creation without boolean parameters.
2025-09-03 14:57:53 +01:00
|
|
|
|
using CipherType = Bit.Core.Vault.Enums.CipherType;
|
2022-12-02 14:24:30 -05:00
|
|
|
|
|
|
|
|
|
|
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
|
|
|
|
|
|
|
|
|
|
|
public class CipherRepositoryTests
|
|
|
|
|
|
{
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task DeleteAsync_UpdatesUserRevisionDate(
|
|
|
|
|
|
IUserRepository userRepository,
|
2023-10-12 05:15:02 -04:00
|
|
|
|
ICipherRepository cipherRepository)
|
2022-12-02 14:24:30 -05:00
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
2022-12-02 14:24:30 -05:00
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
UserId = user.Id,
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Data = "", // TODO: EF does not enforce this as NOT NULL
|
2022-12-02 14:24:30 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await cipherRepository.DeleteAsync(cipher);
|
|
|
|
|
|
|
|
|
|
|
|
var deletedCipher = await cipherRepository.GetByIdAsync(cipher.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Null(deletedCipher);
|
|
|
|
|
|
var updatedUser = await userRepository.GetByIdAsync(user.Id);
|
2024-08-15 17:14:22 -04:00
|
|
|
|
Assert.NotNull(updatedUser);
|
2022-12-02 14:24:30 -05:00
|
|
|
|
Assert.NotEqual(updatedUser.AccountRevisionDate, user.AccountRevisionDate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
2023-05-17 06:14:36 -04:00
|
|
|
|
public async Task CreateAsync_UpdateWithCollections_Works(
|
2022-12-02 14:24:30 -05:00
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
2023-10-12 05:15:02 -04:00
|
|
|
|
ICollectionCipherRepository collectionCipherRepository)
|
2022-12-02 14:24:30 -05:00
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
2022-12-02 14:24:30 -05:00
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-05-30 13:25:55 -04:00
|
|
|
|
user = await userRepository.GetByIdAsync(user.Id);
|
2024-08-15 17:14:22 -04:00
|
|
|
|
Assert.NotNull(user);
|
2023-05-30 13:25:55 -04:00
|
|
|
|
|
2022-12-02 14:24:30 -05:00
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
2023-05-30 13:25:55 -04:00
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test" // TODO: EF does not enforce this as NOT NULL
|
2022-12-02 14:24:30 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
2023-05-30 13:25:55 -04:00
|
|
|
|
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
2022-12-02 14:24:30 -05:00
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
2022-12-02 14:24:30 -05:00
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var collection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-05-30 13:25:55 -04:00
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(collection.Id, new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new CollectionAccessSelection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser.Id,
|
|
|
|
|
|
HidePasswords = true,
|
|
|
|
|
|
ReadOnly = true,
|
[AC-1373] Flexible Collections (#3245)
* [AC-1117] Add manage permission (#3126)
* Update sql files to add Manage permission
* Add migration script
* Rename collection manage migration file to remove duplicate migration date
* Migrations
* Add manage to models
* Add manage to repository
* Add constraint to Manage columns
* Migration lint fixes
* Add manage to OrganizationUserUserDetails_ReadWithCollectionsById
* Add missing manage fields
* Add 'Manage' to UserCollectionDetails
* Use CREATE OR ALTER where possible
* [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145)
* feat: update org table with new column, write migration, refs AC-1374
* feat: update views with new column, refs AC-1374
* feat: Alter sprocs (org create/update) to include new column, refs AC-1374
* feat: update entity/data/request/response models to handle new column, refs AC-1374
* feat: update necessary Provider related views during migration, refs AC-1374
* fix: update org create to default new column to false, refs AC-1374
* feat: added new API/request model for collection management and removed property from update request model, refs AC-1374
* fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374
* fix: dotnet format, refs AC-1374
* feat: add ef migrations to reflect mssql changes, refs AC-1374
* fix: dotnet format, refs AC-1374
* feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374
* fix: merge conflict resolution
* [AC-1174] CollectionUser and CollectionGroup authorization handlers (#3194)
* [AC-1174] Introduce BulkAuthorizationHandler.cs
* [AC-1174] Introduce CollectionUserAuthorizationHandler
* [AC-1174] Add CreateForNewCollection CollectionUser requirement
* [AC-1174] Add some more details to CollectionCustomization
* [AC-1174] Formatting
* [AC-1174] Add CollectionGroupOperation.cs
* [AC-1174] Introduce CollectionGroupAuthorizationHandler.cs
* [AC-1174] Cleanup CollectionFixture customization
Implement and use re-usable extension method to support seeded Guids
* [AC-1174] Introduce WithValueFromList AutoFixtureExtensions
Modify CollectionCustomization to use multiple organization Ids for auto generated test data
* [AC-1174] Simplify CollectionUserAuthorizationHandler.cs
Modify the authorization handler to only perform authorization logic. Validation logic will need to be handled by any calling commands/controllers instead.
* [AC-1174] Introduce shared CollectionAccessAuthorizationHandlerBase
A shared base authorization handler was created for both CollectionUser and CollectionGroup resources, as they share the same underlying management authorization logic.
* [AC-1174] Update CollectionUserAuthorizationHandler and CollectionGroupAuthorizationHandler to use the new CollectionAccessAuthorizationHandlerBase class
* [AC-1174] Formatting
* [AC-1174] Cleanup typo and redundant ToList() call
* [AC-1174] Add check for provider users
* [AC-1174] Reduce nested loops
* [AC-1174] Introduce ICollectionAccess.cs
* [AC-1174] Remove individual CollectionGroup and CollectionUser auth handlers and use base class instead
* [AC-1174] Tweak unit test to fail minimally
* [AC-1174] Reorganize authorization handlers in Core project
* [AC-1174] Introduce new AddCoreAuthorizationHandlers() extension method
* [AC-1174] Move CollectionAccessAuthorizationHandler into Api project
* [AC-1174] Move CollectionFixture to Vault folder
* [AC-1174] Rename operation to CreateUpdateDelete
* [AC-1174] Require single organization for collection access authorization handler
- Add requirement that all target collections must belong to the same organization
- Simplify logic related to multiple organizations
- Update tests and helpers
- Use ToHashSet to improve lookup time
* [AC-1174] Fix null reference exception
* [AC-1174] Throw bad request exception when collections belong to different organizations
* [AC-1174] Switch to CollectionAuthorizationHandler instead of CollectionAccessAuthorizationHandler to reduce complexity
* Fix improper merge conflict resolution
* fix: add permission check for collection management api, refs AC-1647 (#3252)
* [AC-1125] Enforce org setting for creating/deleting collections (#3241)
* [AC-1117] Add manage permission (#3126)
* Update sql files to add Manage permission
* Add migration script
* Rename collection manage migration file to remove duplicate migration date
* Migrations
* Add manage to models
* Add manage to repository
* Add constraint to Manage columns
* Migration lint fixes
* Add manage to OrganizationUserUserDetails_ReadWithCollectionsById
* Add missing manage fields
* Add 'Manage' to UserCollectionDetails
* Use CREATE OR ALTER where possible
* [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145)
* feat: update org table with new column, write migration, refs AC-1374
* feat: update views with new column, refs AC-1374
* feat: Alter sprocs (org create/update) to include new column, refs AC-1374
* feat: update entity/data/request/response models to handle new column, refs AC-1374
* feat: update necessary Provider related views during migration, refs AC-1374
* fix: update org create to default new column to false, refs AC-1374
* feat: added new API/request model for collection management and removed property from update request model, refs AC-1374
* fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374
* fix: dotnet format, refs AC-1374
* feat: add ef migrations to reflect mssql changes, refs AC-1374
* fix: dotnet format, refs AC-1374
* feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374
* feat: created collection auth handler/operations, added LimitCollectionCdOwnerAdmin to CurrentContentOrganization, refs AC-1125
* feat: create vault service collection extensions and register with base services, refs AC-1125
* feat: deprecated CurrentContext.CreateNewCollections, refs AC-1125
* feat: deprecate DeleteAnyCollection for single resource usages, refs AC-1125
* feat: move service registration to api, update references, refs AC-1125
* feat: add bulk delete authorization handler, refs AC-1125
* feat: always assign user and give manage access on create, refs AC-1125
* fix: updated CurrentContextOrganization type, refs AC-1125
* feat: combined existing collection authorization handlers/operations, refs AC-1125
* fix: OrganizationServiceTests -> CurrentContentOrganization typo, refs AC-1125
* fix: format, refs AC-1125
* fix: update collection controller tests, refs AC-1125
* fix: dotnet format, refs AC-1125
* feat: removed extra BulkAuthorizationHandler, refs AC-1125
* fix: dotnet format, refs AC-1125
* fix: change string to guid for org id, update bulk delete request model, refs AC-1125
* fix: remove delete many collection check, refs AC-1125
* fix: clean up collection auth handler, refs AC-1125
* fix: format fix for CollectionOperations, refs AC-1125
* fix: removed unnecessary owner check, add org null check to custom permission validation, refs AC-1125
* fix: remove unused methods in CurrentContext, refs AC-1125
* fix: removed obsolete test, fixed failling delete many test, refs AC-1125
* fix: CollectionAuthorizationHandlerTests fixes, refs AC-1125
* fix: OrganizationServiceTests fix broken test by mocking GetOrganization, refs AC-1125
* fix: CollectionAuthorizationHandler - remove unused repository, refs AC-1125
* feat: moved UserId null check to common method, refs AC-1125
* fix: updated auth handler tests to remove dependency on requirement for common code checks, refs AC-1125
* feat: updated conditionals/comments for create/delete methods within colleciton auth handler, refs AC-1125
* feat: added create/delete collection auth handler success methods, refs AC-1125
* fix: new up permissions to prevent excessive null checks, refs AC-1125
* fix: remove old reference to CreateNewCollections, refs AC-1125
* fix: typo within ViewAssignedCollections method, refs AC-1125
---------
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
* refactor: remove organizationId from CollectionBulkDeleteRequestModel, refs AC-1649 (#3282)
* [AC-1174] Bulk Collection Management (#3229)
* [AC-1174] Update SelectionReadOnlyRequestModel to use Guid for Id property
* [AC-1174] Introduce initial bulk-access collection endpoint
* [AC-1174] Introduce BulkAddCollectionAccessCommand and validation logic/tests
* [AC-1174] Add CreateOrUpdateAccessMany method to CollectionRepository
* [AC-1174] Add event logs for bulk add collection access command
* [AC-1174] Add User_BumpAccountRevisionDateByCollectionIds and database migration script
* [AC-1174] Implement EF repository method
* [AC-1174] Improve null checks
* [AC-1174] Remove unnecessary BulkCollectionAccessRequestModel helpers
* [AC-1174] Add unit tests for new controller endpoint
* [AC-1174] Fix formatting
* [AC-1174] Remove comment
* [AC-1174] Remove redundant organizationId parameter
* [AC-1174] Ensure user and group Ids are distinct
* [AC-1174] Cleanup tests based on PR feedback
* [AC-1174] Formatting
* [AC-1174] Update CollectionGroup alias in the sproc
* [AC-1174] Add some additional comments to SQL sproc
* [AC-1174] Add comment explaining additional SaveChangesAsync call
---------
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
* [AC-1646] Rename LimitCollectionCdOwnerAdmin column (#3300)
* Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion
* Rename and bump migration script
* [AC-1666] Removed EditAnyCollection from Create/Delete permission checks (#3301)
* fix: remove EditAnyCollection from Create/Delete permission check, refs AC-1666
* fix: updated comment, refs AC-1666
* [AC-1669] Bug - Remove obsolete assignUserId from CollectionService.SaveAsync(...) (#3312)
* fix: remove AssignUserId from CollectionService.SaveAsync, refs AC-1669
* fix: add manage access conditional before creating collection, refs AC-1669
* fix: move access logic for create/update, fix all tests, refs AC-1669
* fix: add CollectionAccessSelection fixture, update tests, update bad reqeuest message, refs AC-1669
* fix: format, refs AC-1669
* fix: update null params with specific arg.is null checks, refs Ac-1669
* fix: update attribute class name, refs AC-1669
* [AC-1713] [Flexible collections] Add feature flags to server (#3334)
* Add feature flags for FlexibleCollections and BulkCollectionAccess
* Flag new routes and behaviour
---------
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
* Add joint codeownership for auth handlers (#3346)
* [AC-1717] Update default values for LimitCollectionCreationDeletion (#3365)
* Change default value in organization create sproc to 1
* Drop old column name still present in some QA instances
* Set LimitCollectionCreationDeletion value in code based on feature flag
* Fix: add missing namespace after merging in master
* Fix: add missing namespace after merging in master
* [AC-1683] Fix DB migrations for new Manage permission (#3307)
* [AC-1683] Update migration script and introduce V2 procedures and types
* [AC-1683] Update repository calls to use new V2 procedures / types
* [AC-1684] Update bulk add collection migration script to use new V2 type
* [AC-1683] Undo Manage changes to more original procedures
* [AC-1683] Restore whitespace changes
* [AC-1683] Clarify comments regarding explicit column lists
* [AC-1683] Update migration script dates
* [AC-1683] Split the migration script for readability
* [AC-1683] Re-name SelectReadOnlyArray_V2 to CollectionAccessSelectionType
* [AC-1648] [Flexible Collections] Bump migration scripts before feature branch merge (#3371)
* Bump dates on sql migration scripts
* Bump date on ef migrations
---------
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
2023-11-01 19:30:52 +10:00
|
|
|
|
Manage = true
|
2023-05-30 13:25:55 -04:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
|
2022-12-02 14:24:30 -05:00
|
|
|
|
await cipherRepository.CreateAsync(new CipherDetails
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Data = "", // TODO: EF does not enforce this as NOT NULL
|
2022-12-02 14:24:30 -05:00
|
|
|
|
}, new List<Guid>
|
|
|
|
|
|
{
|
|
|
|
|
|
collection.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var updatedUser = await userRepository.GetByIdAsync(user.Id);
|
|
|
|
|
|
|
2024-08-15 17:14:22 -04:00
|
|
|
|
Assert.NotNull(updatedUser);
|
2023-05-30 13:25:55 -04:00
|
|
|
|
Assert.True(updatedUser.AccountRevisionDate - user.AccountRevisionDate > TimeSpan.Zero,
|
|
|
|
|
|
"The AccountRevisionDate is expected to be changed");
|
2022-12-02 14:24:30 -05:00
|
|
|
|
|
|
|
|
|
|
var collectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
Assert.NotEmpty(collectionCiphers);
|
|
|
|
|
|
}
|
2023-06-30 18:41:11 -04:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task ReplaceAsync_SuccessfullyMovesCipherToOrganization(IUserRepository userRepository,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
2023-10-12 05:15:02 -04:00
|
|
|
|
IFolderRepository folderRepository)
|
2023-06-30 18:41:11 -04:00
|
|
|
|
{
|
|
|
|
|
|
// This tests what happens when a cipher is moved into an organizations
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
user = await userRepository.GetByIdAsync(user.Id);
|
2024-08-15 17:14:22 -04:00
|
|
|
|
Assert.NotNull(user);
|
2023-06-30 18:41:11 -04:00
|
|
|
|
|
|
|
|
|
|
// Create cipher in personal vault
|
|
|
|
|
|
var createdCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
Data = "", // TODO: EF does not enforce this as NOT NULL
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test" // TODO: EF does not enforce this as NOT NULL
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
_ = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var folder = await folderRepository.CreateAsync(new Folder
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "FolderName",
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Move cipher to organization vault
|
|
|
|
|
|
await cipherRepository.ReplaceAsync(new CipherDetails
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = createdCipher.Id,
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
FolderId = folder.Id,
|
|
|
|
|
|
Data = "", // TODO: EF does not enforce this as NOT NULL
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var updatedCipher = await cipherRepository.GetByIdAsync(createdCipher.Id);
|
|
|
|
|
|
|
2024-08-15 17:14:22 -04:00
|
|
|
|
Assert.NotNull(updatedCipher);
|
2023-06-30 18:41:11 -04:00
|
|
|
|
Assert.Null(updatedCipher.UserId);
|
|
|
|
|
|
Assert.Equal(organization.Id, updatedCipher.OrganizationId);
|
|
|
|
|
|
Assert.NotNull(updatedCipher.Folders);
|
|
|
|
|
|
|
|
|
|
|
|
using var foldersJsonDocument = JsonDocument.Parse(updatedCipher.Folders);
|
|
|
|
|
|
var foldersJsonElement = foldersJsonDocument.RootElement;
|
|
|
|
|
|
Assert.Equal(JsonValueKind.Object, foldersJsonElement.ValueKind);
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Should we force similar casing for guids across DB's
|
|
|
|
|
|
// I'd rather we only interact with them as the actual Guid type
|
|
|
|
|
|
var userProperty = foldersJsonElement
|
|
|
|
|
|
.EnumerateObject()
|
|
|
|
|
|
.FirstOrDefault(jp => string.Equals(jp.Name, user.Id.ToString(), StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
|
|
Assert.NotEqual(default, userProperty);
|
|
|
|
|
|
Assert.Equal(folder.Id, userProperty.Value.GetGuid());
|
|
|
|
|
|
}
|
2025-01-09 12:14:24 -08:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetCipherPermissionsForOrganizationAsync_Works(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
|
|
|
|
IGroupRepository groupRepository
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// A group that will be assigned Edit permissions to any collections
|
|
|
|
|
|
var editGroup = await groupRepository.CreateAsync(new Group
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Name = "Edit Group",
|
|
|
|
|
|
});
|
|
|
|
|
|
await groupRepository.UpdateUsersAsync(editGroup.Id, new[] { orgUser.Id });
|
|
|
|
|
|
|
|
|
|
|
|
// MANAGE
|
|
|
|
|
|
|
|
|
|
|
|
var manageCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Manage Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { manageCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(manageCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// EDIT
|
|
|
|
|
|
|
|
|
|
|
|
var editCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Edit Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var editCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(editCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { editCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(editCollection.Id,
|
|
|
|
|
|
new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// EDIT EXCEPT PASSWORD
|
|
|
|
|
|
|
|
|
|
|
|
var editExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Edit Except Password Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var editExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(editExceptPasswordCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { editExceptPasswordCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(editExceptPasswordCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// VIEW ONLY
|
|
|
|
|
|
|
|
|
|
|
|
var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "View Only Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { viewOnlyCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id,
|
|
|
|
|
|
new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Assign the EditGroup to this View Only collection. The user belongs to this group.
|
|
|
|
|
|
// The user permissions specified above (ViewOnly) should take precedence.
|
|
|
|
|
|
await groupRepository.ReplaceAsync(editGroup,
|
|
|
|
|
|
new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new CollectionAccessSelection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = viewOnlyCollection.Id, HidePasswords = false, ReadOnly = false, Manage = false
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// VIEW EXCEPT PASSWORD
|
|
|
|
|
|
|
|
|
|
|
|
var viewExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "View Except Password Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var viewExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewExceptPasswordCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { viewExceptPasswordCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(viewExceptPasswordCollection.Id,
|
|
|
|
|
|
new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = true, Manage = false }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// UNASSIGNED
|
|
|
|
|
|
|
|
|
|
|
|
var unassignedCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.NotEmpty(permissions);
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipherPermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(manageCipherPermission);
|
|
|
|
|
|
Assert.True(manageCipherPermission.Manage);
|
|
|
|
|
|
Assert.True(manageCipherPermission.Edit);
|
|
|
|
|
|
Assert.True(manageCipherPermission.Read);
|
|
|
|
|
|
Assert.True(manageCipherPermission.ViewPassword);
|
|
|
|
|
|
|
|
|
|
|
|
var editCipherPermission = permissions.FirstOrDefault(c => c.Id == editCipher.Id);
|
|
|
|
|
|
Assert.NotNull(editCipherPermission);
|
|
|
|
|
|
Assert.False(editCipherPermission.Manage);
|
|
|
|
|
|
Assert.True(editCipherPermission.Edit);
|
|
|
|
|
|
Assert.True(editCipherPermission.Read);
|
|
|
|
|
|
Assert.True(editCipherPermission.ViewPassword);
|
|
|
|
|
|
|
|
|
|
|
|
var editExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == editExceptPasswordCipher.Id);
|
|
|
|
|
|
Assert.NotNull(editExceptPasswordCipherPermission);
|
|
|
|
|
|
Assert.False(editExceptPasswordCipherPermission.Manage);
|
|
|
|
|
|
Assert.True(editExceptPasswordCipherPermission.Edit);
|
|
|
|
|
|
Assert.True(editExceptPasswordCipherPermission.Read);
|
|
|
|
|
|
Assert.False(editExceptPasswordCipherPermission.ViewPassword);
|
|
|
|
|
|
|
|
|
|
|
|
var viewOnlyCipherPermission = permissions.FirstOrDefault(c => c.Id == viewOnlyCipher.Id);
|
|
|
|
|
|
Assert.NotNull(viewOnlyCipherPermission);
|
|
|
|
|
|
Assert.False(viewOnlyCipherPermission.Manage);
|
|
|
|
|
|
Assert.False(viewOnlyCipherPermission.Edit);
|
|
|
|
|
|
Assert.True(viewOnlyCipherPermission.Read);
|
|
|
|
|
|
Assert.True(viewOnlyCipherPermission.ViewPassword);
|
|
|
|
|
|
|
|
|
|
|
|
var viewExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == viewExceptPasswordCipher.Id);
|
|
|
|
|
|
Assert.NotNull(viewExceptPasswordCipherPermission);
|
|
|
|
|
|
Assert.False(viewExceptPasswordCipherPermission.Manage);
|
|
|
|
|
|
Assert.False(viewExceptPasswordCipherPermission.Edit);
|
|
|
|
|
|
Assert.True(viewExceptPasswordCipherPermission.Read);
|
|
|
|
|
|
Assert.False(viewExceptPasswordCipherPermission.ViewPassword);
|
|
|
|
|
|
|
|
|
|
|
|
var unassignedCipherPermission = permissions.FirstOrDefault(c => c.Id == unassignedCipher.Id);
|
|
|
|
|
|
Assert.NotNull(unassignedCipherPermission);
|
|
|
|
|
|
Assert.False(unassignedCipherPermission.Manage);
|
|
|
|
|
|
Assert.False(unassignedCipherPermission.Edit);
|
|
|
|
|
|
Assert.False(unassignedCipherPermission.Read);
|
|
|
|
|
|
Assert.False(unassignedCipherPermission.ViewPassword);
|
|
|
|
|
|
}
|
2025-02-24 11:40:53 +00:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionUserRules(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: true, "Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: false, "Non-Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
|
|
|
|
|
|
Assert.Equal(2, permissions.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(managePermission);
|
|
|
|
|
|
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(nonManagePermission);
|
|
|
|
|
|
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
|
|
|
|
IGroupRepository groupRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var group = await groupRepository.CreateAsync(new Group
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Name = "Test Group",
|
|
|
|
|
|
});
|
|
|
|
|
|
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
|
|
|
|
|
|
|
|
|
|
|
|
var (manageCipher, nonManageCipher) = await CreateCipherInOrganizationCollectionWithGroup(
|
|
|
|
|
|
organization, group, cipherRepository, collectionRepository, collectionCipherRepository, groupRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
|
|
|
|
|
|
Assert.Equal(2, permissions.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(managePermission);
|
|
|
|
|
|
Assert.True(managePermission.Manage, "Collection with Group Manage=true should grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(nonManagePermission);
|
|
|
|
|
|
Assert.False(nonManagePermission.Manage, "Collection with Group Manage=false should not grant Manage permission");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetManyByUserIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: true, "Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: false, "Non-Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var userCiphers = await cipherRepository.GetManyByUserIdAsync(user.Id);
|
|
|
|
|
|
Assert.Equal(3, userCiphers.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var managePermission = userCiphers.FirstOrDefault(c => c.Id == manageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(managePermission);
|
|
|
|
|
|
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManagePermission = userCiphers.FirstOrDefault(c => c.Id == nonManageCipher.Id);
|
|
|
|
|
|
Assert.NotNull(nonManagePermission);
|
|
|
|
|
|
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var personalPermission = userCiphers.FirstOrDefault(c => c.Id == personalCipher.Id);
|
|
|
|
|
|
Assert.NotNull(personalPermission);
|
|
|
|
|
|
Assert.True(personalPermission.Manage, "Personal ciphers should always have Manage permission");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetByIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: true, "Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageCipher = await CreateCipherInOrganizationCollection(
|
|
|
|
|
|
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
|
|
|
|
|
|
hasManagePermission: false, "Non-Manage Collection");
|
|
|
|
|
|
|
|
|
|
|
|
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var manageDetails = await cipherRepository.GetByIdAsync(manageCipher.Id, user.Id);
|
|
|
|
|
|
Assert.NotNull(manageDetails);
|
|
|
|
|
|
Assert.True(manageDetails.Manage, "Collection with Manage=true should grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageDetails = await cipherRepository.GetByIdAsync(nonManageCipher.Id, user.Id);
|
|
|
|
|
|
Assert.NotNull(nonManageDetails);
|
|
|
|
|
|
Assert.False(nonManageDetails.Manage, "Collection with Manage=false should not grant Manage permission");
|
|
|
|
|
|
|
|
|
|
|
|
var personalDetails = await cipherRepository.GetByIdAsync(personalCipher.Id, user.Id);
|
|
|
|
|
|
Assert.NotNull(personalDetails);
|
|
|
|
|
|
Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-23 12:11:32 -04:00
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetManyByUserIdAsync_WhenOneCipherIsAssignedToTwoCollectionsWithDifferentPermissions_MostPrivilegedAccessIsReturnedOnTheCipher(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
//Arrange
|
|
|
|
|
|
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
|
|
|
|
|
|
|
|
|
|
|
var cipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var managedPermissionsCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Managed",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var unmanagedPermissionsCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Unmanaged",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id,
|
|
|
|
|
|
[managedPermissionsCollection.Id, unmanagedPermissionsCollection.Id]);
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(managedPermissionsCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(unmanagedPermissionsCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var ciphers = await cipherRepository.GetManyByUserIdAsync(user.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
Assert.Single(ciphers);
|
|
|
|
|
|
var deletableCipher = ciphers.SingleOrDefault(x => x.Id == cipher.Id);
|
|
|
|
|
|
Assert.NotNull(deletableCipher);
|
|
|
|
|
|
Assert.True(deletableCipher.Manage);
|
|
|
|
|
|
|
|
|
|
|
|
// Annul
|
|
|
|
|
|
await cipherRepository.DeleteAsync(cipher);
|
|
|
|
|
|
await organizationUserRepository.DeleteAsync(orgUser);
|
|
|
|
|
|
await organizationRepository.DeleteAsync(organization);
|
|
|
|
|
|
await userRepository.DeleteAsync(user);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-24 11:40:53 +00:00
|
|
|
|
private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization(
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (user, organization, orgUser);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<Cipher> CreateCipherInOrganizationCollection(
|
|
|
|
|
|
Organization organization,
|
|
|
|
|
|
OrganizationUser orgUser,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
bool hasManagePermission,
|
|
|
|
|
|
string collectionName)
|
|
|
|
|
|
{
|
|
|
|
|
|
var collection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = collectionName,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { collection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(collection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = hasManagePermission }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return cipher;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<(Cipher manageCipher, Cipher nonManageCipher)> CreateCipherInOrganizationCollectionWithGroup(
|
|
|
|
|
|
Organization organization,
|
|
|
|
|
|
Group group,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
IGroupRepository groupRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var manageCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Group Manage Collection",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Group Non-Manage Collection",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var nonManageCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { manageCollection.Id });
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(nonManageCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { nonManageCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await groupRepository.ReplaceAsync(group,
|
|
|
|
|
|
new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new CollectionAccessSelection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = manageCollection.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
},
|
|
|
|
|
|
new CollectionAccessSelection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = nonManageCollection.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = false
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (manageCipher, nonManageCipher);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<Cipher> CreatePersonalCipher(User user, ICipherRepository cipherRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-02-27 08:34:42 -06:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task GetUserSecurityTasksByCipherIdsAsync_Works(
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
|
|
|
|
IGroupRepository groupRepository
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Users
|
|
|
|
|
|
var user1 = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User 1",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var user2 = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User 2",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Organization
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user1.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Org Users
|
|
|
|
|
|
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user1.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user2.Id,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.User,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// A group that will be assigned Edit permissions to any collections
|
|
|
|
|
|
var editGroup = await groupRepository.CreateAsync(new Group
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Name = "Edit Group",
|
|
|
|
|
|
});
|
|
|
|
|
|
await groupRepository.UpdateUsersAsync(editGroup.Id, new[] { orgUser1.Id });
|
|
|
|
|
|
|
|
|
|
|
|
// Add collections to Org
|
|
|
|
|
|
var manageCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Manage Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Use a 2nd collection to differentiate between the two users
|
|
|
|
|
|
var manageCollection2 = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Manage Collection 2",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "View Only Collection",
|
|
|
|
|
|
OrganizationId = organization.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Ciphers
|
|
|
|
|
|
var manageCipher1 = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var manageCipher2 = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher1.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { manageCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher2.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { manageCollection2.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { viewOnlyCollection.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(manageCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser1.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser2.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Only add second user to the second manage collection
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(manageCollection2.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser2.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, new List<CollectionAccessSelection>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = orgUser1.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = false
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var securityTasks = new List<SecurityTask>
|
|
|
|
|
|
{
|
|
|
|
|
|
new SecurityTask { CipherId = manageCipher1.Id, Id = Guid.NewGuid() },
|
|
|
|
|
|
new SecurityTask { CipherId = manageCipher2.Id, Id = Guid.NewGuid() },
|
|
|
|
|
|
new SecurityTask { CipherId = viewOnlyCipher.Id, Id = Guid.NewGuid() }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var userSecurityTaskCiphers = await cipherRepository.GetUserSecurityTasksByCipherIdsAsync(organization.Id, securityTasks);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.NotEmpty(userSecurityTaskCiphers);
|
|
|
|
|
|
Assert.Equal(3, userSecurityTaskCiphers.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var user1TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user1.Id);
|
|
|
|
|
|
Assert.Single(user1TaskCiphers);
|
|
|
|
|
|
Assert.Equal(user1.Email, user1TaskCiphers.First().Email);
|
|
|
|
|
|
Assert.Equal(user1.Id, user1TaskCiphers.First().UserId);
|
|
|
|
|
|
Assert.Equal(manageCipher1.Id, user1TaskCiphers.First().CipherId);
|
|
|
|
|
|
|
|
|
|
|
|
var user2TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user2.Id);
|
|
|
|
|
|
Assert.NotNull(user2TaskCiphers);
|
|
|
|
|
|
Assert.Equal(2, user2TaskCiphers.Count());
|
|
|
|
|
|
Assert.Equal(user2.Email, user2TaskCiphers.Last().Email);
|
|
|
|
|
|
Assert.Equal(user2.Id, user2TaskCiphers.Last().UserId);
|
|
|
|
|
|
Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher1.Id && t.TaskId == securityTasks[0].Id);
|
|
|
|
|
|
Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher2.Id && t.TaskId == securityTasks[1].Id);
|
|
|
|
|
|
}
|
2025-04-11 15:59:54 -04:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task UpdateCiphersAsync_Works(ICipherRepository cipherRepository, IUserRepository userRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cipher1 = await CreatePersonalCipher(user, cipherRepository);
|
|
|
|
|
|
var cipher2 = await CreatePersonalCipher(user, cipherRepository);
|
|
|
|
|
|
|
|
|
|
|
|
cipher1.Type = CipherType.SecureNote;
|
|
|
|
|
|
cipher2.Attachments = "new_attachments";
|
|
|
|
|
|
|
|
|
|
|
|
await cipherRepository.UpdateCiphersAsync(user.Id, [cipher1, cipher2]);
|
|
|
|
|
|
|
|
|
|
|
|
var updatedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id);
|
|
|
|
|
|
var updatedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(updatedCipher1);
|
|
|
|
|
|
Assert.NotNull(updatedCipher2);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(CipherType.SecureNote, updatedCipher1.Type);
|
|
|
|
|
|
Assert.Equal("new_attachments", updatedCipher2.Attachments);
|
|
|
|
|
|
}
|
2025-04-23 13:16:29 -05:00
|
|
|
|
|
[PM-24233] Use BulkResourceCreationService in CipherRepository (#6201)
* Add constant for CipherRepositoryBulkResourceCreation in FeatureFlagKeys
* Add bulk creation methods for Ciphers, Folders, and CollectionCiphers in BulkResourceCreationService
- Implemented CreateCiphersAsync, CreateFoldersAsync, CreateCollectionCiphersAsync, and CreateTempCiphersAsync methods for bulk insertion.
- Added helper methods to build DataTables for Ciphers, Folders, and CollectionCiphers.
- Enhanced error handling for empty collections during bulk operations.
* Refactor CipherRepository to utilize BulkResourceCreationService
- Introduced IFeatureService to manage feature flag checks for bulk operations.
- Updated methods to conditionally use BulkResourceCreationService for creating Ciphers, Folders, and CollectionCiphers based on feature flag status.
- Enhanced existing bulk copy logic to maintain functionality while integrating feature flag checks.
* Add InlineFeatureService to DatabaseDataAttribute for feature flag management
- Introduced EnabledFeatureFlags property to DatabaseDataAttribute for configuring feature flags.
- Integrated InlineFeatureService to provide feature flag checks within the service collection.
- Enhanced GetData method to utilize feature flags for conditional service registration.
* Add tests for bulk creation of Ciphers in CipherRepositoryTests
- Implemented tests for bulk creation of Ciphers, Folders, and Collections with feature flag checks.
- Added test cases for updating multiple Ciphers to validate bulk update functionality.
- Enhanced existing test structure to ensure comprehensive coverage of bulk operations in the CipherRepository.
* Refactor BulkResourceCreationService to use dynamic types for DataColumns
- Updated DataColumn definitions in BulkResourceCreationService to utilize the actual types of properties from the cipher object instead of hardcoded types.
- Simplified the assignment of nullable properties to directly use their values, improving code readability and maintainability.
* Update BulkResourceCreationService to use specific types for DataColumns
- Changed DataColumn definitions to use specific types (short and string) instead of dynamic types based on cipher properties.
- Improved handling of nullable properties when assigning values to DataTable rows, ensuring proper handling of DBNull for null values.
* Refactor CipherRepositoryTests for improved clarity and consistency
- Renamed test methods to better reflect their purpose and improve readability.
- Updated test data to use more descriptive names for users, folders, and collections.
- Enhanced test structure with clear Arrange, Act, and Assert sections for better understanding of test flow.
- Ensured all tests validate the expected outcomes for bulk operations with feature flag checks.
* Update CipherRepositoryBulkResourceCreation feature flag key
* Refactor DatabaseDataAttribute usage in CipherRepositoryTests to use array syntax for EnabledFeatureFlags
* Update CipherRepositoryTests to use GenerateComb for generating unique IDs
* Refactor CipherRepository methods to accept a boolean parameter for enabling bulk resource creation based on feature flags. Update tests to verify functionality with and without the feature flag enabled.
* Refactor CipherRepository and related services to support new methods for bulk resource creation without boolean parameters.
2025-09-03 14:57:53 +01:00
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task CreateAsync_vNext_WithFolders_Works(
|
|
|
|
|
|
IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"{Guid.NewGuid()}@example.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var folder1 = new Folder { Id = CoreHelpers.GenerateComb(), UserId = user.Id, Name = "Test Folder 1" };
|
|
|
|
|
|
var folder2 = new Folder { Id = CoreHelpers.GenerateComb(), UserId = user.Id, Name = "Test Folder 2" };
|
|
|
|
|
|
var cipher1 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" };
|
|
|
|
|
|
var cipher2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.SecureNote, UserId = user.Id, Data = "" };
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await cipherRepository.CreateAsync_vNext(
|
|
|
|
|
|
userId: user.Id,
|
|
|
|
|
|
ciphers: [cipher1, cipher2],
|
|
|
|
|
|
folders: [folder1, folder2]);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
var readCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id);
|
|
|
|
|
|
var readCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id);
|
|
|
|
|
|
Assert.NotNull(readCipher1);
|
|
|
|
|
|
Assert.NotNull(readCipher2);
|
|
|
|
|
|
|
|
|
|
|
|
var readFolder1 = await folderRepository.GetByIdAsync(folder1.Id);
|
|
|
|
|
|
var readFolder2 = await folderRepository.GetByIdAsync(folder2.Id);
|
|
|
|
|
|
Assert.NotNull(readFolder1);
|
|
|
|
|
|
Assert.NotNull(readFolder2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task CreateAsync_vNext_WithCollectionsAndUsers_Works(
|
|
|
|
|
|
IOrganizationRepository orgRepository,
|
|
|
|
|
|
IOrganizationUserRepository orgUserRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
IUserRepository userRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"{Guid.NewGuid()}@example.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var org = await orgRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var orgUser = await orgUserRepository.CreateAsync(new OrganizationUser
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
OrganizationId = org.Id,
|
|
|
|
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
|
|
|
|
Type = OrganizationUserType.Owner,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var collection = new Collection { Id = CoreHelpers.GenerateComb(), Name = "Test Collection", OrganizationId = org.Id };
|
|
|
|
|
|
var cipher = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, OrganizationId = org.Id, Data = "" };
|
|
|
|
|
|
var collectionCipher = new CollectionCipher { CollectionId = collection.Id, CipherId = cipher.Id };
|
|
|
|
|
|
var collectionUser = new CollectionUser
|
|
|
|
|
|
{
|
|
|
|
|
|
CollectionId = collection.Id,
|
|
|
|
|
|
OrganizationUserId = orgUser.Id,
|
|
|
|
|
|
HidePasswords = false,
|
|
|
|
|
|
ReadOnly = false,
|
|
|
|
|
|
Manage = true
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await cipherRepository.CreateAsync_vNext(
|
|
|
|
|
|
ciphers: [cipher],
|
|
|
|
|
|
collections: [collection],
|
|
|
|
|
|
collectionCiphers: [collectionCipher],
|
|
|
|
|
|
collectionUsers: [collectionUser]);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
var orgCiphers = await cipherRepository.GetManyByOrganizationIdAsync(org.Id);
|
|
|
|
|
|
Assert.Contains(orgCiphers, c => c.Id == cipher.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var collCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(org.Id);
|
|
|
|
|
|
Assert.Contains(collCiphers, cc => cc.CipherId == cipher.Id && cc.CollectionId == collection.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var collectionsInOrg = await collectionRepository.GetManyByOrganizationIdAsync(org.Id);
|
|
|
|
|
|
Assert.Contains(collectionsInOrg, c => c.Id == collection.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var collectionUsers = await collectionRepository.GetManyUsersByIdAsync(collection.Id);
|
|
|
|
|
|
var foundCollectionUser = collectionUsers.FirstOrDefault(cu => cu.Id == orgUser.Id);
|
|
|
|
|
|
Assert.NotNull(foundCollectionUser);
|
|
|
|
|
|
Assert.True(foundCollectionUser.Manage);
|
|
|
|
|
|
Assert.False(foundCollectionUser.ReadOnly);
|
|
|
|
|
|
Assert.False(foundCollectionUser.HidePasswords);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task UpdateCiphersAsync_vNext_Works(
|
|
|
|
|
|
IUserRepository userRepository, ICipherRepository cipherRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var expectedNewType = CipherType.SecureNote;
|
|
|
|
|
|
var expectedNewAttachments = "bulk_new_attachments";
|
|
|
|
|
|
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"{Guid.NewGuid()}@example.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var c1 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" };
|
|
|
|
|
|
var c2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" };
|
|
|
|
|
|
await cipherRepository.CreateAsync(
|
|
|
|
|
|
userId: user.Id,
|
|
|
|
|
|
ciphers: [c1, c2],
|
|
|
|
|
|
folders: []);
|
|
|
|
|
|
|
|
|
|
|
|
c1.Type = expectedNewType;
|
|
|
|
|
|
c2.Attachments = expectedNewAttachments;
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await cipherRepository.UpdateCiphersAsync_vNext(user.Id, [c1, c2]);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
var updated1 = await cipherRepository.GetByIdAsync(c1.Id);
|
|
|
|
|
|
Assert.NotNull(updated1);
|
|
|
|
|
|
Assert.Equal(c1.Id, updated1.Id);
|
|
|
|
|
|
Assert.Equal(expectedNewType, updated1.Type);
|
|
|
|
|
|
Assert.Equal(c1.UserId, updated1.UserId);
|
|
|
|
|
|
Assert.Equal(c1.Data, updated1.Data);
|
|
|
|
|
|
Assert.Equal(c1.OrganizationId, updated1.OrganizationId);
|
|
|
|
|
|
Assert.Equal(c1.Attachments, updated1.Attachments);
|
|
|
|
|
|
|
|
|
|
|
|
var updated2 = await cipherRepository.GetByIdAsync(c2.Id);
|
|
|
|
|
|
Assert.NotNull(updated2);
|
|
|
|
|
|
Assert.Equal(c2.Id, updated2.Id);
|
|
|
|
|
|
Assert.Equal(c2.Type, updated2.Type);
|
|
|
|
|
|
Assert.Equal(c2.UserId, updated2.UserId);
|
|
|
|
|
|
Assert.Equal(c2.Data, updated2.Data);
|
|
|
|
|
|
Assert.Equal(c2.OrganizationId, updated2.OrganizationId);
|
|
|
|
|
|
Assert.Equal(expectedNewAttachments, updated2.Attachments);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 13:16:29 -05:00
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task DeleteCipherWithSecurityTaskAsync_Works(
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
2025-06-27 11:17:47 -05:00
|
|
|
|
IUserRepository userRepository,
|
2025-04-23 13:16:29 -05:00
|
|
|
|
ICipherRepository cipherRepository,
|
2025-06-27 11:17:47 -05:00
|
|
|
|
ISecurityTaskRepository securityTaskRepository,
|
|
|
|
|
|
INotificationRepository notificationRepository,
|
|
|
|
|
|
INotificationStatusRepository notificationStatusRepository)
|
2025-04-23 13:16:29 -05:00
|
|
|
|
{
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Org",
|
|
|
|
|
|
PlanType = PlanType.EnterpriseAnnually,
|
|
|
|
|
|
Plan = "Test Plan",
|
|
|
|
|
|
BillingEmail = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-27 11:17:47 -05:00
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-23 13:16:29 -05:00
|
|
|
|
var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
|
|
|
|
|
await cipherRepository.CreateAsync(cipher1);
|
|
|
|
|
|
|
|
|
|
|
|
var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
|
|
|
|
|
await cipherRepository.CreateAsync(cipher2);
|
|
|
|
|
|
|
|
|
|
|
|
var tasks = new List<SecurityTask>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
CipherId = cipher1.Id,
|
|
|
|
|
|
Status = SecurityTaskStatus.Pending,
|
|
|
|
|
|
Type = SecurityTaskType.UpdateAtRiskCredential,
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
CipherId = cipher2.Id,
|
|
|
|
|
|
Status = SecurityTaskStatus.Completed,
|
|
|
|
|
|
Type = SecurityTaskType.UpdateAtRiskCredential,
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await securityTaskRepository.CreateManyAsync(tasks);
|
2025-06-27 11:17:47 -05:00
|
|
|
|
var notification = await notificationRepository.CreateAsync(new Notification
|
|
|
|
|
|
{
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
TaskId = tasks[1].Id,
|
|
|
|
|
|
CreationDate = DateTime.UtcNow,
|
|
|
|
|
|
RevisionDate = DateTime.UtcNow,
|
|
|
|
|
|
});
|
|
|
|
|
|
await notificationStatusRepository.CreateAsync(new NotificationStatus
|
|
|
|
|
|
{
|
|
|
|
|
|
NotificationId = notification.Id,
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
ReadDate = DateTime.UtcNow,
|
|
|
|
|
|
});
|
2025-04-23 13:16:29 -05:00
|
|
|
|
|
|
|
|
|
|
// Delete cipher with pending security task
|
|
|
|
|
|
await cipherRepository.DeleteAsync(cipher1);
|
|
|
|
|
|
|
|
|
|
|
|
var deletedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Null(deletedCipher1);
|
|
|
|
|
|
|
|
|
|
|
|
// Delete cipher with completed security task
|
|
|
|
|
|
await cipherRepository.DeleteAsync(cipher2);
|
|
|
|
|
|
|
|
|
|
|
|
var deletedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Null(deletedCipher2);
|
|
|
|
|
|
}
|
2025-09-12 13:24:30 -04:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task ArchiveAsync_Works(
|
|
|
|
|
|
ICipherRepository sutRepository,
|
|
|
|
|
|
IUserRepository userRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Ciphers
|
|
|
|
|
|
var cipher = await sutRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
Data = "",
|
|
|
|
|
|
UserId = user.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await sutRepository.ArchiveAsync(new List<Guid> { cipher.Id }, user.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
var archivedCipher = await sutRepository.GetByIdAsync(cipher.Id, user.Id);
|
|
|
|
|
|
Assert.NotNull(archivedCipher);
|
|
|
|
|
|
Assert.NotNull(archivedCipher.ArchivedDate);
|
|
|
|
|
|
}
|
2025-09-24 12:52:04 -05:00
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task DeleteByOrganizationIdAsync_ExcludesDefaultCollectionCiphers(
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var defaultCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Default Collection",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Type = CollectionType.DefaultUserCollection
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var sharedCollection = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Shared Collection",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async Task<Cipher> CreateOrgCipherAsync() => await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cipherInDefaultCollection = await CreateOrgCipherAsync();
|
|
|
|
|
|
var cipherInSharedCollection = await CreateOrgCipherAsync();
|
|
|
|
|
|
var cipherInBothCollections = await CreateOrgCipherAsync();
|
|
|
|
|
|
var unassignedCipher = await CreateOrgCipherAsync();
|
|
|
|
|
|
|
2025-09-26 14:30:34 -07:00
|
|
|
|
async Task LinkCollectionCipherAsync(Guid cipherId, Guid collectionId) =>
|
|
|
|
|
|
await collectionCipherRepository.AddCollectionsForManyCiphersAsync(
|
|
|
|
|
|
organization.Id,
|
|
|
|
|
|
new[] { cipherId },
|
|
|
|
|
|
new[] { collectionId });
|
|
|
|
|
|
|
|
|
|
|
|
await LinkCollectionCipherAsync(cipherInDefaultCollection.Id, defaultCollection.Id);
|
|
|
|
|
|
await LinkCollectionCipherAsync(cipherInSharedCollection.Id, sharedCollection.Id);
|
|
|
|
|
|
await LinkCollectionCipherAsync(cipherInBothCollections.Id, defaultCollection.Id);
|
|
|
|
|
|
await LinkCollectionCipherAsync(cipherInBothCollections.Id, sharedCollection.Id);
|
2025-09-24 12:52:04 -05:00
|
|
|
|
|
|
|
|
|
|
await cipherRepository.DeleteByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var remainingCipherInDefault = await cipherRepository.GetByIdAsync(cipherInDefaultCollection.Id);
|
|
|
|
|
|
var deletedCipherInShared = await cipherRepository.GetByIdAsync(cipherInSharedCollection.Id);
|
|
|
|
|
|
var remainingCipherInBoth = await cipherRepository.GetByIdAsync(cipherInBothCollections.Id);
|
|
|
|
|
|
var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Null(deletedCipherInShared);
|
|
|
|
|
|
Assert.Null(deletedUnassignedCipher);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(remainingCipherInDefault);
|
|
|
|
|
|
Assert.NotNull(remainingCipherInBoth);
|
|
|
|
|
|
|
|
|
|
|
|
var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// Should still have the default collection cipher relationships
|
|
|
|
|
|
Assert.Contains(remainingCollectionCiphers, cc =>
|
|
|
|
|
|
cc.CipherId == cipherInDefaultCollection.Id && cc.CollectionId == defaultCollection.Id);
|
|
|
|
|
|
Assert.Contains(remainingCollectionCiphers, cc =>
|
|
|
|
|
|
cc.CipherId == cipherInBothCollections.Id && cc.CollectionId == defaultCollection.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// Should not have the shared collection cipher relationships
|
|
|
|
|
|
Assert.DoesNotContain(remainingCollectionCiphers, cc => cc.CollectionId == sharedCollection.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[DatabaseTheory, DatabaseData]
|
|
|
|
|
|
public async Task DeleteByOrganizationIdAsync_DeletesAllWhenNoDefaultCollections(
|
|
|
|
|
|
IOrganizationRepository organizationRepository,
|
|
|
|
|
|
IUserRepository userRepository,
|
|
|
|
|
|
ICipherRepository cipherRepository,
|
|
|
|
|
|
ICollectionRepository collectionRepository,
|
|
|
|
|
|
ICollectionCipherRepository collectionCipherRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var user = await userRepository.CreateAsync(new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test User",
|
|
|
|
|
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
|
|
|
|
|
ApiKey = "TEST",
|
|
|
|
|
|
SecurityStamp = "stamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Test Organization",
|
|
|
|
|
|
BillingEmail = user.Email,
|
|
|
|
|
|
Plan = "Test"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var sharedCollection1 = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Shared Collection 1",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Type = CollectionType.SharedCollection
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var sharedCollection2 = await collectionRepository.CreateAsync(new Collection
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "Shared Collection 2",
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Type = CollectionType.SharedCollection
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Create ciphers
|
|
|
|
|
|
var cipherInSharedCollection1 = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cipherInSharedCollection2 = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var unassignedCipher = await cipherRepository.CreateAsync(new Cipher
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = CipherType.Login,
|
|
|
|
|
|
OrganizationId = organization.Id,
|
|
|
|
|
|
Data = ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection1.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { sharedCollection1.Id });
|
|
|
|
|
|
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection2.Id, organization.Id,
|
|
|
|
|
|
new List<Guid> { sharedCollection2.Id });
|
|
|
|
|
|
|
|
|
|
|
|
await cipherRepository.DeleteByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var deletedCipher1 = await cipherRepository.GetByIdAsync(cipherInSharedCollection1.Id);
|
|
|
|
|
|
var deletedCipher2 = await cipherRepository.GetByIdAsync(cipherInSharedCollection2.Id);
|
|
|
|
|
|
var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Null(deletedCipher1);
|
|
|
|
|
|
Assert.Null(deletedCipher2);
|
|
|
|
|
|
Assert.Null(deletedUnassignedCipher);
|
|
|
|
|
|
|
|
|
|
|
|
// All collection cipher relationships should be removed
|
|
|
|
|
|
var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id);
|
|
|
|
|
|
Assert.Empty(remainingCollectionCiphers);
|
|
|
|
|
|
}
|
2022-12-02 14:24:30 -05:00
|
|
|
|
}
|
2025-09-26 14:30:34 -07:00
|
|
|
|
|