mirror of
https://github.com/bitwarden/server
synced 2026-01-28 15:23:38 +00:00
Merge remote-tracking branch 'origin/main' into ac/pm-23768/server-public-api---add-restore/revoke-for-members
This commit is contained in:
@@ -140,10 +140,13 @@ EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi", "util\SeederApi\SeederApi.csproj", "{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi.IntegrationTest", "test\SeederApi.IntegrationTest\SeederApi.IntegrationTest.csproj", "{A2E067EF-609C-4D13-895A-E054C61D48BB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO.Test", "bitwarden_license\test\SSO.Test\SSO.Test.csproj", "{7D98784C-C253-43FB-9873-25B65C6250D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sso.IntegrationTest", "bitwarden_license\test\Sso.IntegrationTest\Sso.IntegrationTest.csproj", "{FFB09376-595B-6F93-36F0-70CAE90AFECB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.IntegrationTest", "test\Server.IntegrationTest\Server.IntegrationTest.csproj", "{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -372,6 +375,10 @@ Global
|
||||
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -432,6 +439,7 @@ Global
|
||||
{A2E067EF-609C-4D13-895A-E054C61D48BB} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
{FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
Validates that new database migration files follow naming conventions and chronological order.
|
||||
|
||||
.DESCRIPTION
|
||||
This script validates migration files in util/Migrator/DbScripts/ to ensure:
|
||||
This script validates migration files to ensure:
|
||||
|
||||
For SQL migrations in util/Migrator/DbScripts/:
|
||||
1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql
|
||||
2. New migrations are chronologically ordered (filename sorts after existing migrations)
|
||||
3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)
|
||||
4. A 2-digit sequence number is included (e.g., _00, _01)
|
||||
|
||||
For Entity Framework migrations in util/MySqlMigrations, util/PostgresMigrations, util/SqliteMigrations:
|
||||
1. New migrations follow the naming format: YYYYMMDDHHMMSS_Description.cs
|
||||
2. Each migration has both .cs and .Designer.cs files
|
||||
3. New migrations are chronologically ordered (timestamp sorts after existing migrations)
|
||||
|
||||
.PARAMETER BaseRef
|
||||
The base git reference to compare against (e.g., 'main', 'HEAD~1')
|
||||
|
||||
@@ -58,75 +65,288 @@ $currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/"
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
$sqlValidationFailed = $false
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new migration files added."
|
||||
exit 0
|
||||
Write-Host "No new SQL migration files added."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
Write-Host "New SQL migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous SQL migrations found (initial commit?). Skipping chronological validation."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last SQL migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$sqlValidationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$sqlValidationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($sqlValidationFailed) {
|
||||
Write-Host "FAILED: One or more SQL migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new SQL migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
}
|
||||
else {
|
||||
Write-Host "SUCCESS: All new SQL migrations are correctly named and in chronological order"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "New migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
# ===========================================================================================
|
||||
# Validate Entity Framework Migrations
|
||||
# ===========================================================================================
|
||||
|
||||
Write-Host "==================================================================="
|
||||
Write-Host "Validating Entity Framework Migrations"
|
||||
Write-Host "==================================================================="
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous migrations found (initial commit?). Skipping validation."
|
||||
exit 0
|
||||
}
|
||||
$efMigrationPaths = @(
|
||||
@{ Path = "util/MySqlMigrations/Migrations"; Name = "MySQL" },
|
||||
@{ Path = "util/PostgresMigrations/Migrations"; Name = "Postgres" },
|
||||
@{ Path = "util/SqliteMigrations/Migrations"; Name = "SQLite" }
|
||||
)
|
||||
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
$efValidationFailed = $false
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
foreach ($migrationPathInfo in $efMigrationPaths) {
|
||||
$efPath = $migrationPathInfo.Path
|
||||
$dbName = $migrationPathInfo.Name
|
||||
|
||||
$validationFailed = $false
|
||||
Write-Host "-------------------------------------------------------------------"
|
||||
Write-Host "Checking $dbName EF migrations in $efPath"
|
||||
Write-Host "-------------------------------------------------------------------"
|
||||
Write-Host ""
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
# Get list of migrations from base reference
|
||||
try {
|
||||
$baseMigrations = git ls-tree -r --name-only $BaseRef -- "$efPath/" 2>$null | Where-Object { $_ -like "*.cs" -and $_ -notlike "*DatabaseContextModelSnapshot.cs" } | Sort-Object
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$validationFailed = $true
|
||||
# Get list of migrations from current reference
|
||||
$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$efPath/" | Where-Object { $_ -like "*.cs" -and $_ -notlike "*DatabaseContextModelSnapshot.cs" } | Sort-Object
|
||||
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new $dbName EF migration files added."
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$validationFailed = $true
|
||||
Write-Host "New $dbName EF migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous $dbName migrations found. Skipping chronological validation."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last $dbName migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Required format regex: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs
|
||||
$efFormatRegex = '^[0-9]{14}_.+\.cs$'
|
||||
|
||||
# Group migrations by base name (without .Designer.cs suffix)
|
||||
$migrationGroups = @{}
|
||||
$unmatchedFiles = @()
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Extract base name (remove .Designer.cs or .cs)
|
||||
if ($migrationName -match '^([0-9]{14}_.+?)(?:\.Designer)?\.cs$') {
|
||||
$baseName = $matches[1]
|
||||
if (-not $migrationGroups.ContainsKey($baseName)) {
|
||||
$migrationGroups[$baseName] = @()
|
||||
}
|
||||
$migrationGroups[$baseName] += $migrationName
|
||||
}
|
||||
else {
|
||||
# Track files that don't match the expected pattern
|
||||
$unmatchedFiles += $migrationName
|
||||
}
|
||||
}
|
||||
|
||||
# Flag any files that don't match the expected pattern
|
||||
if ($unmatchedFiles.Count -gt 0) {
|
||||
Write-Host "ERROR: The following migration files do not match the required format:"
|
||||
foreach ($unmatchedFile in $unmatchedFiles) {
|
||||
Write-Host " - $unmatchedFile"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Required format: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs"
|
||||
Write-Host " - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)"
|
||||
Write-Host " - Description: Descriptive name using PascalCase"
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs"
|
||||
Write-Host ""
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
foreach ($baseName in $migrationGroups.Keys | Sort-Object) {
|
||||
$files = $migrationGroups[$baseName]
|
||||
|
||||
# Validate format
|
||||
$mainFile = "$baseName.cs"
|
||||
$designerFile = "$baseName.Designer.cs"
|
||||
|
||||
if ($mainFile -notmatch $efFormatRegex) {
|
||||
Write-Host "ERROR: Migration '$mainFile' does not match required format"
|
||||
Write-Host "Required format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)"
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs"
|
||||
$efValidationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Check that both .cs and .Designer.cs files exist
|
||||
$hasCsFile = $files -contains $mainFile
|
||||
$hasDesignerFile = $files -contains $designerFile
|
||||
|
||||
if (-not $hasCsFile) {
|
||||
Write-Host "ERROR: Missing main migration file: $mainFile"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
if (-not $hasDesignerFile) {
|
||||
Write-Host "ERROR: Missing designer file: $designerFile"
|
||||
Write-Host "Each EF migration must have both a .cs and .Designer.cs file"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
if (-not $hasCsFile -or -not $hasDesignerFile) {
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration timestamp with last base migration (using ordinal string comparison)
|
||||
if ($baseMigrations.Count -gt 0) {
|
||||
if ([string]::CompareOrdinal($mainFile, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$mainFile' is not chronologically after '$lastBaseMigration'"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$mainFile' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$mainFile' (no previous migrations to compare)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($efValidationFailed) {
|
||||
Write-Host "FAILED: One or more EF migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new EF migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " 2. Include both .cs and .Designer.cs files"
|
||||
Write-Host " 3. Have a timestamp that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in the respective Migrations directory"
|
||||
Write-Host " 2. Ensure both .cs and .Designer.cs files exist"
|
||||
Write-Host " 3. Rename to follow format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " 4. Ensure the timestamp is after the last migration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs"
|
||||
}
|
||||
else {
|
||||
Write-Host "SUCCESS: All new EF migrations are correctly named and in chronological order"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==================================================================="
|
||||
Write-Host "Validation Summary"
|
||||
Write-Host "==================================================================="
|
||||
|
||||
if ($sqlValidationFailed -or $efValidationFailed) {
|
||||
if ($sqlValidationFailed) {
|
||||
Write-Host "❌ SQL migrations validation FAILED"
|
||||
}
|
||||
else {
|
||||
Write-Host "✓ SQL migrations validation PASSED"
|
||||
}
|
||||
|
||||
if ($efValidationFailed) {
|
||||
Write-Host "❌ EF migrations validation FAILED"
|
||||
}
|
||||
else {
|
||||
Write-Host "✓ EF migrations validation PASSED"
|
||||
}
|
||||
|
||||
if ($validationFailed) {
|
||||
Write-Host "FAILED: One or more migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
Write-Host "OVERALL RESULT: FAILED"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "SUCCESS: All new migrations are correctly named and in chronological order"
|
||||
exit 0
|
||||
else {
|
||||
Write-Host "✓ SQL migrations validation PASSED"
|
||||
Write-Host "✓ EF migrations validation PASSED"
|
||||
Write-Host ""
|
||||
Write-Host "OVERALL RESULT: SUCCESS"
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Collections;
|
||||
|
||||
public static class CollectionUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Arranges Collection and CollectionUser objects to create default user collections.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization ID.</param>
|
||||
/// <param name="organizationUserIds">The IDs for organization users who need default collections.</param>
|
||||
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
|
||||
/// <returns>A tuple containing the collections and collection users.</returns>
|
||||
public static (ICollection<Collection> collections, ICollection<CollectionUser> collectionUsers)
|
||||
BuildDefaultUserCollections(Guid organizationId, IEnumerable<Guid> organizationUserIds,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var collectionUsers = new List<CollectionUser>();
|
||||
var collections = new List<Collection>();
|
||||
|
||||
foreach (var orgUserId in organizationUserIds)
|
||||
{
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
|
||||
collections.Add(new Collection
|
||||
{
|
||||
Id = collectionId,
|
||||
OrganizationId = organizationId,
|
||||
Name = defaultCollectionName,
|
||||
CreationDate = now,
|
||||
RevisionDate = now,
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = null
|
||||
|
||||
});
|
||||
|
||||
collectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = orgUserId,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true,
|
||||
});
|
||||
}
|
||||
|
||||
return (collections, collectionUsers);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -83,19 +81,10 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
|
||||
return;
|
||||
}
|
||||
|
||||
await collectionRepository.CreateAsync(
|
||||
new Collection
|
||||
{
|
||||
OrganizationId = request.Organization!.Id,
|
||||
Name = request.DefaultUserCollectionName,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
},
|
||||
groups: null,
|
||||
[new CollectionAccessSelection
|
||||
{
|
||||
Id = request.OrganizationUser!.Id,
|
||||
Manage = true
|
||||
}]);
|
||||
await collectionRepository.CreateDefaultCollectionsAsync(
|
||||
request.Organization!.Id,
|
||||
[request.OrganizationUser!.Id],
|
||||
request.DefaultUserCollectionName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -294,21 +293,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
Name = defaultUserCollectionName,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
};
|
||||
var collectionUser = new CollectionAccessSelection
|
||||
{
|
||||
Id = organizationUser.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
};
|
||||
|
||||
await _collectionRepository.CreateAsync(defaultCollection, groups: null, users: [collectionUser]);
|
||||
await _collectionRepository.CreateDefaultCollectionsAsync(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser.Id],
|
||||
defaultUserCollectionName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -339,7 +327,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
return;
|
||||
}
|
||||
|
||||
await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
|
||||
await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -57,14 +57,15 @@ public class OrganizationDataOwnershipPolicyValidator(
|
||||
var userOrgIds = requirements
|
||||
.Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId))
|
||||
.Where(request => request.ShouldCreateDefaultCollection)
|
||||
.Select(request => request.OrganizationUserId);
|
||||
.Select(request => request.OrganizationUserId)
|
||||
.ToList();
|
||||
|
||||
if (!userOrgIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await collectionRepository.UpsertDefaultCollectionsAsync(
|
||||
await collectionRepository.CreateDefaultCollectionsBulkAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
userOrgIds,
|
||||
defaultCollectionName);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Extensions;
|
||||
@@ -51,7 +52,7 @@ public static class InvoiceExtensions
|
||||
if (string.IsNullOrEmpty(priceInfo) && line.Quantity > 0)
|
||||
{
|
||||
var pricePerItem = (line.Amount / 100m) / line.Quantity;
|
||||
priceInfo = $"(at ${pricePerItem:F2} / month)";
|
||||
priceInfo = string.Format(CultureInfo.InvariantCulture, "(at ${0:F2} / month)", pricePerItem);
|
||||
}
|
||||
|
||||
var taxDescription = $"{line.Quantity} × Tax {priceInfo}";
|
||||
@@ -70,7 +71,7 @@ public static class InvoiceExtensions
|
||||
if (tax > 0)
|
||||
{
|
||||
var taxAmount = tax / 100m;
|
||||
items.Add($"1 × Tax (at ${taxAmount:F2} / month)");
|
||||
items.Add(string.Format(CultureInfo.InvariantCulture, "1 × Tax (at ${0:F2} / month)", taxAmount));
|
||||
}
|
||||
|
||||
return items;
|
||||
|
||||
@@ -164,6 +164,7 @@ public static class FeatureFlagKeys
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||
public const string PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin";
|
||||
public const string PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
|
||||
@@ -64,11 +64,22 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);
|
||||
|
||||
/// <summary>
|
||||
/// Creates default user collections for the specified organization users if they do not already have one.
|
||||
/// Creates default user collections for the specified organization users.
|
||||
/// Filters internally to only create collections for users who don't already have one.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The Organization ID.</param>
|
||||
/// <param name="organizationUserIds">The Organization User IDs to create default collections for.</param>
|
||||
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
|
||||
/// <returns></returns>
|
||||
Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
|
||||
Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates default user collections for the specified organization users using bulk insert operations.
|
||||
/// Use this if you need to create collections for > ~1k users.
|
||||
/// Filters internally to only create collections for users who don't already have one.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The Organization ID.</param>
|
||||
/// <param name="organizationUserIds">The Organization User IDs to create default collections for.</param>
|
||||
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
|
||||
Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
|
||||
|
||||
}
|
||||
|
||||
@@ -160,6 +160,21 @@ public static class DapperHelpers
|
||||
return ids.ToArrayTVP("GuidId");
|
||||
}
|
||||
|
||||
public static DataTable ToTwoGuidIdArrayTVP(this IEnumerable<(Guid id1, Guid id2)> values)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.SetTypeName("[dbo].[TwoGuidIdArray]");
|
||||
table.Columns.Add("Id1", typeof(Guid));
|
||||
table.Columns.Add("Id2", typeof(Guid));
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
table.Rows.Add(value.id1, value.id2);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
public static DataTable ToArrayTVP<T>(this IEnumerable<T> values, string columnName)
|
||||
{
|
||||
var table = new DataTable();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Data;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -360,7 +361,45 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
{
|
||||
organizationUserIds = organizationUserIds.ToList();
|
||||
if (!organizationUserIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationUserCollectionIds = organizationUserIds
|
||||
.Select(ou => (ou, CoreHelpers.GenerateComb()))
|
||||
.ToTwoGuidIdArrayTVP();
|
||||
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
"[dbo].[Collection_CreateDefaultCollections]",
|
||||
new
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
DefaultCollectionName = defaultCollectionName,
|
||||
OrganizationUserCollectionIds = organizationUserCollectionIds
|
||||
},
|
||||
commandType: CommandType.StoredProcedure,
|
||||
transaction: transaction);
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
{
|
||||
organizationUserIds = organizationUserIds.ToList();
|
||||
if (!organizationUserIds.Any())
|
||||
@@ -377,7 +416,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
|
||||
var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);
|
||||
|
||||
var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
|
||||
var (collections, collectionUsers) =
|
||||
CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
|
||||
|
||||
if (!collectionUsers.Any() || !collections.Any())
|
||||
{
|
||||
@@ -387,11 +427,11 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections);
|
||||
await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);
|
||||
|
||||
transaction.Commit();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -421,40 +461,6 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
return organizationUserIds.ToHashSet();
|
||||
}
|
||||
|
||||
private (List<CollectionUser> collectionUser, List<Collection> collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable<Guid> missingDefaultCollectionUserIds, string defaultCollectionName)
|
||||
{
|
||||
var collectionUsers = new List<CollectionUser>();
|
||||
var collections = new List<Collection>();
|
||||
|
||||
foreach (var orgUserId in missingDefaultCollectionUserIds)
|
||||
{
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
|
||||
collections.Add(new Collection
|
||||
{
|
||||
Id = collectionId,
|
||||
OrganizationId = organizationId,
|
||||
Name = defaultCollectionName,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = null
|
||||
|
||||
});
|
||||
|
||||
collectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = orgUserId,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true,
|
||||
});
|
||||
}
|
||||
|
||||
return (collectionUsers, collections);
|
||||
}
|
||||
|
||||
public class CollectionWithGroupsAndUsers : Collection
|
||||
{
|
||||
public CollectionWithGroupsAndUsers() { }
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -794,7 +793,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
// SaveChangesAsync is expected to be called outside this method
|
||||
}
|
||||
|
||||
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
{
|
||||
organizationUserIds = organizationUserIds.ToList();
|
||||
if (!organizationUserIds.Any())
|
||||
@@ -808,15 +807,15 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(dbContext, organizationId);
|
||||
var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);
|
||||
|
||||
var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
|
||||
var (collections, collectionUsers) = CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
|
||||
|
||||
if (!collectionUsers.Any() || !collections.Any())
|
||||
if (!collections.Any() || !collectionUsers.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await dbContext.BulkCopyAsync(collections);
|
||||
await dbContext.BulkCopyAsync(collectionUsers);
|
||||
await dbContext.Collections.AddRangeAsync(Mapper.Map<IEnumerable<Collection>>(collections));
|
||||
await dbContext.CollectionUsers.AddRangeAsync(Mapper.Map<IEnumerable<CollectionUser>>(collectionUsers));
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
@@ -844,37 +843,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
return results.ToHashSet();
|
||||
}
|
||||
|
||||
private (List<CollectionUser> collectionUser, List<Collection> collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable<Guid> missingDefaultCollectionUserIds, string defaultCollectionName)
|
||||
{
|
||||
var collectionUsers = new List<CollectionUser>();
|
||||
var collections = new List<Collection>();
|
||||
|
||||
foreach (var orgUserId in missingDefaultCollectionUserIds)
|
||||
{
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
|
||||
collections.Add(new Collection
|
||||
{
|
||||
Id = collectionId,
|
||||
OrganizationId = organizationId,
|
||||
Name = defaultCollectionName,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = null
|
||||
|
||||
});
|
||||
|
||||
collectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = orgUserId,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true,
|
||||
});
|
||||
}
|
||||
|
||||
return (collectionUsers, collections);
|
||||
}
|
||||
public Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds,
|
||||
string defaultCollectionName) =>
|
||||
CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultCollectionName);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using DP = Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
public class DatabaseContext : DbContext
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- Creates default user collections for organization users
|
||||
-- Filters out existing default collections at database level
|
||||
CREATE PROCEDURE [dbo].[Collection_CreateDefaultCollections]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@DefaultCollectionName VARCHAR(MAX),
|
||||
@OrganizationUserCollectionIds AS [dbo].[TwoGuidIdArray] READONLY -- OrganizationUserId, CollectionId
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DECLARE @Now DATETIME2(7) = GETUTCDATE()
|
||||
|
||||
-- Filter to only users who don't have default collections
|
||||
SELECT ids.Id1, ids.Id2
|
||||
INTO #FilteredIds
|
||||
FROM @OrganizationUserCollectionIds ids
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId
|
||||
WHERE c.OrganizationId = @OrganizationId
|
||||
AND c.[Type] = 1 -- CollectionType.DefaultUserCollection
|
||||
AND cu.OrganizationUserId = ids.Id1
|
||||
);
|
||||
|
||||
-- Insert collections only for users who don't have default collections yet
|
||||
INSERT INTO [dbo].[Collection]
|
||||
(
|
||||
[Id],
|
||||
[OrganizationId],
|
||||
[Name],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Type],
|
||||
[ExternalId],
|
||||
[DefaultUserCollectionEmail]
|
||||
)
|
||||
SELECT
|
||||
ids.Id2, -- CollectionId
|
||||
@OrganizationId,
|
||||
@DefaultCollectionName,
|
||||
@Now,
|
||||
@Now,
|
||||
1, -- CollectionType.DefaultUserCollection
|
||||
NULL,
|
||||
NULL
|
||||
FROM
|
||||
#FilteredIds ids;
|
||||
|
||||
-- Insert collection user mappings
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
ids.Id2, -- CollectionId
|
||||
ids.Id1, -- OrganizationUserId
|
||||
0, -- ReadOnly = false
|
||||
0, -- HidePasswords = false
|
||||
1 -- Manage = true
|
||||
FROM
|
||||
#FilteredIds ids;
|
||||
|
||||
DROP TABLE #FilteredIds;
|
||||
END
|
||||
@@ -10,7 +10,6 @@ using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -204,14 +203,10 @@ public class AutomaticallyConfirmUsersCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.OrganizationId == organization.Id &&
|
||||
c.Name == defaultCollectionName &&
|
||||
c.Type == CollectionType.DefaultUserCollection),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>
|
||||
access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null));
|
||||
.CreateDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == organizationUser.Id),
|
||||
defaultCollectionName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -253,9 +248,7 @@ public class AutomaticallyConfirmUsersCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -291,9 +284,7 @@ public class AutomaticallyConfirmUsersCommandTests
|
||||
|
||||
var collectionException = new Exception("Collection creation failed");
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>())
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>())
|
||||
.ThrowsAsync(collectionException);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -13,7 +13,6 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -493,15 +492,10 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.Name == collectionName &&
|
||||
c.OrganizationId == organization.Id &&
|
||||
c.Type == CollectionType.DefaultUserCollection),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(cu =>
|
||||
cu.Single().Id == orgUser.Id &&
|
||||
cu.Single().Manage));
|
||||
.CreateDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),
|
||||
collectionName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -522,7 +516,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -539,24 +533,15 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
OrganizationUserId = orgUser.Id,
|
||||
IsProvider = false,
|
||||
OrganizationUserStatus = orgUser.Status,
|
||||
OrganizationUserType = orgUser.Type,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
};
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails]));
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -38,7 +38,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -60,7 +60,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -86,7 +86,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await collectionRepository
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
.CreateDefaultCollectionsBulkAsync(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<IEnumerable<Guid>>(),
|
||||
Arg.Any<string>());
|
||||
@@ -172,10 +172,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Act
|
||||
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
// Assert - Should call with all user IDs (repository does internal filtering)
|
||||
await collectionRepository
|
||||
.Received(1)
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
.CreateDefaultCollectionsBulkAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
|
||||
_defaultUserCollectionName);
|
||||
@@ -210,7 +210,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
|
||||
@@ -251,7 +251,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertDefaultCollectionsAsync(default, default, default);
|
||||
.CreateDefaultCollectionsBulkAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -273,7 +273,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertDefaultCollectionsAsync(default, default, default);
|
||||
.CreateDefaultCollectionsBulkAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -299,7 +299,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await collectionRepository
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
.CreateDefaultCollectionsBulkAsync(
|
||||
default,
|
||||
default,
|
||||
default);
|
||||
@@ -336,10 +336,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Act
|
||||
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
// Assert - Should call with all user IDs (repository does internal filtering)
|
||||
await collectionRepository
|
||||
.Received(1)
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
.CreateDefaultCollectionsBulkAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
|
||||
_defaultUserCollectionName);
|
||||
@@ -367,6 +367,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertDefaultCollectionsAsync(default, default, default);
|
||||
.CreateDefaultCollectionsBulkAsync(default, default, default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
|
||||
public class CreateDefaultCollectionsBulkAsyncTests
|
||||
{
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsBulkAsync_CreatesDefaultCollections_Success(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.CreatesDefaultCollections_Success(
|
||||
collectionRepository.CreateDefaultCollectionsBulkAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsBulkAsync_CreatesForNewUsersOnly_AndIgnoresExistingUsers(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.CreatesForNewUsersOnly_AndIgnoresExistingUsers(
|
||||
collectionRepository.CreateDefaultCollectionsBulkAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsBulkAsync_IgnoresAllExistingUsers(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.IgnoresAllExistingUsers(
|
||||
collectionRepository.CreateDefaultCollectionsBulkAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,14 @@ using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class UpsertDefaultCollectionsTests
|
||||
/// <summary>
|
||||
/// Shared tests for CreateDefaultCollections methods - both bulk and non-bulk implementations,
|
||||
/// as they share the same behavior. Both test suites call the tests in this class.
|
||||
/// </summary>
|
||||
public static class CreateDefaultCollectionsSharedTests
|
||||
{
|
||||
[Theory, DatabaseData]
|
||||
public async Task UpsertDefaultCollectionsAsync_ShouldCreateDefaultCollection_WhenUsersDoNotHaveDefaultCollection(
|
||||
public static async Task CreatesDefaultCollections_Success(
|
||||
Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -21,14 +25,13 @@ public class UpsertDefaultCollectionsTests
|
||||
var resultOrganizationUsers = await Task.WhenAll(
|
||||
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization),
|
||||
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id);
|
||||
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
|
||||
var defaultCollectionName = $"default-name-{organization.Id}";
|
||||
|
||||
// Act
|
||||
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id);
|
||||
@@ -36,8 +39,8 @@ public class UpsertDefaultCollectionsTests
|
||||
await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task UpsertDefaultCollectionsAsync_ShouldUpsertCreateDefaultCollection_ForUsersWithAndWithoutDefaultCollectionsExist(
|
||||
public static async Task CreatesForNewUsersOnly_AndIgnoresExistingUsers(
|
||||
Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -51,31 +54,30 @@ public class UpsertDefaultCollectionsTests
|
||||
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
|
||||
);
|
||||
|
||||
var arrangedOrgUserIds = arrangedOrganizationUsers.Select(organizationUser => organizationUser.Id);
|
||||
var arrangedOrgUserIds = arrangedOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
|
||||
var defaultCollectionName = $"default-name-{organization.Id}";
|
||||
|
||||
await CreateUsersWithExistingDefaultCollectionsAsync(createDefaultCollectionsFunc, collectionRepository, organization.Id, arrangedOrgUserIds, defaultCollectionName, arrangedOrganizationUsers);
|
||||
|
||||
await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, arrangedOrgUserIds, defaultCollectionName, arrangedOrganizationUsers);
|
||||
|
||||
var newOrganizationUsers = new List<OrganizationUser>()
|
||||
var newOrganizationUsers = new List<OrganizationUser>
|
||||
{
|
||||
await CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
|
||||
};
|
||||
|
||||
var affectedOrgUsers = newOrganizationUsers.Concat(arrangedOrganizationUsers);
|
||||
var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id);
|
||||
var affectedOrgUsers = newOrganizationUsers.Concat(arrangedOrganizationUsers).ToList();
|
||||
var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id).ToList();
|
||||
|
||||
// Act
|
||||
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, arrangedOrganizationUsers, organization.Id);
|
||||
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, affectedOrgUsers, organization.Id);
|
||||
|
||||
await CleanupAsync(organizationRepository, userRepository, organization, affectedOrgUsers);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task UpsertDefaultCollectionsAsync_ShouldNotCreateDefaultCollection_WhenUsersAlreadyHaveOne(
|
||||
public static async Task IgnoresAllExistingUsers(
|
||||
Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -89,26 +91,29 @@ public class UpsertDefaultCollectionsTests
|
||||
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
|
||||
);
|
||||
|
||||
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id);
|
||||
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
|
||||
var defaultCollectionName = $"default-name-{organization.Id}";
|
||||
|
||||
await CreateUsersWithExistingDefaultCollectionsAsync(createDefaultCollectionsFunc, collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers);
|
||||
|
||||
await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers);
|
||||
// Act - Try to create again, should silently filter and not create duplicates
|
||||
await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
|
||||
// Act
|
||||
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
// Assert - Original collections should remain unchanged, still only one per user
|
||||
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id);
|
||||
|
||||
await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers);
|
||||
}
|
||||
|
||||
private static async Task CreateUsersWithExistingDefaultCollectionsAsync(ICollectionRepository collectionRepository,
|
||||
Guid organizationId, IEnumerable<Guid> affectedOrgUserIds, string defaultCollectionName,
|
||||
private static async Task CreateUsersWithExistingDefaultCollectionsAsync(
|
||||
Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
|
||||
ICollectionRepository collectionRepository,
|
||||
Guid organizationId,
|
||||
IEnumerable<Guid> affectedOrgUserIds,
|
||||
string defaultCollectionName,
|
||||
OrganizationUser[] resultOrganizationUsers)
|
||||
{
|
||||
await collectionRepository.UpsertDefaultCollectionsAsync(organizationId, affectedOrgUserIds, defaultCollectionName);
|
||||
await createDefaultCollectionsFunc(organizationId, affectedOrgUserIds, defaultCollectionName);
|
||||
|
||||
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organizationId);
|
||||
}
|
||||
@@ -131,7 +136,6 @@ public class UpsertDefaultCollectionsTests
|
||||
private static async Task<OrganizationUser> CreateUserForOrgAsync(IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository, Organization organization)
|
||||
{
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CreateDefaultCollectionsAsyncTests
|
||||
{
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsAsync_CreatesDefaultCollections_Success(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.CreatesDefaultCollections_Success(
|
||||
collectionRepository.CreateDefaultCollectionsAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsAsync_CreatesForNewUsersOnly_AndIgnoresExistingUsers(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.CreatesForNewUsersOnly_AndIgnoresExistingUsers(
|
||||
collectionRepository.CreateDefaultCollectionsAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task CreateDefaultCollectionsAsync_IgnoresAllExistingUsers(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
await CreateDefaultCollectionsSharedTests.IgnoresAllExistingUsers(
|
||||
collectionRepository.CreateDefaultCollectionsAsync,
|
||||
organizationRepository,
|
||||
userRepository,
|
||||
organizationUserRepository,
|
||||
collectionRepository);
|
||||
}
|
||||
}
|
||||
1
test/Server.IntegrationTest/Properties/AssemblyInfo.cs
Normal file
1
test/Server.IntegrationTest/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: CaptureTrace]
|
||||
23
test/Server.IntegrationTest/Server.IntegrationTest.csproj
Normal file
23
test/Server.IntegrationTest/Server.IntegrationTest.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\util\Server\Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
45
test/Server.IntegrationTest/Server.cs
Normal file
45
test/Server.IntegrationTest/Server.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
|
||||
namespace Bit.Server.IntegrationTest;
|
||||
|
||||
public class Server : WebApplicationFactory<Program>
|
||||
{
|
||||
public string? ContentRoot { get; set; }
|
||||
public string? WebRoot { get; set; }
|
||||
public bool ServeUnknown { get; set; }
|
||||
public bool? WebVault { get; set; }
|
||||
public string? AppIdLocation { get; set; }
|
||||
|
||||
protected override IWebHostBuilder? CreateWebHostBuilder()
|
||||
{
|
||||
var args = new List<string>
|
||||
{
|
||||
"/contentRoot",
|
||||
ContentRoot ?? "",
|
||||
"/webRoot",
|
||||
WebRoot ?? "",
|
||||
"/serveUnknown",
|
||||
ServeUnknown.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
if (WebVault.HasValue)
|
||||
{
|
||||
args.Add("/webVault");
|
||||
args.Add(WebVault.Value.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(AppIdLocation))
|
||||
{
|
||||
args.Add("/appIdLocation");
|
||||
args.Add(AppIdLocation);
|
||||
}
|
||||
|
||||
var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint<Program>([.. args])
|
||||
?? throw new InvalidProgramException("Could not create builder from assembly.");
|
||||
|
||||
builder.UseSetting("TEST_CONTENTROOT_SERVER", ContentRoot);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
102
test/Server.IntegrationTest/ServerTests.cs
Normal file
102
test/Server.IntegrationTest/ServerTests.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Bit.Server.IntegrationTest;
|
||||
|
||||
public class ServerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AttachmentsStyleUse()
|
||||
{
|
||||
using var tempDir = new TempDir();
|
||||
|
||||
await tempDir.WriteAsync("my-file.txt", "Hello!");
|
||||
|
||||
using var server = new Server
|
||||
{
|
||||
ContentRoot = tempDir.Info.FullName,
|
||||
WebRoot = ".",
|
||||
ServeUnknown = true,
|
||||
};
|
||||
|
||||
var client = server.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/my-file.txt", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello!", await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WebVaultStyleUse()
|
||||
{
|
||||
using var tempDir = new TempDir();
|
||||
|
||||
await tempDir.WriteAsync("index.html", "<html></html>");
|
||||
await tempDir.WriteAsync(Path.Join("app", "file.js"), "AppStuff");
|
||||
await tempDir.WriteAsync(Path.Join("locales", "file.json"), "LocalesStuff");
|
||||
await tempDir.WriteAsync(Path.Join("fonts", "file.ttf"), "FontsStuff");
|
||||
await tempDir.WriteAsync(Path.Join("connectors", "file.js"), "ConnectorsStuff");
|
||||
await tempDir.WriteAsync(Path.Join("scripts", "file.js"), "ScriptsStuff");
|
||||
await tempDir.WriteAsync(Path.Join("images", "file.avif"), "ImagesStuff");
|
||||
await tempDir.WriteAsync(Path.Join("test", "file.json"), "{}");
|
||||
|
||||
using var server = new Server
|
||||
{
|
||||
ContentRoot = tempDir.Info.FullName,
|
||||
WebRoot = ".",
|
||||
ServeUnknown = false,
|
||||
WebVault = true,
|
||||
AppIdLocation = Path.Join(tempDir.Info.FullName, "test", "file.json"),
|
||||
};
|
||||
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Going to root should return the default file
|
||||
var response = await client.GetAsync("", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("<html></html>", await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
||||
// No caching on the default document
|
||||
Assert.Null(response.Headers.CacheControl?.MaxAge);
|
||||
|
||||
await ExpectMaxAgeAsync("app/file.js", TimeSpan.FromDays(14));
|
||||
await ExpectMaxAgeAsync("locales/file.json", TimeSpan.FromDays(14));
|
||||
await ExpectMaxAgeAsync("fonts/file.ttf", TimeSpan.FromDays(14));
|
||||
await ExpectMaxAgeAsync("connectors/file.js", TimeSpan.FromDays(14));
|
||||
await ExpectMaxAgeAsync("scripts/file.js", TimeSpan.FromDays(14));
|
||||
await ExpectMaxAgeAsync("images/file.avif", TimeSpan.FromDays(7));
|
||||
|
||||
response = await client.GetAsync("app-id.json", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
async Task ExpectMaxAgeAsync(string path, TimeSpan maxAge)
|
||||
{
|
||||
response = await client.GetAsync(path);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(response.Headers.CacheControl);
|
||||
Assert.Equal(maxAge, response.Headers.CacheControl.MaxAge);
|
||||
}
|
||||
}
|
||||
|
||||
private class TempDir([CallerMemberName] string test = null!) : IDisposable
|
||||
{
|
||||
public DirectoryInfo Info { get; } = Directory.CreateTempSubdirectory(test);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Info.Delete(recursive: true);
|
||||
}
|
||||
|
||||
public async Task WriteAsync(string fileName, string content)
|
||||
{
|
||||
var fullPath = Path.Join(Info.FullName, fileName);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (directory != null)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(fullPath, content, TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Creates default user collections for organization users
|
||||
-- Filters out existing default collections at database level
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_CreateDefaultCollections]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@DefaultCollectionName VARCHAR(MAX),
|
||||
@OrganizationUserCollectionIds AS [dbo].[TwoGuidIdArray] READONLY -- OrganizationUserId, CollectionId
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DECLARE @Now DATETIME2(7) = GETUTCDATE()
|
||||
|
||||
-- Filter to only users who don't have default collections
|
||||
SELECT ids.Id1, ids.Id2
|
||||
INTO #FilteredIds
|
||||
FROM @OrganizationUserCollectionIds ids
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId
|
||||
WHERE c.OrganizationId = @OrganizationId
|
||||
AND c.[Type] = 1 -- CollectionType.DefaultUserCollection
|
||||
AND cu.OrganizationUserId = ids.Id1
|
||||
);
|
||||
|
||||
-- Insert collections only for users who don't have default collections yet
|
||||
INSERT INTO [dbo].[Collection]
|
||||
(
|
||||
[Id],
|
||||
[OrganizationId],
|
||||
[Name],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[Type],
|
||||
[ExternalId],
|
||||
[DefaultUserCollectionEmail]
|
||||
)
|
||||
SELECT
|
||||
ids.Id2, -- CollectionId
|
||||
@OrganizationId,
|
||||
@DefaultCollectionName,
|
||||
@Now,
|
||||
@Now,
|
||||
1, -- CollectionType.DefaultUserCollection
|
||||
NULL,
|
||||
NULL
|
||||
FROM
|
||||
#FilteredIds ids;
|
||||
|
||||
-- Insert collection user mappings
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
ids.Id2, -- CollectionId
|
||||
ids.Id1, -- OrganizationUserId
|
||||
0, -- ReadOnly = false
|
||||
0, -- HidePasswords = false
|
||||
1 -- Manage = true
|
||||
FROM
|
||||
#FilteredIds ids;
|
||||
|
||||
DROP TABLE #FilteredIds;
|
||||
END
|
||||
GO
|
||||
@@ -6,6 +6,13 @@ namespace Bit.Server;
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = CreateWebHostBuilder(args);
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
}
|
||||
|
||||
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddCommandLine(args)
|
||||
@@ -37,7 +44,6 @@ public class Program
|
||||
builder.UseWebRoot(webRoot);
|
||||
}
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user